From 92f7caae14b3c2fb4e750c276d579552af2c1270 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 17 Jul 2026 15:06:59 -0700 Subject: [PATCH 1/6] docs: specify Wasmtime executor architecture --- docs/design/runtime-neutral-executors.md | 754 ++++++++++++++++++ docs/design/wasmtime-executor.md | 943 +++++++++++++++++++++++ docs/design/wasmtime-phase-0.md | 394 ++++++++++ 3 files changed, 2091 insertions(+) create mode 100644 docs/design/runtime-neutral-executors.md create mode 100644 docs/design/wasmtime-executor.md create mode 100644 docs/design/wasmtime-phase-0.md diff --git a/docs/design/runtime-neutral-executors.md b/docs/design/runtime-neutral-executors.md new file mode 100644 index 0000000000..9728d09eff --- /dev/null +++ b/docs/design/runtime-neutral-executors.md @@ -0,0 +1,754 @@ +# Runtime-Neutral Executors and Kernel Host Services + +Status: ready for implementation; prerequisite for the Wasmtime executor + +Audience: AgentOS kernel, runtime, execution, native-sidecar, VFS, and language +executor owners + +## 1. Executive decision + +AgentOS will refactor the boundary between the kernel, sidecar, and guest +executors before adding Wasmtime as a second standalone-WASM backend. + +- The kernel owns process identity, descriptors, VFS state, permissions, + signals, process groups, wait status, virtual sockets, PTYs, and resource + accounting. +- The sidecar owns runtime selection, external asynchronous I/O, the one Tokio + runtime, package resolution, lifecycle coordination, and host-visible events. +- Guest execution never runs on Tokio. Work that can wait uses async readiness; + unavoidable blocking work requires admission to the fixed bounded blocking + executor. No subsystem creates another runtime or blocks a Tokio worker. +- An executor owns only engine state, guest-memory/ABI adaptation, guest + scheduling, and engine-specific interruption mechanics. +- The kernel defines a runtime-neutral process-control contract. V8, Wasmtime, + Python, and binding executions expose control endpoints implementing that + contract; the kernel never imports concrete executor types. +- Executors consume capability-sized filesystem, network, process, terminal, + signal, clock, entropy, and identity services. They do not carry parallel + policy or resource tables. +- Every Linux/POSIX operation supported by AgentOS has one semantic + implementation in the kernel or its kernel-owned resource layer. Executors + adapt guest ABIs to that implementation; they do not provide per-engine + versions. +- The first implementation stays in the existing crates. A new shared crate is + not required unless dependency pressure proves that one is necessary. + +This is prerequisite architecture, not Wasmtime adapter work. The Wasmtime +executor specification depends on the exit gates in this document. All work in +this specification lands as one independent prerequisite JJ revision after the +specification/baseline revision and before any Wasmtime executor code. The +capability sequence in Section 12 is an implementation workstream order inside +that one revision, not permission to interleave Wasmtime or create one landing +revision per capability. + +### 1.1 Non-goals + +This refactor does not: + +- force JavaScript, WASM, and Python to expose the same guest API; +- move Node streams, WASI layouts, Python objects, or engine event loops into + the kernel; +- make the kernel own or poll executor instances; +- move external Tokio networking into the kernel; +- replace working kernel VFS/socket/PTY/process implementations; +- require every capability family to migrate in one atomic change; or +- compile, repair, or reactivate the browser runtime. + +The shared layer standardizes authority, state, typed operations, completion, +and control. Adapters remain free to expose language-appropriate APIs. + +### 1.2 Scope and parity target + +The scope is **feature parity or better with the complete currently supported +V8-hosted standalone-WASM environment**, not an abstract project to implement +every syscall ever shipped by Linux. + +The baseline includes: + +- every active `wasi_snapshot_preview1` and AgentOS `host_*` import; +- the owned patched Rust/libc/sysroot surface used by current software; +- commands and interactive programs that already work, including `ls`, `vim`, + `grep`, `curl`, shell/process pipelines, sqlite, and the registry command + suite; +- current filesystem, descriptor, networking, process, signal, TTY, identity, + clock, permission, and resource-limit behavior; and +- hostile raw modules that call imports directly without libc. + +If the current implementation has multiple engine-specific versions of one +operation, the refactor selects or builds one Linux-correct kernel +implementation and migrates all current executors to it. Correctness fixes are +in scope and may intentionally change existing V8 behavior. + +Future supported Linux APIs follow the same rule: add the semantic operation to +the kernel/shared resource owner first, then expose it through adapters. The +project is complete when the existing V8-WASM feature and software corpus runs +through this single implementation and Wasmtime can consume it without adding +semantic host code. + +### 1.3 Wasmtime research constraints on this refactor + +No Wasmtime spike is required, but the published embedding API imposes concrete +requirements that the shared boundary must satisfy: + +- Wasmtime's `Linker` can provide the existing AgentOS Preview1 and `host_*` + imports directly; the refactor must not assume `wasmtime-wasi` resource + ownership. +- Async host imports suspend a Wasmtime call as a future. The host service must + accept bounded owned values and return through an awaitable direct reply; + guest-memory borrows cannot cross the wait. +- Wasmtime does not provide the embedding's execution pool. The sidecar must + poll guest execution on the existing bounded non-Tokio VM executor while + Tokio owns external async I/O. +- A Wasmtime `Store` needs cloneable generation-bound handles to host services, + cancellation, readiness, limits, and the kernel process reporter. It cannot + own or mutably borrow the sidecar's `KernelVm`. +- Epoch interruption and Store cancellation need a thread-safe control handle + that maps naturally to the same kernel process endpoint used by V8. +- Compiled `Module` sharing and caching are engine concerns and must not affect + kernel process, fd, filesystem, or signal ownership. +- Shared-memory threads are not part of the initial parity target; the current + V8-WASM software surface is the first Wasmtime admission gate. + +These constraints are sufficient to design the kernel interface without +building a provisional Wasmtime implementation. + +## 2. Why this refactor is needed + +The current code already contains the beginning of the correct abstraction, +but it is not connected to the real executors. + +`agentos-kernel` defines `DriverProcess`, stores an +`Arc` in every process-table entry, and owns blocked and +pending signal sets. However, `KernelVm::register_process` always registers a +`StubDriverProcess`. The stub records signals and synthesizes exits; it is not +the `ActiveExecution` actually running the guest. + +The native sidecar separately stores: + +- `ActiveExecution::{Javascript, Python, Wasm, Binding}`; +- an additional signal-disposition map; +- an additional pending-WASM-signal set; +- V8-specific signal/session delivery; +- runtime pause, resume, termination, and OS-process signal logic; and +- readiness targets containing `V8SessionHandle`. + +This produces two process-control planes: + +```text +kernel ProcessTable + -> DriverProcess + -> StubDriverProcess + +sidecar ActiveProcess + -> ActiveExecution enum + -> V8 / Python / V8-WASM / binding-specific control +``` + +Signals generated by kernel operations such as `EPIPE`, child exit, or PTY +resize can therefore take a different path from signals delivered through the +sidecar. Wasmtime would add another path unless this boundary is fixed first. + +The same pattern exists in less concentrated form for fd aliases, filesystem +permissions, mutable rlimits, clocks, identity, terminal metadata, and socket +readiness. The semantic implementation is often already in Rust, but the +effective state or transport remains executor-specific. + +## 3. Required dependency direction + +The kernel must not own or depend on V8, Wasmtime, Pyodide, JavaScript bridge +types, Tokio tasks, or concrete sidecar process records. + +```text + kernel-owned durable state + process / fd / VFS / signal / PTY + ^ | + | | control wake + typed host services + | v +sidecar lifecycle + I/O ---- runtime-neutral execution contract + ^ + | + +----------------+----------------+ + | | | + V8 adapter Wasmtime adapter Python adapter +``` + +There are three distinct interfaces: + +1. **Kernel to executor:** nonblocking, coalesced process-control requests. +2. **Executor to host:** typed, bounded operations over kernel/sidecar + capabilities. +3. **Sidecar to executor:** start, event, exec-replacement, cancellation, and + teardown lifecycle. + +Combining these into one enormous `Executor` or `GuestHost` trait would couple +unrelated capabilities and recreate the current switchboard under a new name. + +## 4. Kernel-facing process runtime endpoint + +### 4.1 Replace the stub-only driver connection + +Evolve `DriverProcess` into a narrow runtime endpoint registered with the +process-table entry: + +```rust +pub trait ProcessRuntimeEndpoint: Send + Sync { + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError>; +} + +pub enum ProcessControlRequest { + Checkpoint, + Stop, + Continue, + Terminate(ProcessTermination), + Cancel(CancellationReason), +} +``` + +The exact names are not normative. The behavioral requirements are: + +- calls never execute guest code inline; +- calls never block a kernel lock or Tokio worker; +- standard-signal notifications are coalesced into durable kernel state; +- each execution has at most one queued wake; +- stop, terminate, and cancellation cannot be dropped because an ordinary + event queue is full; +- endpoint failure is typed and observable; and +- the endpoint contains no authority beyond its registered VM generation and + kernel PID. + +The endpoint implementation should be a cloneable control handle separate from +the owned engine instance. It may set atomic control bits, request a Wasmtime +epoch interruption, interrupt a V8 session, notify an admitted VM-executor +thread, or signal a native child process. Those mechanics stay inside the +adapter. + +Process allocation and backend construction currently have a PID dependency in +both directions. Resolve it with a two-part control cell: + +1. the sidecar creates a bounded `RuntimeControlCell` and registers its producer + endpoint while the kernel allocates the PID; +2. the sidecar constructs the backend with that PID and attaches the one + consumer to the cell before starting guest instructions. + +Control requested during construction remains durable in the cell. A +termination requested before attachment prevents the guest from starting; no +temporary production stub and no lost-signal window are allowed. + +`StubDriverProcess` remains only for kernel unit tests and deliberately virtual +processes. Production guest processes register their real runtime endpoint. + +### 4.2 Executor-to-kernel exit reporting + +Do not retain `DriverProcess::wait` as a second source of process status. The +kernel process table is authoritative for wait and zombie state. + +At registration, the sidecar receives a generation-bound reporter capability: + +```text +report_exit(Exited(code)) +report_exit(Signaled { signal, core_dumped }) +report_runtime_fault(typed_error) +``` + +Reporting is idempotent and first-terminal-result wins. The kernel records the +exit, closes or releases process resources through the existing lifecycle, +creates `SIGCHLD`, wakes waiters, and exposes exact signal metadata. An exit +code of `128 + signal` is never used to infer that a signal occurred. + +Kernel-wide termination requests control through every endpoint and then waits +on process-table terminal state with bounded grace and kill phases. It does not +call an endpoint-specific `wait` method, because that would restore a second +source of process status. + +## 5. Kernel-owned signal model + +Signals should be handled at the kernel level. The current kernel owns only +part of the state; the refactor completes that ownership. + +### 5.1 Authoritative state + +Each kernel process owns bounded signal state: + +- disposition for signals 1 through 64: default, ignore, or user; +- disposition flags and handler mask, but not a guest function pointer; +- blocked signal set; +- coalesced pending standard-signal set; +- running, stopped, and exited state; +- bounded in-progress delivery tokens needed for nested handlers; and +- exact terminating signal and core-dump metadata. + +Guest handler pointers remain inside V8 or WASM linear memory and are never +kernel capabilities. A `user` disposition means the adapter must deliver the +signal at a guest safe point. + +`sigaction`, `sigprocmask`, `sigpending`, signal generation, exec disposition +reset, and wait-state changes all operate on this one record. The sidecar +`signal_states` map and `ActiveProcess.pending_wasm_signals` are deleted. + +### 5.2 Delivery decision + +`signal_process` performs Linux-compatible target validation and then makes the +delivery decision while holding the process-table state: + +- signal 0 validates only; +- a blocked catchable signal becomes pending; +- an ignored signal is discarded, except for required `SIGCONT` resume + behavior; +- a caught signal becomes pending and requests `Checkpoint`; +- a default stop signal changes kernel status and requests `Stop`; +- `SIGCONT` changes kernel status and requests `Continue` before any caught + handler runs; +- a default terminating signal requests `Terminate`; and +- `SIGKILL` and `SIGSTOP` cannot be blocked, ignored, or caught. + +Kernel-generated `SIGPIPE`, `SIGCHLD`, and PTY foreground-group `SIGWINCH` use +this exact path. The sidecar does not generate duplicates. + +### 5.3 Guest handler checkpoint + +At a safe point, an adapter asks the kernel to begin one pending delivery. The +kernel atomically selects an unblocked signal, applies `sa_mask`, +`SA_NODEFER`, and `SA_RESETHAND`, and returns a bounded delivery token plus the +signal number and flags. The adapter invokes its guest handler and then closes +the token so the previous mask is restored. + +For WASM, the owned libc calls `__wasi_signal_trampoline`. For Node/V8, the V8 +adapter schedules the matching process signal event. The kernel does not call +either engine. + +An interruptible host operation registers its waiter and temporary `ppoll` +mask atomically with the signal state. A caught signal wakes the operation and +returns `EINTR` or a restart checkpoint. Ignored and still-blocked signals do +not spuriously interrupt it. + +`SA_RESTART` requires one documented, shared set of restartable operations. +Before real threads, the mask is process-scoped. The later threads project +moves masks and in-progress delivery stacks to kernel thread records without +changing executor control. + +## 6. Sidecar-facing execution backend contract + +`ActiveExecution` currently implements common behavior through a growing enum +match and also exposes V8-specific session and sync-RPC methods. Replace that +surface with a small common backend contract and adapter-owned extensions. + +The common lifecycle is: + +```rust +pub trait ExecutionBackend { + fn runtime_kind(&self) -> GuestRuntimeKind; + fn control_endpoint(&self) -> Arc; + fn start_prepared(&mut self) -> Result<(), ExecutionError>; + fn poll_event( + &mut self, + cx: &mut Context<'_>, + ) -> Poll, ExecutionError>>; + fn begin_shutdown(&mut self, reason: ShutdownReason) + -> Result<(), ExecutionError>; +} +``` + +This is an architectural shape, not a requirement to use `async_trait` or +dynamic dispatch. An enum may remain as storage if all common callers use the +contract and engine-specific matches are confined to construction/adapters. +The owned backend is deliberately not required to be `Send`: V8 remains +thread-affine on its admitted VM-executor thread. Only its control and wake +handles cross threads. + +Common events are bounded and runtime-neutral: + +```text +stdout(bytes + reservation) +stderr(bytes + reservation) +host_call(typed request + direct reply handle) +warning(typed warning) +exited(process termination) +``` + +A host-call event carries its response capability. Shared code must not call +methods such as `respond_javascript_sync_rpc_*` on the execution enum. Python +VFS requests, V8 synchronous bridge calls, and Wasmtime async imports normalize +to the same typed host operations where their semantics are shared. + +V8-only stream events and Node-specific callbacks remain adapter extensions; +their types do not appear in filesystem, process, signal, or readiness owners. + +## 7. Executor-facing host services + +The executor-facing API is split by capability rather than engine: + +```text +GuestFileHost paths, descriptors, metadata, directory operations +GuestNetworkHost sockets, DNS, connect/listen/data/options/readiness +GuestProcessHost spawn, exec, wait, groups, rlimits, descriptor actions +GuestTerminalHost PTYs, termios, window size, foreground group, stdio +GuestSignalHost sigaction, masks, pending delivery checkpoints +GuestIdentityHost uid/gid/groups and passwd/group lookup +GuestClockHost realtime, monotonic time, timers +GuestEntropyHost bounded random bytes +``` + +These are typed operation families, not necessarily Rust async traits. The +initial implementation can use bounded request messages with direct reply +handles because the sidecar owns mutable `KernelVm` and the process-wide Tokio +reactor. The requirements are: + +- request inputs are owned, bounded values; +- the caller supplies only VM generation, kernel PID, and registered + capability identity—not authority chosen by guest bytes; +- replies carry typed `{ code, message, details }` errors; +- every async request has one registered waiter and cancellation path; +- no synchronous waiter scans or consumes unrelated execution events; +- overload is a typed limit error, never an infinite retry loop; +- kernel operations remain the semantic and permission authority; and +- external OS I/O remains in sidecar runtime services, not the kernel crate. + +### 7.1 Async and blocking execution contract + +Synchronous guest semantics do not authorize blocking a Tokio worker. Every +operation is classified by where work executes and how it waits: + +| Work | Owner | Waiting rule | +| --- | --- | --- | +| V8, Wasmtime, Python, or binding guest instructions | Bounded non-Tokio VM executor | May occupy only its admitted VM-executor capacity; never polled or entered synchronously by Tokio | +| Bounded in-memory kernel operation | Kernel called by sidecar host service | May execute inline only when it cannot wait and has a bounded work quantum | +| Kernel fd read/write/poll that would block | Kernel readiness plus sidecar waiter | Return readiness/`EAGAIN` state and suspend the guest operation; never wait on a condvar or blocking channel on Tokio | +| Native TCP/UDP/Unix/TLS/DNS and timers | One process-wide Tokio runtime | Use async I/O, bounded commands, direct completion waiters, and cancellation | +| Unavoidably blocking host filesystem/library work | Fixed bounded blocking executor | Acquire admission before submission; bounded queue and deadline; never use Tokio's elastic blocking pool as unbounded admission | +| Guest-visible sleep, child wait, terminal input, and record locks | Durable kernel/sidecar wait registration | Suspend until readiness, signal, timeout, cancellation, or teardown; no polling timer and no executor-specific child pumping | + +The process contains exactly one Tokio runtime. No VM, executor, socket, +filesystem adapter, or child process creates another runtime. No code running +on a Tokio worker may use `block_on`, `Atomics.wait`, a condition-variable wait, +a blocking channel receive/send, synchronous guest entry, or an unadmitted +blocking syscall. + +An asynchronous host call follows this sequence: + +1. The guest adapter validates and copies bounded owned input. +2. It submits a typed request with a registered direct reply handle and + cancellation identity. +3. The Wasmtime/V8/Python execution yields or parks only its admitted + non-Tokio execution context. +4. The sidecar performs bounded kernel work, starts async Tokio I/O, or admits + unavoidable blocking work to the fixed blocking executor. +5. Completion settles only the registered waiter, updates durable readiness, + and enqueues at most one coalesced execution wake. +6. The guest adapter resumes, revalidates guest memory if applicable, and + encodes the typed result. + +Every path has explicit limits for request count, request bytes, retained +buffers, outstanding waiters, blocking jobs, and completion bytes. Cancellation +and VM teardown settle or fail every waiter; fire-and-forget work that can lose +an error is prohibited. + +The V8 RPC decoder and Wasmtime linker are two transports into these same +operations. A transport may decode a different ABI, but it may not implement a +second fd table, signal state, network policy, or filesystem permission model. + +## 8. Filesystem and descriptor requirements + +The kernel fd table is the only authoritative guest descriptor namespace. +Kernel state owns open descriptions, offsets, flags, rights, cwd/path-at +resolution, preopen metadata, filesystem permission tier, rlimits, and errno. + +The current native sidecar contains a bidirectional mutable shadow filesystem: +some embedded-Node operations write host paths, sidecar calls copy those paths +into the kernel, kernel mutations are mirrored back to host paths, and exit-time +walks reconcile them again. This exists because parts of the V8/Node filesystem +and module loader still access a materialized host tree. It can resurrect stale +files, requires a second inventory, duplicates permissions/metadata, and makes +the source of truth timing-dependent. + +That mechanism is migration debt. It is not part of the shared host service and +is not inherited by Wasmtime. + +Current evidence is concentrated in +`crates/native-sidecar/src/filesystem.rs`: `guest_filesystem_call` invokes +`sync_guest_filesystem_shadow_before_call` and +`mirror_guest_filesystem_shadow_after_call`; `ProcessModuleFsReader` reads the +process shadow before the kernel; and launch/exit paths call +`sync_process_host_roots_to_kernel`. The managed WebAssembly path already +prefers kernel filesystem RPCs when its execution root is configured, so this +mirror is primarily embedded-runtime compatibility debt rather than a Wasmtime +requirement. + +The target has one mutable source of truth: + +- all guest filesystem operations, including embedded V8 `fs`, module + resolution, WASI, Python, and wire filesystem calls, use `GuestFileHost` over + the kernel VFS and fd table; +- host-directory access occurs through explicit confined kernel mount/plugin + resources, not by copying a directory tree into and out of the VFS; +- `/opt/agentos` package projection means kernel/VFS mounts of immutable package + resources, not a mutable shadow copy; +- a module loader may cache immutable bytes under ordinary bounded cache rules, + but it cannot maintain writable filesystem state outside the kernel; and +- V8 may temporarily retain an ABI fd-alias map while Node-WASI is migrated, + but aliases resolve to kernel descriptors and cannot own offsets, rights, + contents, or lifecycle. + +Cutover requirements include: + +- remove mutable host-shadow reconciliation, including shadow inventories, + pre-call host-to-kernel sync, post-call kernel-to-host mirroring, and exit-time + tree walks; +- route embedded V8 filesystem builtins and module reads to the kernel-backed + service before declaring the prerequisite complete; +- raw `host_fs` calls cannot bypass the configured filesystem tier; +- absolute paths and dirfd-relative paths share kernel resolution; +- preopens and descriptor rights come from kernel metadata; +- mutable `RLIMIT_NOFILE` moves into kernel process state; +- descriptor allocation has one limit and warning path; and +- kernel errors cross the sidecar as typed values instead of strings. + +## 9. Network and readiness requirements + +The kernel remains the owner of virtual socket state and the sidecar remains +the owner of external TCP/UDP/Unix/TLS transports. + +Replace `V8SessionHandle` readiness targets with a generation-bound +`ExecutionWakeHandle`. Readiness is durable level state in the resource owner; +the wake is only a coalesced hint. Each execution has at most one queued wake, +and each in-flight operation has one direct completion waiter. + +The wake handle cannot: + +- clear readiness merely because an adapter consumed a hint; +- select another VM generation; +- enqueue unbounded packet/chunk events; +- run guest code on a Tokio thread; or +- own a second socket registry. + +V8 translates the wake into its event-loop checkpoint. Wasmtime wakes the +future polled by the bounded VM executor. Python uses the same operation and +readiness state rather than a polling timer. + +## 10. Process, terminal, identity, clock, and entropy requirements + +- Spawn and exec are sidecar lifecycle operations over kernel process/fd state. + Runtime selection is not an executor responsibility. +- Process file actions use kernel descriptors directly and preserve atomic + commit/rollback semantics. +- PTY state, line discipline, termios, foreground pgid, and window size stay in + the kernel. Host stdout ordering, raw-mode cleanup leases, and tracked-runtime + `SIGWINCH` wakes stay in the sidecar service. +- UID/GID/effective IDs, supplementary groups, umask, and mutable rlimits are + kernel process state. Executors do not reconstruct them from environment + variables. +- Realtime policy and monotonic process time come from a shared clock service; + Wasmtime and V8 do not use ambient engine clocks. +- Random reads validate the guest range first and use a bounded/chunked shared + entropy service backed by the same source as virtual `/dev/urandom`. +- Intentional Linux-compatibility stubs such as fixed identity mutation, + loopback-only interface enumeration, or unsupported `mlock` stay in the + owned sysroot and behave identically under both engines. + +## 11. Proposed code organization + +No new crate is required initially: + +```text +crates/kernel/src/ + process_table.rs authoritative process/signal/wait state + process_runtime.rs runtime endpoint, control requests, exit reporter + signal.rs dispositions and delivery checkpoints + +crates/execution/src/ + backend/ + mod.rs common lifecycle/event contract + control.rs engine-side endpoint helpers + event.rs runtime-neutral bounded events/reply handles + host/ + mod.rs + error.rs + filesystem.rs + network.rs + process.rs + terminal.rs + signal.rs + identity.rs + clock.rs + +crates/native-sidecar/src/execution/ + registry.rs PID/generation to backend + wake/control handles + host/ + filesystem.rs kernel/mount-backed filesystem operations + network.rs kernel/native transport capability operations + process.rs + terminal.rs + signal.rs + identity.rs + clock.rs +``` + +The exact file split can follow the implementation, but these ownership +boundaries are normative. Do not put every operation in one `executor.rs`, one +`host.rs`, or one mega-trait. + +A later `agentos-executor-host` crate is justified only if the existing crate +graph creates a real dependency cycle or if independent fuzzing/linkage needs +it. Creating a crate merely to hold traits adds packaging and versioning cost +without improving ownership. + +## 12. Prerequisite-revision workstream sequence + +The entire sequence below is **one delivery phase and one JJ revision**. These +workstreams provide implementation and review checkpoints while the revision is +being developed. The revision is ready to land only when every workstream and +every Section 13 exit gate is complete. Local intermediate revisions may be +used while developing it, but they are folded before handoff. + +### Workstream 0: Freeze contracts and parity tests + +- Inventory every active Preview1 and `host_*` import and every owned-sysroot + extension used by the current software suite. +- Record signal, fd, process, terminal, network, errno, permission, async-wait, + and resource-limit behavior. +- Freeze the working V8-WASM command corpus, including interactive and + process/network-heavy programs, as the minimum parity suite. +- Add hostile raw-import tests for permission and ambient-host bypasses. +- Add exact exit-code-versus-signal assertions. + +### Workstream 1: Connect kernel processes to real runtime endpoints + +- Add `ProcessRuntimeEndpoint` and the generation-bound exit reporter. +- Register real control handles for V8, Python, binding, and compatibility + WASM executions. +- Remove production dependence on `StubDriverProcess`. +- Preserve current sidecar signal behavior temporarily behind the endpoint. + +### Workstream 2: Make signals kernel-authoritative + +- Move dispositions, masks, pending state, and exec reset into the process + table. +- Route `SIGPIPE`, `SIGCHLD`, PTY signals, kill, and process-group delivery + through one path. +- Add begin/end handler-delivery checkpoints and atomic `ppoll` masks. +- Delete sidecar and runner duplicate signal state. + +### Workstream 3: Introduce typed backend events and host calls + +- Add direct reply handles and typed errors. +- Stop routing shared operations through unrelated session-event scanning. +- Confine V8 sync-RPC details to the V8 adapter. + +### Workstream 4: Consolidate filesystem and descriptor authority + +- Move permission tier, preopens, fd rights, rlimits, and path-at resolution to + kernel process state. +- Implement the shared filesystem host service. +- Route embedded V8 filesystem and module loading through that service. +- Delete mutable host-shadow inventories and bidirectional reconciliation. +- Preserve host access only through explicit confined mounts/plugins. + +### Workstream 5: Generalize readiness and networking + +- Replace V8 session readiness targets with execution wake handles. +- Route V8, Python, and later Wasmtime through the same bounded operations and + direct waiters. +- Remove adapter-owned socket state that duplicates kernel/capability state. + +### Workstream 6: Finish process, terminal, identity, clock, and entropy services + +- Remove runner-local rlimits, identity, TTY caches, and clock/random providers. +- Complete live kernel termios, pgid, and supplementary-group operations. +- Close typed-error and Linux-conformance gaps. + +### Workstream 7: Close the prerequisite revision + +- Require new executors to implement only the common lifecycle/control + contract and the ABI adapter over shared host services. +- Start the Wasmtime implementation only after the complete current V8-WASM + ABI and software parity surface, including signal/readiness foundations, has + closed its exit gates. +- Do not build a separate Wasmtime spike or provisional semantic host layer. +- Verify that the final tree contains no Wasmtime executor implementation and + passes the complete current-executor parity suite before the next JJ revision + begins. + +## 13. Exit gates + +The prerequisite refactor is complete when: + +- every production kernel process has a real runtime endpoint; +- `StubDriverProcess` is test/virtual-process-only; +- the kernel process table is the only owner of signal dispositions, masks, + pending sets, process status, and wait events; +- kernel-generated and externally requested signals use one delivery path; +- filesystem permissions, fd rights, preopens, rlimits, identity, and umask are + authoritative kernel process state; +- no mutable guest filesystem state is synchronized between a kernel VFS and a + host shadow tree; +- embedded V8 filesystem calls and module resolution observe kernel state + directly; +- shared readiness targets contain no `V8SessionHandle`; +- common host services contain no `Javascript*`, `V8*`, `Wasmtime*`, or + `Python*` types; +- common sidecar code does not match an executor variant to perform signal, + filesystem, network, process, or terminal semantics; +- every request, reply, queue, waiter, and retained buffer is bounded and + accounted; +- kernel error codes cross the host boundary without string parsing; and +- V8, Python, and compatibility WASM pass the complete current V8-WASM ABI and + working-software parity suite through the new services before Wasmtime work + begins. + +## 14. Current extraction inventory + +| Capability | Existing shared owner | Executor/sidecar-specific debt | Required target | +| --- | --- | --- | --- | +| Backend lifecycle | `ActiveExecution` normalizes some start/poll/control operations | Large enum switchboard exposes V8 session and JavaScript sync-RPC methods | Common lifecycle, control handle, bounded events, and adapter-owned extensions | +| Signals | Kernel process table owns target selection, masks, pending bits, groups, and wait status | Production kernel process uses a stub; sidecar owns dispositions; WASM owns another pending set | Real runtime endpoint plus fully kernel-owned dispositions/delivery state | +| Filesystem | Kernel VFS and fd tables implement most operations | Node-WASI fd aliases, bidirectional shadow-tree reconciliation, raw-import permission gaps, duplicated limits | `GuestFileHost` over the sole mutable kernel state; explicit mounts for host resources; delete shadow synchronization | +| Networking | Kernel owns virtual sockets/policy; sidecar owns external Tokio transports | Readiness and some socket state contain `V8SessionHandle` or JavaScript naming | Shared capability operations, direct waiters, and runtime-neutral coalesced wakes | +| Processes | Kernel owns PID/fd/group/wait/exec state; sidecar owns runtime selection | Spawn/event paths and descendant pumping are JavaScript-shaped; mutable rlimits live in runner | Shared process host service and kernel process limits with adapter-neutral lifecycle | +| TTY/PTY | Kernel owns PTYs, buffers, line discipline, termios core, pgid, and window size | Runner `isatty` cache, libc termios shadow, stubbed `pty_open`, adapter wait loops | Live kernel terminal operations plus sidecar lifecycle/output hooks | +| Identity/Linux | Kernel owns process identity, groups, `/proc`, `/dev`, and umask | Environment reconstruction, primary-GID-only groups, hardcoded hostname, clock quirks | Kernel identity/rlimits plus shared clock, entropy, and system-identity providers | +| Errors | Kernel errors contain stable errno-like codes | Sidecar converts them to strings and adapters reconstruct errno | Typed code/message/details through every shared operation | + +The complete import-by-import mapping is normative in +[`wasmtime-phase-0.md`](./wasmtime-phase-0.md). It identifies every import's host +service operation, authority checks, limits, async wait, guest-memory direction, +compatibility status, and parity tests. Phase 1 keeps the generated ABI manifest +and rebuilt-module import audit synchronized with that mapping. + +## 15. Principal risks + +| Risk | Severity | Mitigation | +| --- | --- | --- | +| A generic interface becomes a mega-trait mirroring `ActiveExecution` | High | Split lifecycle, control, events, and capability-sized host services. | +| Kernel calls guest code or blocks on an executor | Critical | Runtime endpoint only sets bounded/coalesced control state and wakes the admitted executor. | +| Signal state moves but remains duplicated during migration | Critical | Declare kernel state authoritative phase by phase; adapters become subscribers, not mirrors. | +| Endpoint queue saturation drops `SIGKILL`, stop, or cancellation | Critical | Durable atomic control state with at most one wake; no ordinary event queue for control. | +| Mutable V8 host shadows survive and remain a second filesystem truth | Critical | Migrate embedded V8 `fs` and module reads to `GuestFileHost`; delete shadow inventories and bidirectional sync before Wasmtime. | +| Host-operation traits hide unbounded allocation or waiting | Critical | Owned bounded request types, direct waiters, resource reservations, typed overload. | +| Kernel grows Tokio or engine dependencies | High | Keep external I/O and executor mechanics in sidecar/adapters; kernel contracts remain engine-neutral. | +| Large refactor blocks all feature work | High | Migrate one capability family at a time inside the prerequisite revision, keeping V8 parity at every workstream checkpoint. | + +## 16. Resolved owner decisions + +1. The kernel owns every executor-independent Linux/POSIX semantic operation, + including complete signal state and delivery decisions. +2. Production kernel processes register real runtime control endpoints; + `StubDriverProcess` remains test/explicit-virtual-process-only. +3. The parity target is the entire currently supported V8-WASM ABI and working + software surface, not a hand-picked Wasmtime subset and not every + theoretical Linux syscall. +4. Correctness, security, errno, and Linux-behavior fixes may ship during the + refactor even when they intentionally change current V8 behavior. +5. The browser runtime is entirely out of scope, including compile fixes and + migration gates. +6. `ActiveExecution` may remain a sealed enum behind the common contracts; + dynamic dispatch is not a project goal. +7. Mutable filesystem state has one source of truth in the kernel. Existing + host-shadow synchronization is removed rather than generalized. +8. No Wasmtime spike is built. Wasmtime implementation starts after the + prerequisite parity gates close and uses only the resulting shared services. +9. The complete runtime-neutral refactor is one independent JJ revision. The + following Wasmtime implementation is a different revision; engine code is + never used to paper over an incomplete prerequisite. diff --git a/docs/design/wasmtime-executor.md b/docs/design/wasmtime-executor.md new file mode 100644 index 0000000000..3cd7691f4d --- /dev/null +++ b/docs/design/wasmtime-executor.md @@ -0,0 +1,943 @@ +# Wasmtime Executor and Shared WASM Host ABI + +Status: ready for implementation; depends on the runtime-neutral executor +refactor + +Audience: AgentOS kernel, sidecar runtime, execution, VFS, toolchain, and +registry-software owners + +## 1. Executive summary and decision + +- **Keep V8 permanently for JavaScript.** JavaScript's `WebAssembly.*` APIs also + remain inside V8; there is no V8-to-Wasmtime memory bridge. +- **Add Wasmtime as a permanent standalone-WASM executor alongside V8-WASM.** + Wasmtime becomes the preferred backend after its parity, safety, and + performance gates close, but the V8-WASM executor remains a maintained, + selectable compatibility backend. +- **Do not create a crate or one giant Rust file initially.** Use a module tree + under `crates/execution/src/wasm/`; extract a crate only for a measured + dependency/build/composition benefit. +- **Do not rewrite filesystem, network, process, TTY, signal, or identity + semantics.** Most already live in the kernel/sidecar. Consolidate the pieces + still duplicated in the JavaScript runner through the prerequisite + [runtime-neutral executor refactor](./runtime-neutral-executors.md). +- **Keep the AgentOS-owned WASI/POSIX ABI.** Wasmtime does not require + `wasmtime-wasi`; link the existing Preview1 plus `host_*` functions to + AgentOS resources and install no ambient host capabilities. +- **Async imports require bounded copies, not a new architecture.** Decode and + copy guest input, release memory before awaiting shared I/O, then reacquire + and revalidate memory before writing results. +- **Current native V8-WASM does not materially depend on shared memory between + isolates.** Its `SharedArrayBuffer` use is local blocking coordination. + Wasmtime threads are a separate later project because the sysroot still needs + real pthread semantics and bounded thread-group lifecycle. +- **Snapshotting is limited initially to compiled in-memory Module reuse and + Wasmtime's eligible copy-on-write memory initialization.** Live instance + snapshot/fork and serialized AOT artifacts are out of scope. +- **No engine performance claim is justified yet.** The current ordinary warm + V8-WASM baseline is 11.2-20.0 MiB incremental sidecar high-water memory, but + cold compile, RSS/PSS, address-space reservation, async-stack cost, and + concurrency must be measured directly against the completed initial + implementation. + +AgentOS will add Wasmtime as a native executor for standalone WebAssembly +commands. V8 remains the permanent JavaScript executor, including JavaScript +code that uses `WebAssembly.Module`, `WebAssembly.Instance`, or the asynchronous +JavaScript WebAssembly APIs. The existing V8-hosted standalone-WASM runner also +remains available as a compatibility executor. There is no direct bridge +between the two engines: a standalone-WASM process is created under exactly one +selected backend, and both backends reach the same kernel-owned resources +through their adapters. + +Wasmtime becomes the preferred standalone-WASM backend only after conformance, +safety, and performance exit gates close. V8-WASM must remain selectable for +compatibility and diagnosis, must run the shared parity suite, and must not +retain private implementations of Linux semantics. The lockstep client and +protocol surface carries an optional sealed `wasmtime`/`v8` override; omission +uses the sidecar-owned default, so clients do not independently choose or drift +that default. + +The first implementation lives under `crates/execution/src/wasm/`. A separate +crate is not required for the initial implementation. Extraction is allowed +later only if it produces a measured build, dependency, fuzzing, or binary +composition benefit. + +AgentOS will continue to use its owned patched `wasm32-wasip1` sysroot and its +existing Preview1 plus `host_*` imports. Wasmtime is the core WebAssembly +engine; it does not become the owner of filesystem, network, process, terminal, +identity, permission, or resource semantics. + +Ahead-of-time compilation, serialized Wasmtime artifacts, components, and live +process snapshots are outside the first implementation. The initial executor +uses ordinary Wasmtime compilation and a bounded in-process compiled `Module` +cache. + +The kernel/executor boundary, signal ownership, shared host-operation services, +and runtime-neutral readiness contract are specified normatively in +[`runtime-neutral-executors.md`](./runtime-neutral-executors.md). This document +owns the Wasmtime engine, linker, guest-memory, limits, feature-profile, +performance, and preferred-backend decisions. Wasmtime-specific code must not +work around an unfinished prerequisite by adding another process-control or +host-service implementation. + +## 2. Outcomes + +The completed migration has these outcomes: + +1. Standalone WASM can execute without a V8 isolate or JavaScript WASI runner + when the Wasmtime backend is selected. +2. JavaScript execution and JavaScript's WebAssembly API remain on V8. +3. V8, Wasmtime, and Python adapters use the same kernel-owned filesystem, + descriptor, process, signal, terminal, identity, permission, and accounting + semantics. +4. External asynchronous I/O remains owned by the process-wide sidecar Tokio + runtime and its bounded capability/readiness machinery. +5. A Wasmtime host import performs ABI decoding and result encoding only. It + does not implement a second filesystem, socket table, process table, or + permission model. +6. Guest execution never runs on a Tokio runtime worker. +7. The initial Wasmtime executor does not depend on pthreads, shared WebAssembly + memory, AOT artifacts, Wizer, pooling allocation, or live snapshots. +8. V8-WASM remains a maintained compatibility executor over the same shared + services; neither backend is implemented in terms of the other. +9. Every limit is bounded by default and fails with a typed error naming the + limit and configuration field. + +## 3. Non-goals + +The initial Wasmtime executor does not: + +- replace V8 for JavaScript; +- route JavaScript `WebAssembly.*` calls into Wasmtime; +- create a V8-to-Wasmtime memory or function bridge; +- adopt ambient host filesystem, network, clock, or process access from + `wasmtime-wasi`; +- promise pthread, OpenMP, or general threaded-software compatibility; +- deserialize `.cwasm` or another native-code cache format; +- implement a general live `Store`/`Instance` snapshot or OS-style `fork()`; +- enable every proposal supported by the selected Wasmtime release; +- build a provisional Wasmtime spike before the shared executor/kernel + prerequisite is complete; +- move the process-wide Tokio reactor into `crates/kernel` or create another + Tokio runtime. + +## 4. Current architecture + +Standalone WASM is currently implemented as a JavaScript execution: + +```text +standalone WASM request + -> native sidecar process lifecycle + -> WasmExecutionEngine + -> JavascriptExecutionEngine + -> V8 isolate + -> wasm-runner.mjs + -> WebAssembly.Module / WebAssembly.Instance + -> JavaScript Preview1 and host_* adapters + -> sidecar RPC + -> AgentOS kernel, VFS, and native I/O owners +``` + +`WasmExecution` contains a `JavascriptExecution`, and `WasmExecutionEngine` +owns a `JavascriptExecutionEngine`. The JavaScript runner currently owns four +different kinds of code that must be distinguished during migration: + +1. **ABI marshalling**: reading pointers, iovecs, strings, arrays, and structures + from guest linear memory and writing results back. +2. **Transport adaptation**: translating imports into sidecar bridge calls and + translating sidecar errors into Preview1 errno values. +3. **Node-WASI compensation**: descriptor shadow maps, synthetic descriptors, + synthetic pipes, preopen collision handling, child polling, and local + `Atomics.wait` loops required by the V8/JavaScript host topology. +4. **Actual semantics**: any behavior that still exists only in the runner and + has not yet moved into the kernel or shared sidecar services. + +The first three categories do not justify a second kernel. Category four must +be inventoried operation by operation and either moved to the shared kernel or +explicitly retained as a narrow runtime adapter behavior. + +### 4.1 Current memory evidence + +The current V8 runner has a 2 GiB JavaScript heap ceiling because large WASM +module compilation exceeded the ordinary 128 MiB runner heap. This is a lazy +ceiling rather than immediate resident memory, but guest-driven compilation can +approach it. + +The committed local warm benchmark in +`packages/runtime-benchmarks/results/baseline-local.json` reports incremental +sidecar high-water memory above a prewarmed baseline for 19 current WASM lanes: + +- 11.2 MiB minimum; +- 14.6 MiB median across all measured lanes; +- 11.2-20.0 MiB for lanes that do not intentionally move large buffers; +- up to 56.3 MiB for the measured large stream-copy lane. + +These values are useful as the current warm V8-WASM acceptance baseline. They +are not a V8-versus-Wasmtime result: they include sidecar and adapter behavior, +exclude cold compilation through prewarming, and do not separate V8 isolate, +compiled code, linear memory, and kernel buffers. + +## 5. Target architecture + +This design extends the guest-adapter contract in +[`unified-sidecar-runtime.md`](./unified-sidecar-runtime.md): one sidecar +capability registry, one process-wide Tokio runtime, and no executor-owned +descriptor, poller, resource policy, or permission decision. + +```text +clients / ACP + | +native sidecar + | + +-- process lifecycle and runtime selection + +-- process-wide Tokio runtime and native I/O owners + +-- capability, readiness, and cancellation brokers + +-- AgentOS kernel + | +-- VFS and mounts + | +-- fd/open-description tables + | +-- pipes and PTYs + | +-- process table and signals + | +-- virtual sockets and DNS policy + | +-- identity, permissions, and resource accounting + | + +-- V8 JavaScript adapter + | +-- JavaScript and JavaScript WebAssembly API + | + +-- Wasmtime standalone-WASM adapter + +-- Engine and bounded Module cache + +-- Store per execution + +-- Preview1 and host_* Linker functions + +-- guest-memory ABI codec +``` + +There is no direct V8-to-Wasmtime bridge. A JavaScript process that spawns a +standalone WASM child uses the existing kernel process API. The sidecar runtime +selector starts the child under the requested standalone-WASM backend, and +stdio, signals, wait status, and exit events use the same cross-runtime process +model as every other child. + +## 6. Code organization + +The expected initial organization is: + +```text +crates/execution/src/wasm/ + mod.rs public facade and backend selection + types.rs runtime-neutral requests, events, limits, errors + backend.rs internal dual-backend selection contract + + v8_compat/ + mod.rs current implementation moved mostly intact + prewarm.rs + memory_limits.rs + + wasmtime/ + mod.rs execution and engine facade + engine.rs Config and bounded Engine profiles + store.rs per-execution host state + module.rs validation and module loading + cache.rs bounded in-memory Module cache + limits.rs memory, stack, CPU, and cancellation + lifecycle.rs start, exit, traps, signals, teardown + memory.rs checked guest-memory ABI primitives + + linker/ + mod.rs + preview1.rs + filesystem.rs + network.rs + process.rs + terminal.rs + identity.rs + + threads/ later milestone, absent from initial parity + mod.rs + group.rs + admission.rs +``` + +The current `crates/execution/src/wasm.rs` should initially move mostly intact +under `v8_compat/`. The Wasmtime migration must not be coupled to a wholesale +cleanup of the working compatibility implementation. + +Runtime-neutral host operations do not belong exclusively under `wasm/`. +Existing kernel and sidecar operations should be exposed through small +capability-oriented services used by both the V8 RPC adapter and Wasmtime +linker. Avoid one enormous `GuestHost` trait and avoid types named +`Javascript*` when Python and Wasmtime use the same operation. + +## 7. WASI and the owned AgentOS ABI + +### 7.1 Wasmtime does not force its WASI implementation + +The `wasmtime` engine and `wasmtime-wasi` are separate crates. A core Wasm +module imports functions by module and function name, and a Wasmtime `Linker` +supplies whichever definitions the embedder chooses. AgentOS can therefore +provide its existing `wasi_snapshot_preview1`, `host_process`, `host_net`, +`host_user`, `host_fs`, and `host_tty` modules without installing ambient +`wasmtime-wasi` host resources. + +There is no conflict between using Wasmtime as the engine and using the +AgentOS-owned WASI/POSIX ABI. The danger is only in accidentally linking a +second ambient implementation that opens host files or sockets outside the +AgentOS kernel. + +### 7.2 Initial integration decision + +The first implementation will: + +- treat the patched sysroot and `toolchain/crates/wasi-ext` imports as the + guest ABI source of truth; +- link exactly the Preview1 subset listed in + `crates/execution/assets/wasi-preview1-imports.json` plus the custom imports + required by built software; +- route every resource-bearing operation to an AgentOS kernel or sidecar + service; +- avoid constructing a default ambient `WasiCtx` with host preopens, host + sockets, or inherited host stdio; +- generate Preview1 signatures and value layouts from the pinned checked-in + WITX description, but do not adopt upstream resource ownership or policy; +- generate custom-import signatures and repetitive bindings from the one + checked-in AgentOS ABI manifest instead of manually duplicating signatures in + Rust and JavaScript. + +If an upstream helper cannot be backed by AgentOS descriptors without creating +parallel state or ambient authority, the Wasmtime linker will implement that +thin ABI function directly. + +## 8. Dependency on shared host services + +The required refactor lives in +[`runtime-neutral-executors.md`](./runtime-neutral-executors.md). The completed +function-level inventory, baseline, and locked Phase 0 decisions live in +[`wasmtime-phase-0.md`](./wasmtime-phase-0.md). Wasmtime is admitted only after +the runtime-neutral document's exit gates close for the entire currently +supported V8-WASM ABI and working-software surface. + +The Wasmtime adapter receives: + +- a generation-bound execution control cell registered with the kernel process; +- a kernel PID and exit-reporter capability; +- capability-sized filesystem, network, process, terminal, signal, identity, + clock, and entropy host services; +- a runtime-neutral coalesced wake handle and direct operation waiters; and +- typed limits, permissions, cancellation, and error mapping. + +The linker does not know about `ActiveProcess`, `V8SessionHandle`, Node-WASI +fd aliases, sidecar signal maps, native Tokio handles, or mutable `KernelVm` +ownership. It decodes the owned AgentOS ABI into bounded values and calls the +shared services. + +Engine-specific responsibilities remaining in this document are safe linear +memory access, async suspension, Wasmtime interruption, module validation and +caching, feature configuration, trap normalization, and execution teardown. +## 9. Async guest-memory contract + +Wasmtime async host imports cannot retain a borrowed guest-memory slice or a +`Caller`-derived view across an `.await`. This is not an architectural blocker; +it defines the adapter boundary. + +Every async import uses three phases: + +1. **Decode and prevalidate:** validate all pointers, lengths, iovec counts, and + output ranges; enforce byte/count limits; copy input strings, structures, + address data, and write payloads into bounded owned Rust values. +2. **Await shared operation:** call the kernel/sidecar service using owned values + and opaque process/capability identity. Retain no raw guest pointer, slice, + or store borrow. +3. **Reacquire and encode:** reacquire the Wasmtime memory through the Store, + validate output ranges again, and copy the bounded result back. + +Examples: + +- `fd_write` snapshots iovec metadata and payload bytes before awaiting the + kernel write. +- `fd_read` snapshots destination iovecs, awaits an owned result buffer, then + reacquires memory and scatters the bytes. +- `net_connect` copies the address before awaiting readiness. +- `recv`, `accept`, and DNS calls await owned results and only then write guest + memory. +- `proc_spawn` copies command, argv, env, and actions and prevalidates the pid + result pointer before performing the side effect. +- `waitpid` prevalidates status outputs before reaping a child. + +For the initial single-threaded executor, the suspended Store cannot execute +guest code concurrently and linear memory cannot shrink. Reacquisition is still +required because memory growth can relocate backing storage. With future shared +memory, another guest thread may mutate memory while an import is suspended; +input structures and destination addresses therefore remain snapshotted once +rather than reread after the await. + +This introduces an owned-buffer copy for asynchronous I/O. The current V8 path +already performs JavaScript and bridge copies, so the Wasmtime path may still +reduce total copying, but the benchmark must measure this rather than assume it. + +## 10. Runtime placement and scheduling + +The normative async/blocking ownership and waiter sequence are defined in +[`runtime-neutral-executors.md`](./runtime-neutral-executors.md#71-async-and-blocking-execution-contract). +Wasmtime does not introduce a scheduler exception to that contract. + +Wasmtime does not provide an execution thread pool. Guest execution occurs +synchronously while a Wasmtime future is polled. Therefore: + +- Wasmtime execution futures run on the bounded non-Tokio VM executor; +- async host operations use the one process-wide sidecar Tokio runtime; +- no Tokio worker synchronously enters Wasmtime guest code; +- no executor or VM creates another Tokio runtime; +- epoch/fuel yields bound uninterrupted guest work and provide cancellation + points; +- blocking host work uses the existing fixed, bounded blocking executor with + admission. + +The Wasmtime Store carries VM id, generation, process id, permission profile, +limit ledger, cancellation state, readiness sink, and access to the shared +host-operation services. Guest-controlled payloads do not supply authority. + +## 11. Safety and limits + +The initial executor must preserve or improve the existing controls: + +| Control | Initial Wasmtime behavior | +| --- | --- | +| Module bytes and parser work | Preserve the 256 MiB file cap and bounded import/memory/varuint parsing before compilation. | +| Linear memory | Preserve the 128 MiB default, validate declarations, enforce growth through Store limits, and account aggregate guest memory outside per-memory limits. | +| Stack | Use Wasmtime's stack cap through a bounded set of Engine profiles because stack configuration is Engine-wide while AgentOS configuration is per VM. | +| CPU and cancellation | Use epoch checks as the unbypassable interruption mechanism, but preserve the current active-CPU rather than wall-time policy: executor accounting tracks only guest-running intervals and refreshes the Store deadline after async waits. Use fuel only for an explicitly deterministic budget. | +| Wall time | Remain opt-in for interactive commands; use an outer cancellable deadline when configured. | +| Files, fds, pipes, PTYs, sockets, processes | Continue to enforce in the kernel and shared sidecar ledgers. | +| Output and queues | Preserve current bounded output, reactor, bridge, readiness, and completion limits. | +| Permissions | Omit prohibited imports at link time and repeat authorization at the kernel operation. | +| Errors | Return stable AgentOS/POSIX typed errors; do not expose engine error strings as API contracts. | + +The current `maxWasmFuel` field means milliseconds in the V8 runner, not fuel. +It must not silently change meaning. Phase 1 removes it lockstep and adds +`activeCpuTimeLimitMs`, optional `wallClockLimitMs`, and optional +`deterministicFuel` as three distinct fields. +The default runaway-guest safeguard is currently 30 seconds of active V8 CPU, +while an explicitly configured `maxWasmFuel` is an opt-in wall-clock timeout. +Wasmtime must preserve that distinction: time spent blocked on terminal, +network, filesystem, child, or timer waits cannot exhaust the default active +execution budget. + +## 12. Shared memory and threads + +Three mechanisms must remain distinct: + +1. The current runner creates small local `SharedArrayBuffer` objects so + `Atomics.wait` can block its own V8 execution thread without busy-spinning. +2. Legacy synchronous bridges can use shared buffers to coordinate a + JavaScript worker with a host thread. +3. WebAssembly threads use shared linear memory and atomic WASM instructions + across multiple executing agents. + +The native standalone runner does not rely on shared memory between V8 isolates +for filesystem, networking, process, or kernel state. Guest `worker_threads` +is an inert compatibility surface, not a source of real V8 worker isolates. +Removing the V8-hosted standalone runner therefore does not require sharing +memory between V8 and Wasmtime. + +Wasmtime shared memory is a later milestone. Runtime support alone is +insufficient because the current AgentOS sysroot links emulated single-thread +pthreads. Real pthread support requires a threaded sysroot, a bounded +thread-spawn ABI, real mutex/condvar/TLS behavior, group cancellation, and +per-VM plus process-wide thread admission. + +The first executor rejects modules that define or import shared memories and +does not expose a thread-spawn import, regardless of Wasmtime's compile-time +threads feature default. It must not use the experimental upstream +WASI-threads integration that can terminate the entire host process when one +guest thread traps. + +## 13. Compilation cache and snapshots + +The first implementation does not persist native compiled artifacts and does +not deserialize AOT files. The unsafe-artifact concern is therefore deferred, +not an initial blocker. + +The allowed initial cache is a bounded, process-memory cache of trusted +`Module` values keyed by module contents and Engine profile. It improves +repeat execution within one sidecar process but does not survive restart. +Wasmtime `Module` compilation is synchronous and complete at construction; +there is no later optimizing tier. `Module` clones are shallow and the compiled +code is shareable across threads, so this cache avoids recompilation without +copying the native code image. The implementation should also benchmark caching +`InstancePre` values, which can reuse import resolution and type checking when +all closed-over imports are Store-independent. + +Snapshot support is classified as follows: + +- **V8 JavaScript heap snapshot:** remains available to the JavaScript + executor; it is unrelated to a running standalone WASM process. +- **In-memory compiled Module reuse:** in scope for the first Wasmtime + executor. +- **Wasmtime serialized/AOT module:** explicitly deferred. +- **Copy-on-write module memory initialization:** may be benchmarked after the + basic executor works. Wasmtime's `memory_init_cow` is enabled by default and + can use Linux memory mappings or `memfd_create` for eligible modules whose + initial data has static, in-bounds offsets. This speeds memory initialization; + it is not a live guest snapshot and does not require serialized AOT input. +- **Wizer build-time preinitialization:** deferred and opt-in if later useful to + individual software packages. +- **Live Store/Instance snapshot or fork:** unsupported in the first design. + +Open files, sockets, processes, timers, permissions, host capabilities, and +threads are sidecar/kernel state and cannot be captured by merely copying WASM +linear memory and globals. + +## 14. Performance and memory validation + +No external benchmark is accepted as the AgentOS answer because host imports, +module sizes, V8 topology, kernel bridges, and cache configuration dominate the +comparison. The repository must measure both backends under the same sidecar, +kernel, command modules, release build, and hardware. + +There is no defensible numeric V8-versus-Wasmtime result yet. The existing +11.2-20.0 MiB ordinary warm-command range is the V8-WASM baseline, not an +engine comparison. The initial Wasmtime implementation and direct benchmark +are required before claiming a cold-start or resident-memory win. + +The benchmark records these phases independently: + +```text +sidecar and VM baseline +runtime/package projection +Engine lookup or creation +module read and validation +module compilation or in-memory cache lookup +Linker/import resolution +Store and async-stack allocation +instantiation and memory initialization +_start to first host call +first stdout byte +completion and teardown +``` + +The matrix includes: + +- current V8-WASM cold compile and warm compile-cache paths; +- Wasmtime cold compile and warm in-memory Module-cache paths; +- trivial, coreutils, shell, curl, sqlite, vim, and large-module commands; +- compute-heavy and host-call-heavy workloads; +- concurrency 1, 10, 50, 100, and 200; +- repeated-module and diverse-module workloads; +- success, denied permission, cancellation, and resource-limit paths. + +Memory measurements include process baseline, incremental RSS/PSS, peak RSS, +virtual-address reservation, committed linear-memory bytes, compiled-code +cache bytes, async stack bytes, kernel buffer bytes, page faults, and memory +retained after teardown. Large virtual reservations must not be reported as +resident memory. + +On a 64-bit host, current Wasmtime defaults reserve 4 GiB of virtual address +space plus guard regions for each 32-bit linear memory so generated code can +elide many explicit bounds checks. Reservation is not committed RSS. AgentOS's +128 MiB accessible-memory policy still requires a Store resource limiter and +aggregate accounting; it does not by itself reduce Wasmtime's virtual +reservation. The initial implementation must benchmark default reservation +against a smaller reservation because reducing it can add bounds checks or +memory-growth relocation. + +Wasmtime async execution also allocates a separate native stack used for stack +switching when an async host function suspends. Measure this per active Store at +the target concurrency; configure a bounded `async_stack_size` rather than +assuming the default is negligible. + +The pooling allocator is deferred. It can improve reuse and high-concurrency +instantiation, but requires fixed process-wide slot counts, can reserve roughly +one multi-gigabyte virtual-memory slot per admitted linear memory, and may +retain resident pages in warm slots. Start with on-demand allocation, establish +the workload and memory baseline, then evaluate pooling as an independent +optimization. + +The hypotheses to test are: + +- Wasmtime removes the per-execution V8 isolate and JavaScript runner heap; +- process-global V8 memory remains because JavaScript still uses V8; +- compiled Wasmtime Modules can be shared across executions in one process; +- Wasmtime may reserve more virtual address space for linear memories while + consuming less resident adapter memory; +- direct typed host calls reduce JavaScript/bridge overhead, but owned buffers + required across async waits still impose copying; +- threaded execution, when added, materially increases memory through one + Store/instance/native stack per admitted thread. + +Cutover requires no regression against the current warm V8-WASM memory and +latency baselines for representative commands, or an explicitly approved +tradeoff supported by measurements. + +## 15. Behavioral parity and errors + +AgentOS does not promise V8 error strings, Wasmtime error strings, or identical +compiler diagnostics. It does promise stable sidecar error categories and +Linux-compatible guest-visible behavior. + +The adapter normalizes: + +- malformed/unsupported module; +- missing or incompatible import; +- memory, table, and stack limit; +- CPU/fuel exhaustion; +- explicit termination; +- guest trap; +- host-operation errno; +- process exit and terminating signal; +- internal executor fault. + +Differential tests assert stdout, stderr, exit status, errno, signal behavior, +fd inheritance, permissions, and side effects. Floating-point and proposal +behavior follow the selected published AgentOS WASM feature profile rather than +whichever features an engine happens to enable by default. + +## 16. Delivery phases and JJ revision contract + +Each phase below lands as one distinct JJ revision, in order. A phase may be +developed through temporary local revisions, but those revisions are folded +before handoff so the review and landing stack has one revision per phase. Do +not mix Wasmtime implementation into the prerequisite refactor revision. Do +not collapse the prerequisite refactor and Wasmtime executor into one revision. + +This intentionally produces a small semantic stack instead of one revision per +capability family: + +1. specification and frozen baseline; +2. complete runtime-neutral kernel/executor refactor; +3. complete initial Wasmtime executor at current V8-WASM feature parity; and +4. performance validation and preferred-backend enablement. + +The later threaded-WASM project is not part of initial parity and begins in its +own revision after these four phases. + +The initial Wasmtime project is complete at the end of Phase 3: Wasmtime passes +the entire current V8-WASM ABI and working-software corpus, is production-ready, +and can be the preferred backend while V8-WASM remains a supported selection. +Phase 4 expands that completed executor with threads; it is not required to +claim parity with the current single-threaded V8-WASM surface. + +This section is the canonical implementation tracker. Check an item only in +the JJ revision that supplies its implementation and evidence. A phase summary +is checked only when every required item under that phase is checked and the +revision has been sealed: + +- [x] Phase 0: specification, inventory, baseline, and locked decisions. +- [ ] Phase 1: complete runtime-neutral kernel/executor prerequisite. +- [ ] Phase 2: production Wasmtime executor at current V8-WASM parity. +- [ ] Phase 3: performance decision, preferred-backend rollout, and initial + project completion. +- [ ] Phase 4: separately gated threaded-WASM roadmap completion. + +### Phase 0 revision: Specification, inventory, and baseline + +- [x] Land the architecture specifications without production runtime behavior + changes. +- [x] Inventory every Preview1 and `host_*` import, including aliases, versions, + permission tiers, memory direction, async behavior, limits, and parity + tests. +- [x] Identify the kernel/sidecar semantic owner and JavaScript-only behavior + for every import. +- [x] Record the current V8-WASM cold/warm latency and memory evidence on the + canonical machine. +- [x] Freeze the differential command, raw-ABI, and hostile-module corpus. +- [x] Lock the feature profile, code placement, ABI generation, CPU fields, + Engine/profile limits, Module-cache limits, release platforms, + performance thresholds, selector, and deferred features in + [`wasmtime-phase-0.md`](./wasmtime-phase-0.md). +- [x] Audit every module produced before the current canonical toolchain build + failure and record all live legacy imports. +- [x] Seal Phase 0 as one independently reviewable JJ revision. + +### Phase 1 revision: Complete the runtime-neutral executor prerequisite + +- [ ] Resolve the duplicate ownership symbols in the owned wasi-libc and make + `just tools-rebuild` produce the complete canonical command set. +- [ ] Add the narrow, generation-bound `ProcessRuntimeEndpoint`, durable + bounded/coalesced control state, and executor-to-kernel exit reporter. +- [ ] Register real runtime endpoints for every production V8, Python, + binding, and compatibility-WASM process; restrict `StubDriverProcess` to + tests and explicitly virtual processes. +- [ ] Introduce the runtime-neutral execution lifecycle, control handle, + bounded event types, and direct host-call reply handles without requiring + V8's owned backend object to be `Send`. +- [ ] Split executor-facing operations into typed filesystem, fd, network, + process, terminal, signal, identity, clock, and entropy capabilities; + reject a single mega-trait or executor switchboard. +- [ ] Preserve typed `{ code, message, details }` errors from the kernel through + the sidecar and adapters without string-to-errno reconstruction. +- [ ] Make the kernel process table the only owner of signal dispositions, + masks, pending state, stop/continue state, terminating state, and wait + events. +- [ ] Route external signals, `SIGPIPE`, `SIGCHLD`, PTY control signals, + process-group signals, cancellation, and termination through one bounded + delivery path. +- [ ] Add handler begin/end checkpoints, exec disposition reset, restart + behavior, and atomic temporary signal masks for `ppoll`. +- [ ] Replace every `V8SessionHandle` readiness target with a runtime-neutral, + bounded, coalesced execution wake handle. +- [ ] Make kernel VFS, fd tables, permission tier, preopens, descriptor rights, + rlimits, identity, umask, and mount policy the sole mutable filesystem and + descriptor authority. +- [ ] Route embedded V8 filesystem calls and module resolution through the + shared kernel-backed filesystem service. +- [ ] Delete mutable host-shadow inventories, bidirectional reconciliation, + and adapter-owned descriptor/socket state that duplicates kernel state; + retain host access only through explicit confined mounts and plugins. +- [ ] Move shared DNS, TCP, UDP, Unix socket, TLS, options, polling, and + readiness behavior behind the same sidecar reactor capabilities used by + all executors. +- [ ] Move spawn, exec, wait, fd actions, rlimits, locks, terminal/PTY, + credentials, account lookup, clocks, timers, entropy, and system identity + behind the shared operations. +- [ ] Enforce the async/blocking contract: one process Tokio runtime, guest + execution on the bounded non-Tokio executor, direct async waiters, and + bounded admission to fixed workers for unavoidable blocking work. +- [ ] Bound and account every request, reply, queue, waiter, decoded array, + retained buffer, blocking job, deadline, fd, socket, process, PTY, and + guest-visible output path; warn near limits and return named typed limit + errors. +- [ ] Fix process-aware DAC/sticky/read-only checks for link, remove, rename, + symlink, and unlink operations. +- [ ] Reject oversized writes, iovecs, subscriptions, pollfds, groups, argv, + env, paths, records, and result encodings before allocation, copying, or + side effects. +- [ ] Return `ERANGE` with required lengths for short account buffers and cap + supplementary groups before reading guest memory. +- [ ] Correct the `socketpair(kind, nonblock, cloexec)` ABI and implement the + existing bounded kernel `pty_open` path. +- [ ] Make fd xattrs and metadata operate on canonical open descriptions after + rename/unlink; remove ambient Node filesystem fallbacks and sentinel + errors. +- [ ] Replace terminal fd caches and libc shadow state with live kernel + terminal identity, termios, foreground-group, resize, and raw-mode state. +- [ ] Generate and check in the pinned Preview1 types/layouts and AgentOS custom + ABI manifest used by both adapters and import-audit tooling. +- [ ] Rebuild and inspect every owned-sysroot command; require zero undeclared + imports and explain or remove every compatibility alias/version. +- [ ] Pass the raw ABI, software, filesystem, process/signal, network, terminal, + identity/system, hostile-import, and resource-attack suites listed in + [`wasmtime-phase-0.md`](./wasmtime-phase-0.md#9-required-differential-proof-suites) + through V8-WASM. +- [ ] Pass all exit gates in + [`runtime-neutral-executors.md`](./runtime-neutral-executors.md#13-exit-gates) + for V8, Python, and compatibility WASM. +- [ ] Verify common host services contain no V8, JavaScript, Python, or + Wasmtime types and that the Phase 1 tree contains no Wasmtime executor. +- [ ] Seal all Phase 1 work as one independently reviewable JJ revision on top + of Phase 0. + +### Phase 2 revision: Add Wasmtime at full current feature parity + +- [ ] Pin one reviewed Wasmtime version and revalidate the referenced API + defaults, safety contracts, supported platforms, and Cargo feature set. +- [ ] Add the multi-file `crates/execution/src/wasm/wasmtime/` Engine, Store, + Module cache, Linker, ABI, memory, error, interruption, and execution + modules without creating a new crate or giant source file. +- [ ] Configure a bounded process-wide Engine registry keyed by the exact + AgentOS feature profile and stack cap; enforce the eight-profile default + limit and 80% warning. +- [ ] Prevalidate every module with the shared `wasmparser` profile so V8-WASM + and Wasmtime accept and reject the same features independently of engine + defaults. +- [ ] Add the bounded per-Engine 32-entry/256 MiB charged in-memory Module LRU, + exact cache keys, metrics, and eviction behavior; never deserialize + native artifacts. +- [ ] Build Store context from trusted VM generation, kernel PID, permission + profile, limit ledger, cancellation state, and shared host-service + handles only. +- [ ] Enforce linear-memory, table, instance, stack, aggregate memory, active + CPU, optional wall-clock, deterministic-fuel, and interruption limits + with typed errors. +- [ ] Implement epoch-based termination and active-CPU accounting that pauses + while an import is asynchronously waiting. +- [ ] Generate and link the owned Preview1 ABI, `wasi_unstable` alias, and every + `host_fs`, `host_net`, `host_process`, `host_tty`, and `host_user` + function/version over the Phase 1 shared operations. +- [ ] Do not create a `wasmtime-wasi` context or install ambient filesystem, + network, process, environment, clock, random, or stdio capabilities. +- [ ] Apply the three-phase async guest-memory contract to every waiting import: + validate/copy bounded input, await with no guest borrow, then reacquire + and revalidate output before commit. +- [ ] Prevalidate all output ranges before side effects and make fd/resource + allocation transactional when result encoding can fail. +- [ ] Run guest code only on the bounded non-Tokio VM executor while async host + work continues to use the one sidecar Tokio runtime and its direct waiters. +- [ ] Normalize validation failures, traps, stack exhaustion, cancellation, + timeout, fuel exhaustion, exit, terminating signal, errno, and internal + faults into stable AgentOS typed outcomes. +- [ ] Add the optional sealed `wasmtime`/`v8` protocol and client selector; + omission remains the sidecar-owned V8 default during Phase 2. +- [ ] Keep V8 permanently for JavaScript and keep V8-WASM as an independent, + maintained compatibility backend; add no V8-to-Wasmtime bridge. +- [ ] Keep shared memory, threads, memory64, multi-memory, relaxed SIMD, tail + calls, GC/function references, exceptions, components, custom page sizes, + AOT, pooling, Wizer, and live snapshots disabled for initial parity. +- [ ] Pass the complete differential ABI and working-software corpus—including + `ls`, `vim`, `grep`, `curl`, shell pipelines, sqlite, git, tar/gzip, and + metadata tools—against both standalone-WASM backends. +- [ ] Pass permission-tier, errno, malformed-module, hostile-import, + cancellation, signal, fd/process/TTY/network, and every limit-at/over-bound + test against Wasmtime with no ambient-host escape. +- [ ] Pass full Linux x86-64 conformance plus Linux arm64 and macOS x86-64/arm64 + build and smoke/conformance release gates; keep browser builds out of + scope. +- [ ] Verify teardown releases Store, waiter, fd, socket, process, memory, + compiled-code, and kernel reservations without cross-VM state retention. +- [ ] Seal the complete executor and parity/safety proof as one independently + reviewable Phase 2 JJ revision on top of Phase 1; do not land a partial + linker or spike. + +### Phase 3 revision: Measure and enable the preferred backend + +- [ ] Run release builds of both backends on the same canonical machine with + identical module bytes, host-service path, output capture, permissions, + limits, and cache state. +- [ ] Measure at least five independent fresh-cache processes with five samples + each, plus warm cache-hit runs, for trivial, coreutils, shell, curl, + sqlite, vim, large-module, compute-heavy, and host-call-heavy workloads. +- [ ] Run concurrency 1/10/50/100/200 with repeated-module and diverse-module + workloads, success, denial, cancellation, and resource-limit paths. +- [ ] Record phase timing for VM/package setup, Engine lookup, module read, + validation, compilation/cache, linking, Store/async stack, instantiation, + memory initialization, first host call, first output byte, completion, + and teardown. +- [ ] Record baseline/incremental/peak RSS and PSS, VIRT separately, committed + linear memory, compiled-code/cache bytes, async-stack bytes, kernel + buffers, page faults, and retained memory after teardown. +- [ ] Benchmark on-demand memory allocation and eligible copy-on-write module + memory initialization; do not enable pooling, AOT, Wizer, or live + snapshots in this phase. +- [ ] Tune only evidence-supported Engine, Module-cache, memory-reservation, + async-stack, and concurrency defaults while retaining all named bounds. +- [ ] Require zero correctness/safety regression, geometric-mean p50 regression + no worse than 10%, no individual p95 regression worse than 20%, throughput + regression no worse than 10%, and retained RSS/PSS regression no worse + than the greater of 10% or 4 MiB. +- [ ] If every preferred-backend threshold passes, make omission select + Wasmtime; otherwise keep omission on V8 while leaving Wasmtime explicitly + selectable and record the failed thresholds. +- [ ] Add operator metrics, warnings, explicit backend override, rollback + control, cache/profile-limit visibility, and stable error attribution. +- [ ] Keep V8-WASM selectable, supported, and on the shared parity suite; never + shadow-run side-effecting executions through both engines. +- [ ] Commit the raw benchmark results, selected defaults, threshold decision, + rollback criteria, and operator documentation with the implementation. +- [ ] Seal Phase 3 as one independently reviewable JJ revision on top of Phase + 2. Completion of this checkbox means the initial production Wasmtime + project is complete. + +### Phase 4 revision: Threading as a separate later project + +- [ ] Confirm Phase 3 is complete before enabling shared memory or threads. +- [ ] If threading cannot remain reviewable as one revision, approve a + replacement multi-revision threading specification before implementation; + do not silently fragment this phase. +- [ ] Rebuild the owned sysroot and libc for real pthread semantics instead of + the current emulated single-thread implementation. +- [ ] Add an explicit AgentOS thread-spawn ABI and enable the exact shared-memory + and atomic WASM feature profile only for configured threaded executions. +- [ ] Implement bounded per-VM and process-wide thread admission, one accounted + Store/instance/native stack per admitted guest thread where required, and + transactional failure when capacity is unavailable. +- [ ] Implement pthread mutex, condition variable, TLS, join/detach, exit, + cancellation, robust teardown, and required libc behavior. +- [ ] Move masks and in-progress signal delivery to per-thread kernel records + while retaining process-wide dispositions and correct process/thread + signal selection. +- [ ] Define shared-memory ownership, growth, atomic wait/notify, limits, + retained-memory accounting, and cross-thread guest-memory mutation rules. +- [ ] Make trap, exit, cancellation, timeout, and VM teardown terminate and reap + the complete thread group without terminating or corrupting the sidecar. +- [ ] Pass pthread/libc, signal, shared-memory, race, resource-exhaustion, + teardown, isolation, and high-concurrency memory tests for hostile VMs. +- [ ] Re-run the full single-thread parity and performance gates to prove the + threaded profile does not regress ordinary V8-WASM or Wasmtime execution. +- [ ] Keep browser support, AOT artifacts, Wizer, components, pooling, and live + process snapshot/fork outside the threading milestone unless separately + specified and approved. +- [ ] Seal the approved threading implementation and conformance evidence in + its own JJ revision or approved replacement revision stack. Completion of + this checkbox means the full roadmap in this document is complete. + +## 17. Principal risks + +| Risk | Severity | Required mitigation | +| --- | --- | --- | +| Porting JavaScript compatibility state as a second kernel | Critical | Inventory each operation; move semantics to the kernel/shared service; keep linker code to ABI marshalling. | +| Accidentally installing ambient Wasmtime WASI resources | Critical | Link only AgentOS-owned imports and test host-escape denial. | +| Raw imports bypass the filesystem permission tier | Critical | Put the effective tier and descriptor rights in kernel process state; test malicious direct imports. | +| Retaining guest-memory borrows across await | Critical | Enforce the three-phase owned-value memory contract and focused tests. | +| Side effect succeeds before an invalid result pointer is detected | High | Prevalidate all result ranges before spawn/reap/write-like side effects and specify commit ordering. | +| Readiness remains tied to `V8SessionHandle` | High | Add runtime-neutral bounded execution readiness sinks. | +| Separate Wasmtime and kernel descriptor namespaces | High | Use the kernel fd table directly and delete Node-WASI shadow descriptor machinery. | +| Signal masks, dispositions, and pending state remain split across three layers | Critical | Consolidate a bounded runtime-neutral signal broker before Wasmtime admission; test `SIGPIPE`, `SIGCHLD`, `ppoll`, and stop/continue. | +| Ambient clocks, randomness, hostname, procfs, or devfs leak host state | Critical | Link owned providers only and add hostile raw-import escape tests. | +| Runner-local identity and rlimits become Wasmtime Store state | High | Move mutable process state into the kernel and keep only process identity handles in the Store. | +| Kernel errno is converted to text and reparsed by adapters | High | Carry typed error code/message/details across the shared boundary. | +| Engine-specific behavior leaks into public errors | High | Normalize typed executor errors and test guest-visible errno/status. | +| `maxWasmFuel` silently changes meaning | High | Phase 0 replaces it lockstep with active CPU time, optional wall-clock time, and optional deterministic fuel fields. | +| Claimed memory win is based on heap ceilings or VIRT | High | Measure RSS/PSS, peaks, cache retention, and virtual reservations separately. | +| Default 4 GiB-per-memory virtual reservations exhaust address space under high concurrency | High | Account and cap memories process-wide; benchmark reservation settings; defer pooling. | +| Async Wasmtime stacks create unaccounted per-execution RSS | High | Bound `async_stack_size`, include it in admission, and measure at concurrency gates. | +| Wasmtime compile/binary cost affects all builds | Medium | Measure; use Cargo feature composition or later crate extraction only if justified. | +| Permanent dual backends drift | High | Run the same owned-ABI and software parity corpus against both in CI; keep all Linux semantics below the adapters; version one explicit engine feature profile. | +| Thread support is mistaken for pthread compatibility | Critical | Keep threads out of initial scope and require a separate sysroot/runtime milestone. | +| Fake libc terminal state diverges from kernel PTYs | High | Replace process-global termios/pgid/winsize stubs with live typed kernel operations. | + +## 18. Resolved implementation decisions + +Phase 0 resolved the feature profile, code placement, ABI generation, CPU limit +fields, Engine profile bound, Module cache limits, release platforms, +performance thresholds, backend selector, and deferred threading/snapshot +scope. The decisions in +[`wasmtime-phase-0.md`](./wasmtime-phase-0.md#11-locked-implementation-decisions) +are normative. The initial implementation has no unresolved architectural +question; new evidence changes a decision through a specification revision. + +## Appendix A: Research and inventory sources + +The subsystem inventory was performed against these current implementation +owners: + +- standalone WASM adapter and runner: + `crates/execution/src/wasm.rs`, + `crates/execution/assets/runners/wasm-runner.mjs`, and + `crates/execution/assets/runners/wasi-module.js`; +- ABI declarations and libc behavior: + `toolchain/crates/wasi-ext/src/lib.rs`, `toolchain/std-patches/`, and + `toolchain/std-patches/wasi-libc-overrides/`; +- kernel semantics: `crates/kernel/src/kernel.rs`, `process_table.rs`, + `pty.rs`, `user.rs`, `device_layer.rs`, and socket/VFS modules; +- runtime lifecycle and external I/O: + `crates/native-sidecar/src/execution/`, `state.rs`, `filesystem.rs`, and + `service.rs`; and +- current performance evidence: + `packages/runtime-benchmarks/results/baseline-local.json`. + +The Wasmtime API conclusions were checked against the current upstream API: + +- [`wasmtime::Module`](https://docs.wasmtime.dev/api/wasmtime/struct.Module.html) + for synchronous compilation, cheap cloning, thread-safe sharing, and unsafe + serialized-artifact loading; +- [`wasmtime::Config`](https://docs.wasmtime.dev/api/wasmtime/struct.Config.html) + for proposal flags, epochs/fuel, async stack configuration, memory + reservation, and copy-on-write initialization; +- [`wasmtime::Memory`](https://docs.wasmtime.dev/api/wasmtime/struct.Memory.html) + for borrow, relocation, and shared-memory safety rules; +- [`wasmtime::Linker`](https://docs.wasmtime.dev/api/wasmtime/struct.Linker.html) + and [`InstancePre`](https://docs.wasmtime.dev/api/wasmtime/struct.InstancePre.html) + for Store-independent host functions and reusable import resolution; +- [`StoreLimitsBuilder`](https://docs.wasmtime.dev/api/wasmtime/struct.StoreLimitsBuilder.html) + for per-memory and per-Store limits; +- [Wasmtime async execution](https://docs.wasmtime.dev/api/wasmtime/#asynchronous-wasm) + for embedder-owned scheduling and native stack switching; +- [`wasmtime-wasi` Preview1 linker integration](https://docs.wasmtime.dev/api/wasmtime_wasi/p1/fn.add_to_linker_async.html) + for the optional `WasiP1Ctx` ownership model that AgentOS is not adopting; +- [`PoolingAllocationConfig`](https://docs.wasmtime.dev/api/wasmtime/struct.PoolingAllocationConfig.html) + for address-space reservation and resident warm-slot tradeoffs; and +- the upstream + [`wasmtime-wasi-threads` implementation](https://docs.wasmtime.dev/api/src/wasmtime_wasi_threads/lib.rs.html) + for the process-exit behavior that is unsuitable for an in-process + multi-tenant embedding. + +These links describe the current upstream release at the time of writing. The +implementation must pin an exact Wasmtime version and revalidate all referenced +defaults and safety contracts during dependency review. diff --git a/docs/design/wasmtime-phase-0.md b/docs/design/wasmtime-phase-0.md new file mode 100644 index 0000000000..40bbd08cb4 --- /dev/null +++ b/docs/design/wasmtime-phase-0.md @@ -0,0 +1,394 @@ +# Wasmtime Phase 0: ABI Inventory, Baseline, and Locked Decisions + +Status: complete; normative input to the runtime-neutral refactor and Wasmtime +executor revisions + +Audience: AgentOS kernel, sidecar runtime, execution, VFS, toolchain, test, and +registry-software owners + +## 1. Purpose and completion statement + +This document closes Phase 0 of the Wasmtime executor project. It records: + +- every import exposed by the current standalone V8-WASM runner; +- the semantic owner, authority, bounds, waiting behavior, guest-memory + direction, shared operation, and parity proof for each import; +- compatibility aliases, hard stubs, and unsupported imports; +- the current V8-WASM cold/warm latency and memory baseline; and +- the implementation decisions that must not be reopened implicitly during the + runtime-neutral refactor. + +The inventory was checked against: + +- `crates/execution/assets/wasi-preview1-imports.json`; +- `crates/execution/assets/runners/wasm-runner.mjs`; +- `crates/execution/assets/runners/wasi-module.js`; +- `toolchain/crates/wasi-ext/src/lib.rs`; +- `toolchain/std-patches/` and `toolchain/std-patches/wasi-libc-overrides/`; +- kernel process, VFS, fd, socket, PTY, identity, and resource-accounting APIs; + and +- native-sidecar execution, filesystem, and network services. + +No import below permits Wasmtime to use ambient host resources. `wasmtime-wasi` +does not own the context. Both engines call the same AgentOS host services. + +## 2. Inventory notation and cross-cutting contract + +The tables use these abbreviations: + +- **C**: canonical current ABI; implement for both engines. +- **A**: active compatibility alias/version; keep the linker function but map it + to the canonical operation. +- **S**: current hard stub; Phase 1 either implements it in the shared owner or + removes it only after a rebuilt-artifact import audit proves it unused. +- **U**: intentionally unsupported; validation/linking fails deterministically. +- **all/full/limited**: permission tier availability. Authority is checked again + in the shared operation even when an import is omitted at link time. +- **sync**: bounded non-waiting semantic operation. +- **async**: readiness, timer, network, lock, FIFO, PTY, child, or cancellation + wait. The V8 adapter may expose it through its existing synchronous bridge; + the shared operation itself is asynchronous. +- **in/out**: bytes copied from/to guest memory. All ranges and aggregate sizes + are validated before a side effect. No guest-memory borrow crosses an await. + +Common default limits are `maxOpenFds=256`, `maxPipes=128`, `maxPtys=128`, +`maxSockets=256`, `maxConnections=256`, `maxSocketBufferedBytes=4 MiB`, +`maxSocketDatagramQueueLen=1024`, `maxPreadBytes=64 MiB`, +`maxFdWriteBytes=64 MiB`, `maxProcessArgvBytes=1 MiB`, +`maxProcessEnvBytes=1 MiB`, `maxReaddirEntries=4096`, a 4096-byte path, 40 +symlink traversals, 64 supplementary groups, 255-byte xattr names, and 64 KiB +xattr values. Every adapter additionally caps one decoded iovec/subscription/fd +array at 4096 entries and its encoded descriptor bytes at 1 MiB before copying. +These two adapter caps become named configurable limits if real software reaches +them; they never silently become unbounded. + +Multi-output imports prevalidate every output range before committing a side +effect. Operations that allocate guest fds reserve fd capacity before the host +operation and roll back completely if result encoding fails. Errors remain +typed errno/status values across the shared boundary. + +## 3. Preview1 inventory + +The runner exposes the same object as `wasi_snapshot_preview1` and +`wasi_unstable`; the latter is an ABI alias, not another implementation. + +| Imports | Status; mode; memory | Shared semantic owner and operation | Authority, bounds, and parity proof | +| --- | --- | --- | --- | +| `args_sizes_get`, `args_get` | C; sync; out | `GuestProcessHost::image(pid).argv` from the committed kernel process image | 1 MiB argv cap; prevalidate pointer table and strings; direct-WAT exact bytes/order/OOB plus spawn/exec corpus | +| `environ_sizes_get`, `environ_get` | C; sync; out | `GuestProcessHost::image(pid).env`, after the one sidecar-owned guest filtering/default pass | 1 MiB env cap; deterministic ordering; internal-key exclusion and exec replacement tests | +| `clock_time_get`, `clock_res_get` | C; sync; out | `GuestClockHost`; frozen execution realtime plus monotonic/process/thread clocks | One u64 output; exact realtime/resolution, monotonicity, invalid-id and OOB tests | +| `random_get` | C; sync/chunked; out | `GuestEntropyHost`, same provider as kernel `/dev/urandom` | Validate full guest range first; fill in at most 64 KiB host chunks with a 16 MiB per-call cap; zero/OOB/limit/provider-failure tests | +| `fd_close` | C; sync; none | `GuestFdHost::close` on the kernel fd table | fd ownership; reuse/refcount/EBADF parity | +| `fd_datasync`, `fd_sync` | C; sync; none | `GuestFdHost::sync(DataOnly/All)` | writable/open fd; synchronous-VFS commit and exact errno tests | +| `fd_fdstat_get` | C; sync; out | `GuestFdHost::status -> FdStatus`, then Preview1 encoding | Kernel-derived type/flags/rights; exact byte-layout tests | +| `fd_fdstat_set_flags` | C; sync; scalar in | `GuestFdHost::set_status_flags` | append/nonblock only; shared-open-description parity | +| `fd_filestat_get` | C; sync; out | `GuestMetadataHost::stat_fd` | Exact dev/ino/type/nlink/size/time encoding; open-unlinked fd test | +| `fd_filestat_set_size` | C; sync; scalar in | `GuestFdHost::set_len` | write right, read-only mount, filesystem quota; position and rollback tests | +| `fd_filestat_set_times` | C compatibility export; sync; scalar in | `GuestMetadataHost::set_times_fd` | NOW/OMIT validation; open-unlinked fd and DAC tests | +| `fd_pread` | C; sync; in iovecs/out bytes+count | `GuestFdHost::read_at` | 4096 iovecs/1 MiB descriptors/64 MiB payload; offset unchanged and malformed-iovec tests | +| `fd_pwrite` | C; sync; in iovecs/out count | `GuestFdHost::write_at` | Pre-copy 64 MiB cap, quota and read-only checks; vector ordering/rollback tests | +| `fd_read` | C; async for pipe/PTY/socket/FIFO; in iovecs/out bytes+count | `GuestFdHost::read` | Same iovec caps, configured blocking deadline, signal/cancel; EOF/EAGAIN/EINTR/backpressure tests | +| `fd_write` | C; async for backpressured pipe/socket/PTY; in iovecs/out count | `GuestFdHost::write` or sidecar `write_stdio` for host-visible stdio | Pre-copy 64 MiB cap; partial write, EPIPE/SIGPIPE, ordering and backpressure tests | +| `fd_readdir` | C; sync; out dirents+count | `GuestFdHost::read_dir` | 4096 entries/call and caller buffer; cookie/partial-record/Linux dot ordering tests | +| `fd_seek`, `fd_tell` | C; sync; out offset | `GuestFdHost::seek/tell` | Checked signed offsets; ESPIPE/overflow/positioned-I/O tests | +| `fd_prestat_get`, `fd_prestat_dir_name` | C; sync; out | Adapter reads immutable process preopen capability descriptors | Hidden capabilities never enter guest fd namespace; list/name/short-buffer tests | +| `fd_allocate` | C compatibility export; sync; scalar in | `GuestExtentHost::allocate` | write right and filesystem quota; sparse/overflow/rollback tests | +| `fd_renumber` | C compatibility export; sync; none | `GuestFdHost::renumber` | fd and rlimit bounds; source consumed, target closed atomically | +| `sock_shutdown` | C compatibility export; sync command; none | `GuestNetworkHost::shutdown(fd, how)` for canonical socket descriptions | Validate direction; socketpair half-close/EOF, bad fd, unsupported direction and host-net parity tests | +| `path_open` | C; async only for blocking FIFO rendezvous; path in/fd out | `GuestPathHost::open_at` | directory-fd rights, DAC, symlink, permission tier, read-only mount, fd/quota limits; openat/FIFO/escape tests | +| `path_create_directory` | C; sync; path in | `GuestPathHost::mkdir_at` | parent DAC, umask, read-only/quota; mkdirat parity | +| `path_filestat_get` | C; sync; path in/stat out | `GuestMetadataHost::stat_at` | traversal/follow rights; symlink and exact metadata tests | +| `path_filestat_set_times` | C compatibility export; sync; path in | `GuestMetadataHost::set_times_at` | DAC/read-only/NOW/OMIT/follow tests | +| `path_link` | C; sync; two paths in | `GuestPathHost::link_at` using process-aware kernel checks | Phase 1 fixes current generic-kernel DAC bypass; hardlink/sticky/read-only tests | +| `path_readlink` | C; sync; path in/target+count out | `GuestPathHost::readlink_at` | traversal/output bound; truncation/proc-fd/OOB tests | +| `path_remove_directory` | C; sync; path in | `GuestPathHost::remove_dir_at` | Phase 1 replaces current generic `remove_dir`; DAC/sticky/read-only tests | +| `path_rename` | C; sync; two paths in | `GuestPathHost::rename_at` | Phase 1 replaces current generic `rename`; atomic/DAC/sticky/cross-mount tests | +| `path_symlink` | C; sync; target/path in | `GuestPathHost::symlink_at` | Phase 1 replaces current generic `symlink`; parent DAC/read-only/dangling tests | +| `path_unlink_file` | C; sync; path in | `GuestPathHost::unlink_at` | Phase 1 replaces current generic `remove_file`; sticky/open-description tests | +| `poll_oneoff` | C; async; subscriptions in/events+count out | `GuestProcessHost::poll` over kernel readiness, shared clock, signal and cancel broker | 4096 subscriptions/1 MiB descriptors; fd+clock, HUP, timeout, signal race and lost-wakeup tests | +| `proc_exit` | C; terminal control flow; none | `GuestProcessHost::exit(status) -> !`; sidecar finalizes kernel process once | Normal exit distinct from trap/signal; 0/42/137, output-drain and child-wait tests | +| `sched_yield` | C; async yield/checkpoint; none | VM executor yield plus cancellation/signal checkpoint | Fairness, STOP/terminate observation and no-busy-spin tests | +| `fd_advise`, `fd_fdstat_set_rights` | U | No current runner function | Direct import must fail with stable unsupported-import validation error | + +## 4. `host_process` inventory + +Full permission links the whole module. Read-only/read-write link only +`fd_dup_min`, `fd_flock`, `fd_getfd`, `fd_setfd`, `fd_record_lock`, +`proc_getrlimit`, `proc_setrlimit`, `proc_umask`, and `umask`. Isolated links no +`host_process`. Kernel authority remains mandatory in every case. + +| Imports | Status; mode; memory | Canonical operation | Bounds, required correction, and parity proof | +| --- | --- | --- | --- | +| `proc_spawn`, `proc_spawn_v2`, `proc_spawn_v3`, `proc_spawn_v4` | A/A/A/C; sync setup with async lifecycle; in/out | Decode all versions to one `GuestProcessHost::spawn(SpawnRequest)` | 256 processes, 1 MiB argv/env, 4096 actions/1 MiB action bytes, fd limits; legacy artifact plus full posix_spawn actions/masks/groups tests | +| `proc_exec`, `proc_fexec` | C; terminal control flow; in | `GuestProcessHost::prepare_exec -> ExecPlan`; validate/compile before atomic kernel commit, then replace Store outside import | 256 MiB module, argv/env/fd limits; failed-exec atomicity, CLOEXEC, fd-offset and deleted-fd tests | +| `proc_waitpid`, `proc_waitpid_v2`, `proc_waitpid_v3` | A/A/C; async; out | `GuestProcessHost::wait(selector, flags) -> WaitEvent`, with three encoders | Child-count bound; exit-vs-signal/core/stopped/continued/selectors/EINTR tests | +| `proc_kill` | C; sync; none | `GuestSignalHost::send` through kernel signal broker | Signals 0..64, standard pending coalescing; self/child/group/permission/default/caught tests | +| `proc_getpid`, `proc_getppid` | C; sync; out | Kernel process identity accessors | Spawn/exec/reparent stability tests; no environment-derived PID state | +| `proc_getrlimit`, `proc_setrlimit` | C; sync; out/none | Kernel-owned `GuestProcessHost::get/set_rlimit` | Initial `RLIMIT_NOFILE`; lowering/inheritance/EMFILE/EPERM tests; no runner shadow | +| `proc_umask`, `umask` | C/A; sync; out | Kernel `GuestProcessHost::set/query_umask` | 0777 mask; create/mkdir/spawn/exec/query tests | +| `proc_itimer_real` | C; async delivery; out | Shared timer service plus `GuestSignalHost` SIGALRM path | One bounded timer/process; arm/disarm/interval/blocked/coalesced/exec tests | +| `proc_getpgid`, `proc_setpgid` | C; sync; out/none | Kernel process-group operations | Session/leader/child/cross-group/ESRCH tests | +| `fd_pipe` | C; sync allocation, async I/O; out | Kernel `GuestFdHost::pipe` | Pipe/fd limits; EOF/refcounts/EPIPE/SIGPIPE/nonblock/inheritance tests | +| `fd_dup`, `fd_dup2`, `fd_dup_min` | C; sync; out/none | Kernel `GuestFdHost::dup/dup_to/dup_min` | One fd namespace and RLIMIT; shared offset, target replacement, CLOEXEC and EMFILE tests | +| `fd_getfd`, `fd_setfd` | C; sync; out/none | Kernel descriptor flags | Supported bits and exact exec-close tests | +| `fd_flock`, `fd_record_lock` | C; async when waiting; out for GETLK | Kernel lock manager with durable waiter and cancellation guard | Lock/waiter tables bounded from fd limit and blocking deadline; contention/EINTR/deadlock/cleanup tests | +| `proc_closefrom` | C; sync; none | Kernel fd table `closefrom`; private preopens are not guest fds | Sparse/high fds, stdio, resource/lock release tests | +| `fd_socketpair` | C with Phase 1 ABI fix; sync; out | Kernel `GuestProcessHost::socketpair(kind, flags)` | Current Rust wrapper mistakenly treats `(kind, nonblock, cloexec)` as `(domain,type,protocol)`; fix and test stream/datagram/seqpacket/flags/limits | +| `fd_sendmsg_rights`, `fd_recvmsg_rights` | C; send sync/async-capable, recv async; in/out | Kernel Unix-socket SCM_RIGHTS operations on canonical fd descriptions | 253 rights and 64 MiB payload caps; atomic EBADF/EMFILE rollback, PEEK/WAITALL/CLOEXEC/EINTR tests | +| `sleep_ms` | A; async; none | Shared clock/timer wait raced with signal/cancel | Deadline bound; zero/duration/EINTR/restart/frozen-realtime tests | +| `pty_open` | S, but active library surface; sync allocation; out | Phase 1 wires existing kernel/sidecar `open_pty` | 128 PTYs and fd limits; master/slave/isatty/spawn/resize/SIGWINCH tests | +| `proc_sigaction` | C; sync; none | Kernel-owned dispositions/masks/flags; guest handler pointer remains adapter state | KILL/STOP rejection, IGN/DFL/user, NODEFER/RESETHAND/RESTART/exec tests | +| `proc_signal_mask_v2` | C; sync; out | Kernel signal broker `update_mask` | KILL/STOP filtering, pending-on-unblock and future per-thread tests | +| `proc_ppoll_v1` | C; async; in/out | Atomic temporary-mask registration plus shared poll/signal/timer broker | Same 4096/1 MiB poll caps; signal/readiness ordering, restore and lost-wakeup tests | + +## 5. `host_net` inventory + +`host_net` is linked only for full permission. The current runner maintains a +second host-net fd map; Phase 1 removes it and places network descriptions in +the canonical kernel fd/resource namespace. The sidecar's one Tokio runtime +continues to own external DNS and socket I/O. + +| Imports | Status; mode; memory | Canonical operation | Authority, bounds, and parity proof | +| --- | --- | --- | --- | +| `net_socket` | C; sync allocation; out fd | `GuestNetworkHost::socket` | Network policy, socket/fd limits; domain/type/protocol/permission tests | +| `net_set_nonblock` | C; sync; none | `GuestFdHost::set_status_flags` | Canonical open description; dup/fcntl/nonblock tests | +| `net_connect` | C; async; address in | `GuestNetworkHost::connect` on sidecar reactor | Network policy, connection/reactor/deadline limits; numeric/DNS/Unix/EINPROGRESS/cancel tests | +| `net_getaddrinfo` | C; async; name/service in, addresses+length out | `GuestNetworkHost::resolve_addresses` | Network/DNS policy, 4096-byte name/service and 256-result/64 KiB encoded-result caps; family/order/error tests | +| `net_dns_query_rr_v1` | C; async; query in/records out | `GuestNetworkHost::query_dns` | Same input/result bounds; A/AAAA/PTR/SSHFP, denied target, truncation and timeout tests | +| `net_bind` | C; async-capable sidecar operation; address in | `GuestNetworkHost::bind` | Listen policy, address/path ownership, reactor deadline; TCP/UDP/Unix/abstract/DAC tests | +| `net_listen` | C; sync command; none | `GuestNetworkHost::listen` | Backlog clamped to bounded accept capacity; state/error tests | +| `net_accept` | C; async; fd/address out | `GuestNetworkHost::accept` | Reserve connection+fd before wait; accept quantum/backlog/deadline; blocking/nonblock/cancel/rollback tests | +| `net_validate_socket`, `net_validate_accept` | A preflight helpers; sync; none | Fold into transactional socket/accept validation; keep aliases for existing libc | No state consumption; bad fd/type/state tests | +| `net_getsockname`, `net_getpeername` | C; sync snapshot; address out | `GuestNetworkHost::local/peer_address` | Caller capacity and 64 KiB response cap; IPv4/IPv6/Unix/truncation tests | +| `net_send` | C; async on backpressure; payload in/count out | `GuestNetworkHost::send` | Pre-copy at most 64 MiB and reactor byte quantum; partial/nonblock/EPIPE/cancel tests | +| `net_recv` | C; async; capacity in/data+count out | `GuestNetworkHost::recv` | Read at most min(caller, 64 KiB quantum) per completion; EOF/PEEK/WAITALL/nonblock tests | +| `net_sendto` | C; async; payload+address in/count out | `GuestNetworkHost::send_to` | UDP max 64 KiB, policy and datagram quotas; oversize/atomic-address tests | +| `net_recvfrom` | C; async; capacities in/data+address out | `GuestNetworkHost::recv_from` | UDP 64 KiB, queue/buffer limits; truncation/source/nonblock/cancel tests | +| `net_setsockopt` | C; sync command; option bytes in | `GuestNetworkHost::set_option` | Option payload capped at 64 KiB; exact supported level/name/type and timeout tests | +| `net_getsockopt` | C; sync snapshot; option bytes out | `GuestNetworkHost::get_option` | Caller/64 KiB cap; error/length/value parity | +| `net_poll` | C; async; pollfds in/out+ready count | Shared readiness broker, not a network-private loop | 4096 fds/1 MiB descriptors, bounded deadline; mixed fd, duplicate, signal/cancel/HUP tests | +| `net_close` | A explicit close; async-capable teardown; none | `GuestFdHost::close` | Idempotence is not promised; resource-release and waiter-cancel tests | +| `net_tls_connect` | C; async handshake; hostname in | `GuestNetworkHost::upgrade_tls` | TLS buffer/reactor/deadline limits, 4096-byte hostname, permission/cert/SNI/cancel tests | + +## 6. `host_user`, `host_tty`, and remaining system services + +Both modules are linked at every permission tier. The kernel process identity +and live fd/PTY state remain authoritative. + +| Imports | Status; mode; memory | Canonical operation | Bounds, correction, and parity proof | +| --- | --- | --- | --- | +| `getuid`, `getgid`, `geteuid`, `getegid` | C; sync; out u32 | `GuestIdentityHost::identity` | Configured root/nonroot, child/exec, permission-tier and OOB tests | +| `getresuid`, `getresgid` | C; sync; three u32 out | Same typed identity snapshot | Prevalidate all outputs; real/effective/saved transition tests | +| `setuid`, `seteuid`, `setreuid`, `setresuid`, `setgid`, `setegid`, `setregid`, `setresgid` | C; sync; scalar in | Kernel Linux credential-transition operations | `u32::MAX` means unchanged where applicable; root/drop/restore/EPERM/inheritance tests | +| `getgroups` | C; sync; groups+count out | `GuestIdentityHost::supplementary_groups` | Maximum 64; sizing/exact/short/OOB/order tests | +| `setgroups` | C; sync; groups in | Kernel group mutation | Phase 1 rejects count >64 before reading; root/EPERM/dedup/65-entry tests | +| `getpwuid`, `getpwnam`, `getpwent` | C; sync; optional name in/record+length out | `GuestIdentityHost` over kernel `UserManager` | 4096-byte record/name and 256 enumeration cap; Phase 1 returns `ERANGE` rather than silent truncation; known/unknown/iteration/OOB tests | +| `getgrgid`, `getgrnam`, `getgrent` | C; sync; optional name in/record+length out | Same kernel account database | Same bounds; member/enumeration/ERANGE tests | +| `host_user.isatty` | A duplicate ABI; sync; bool out | `GuestTerminalHost::is_terminal(fd)` live kernel lookup | Remove fd<=2/cache restriction; duplicated/replaced/closed/pipe/master/slave tests | +| `host_tty.read` | A active crossterm ABI; async; bytes out | `GuestFdHost::read(fd=0, deadline)` | 64 KiB; legacy zero conflates EOF/timeout; byte/EOF/timeout/signal/OOB tests | +| `host_tty.isatty` | A; sync; no memory | Same live terminal lookup | Same fd identity tests; no runner cache | +| `host_tty.get_size` | A; sync; two u16 out | `GuestTerminalHost::window_size` | Prevalidate both outputs; resize/ENOTTY/OOB tests | +| `host_tty.set_raw_mode` | A; sync; none | Sidecar `GuestTerminalHost::set_raw_mode`, including generation lease bookkeeping | Enable/disable/nesting/exit restore/background/ENOTTY tests | + +Phase 1 also replaces libc's process-global shadow termios, fixed foreground +process group, no-op `tcsetpgrp`, and missing resize path with live +`GuestTerminalHost` operations. Static libc passwd/group enumeration state is +acceptable only for the initial single-threaded ABI and is a Phase 4 threading +blocker. + +## 7. `host_fs` inventory + +`host_fs` is linked at every permission tier, so its shared implementation must +enforce all fd rights, process DAC, mount read-only policy, permission tiers, +and quotas. No Wasmtime linker availability check is treated as sufficient. + +| Imports | Status; mode; memory | Canonical operation | Bounds, required correction, and parity proof | +| --- | --- | --- | --- | +| `open_tmpfile`, `fd_link` | C; sync; path in/fd out or path in | `GuestPathHost::open_tmpfile_at/link_tmpfile_at` | Path/fd/quota/DAC; O_EXCL, linkability, closed-source and rollback tests | +| `remount` | C; sync; path/options in | Sidecar mount host over kernel process check | euid 0, mount permission, 4096-byte path and 64 KiB options; nonroot/oversize tests | +| `path_mknod` | C; sync; path/scalars in | `GuestPathHost::mknod_at` | DAC/read-only/quota/type/rdev/umask tests | +| `path_renameat2` | C; sync; two paths in | `GuestPathHost::rename_at2` | Supported Linux flags, DAC/read-only/cross-mount tests | +| `path_statfs` | C; sync; path in/five u64 out | `GuestMetadataHost::statfs_at` | Prevalidate all outputs; authoritative quota/space tests | +| `fd_fiemap` | C; sync; three outputs | `GuestExtentHost::get(fd,index)` returns one bounded extent | Do not materialize all extents; sparse/unwritten/many-extent/index tests | +| `fd_punch_hole`, `fd_zero_range`, `fd_insert_range`, `fd_collapse_range` | C; sync; scalar in | `GuestExtentHost` range operations | Write right/read-only/quota/offset/alignment/overflow/rollback tests | +| `set_open_mode`, `set_open_direct` | A but architecturally obsolete; sync; scalar in | Phase 1 folds values into one atomic `OpenOptions`; legacy adapters use a generation-bound one-shot token | No process-global latch; nested/reentrant open tests now and interleaved-thread tests in Phase 4 | +| `path_owner`, `path_mode`, `path_size`, `path_blocks`, `path_rdev` | C; sync; path in/scalar outputs | One `GuestMetadataHost::stat_at` | Phase 1 removes ambient Node stat and sentinel errors; DAC/no-host-leak/symlink/sparse/device tests | +| `fd_owner`, `fd_mode`, `fd_size`, `fd_blocks` | C; sync; scalar outputs | One `GuestMetadataHost::stat_fd` | Phase 1 removes ambient fallbacks/caches/sentinels; open-unlinked/pipe/socket/truncate tests | +| `path_access` | C; sync; path in | `GuestPathHost::access_at(real_or_effective_ids)` | DAC/root/symlink/read-only tests | +| `path_chown`, `fd_chown` | A legacy names | Alias `GuestMetadataHost::chown_at/chown_fd`; remove only after rebuilt-artifact audit | Ownership/-1/symlink/open-unlinked tests | +| `chown`, `fchown` | C current names; sync; path or scalar in | Same process-aware metadata operations | Exact EPERM/DAC/ownership tests | +| `chmod`, `fchmod` | C; sync; path/scalar in | `GuestMetadataHost::chmod_at/chmod_fd` | Phase 1 removes mapped-host/ambient fallbacks; DAC/read-only/open-unlinked tests | +| `path_getxattr`, `path_listxattr`, `path_setxattr`, `path_removexattr` | C; sync; path/name/value in and value/list/size out | `GuestXattrHost::*_at` | 255-byte name/64 KiB value+list, DAC/read-only/quota; ERANGE/flags/namespace tests | +| `fd_getxattr`, `fd_listxattr`, `fd_setxattr`, `fd_removexattr` | C; sync; name/value in and value/list/size out | Real `GuestXattrHost::*_fd` operations | Phase 1 stops converting fd to a path; open-then-unlink/rename tests | +| `ftruncate` | A compatibility name; sync; scalar in | Alias `GuestFdHost::set_len` | Phase 1 removes ambient fallback and incorrect errno `1`; negative/overflow/quota tests | + +## 8. Correctness fixes admitted into the prerequisite revision + +The runtime-neutral revision intentionally changes current V8 behavior where +the inventory found a Linux/security defect: + +1. canonical link, remove, rename, symlink, and unlink use process-aware kernel + DAC/sticky/read-only checks; +2. all writes and decoded arrays are bounded before allocating or copying; +3. account lookup returns `ERANGE` and required length instead of successful + truncation; +4. supplementary groups are capped before guest memory is read; +5. `socketpair` uses the actual `(kind, nonblock, cloexec)` ABI; +6. fd xattrs operate on the open file description, including after unlink; +7. terminal identity is a live fd lookup, not an fd<=2 cache; +8. metadata never falls through to ambient Node filesystem access or sentinel + error values; and +9. `pty_open` calls the existing bounded kernel implementation instead of + returning `FAULT`. + +These fixes run through the V8-WASM adapter before Wasmtime work starts. + +## 9. Required differential proof suites + +| Suite | Required coverage | +| --- | --- | +| Raw ABI | One direct-WAT fixture per import row or grouped signature family; invalid pointers, overflow, undersized outputs, unsupported import, tier omission | +| Owned sysroot | Rebuild the complete default command set and inspect every module import; no undeclared import and no unexplained legacy version | +| Software | `ls`, `vim`, `grep`, `curl`, shell pipelines, sqlite, git, tar/gzip, xfs/metadata utilities, and registry software corpus | +| Filesystem | openat/path traversal, DAC/sticky/umask, quotas, fd lifetime, FIFO, xattrs, extents, metadata, read-only mounts, no ambient-host escape | +| Process/signal | spawn/exec/wait, fd actions/CLOEXEC, groups/sessions, rlimits, locks, SCM_RIGHTS, dispositions/masks/pending, stop/continue/kill | +| Network | TCP/UDP/Unix/DNS/TLS, policy denial, readiness, backpressure, cancellation, limits, fd duplication and mixed poll sets | +| Terminal | canonical/raw input, echo/control signals, duplicated fds, window resize/SIGWINCH, foreground group, raw-mode restoration, guest-created PTY | +| Identity/system | credential transitions, groups, passwd/group database, argv/env, clocks, entropy, `/proc`, `/dev`, hostname/system identity | +| Resource attacks | Every named count/byte/deadline cap at limit and limit+1; near-limit warning; transactional rollback and typed error | + +Both backend selections execute the same corpus. Tests compare stdout, stderr, +bytes, errno, exit status, terminating signal, kernel side effects, and resource +accounting—not engine error strings. + +### Phase 0 artifact evidence + +On the rebased `main` tree, `pnpm install --frozen-lockfile` completed and the +Rust half of `just tools-rebuild` produced 139 command modules. The C build +produced 33 top-level modules before stopping in the upstream Git link. Auditing +all 172 produced modules with Binaryen `wasm-dis` found 121 unique +module/function import pairs. Every pair is declared in Sections 3–7; the +artifact set specifically confirms that both `path_chown`/`fd_chown` legacy +aliases and `proc_spawn_v3`/`proc_spawn_v4` plus +`proc_waitpid_v2`/`proc_waitpid_v3` version pairs remain live. + +The complete release-artifact gate is not green on this base revision. The +owned `libc.a` contains duplicate `chown`, `fchown`, `lchown`, and `fchownat` +definitions from patched `posix.o` and `override_ownership.o`, so upstream Git +cannot link and later C tools are not produced. This is a pre-existing +toolchain correctness defect: patch +`0034-posix-ownership-and-access.patch` added the canonical implementation while +`wasi-libc-overrides/ownership.c` retained the old implementation. The +runtime-neutral revision must select one implementation, rerun the complete +rebuild, and commit an automated import audit before satisfying its exit gate. +Phase 0's source inventory remains complete; it does not misreport the partial +artifact build as release validation. + +## 10. V8-WASM performance and memory baseline + +Two current measurements serve different purposes. + +The committed warm resource matrix at +`packages/runtime-benchmarks/results/baseline-local.json` was captured on a +12th-generation Intel i7-12700KF, 20 logical cores, 62.6 GiB RAM, Node 24.17.0, +Linux 6.1 x86-64, with 20 iterations after five warmups. Across ordinary +V8-WASM lanes the incremental sidecar VmHWM range is 11.2-20.0 MiB, the median +is 14.6 MiB, and the large stream-copy case reaches 56.3 MiB. This is a +sidecar-level high-water delta, not isolated engine RSS/PSS. + +The focused command-floor capture from revision `38b6a84b` on the same machine +used one fresh-cache iteration, no benchmark warmup, three serial executions, +and warmup diagnostics proving the first execution performed V8 prewarm while +the next two used the cache: + +| Command | Module bytes | Fresh-cache first execution | Cached warm p50 | +| --- | ---: | ---: | ---: | +| `true` | 259,407 | 59.04 ms | 35.31 ms | +| `printf` (0 bytes) | 501,725 | 69.95 ms | 42.45 ms | +| `pwd` | 350,044 | 64.53 ms | 41.59 ms | +| `ls` (empty directory) | 1,054,728 | 83.24 ms | 56.11 ms | +| `date --version` | 2,468,903 | 94.44 ms | 58.31 ms | + +This is a captured baseline, not a statistically strong acceptance run. Phase +3 reruns at least five independent fresh-cache processes with five measured +iterations each, records p50/p95 and RSS/PSS/VIRT separately, and compares the +two engines using identical module bytes, cache state, host-service path, output +capture, and concurrency. + +The reproducible command is: + +```bash +BENCH_ONLY=wasm-command-floor \ +BENCH_WASM_COMMAND_FLOOR_ITERATIONS=5 \ +BENCH_WASM_COMMAND_FLOOR_WARMUP=0 \ +BENCH_WASM_COMMAND_FLOOR_SERIAL_RUNS=3 \ +BENCH_WASM_COMMAND_FLOOR_WARMUP_DEBUG=1 \ +pnpm --dir packages/runtime-benchmarks bench +``` + +Run it with a fresh sidecar cache root for each measured cold sample. Generated +tool binaries remain uncommitted; use `pnpm install --frozen-lockfile` followed +by `just tools-rebuild` before a release-quality capture. + +## 11. Locked implementation decisions + +1. **Feature profile.** Both standalone backends prevalidate with one + `wasmparser` profile. Enable MVP, mutable globals, sign extension, + nontrapping float-to-int, bulk memory, reference types, multivalue, and + SIMD128. Disable threads/shared memory, memory64, multi-memory, relaxed SIMD, + tail calls, function references/GC, exceptions, components, custom page + sizes, and other proposals until an explicit profile revision. Engine + defaults never silently expand the accepted language. +2. **Code placement.** Kernel semantic APIs stay in `agentos-kernel`. Shared + request/reply types and capability-sized host traits live under + `crates/execution/src/host/`; native-sidecar implements them using kernel and + process-lifecycle context. Wasmtime remains under + `crates/execution/src/wasm/wasmtime/`. No new crate is created unless the + actual dependency graph produces a cycle that cannot be removed by moving + transport-neutral types to the existing bridge crate. +3. **ABI generation.** Generate Preview1 layouts/types from a checked-in pinned + Preview1 WITX description; generated glue implements AgentOS host traits and + does not construct a `wasmtime-wasi` context. Generate custom-import + signatures from one checked-in AgentOS ABI manifest shared by linker tests + and import-audit tooling. Handwritten code remains only for bounded memory + copying and version-to-canonical request conversion. +4. **CPU fields.** Remove the misleading lockstep `maxWasmFuel` field. Replace + it with `activeCpuTimeLimitMs` (default runaway safeguard), optional + `wallClockLimitMs`, and optional `deterministicFuel`. No compatibility alias + is needed because protocol/client/sidecar ship together. +5. **Engine profiles.** The process caches Engines by exact feature profile and + exact stack cap. Unspecified stack uses 512 KiB. Admit at most eight distinct + profiles process-wide by default, warn at 80%, and reject the ninth with a + typed limit naming the engine-profile setting. Modules never cross Engines. +6. **Module cache.** Per Engine, use a 32-entry LRU and a 256 MiB conservative + admission budget. Charge `max(module_bytes * 8, 1 MiB)` per compiled Module, + warn at 80%, never deserialize native artifacts, and expose hits, misses, + evictions, source bytes, charged bytes, compile latency, and retained RSS in + metrics. Phase 3 may tune defaults from measured evidence without changing + ownership. +7. **Platforms.** Linux x86-64, Linux arm64, macOS x86-64, and macOS arm64 are + initial release blockers because all four native sidecars are published. + Full conformance and performance gates run on canonical Linux x86-64; + cross-compile plus smoke/conformance subsets run on the other three. No + browser build or browser compile repair is in scope. +8. **Preferred-backend gate.** Correctness and safety permit no regression. On + the canonical machine, Wasmtime may become preferred only when the command + corpus geometric-mean p50 regresses no more than 10%, no individual p95 + regresses more than 20%, concurrency throughput regresses no more than 10%, + and per-execution retained RSS/PSS regresses no more than the greater of 10% + or 4 MiB. VIRT is reported separately. A threshold miss keeps Wasmtime + selectable but does not make it preferred. +9. **Backend selection.** The sidecar protocol carries an optional sealed + standalone-WASM backend enum: `wasmtime` or `v8`. Omission means the + sidecar-owned default. Phase 2 initially defaults to V8; Phase 3 may switch + omission to Wasmtime after gates pass. Both clients mirror the explicit + override and neither owns the default. +10. **Threads and snapshots.** Threads/shared memory remain Phase 4 and require + a new per-thread signal/libc audit. Process isolation is decided in that + phase from teardown and containment evidence. Live snapshots/fork, Wizer, + serialized AOT artifacts, pooling, and components remain outside initial + completion. + +These decisions close the implementation questions in the parent Wasmtime +specification. A later change requires an explicit spec revision, not an +adapter-local exception. From 9ab745abeaff1e4815fe8ccc8a2527231fea53cf Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sat, 18 Jul 2026 15:23:34 -0700 Subject: [PATCH 2/6] refactor: unify executor host services --- Cargo.lock | 100 +- Cargo.toml | 4 + crates/CLAUDE.md | 12 +- crates/agentos-sidecar/src/acp/mod.rs | 5 + crates/bridge/bridge-contract.json | 5 + crates/bridge/src/queue_tracker.rs | 46 +- crates/client/src/agent_os.rs | 8 +- crates/client/src/config.rs | 34 +- crates/client/tests/native_root_mount_e2e.rs | 14 +- crates/execution/Cargo.toml | 1 + .../abi/wasi_snapshot_preview1/UPSTREAM.md | 13 + .../abi/wasi_snapshot_preview1/typenames.witx | 749 ++ .../wasi_snapshot_preview1.witx | 511 ++ crates/execution/assets/agentos-wasm-abi.json | 5994 ++++++++++++++ .../assets/runners/python-runner.mjs | 174 +- .../execution/assets/runners/wasi-module.js | 31 +- .../execution/assets/runners/wasm-runner.mjs | 4578 +++++++---- .../assets/wasi-preview1-imports.json | 37 - crates/execution/src/abi/generated.rs | 3938 +++++++++ crates/execution/src/abi/mod.rs | 57 + crates/execution/src/backend/error.rs | 60 + crates/execution/src/backend/event.rs | 140 + crates/execution/src/backend/lifecycle.rs | 165 + crates/execution/src/backend/mod.rs | 27 + crates/execution/src/backend/payload.rs | 268 + crates/execution/src/backend/reply.rs | 834 ++ crates/execution/src/backend/submission.rs | 605 ++ crates/execution/src/backend/wake.rs | 258 + crates/execution/src/host/clock.rs | 33 + crates/execution/src/host/entropy.rs | 6 + crates/execution/src/host/filesystem.rs | 376 + crates/execution/src/host/identity.rs | 72 + crates/execution/src/host/mod.rs | 605 ++ crates/execution/src/host/network.rs | 357 + crates/execution/src/host/process.rs | 326 + crates/execution/src/host/signal.rs | 51 + crates/execution/src/host/terminal.rs | 56 + crates/execution/src/javascript.rs | 1904 ++++- crates/execution/src/lib.rs | 15 +- crates/execution/src/node_import_cache.rs | 125 +- crates/execution/src/python.rs | 1678 +++- crates/execution/src/signal.rs | 6 +- crates/execution/src/v8_host.rs | 84 +- crates/execution/src/wasm.rs | 2196 ++++- crates/execution/tests/backend_lifecycle.rs | 11 + .../execution/tests/backend_payload_bounds.rs | 138 + crates/execution/tests/javascript_v8.rs | 88 +- crates/execution/tests/module_resolution.rs | 27 + crates/execution/tests/permission_flags.rs | 6 +- crates/execution/tests/process.rs | 14 +- crates/execution/tests/python.rs | 42 +- crates/execution/tests/python_prewarm.rs | 2 +- crates/execution/tests/runtime_topology.rs | 1 + crates/execution/tests/wasm.rs | 400 +- .../execution/tests/wasm_abi_link_contract.rs | 160 + .../tests/wasm_host_fs_errno_contract.rs | 11 +- .../tests/wasm_host_net_errno_contract.rs | 31 +- .../execution/tests/wasm_nonblocking_stdin.rs | 51 +- .../tests/wasm_preview1_memory_bounds.rs | 109 + .../tests/wasm_structured_error_contract.rs | 21 + crates/kernel/Cargo.toml | 4 +- crates/kernel/src/command_registry.rs | 41 +- crates/kernel/src/device_layer.rs | 13 +- crates/kernel/src/dns.rs | 379 +- crates/kernel/src/fd_table.rs | 674 +- crates/kernel/src/kernel.rs | 4153 +++++++++- crates/kernel/src/lib.rs | 3 + crates/kernel/src/permissions.rs | 9 +- crates/kernel/src/pipe_manager.rs | 137 +- crates/kernel/src/poll.rs | 2 + crates/kernel/src/process_runtime.rs | 931 +++ crates/kernel/src/process_table.rs | 2012 ++++- crates/kernel/src/pty.rs | 539 +- crates/kernel/src/resource_accounting.rs | 343 +- crates/kernel/src/socket_table.rs | 8 +- crates/kernel/src/system.rs | 40 + crates/kernel/src/user.rs | 173 +- crates/kernel/tests/agentos_read_only.rs | 248 + crates/kernel/tests/api_surface.rs | 27 +- crates/kernel/tests/command_registry.rs | 36 + crates/kernel/tests/dac.rs | 424 + crates/kernel/tests/dns_resolution.rs | 86 +- crates/kernel/tests/fd_table.rs | 49 +- crates/kernel/tests/identity.rs | 224 +- crates/kernel/tests/kernel_integration.rs | 67 +- crates/kernel/tests/pipe_manager.rs | 2 +- crates/kernel/tests/poll.rs | 29 +- crates/kernel/tests/process_table.rs | 447 +- crates/kernel/tests/pty.rs | 2 +- crates/kernel/tests/resource_accounting.rs | 168 +- crates/kernel/tests/stdio_devices.rs | 35 + crates/kernel/tests/user.rs | 55 + .../native-sidecar-core/src/bridge_bytes.rs | 36 +- crates/native-sidecar-core/src/diagnostics.rs | 3 + crates/native-sidecar-core/src/guest_fs.rs | 15 +- crates/native-sidecar-core/src/guest_net.rs | 85 +- crates/native-sidecar-core/src/guest_pty.rs | 2 +- crates/native-sidecar-core/src/identity.rs | 50 +- crates/native-sidecar-core/src/lib.rs | 5 +- crates/native-sidecar-core/src/limits.rs | 135 +- crates/native-sidecar-core/src/root_fs.rs | 26 +- crates/native-sidecar-core/src/signals.rs | 111 +- crates/native-sidecar/Cargo.toml | 2 + .../assets/base-filesystem.json | 3 +- crates/native-sidecar/src/bindings.rs | 99 +- crates/native-sidecar/src/bootstrap.rs | 92 +- .../src/execution/child_process.rs | 7204 ++++++++++++----- .../src/execution/coordinator.rs | 200 +- .../src/execution/host_dispatch/clock.rs | 62 + .../src/execution/host_dispatch/entropy.rs | 24 + .../src/execution/host_dispatch/filesystem.rs | 3281 ++++++++ .../src/execution/host_dispatch/identity.rs | 250 + .../src/execution/host_dispatch/inventory.rs | 365 + .../src/execution/host_dispatch/mod.rs | 4012 +++++++++ .../src/execution/host_dispatch/network.rs | 272 + .../execution/host_dispatch/network_compat.rs | 4854 +++++++++++ .../src/execution/host_dispatch/process.rs | 415 + .../src/execution/host_dispatch/signal.rs | 164 + .../src/execution/host_dispatch/terminal.rs | 132 + .../src/execution/javascript/crypto.rs | 46 +- .../src/execution/javascript/http.rs | 598 +- .../src/execution/javascript/mod.rs | 19 +- .../src/execution/javascript/rpc.rs | 2230 +++-- .../src/execution/javascript/sqlite.rs | 60 +- crates/native-sidecar/src/execution/launch.rs | 3633 +++++---- crates/native-sidecar/src/execution/mod.rs | 436 +- .../src/execution/network/dns.rs | 289 +- .../src/execution/network/http2.rs | 565 +- .../src/execution/network/http_client.rs | 313 + .../src/execution/network/managed.rs | 665 ++ .../src/execution/network/managed_endpoint.rs | 1071 +++ .../src/execution/network/mod.rs | 11 +- .../src/execution/network/resolver.rs | 302 + .../src/execution/network/tcp.rs | 647 +- .../src/execution/network/tls.rs | 184 +- .../src/execution/network/udp.rs | 1017 ++- .../src/execution/network/unix.rs | 285 +- .../native-sidecar/src/execution/process.rs | 3016 +++++-- .../src/execution/process_events.rs | 948 ++- .../src/execution/python/mod.rs | 3 - .../src/execution/python/rpc.rs | 250 - .../src/execution/python/sockets.rs | 1072 --- .../src/execution/python/subprocess.rs | 80 - .../native-sidecar/src/execution/signals.rs | 579 +- crates/native-sidecar/src/execution/stdio.rs | 612 +- crates/native-sidecar/src/filesystem.rs | 5259 +----------- crates/native-sidecar/src/limits.rs | 1 + crates/native-sidecar/src/plugins/host_dir.rs | 192 +- crates/native-sidecar/src/service.rs | 1392 ++-- crates/native-sidecar/src/state.rs | 1441 +++- crates/native-sidecar/src/stdio.rs | 16 +- crates/native-sidecar/src/vm.rs | 1371 +--- .../tests/architecture_guards.rs | 2164 ++++- .../tests/bidirectional_frames.rs | 53 +- .../tests/builtin_completeness.rs | 7 +- .../tests/builtin_conformance.rs | 208 +- crates/native-sidecar/tests/filesystem.rs | 781 +- .../tests/fixtures/limits-inventory.json | 451 +- .../tests/fs_watch_and_streams.rs | 4 +- crates/native-sidecar/tests/guest_identity.rs | 477 +- crates/native-sidecar/tests/host_dir.rs | 16 + crates/native-sidecar/tests/limits.rs | 17 +- crates/native-sidecar/tests/limits_audit.rs | 1 - .../native-sidecar/tests/permission_flags.rs | 2 +- .../native-sidecar/tests/posix_compliance.rs | 139 +- crates/native-sidecar/tests/python.rs | 36 +- crates/native-sidecar/tests/security_audit.rs | 9 +- .../tests/security_hardening.rs | 2 +- crates/native-sidecar/tests/service.rs | 5003 ++++++------ crates/native-sidecar/tests/signal.rs | 31 +- .../tests/socket_state_queries.rs | 11 +- crates/native-sidecar/tests/stdio_binary.rs | 12 +- crates/native-sidecar/tests/support/mod.rs | 45 + crates/native-sidecar/tests/wasm_raw_abi.rs | 1468 ++++ .../tests/xfstests_correctness.rs | 2 +- crates/resource/Cargo.toml | 10 + crates/resource/src/lib.rs | 915 +++ crates/runtime/Cargo.toml | 1 + crates/runtime/src/accounting.rs | 858 +- crates/runtime/src/executor.rs | 293 + crates/runtime/src/lib.rs | 23 +- crates/runtime/src/readiness.rs | 40 +- .../protocol/agentos_sidecar_v1.bare | 1 + crates/sidecar-protocol/src/protocol.rs | 12 +- crates/sidecar-protocol/src/wire.rs | 44 +- crates/v8-runtime/src/bridge.rs | 220 +- crates/v8-runtime/src/embedded_runtime.rs | 202 +- crates/v8-runtime/src/execution.rs | 260 +- crates/v8-runtime/src/host_call.rs | 504 +- crates/v8-runtime/src/lib.rs | 24 + crates/v8-runtime/src/runtime_protocol.rs | 1 + crates/v8-runtime/src/session.rs | 461 +- crates/v8-runtime/src/stream.rs | 14 +- .../tests/embedded_runtime_session.rs | 12 +- .../tests/runtime_context_architecture.rs | 70 +- crates/vfs-store/tests/local.rs | 22 +- crates/vfs/assets/base-filesystem.json | 3 +- crates/vfs/src/adapter/mounted_fs.rs | 36 +- crates/vfs/src/engine/engines/chunked.rs | 22 +- crates/vfs/src/engine/types.rs | 62 +- crates/vfs/src/engine/vfs.rs | 17 +- crates/vfs/src/extent.rs | 121 + crates/vfs/src/lib.rs | 1 + crates/vfs/src/posix/mount_table.rs | 36 +- crates/vfs/src/posix/overlay_fs.rs | 18 +- crates/vfs/src/posix/root_fs.rs | 12 +- crates/vfs/src/posix/vfs.rs | 71 +- crates/vfs/tests/conformance.rs | 36 +- crates/vfs/tests/posix_root_fs.rs | 10 + crates/vfs/tests/posix_vfs.rs | 90 +- crates/vm-config/src/lib.rs | 376 +- crates/wasm-abi-generator/Cargo.toml | 14 + crates/wasm-abi-generator/src/lib.rs | 1045 +++ crates/wasm-abi-generator/src/main.rs | 184 + docs/design/runtime-neutral-executors.md | 30 +- docs/design/wasmtime-executor.md | 131 +- docs/design/wasmtime-phase-0.md | 165 +- examples/resource-limits/server.ts | 3 +- justfile | 11 +- .../bridge-src/builtins/child-process.ts | 16 +- .../bridge-src/builtins/console.ts | 2 +- .../build-tools/bridge-src/builtins/fs.ts | 28 +- .../bridge-src/builtins/process.ts | 18 +- .../build-tools/bridge-src/global-exposure.ts | 5 + .../scripts/build-base-filesystem.mjs | 4 + packages/core/src/agent-os.ts | 11 +- .../core/src/generated/ProcessLimitsConfig.ts | 2 +- .../src/generated/ResourceLimitsConfig.ts | 2 +- .../core/src/generated/WasmLimitsConfig.ts | 2 +- packages/core/src/options-schema.ts | 185 +- packages/core/tests/options-schema.test.ts | 67 + packages/runtime-benchmarks/src/leak.ts | 1 + packages/runtime-benchmarks/src/lib/memory.ts | 2 + .../runtime-core/src/generated-protocol.ts | 3 + .../src/generated/ProcessLimitsConfig.ts | 2 +- .../src/generated/ResourceLimitsConfig.ts | 2 +- .../src/generated/VmGroupConfig.ts | 7 +- .../src/generated/VmUserAccountConfig.ts | 7 +- .../src/generated/VmUserConfig.ts | 7 +- .../src/generated/WasmLimitsConfig.ts | 2 +- .../src/node-runtime-options-schema.ts | 187 +- packages/runtime-core/src/node-runtime.ts | 1 + .../runtime-core/src/response-payloads.ts | 5 + packages/runtime-core/src/sidecar-process.ts | 2 + packages/runtime-core/src/test-runtime.ts | 5 +- .../tests/node-runtime-options-schema.test.ts | 55 +- .../tests/response-payloads.test.ts | 2 + .../tests/test-runtime-bootstrap.test.ts | 48 + scripts/audit-wasm-imports.mjs | 228 + scripts/generate-wasm-abi-manifest.mjs | 866 ++ software/acl/package.json | 2 +- software/acl/test/acl.test.ts | 131 + software/attr/package.json | 2 +- software/attr/test/attr.test.ts | 183 + software/coreutils/test/kill.nightly.test.ts | 2 +- .../test/shell-redirect.nightly.test.ts | 26 +- .../test/shell-terminal.nightly.test.ts | 24 +- software/curl/test/curl.nightly.test.ts | 2 +- software/duckdb/test/duckdb.nightly.test.ts | 40 +- .../envsubst/test/envsubst.nightly.test.ts | 2 +- software/git/test/git.nightly.test.ts | 4 +- software/unzip/test/unzip.nightly.test.ts | 40 +- software/wget/test/wget.nightly.test.ts | 68 +- software/xfsprogs/package.json | 2 +- software/xfsprogs/test/xfs-io.test.ts | 137 + software/zip/test/zip.nightly.test.ts | 8 +- toolchain/c/Makefile | 2 +- toolchain/conformance/c-parity.test.ts | 156 +- toolchain/crates/wasi-ext/src/lib.rs | 96 +- .../0001-wasi-agentos-linux-identity.patch | 34 - .../std-patches/wasi-libc/0008-sockets.patch | 22 +- ...018-posix-spawn-and-terminal-headers.patch | 124 +- .../0037-user-account-database.patch | 47 +- .../wasi-libc/0047-host-system-identity.patch | 57 + toolchain/test-programs/getgrouplist_bounds.c | 54 + toolchain/test-programs/pipe_edge.c | 5 - toolchain/test-programs/ppoll_contract.c | 10 +- website/public/docs/docs/resource-limits.md | 11 +- .../src/content/docs/docs/resource-limits.mdx | 10 +- 279 files changed, 85572 insertions(+), 24455 deletions(-) create mode 100644 crates/execution/abi/wasi_snapshot_preview1/UPSTREAM.md create mode 100644 crates/execution/abi/wasi_snapshot_preview1/typenames.witx create mode 100644 crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx create mode 100644 crates/execution/assets/agentos-wasm-abi.json delete mode 100644 crates/execution/assets/wasi-preview1-imports.json create mode 100644 crates/execution/src/abi/generated.rs create mode 100644 crates/execution/src/abi/mod.rs create mode 100644 crates/execution/src/backend/error.rs create mode 100644 crates/execution/src/backend/event.rs create mode 100644 crates/execution/src/backend/lifecycle.rs create mode 100644 crates/execution/src/backend/mod.rs create mode 100644 crates/execution/src/backend/payload.rs create mode 100644 crates/execution/src/backend/reply.rs create mode 100644 crates/execution/src/backend/submission.rs create mode 100644 crates/execution/src/backend/wake.rs create mode 100644 crates/execution/src/host/clock.rs create mode 100644 crates/execution/src/host/entropy.rs create mode 100644 crates/execution/src/host/filesystem.rs create mode 100644 crates/execution/src/host/identity.rs create mode 100644 crates/execution/src/host/mod.rs create mode 100644 crates/execution/src/host/network.rs create mode 100644 crates/execution/src/host/process.rs create mode 100644 crates/execution/src/host/signal.rs create mode 100644 crates/execution/src/host/terminal.rs create mode 100644 crates/execution/tests/backend_lifecycle.rs create mode 100644 crates/execution/tests/backend_payload_bounds.rs create mode 100644 crates/execution/tests/wasm_abi_link_contract.rs create mode 100644 crates/execution/tests/wasm_preview1_memory_bounds.rs create mode 100644 crates/execution/tests/wasm_structured_error_contract.rs create mode 100644 crates/kernel/src/process_runtime.rs create mode 100644 crates/kernel/src/system.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/clock.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/entropy.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/filesystem.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/identity.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/inventory.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/mod.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/network.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/network_compat.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/process.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/signal.rs create mode 100644 crates/native-sidecar/src/execution/host_dispatch/terminal.rs create mode 100644 crates/native-sidecar/src/execution/network/http_client.rs create mode 100644 crates/native-sidecar/src/execution/network/managed.rs create mode 100644 crates/native-sidecar/src/execution/network/managed_endpoint.rs create mode 100644 crates/native-sidecar/src/execution/network/resolver.rs delete mode 100644 crates/native-sidecar/src/execution/python/mod.rs delete mode 100644 crates/native-sidecar/src/execution/python/rpc.rs delete mode 100644 crates/native-sidecar/src/execution/python/sockets.rs delete mode 100644 crates/native-sidecar/src/execution/python/subprocess.rs create mode 100644 crates/native-sidecar/tests/wasm_raw_abi.rs create mode 100644 crates/resource/Cargo.toml create mode 100644 crates/resource/src/lib.rs create mode 100644 crates/runtime/src/executor.rs create mode 100644 crates/vfs/src/extent.rs create mode 100644 crates/wasm-abi-generator/Cargo.toml create mode 100644 crates/wasm-abi-generator/src/lib.rs create mode 100644 crates/wasm-abi-generator/src/main.rs create mode 100644 packages/runtime-core/tests/test-runtime-bootstrap.test.ts create mode 100644 scripts/audit-wasm-imports.mjs create mode 100644 scripts/generate-wasm-abi-manifest.mjs create mode 100644 software/acl/test/acl.test.ts create mode 100644 software/attr/test/attr.test.ts create mode 100644 software/xfsprogs/test/xfs-io.test.ts delete mode 100644 toolchain/std-patches/crates/uu_uname/0001-wasi-agentos-linux-identity.patch create mode 100644 toolchain/std-patches/wasi-libc/0047-host-system-identity.patch create mode 100644 toolchain/test-programs/getgrouplist_bounds.c diff --git a/Cargo.lock b/Cargo.lock index c8740a18e0..4818196752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ dependencies = [ "serde", "serde_bare", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "vbare", ] @@ -113,7 +113,7 @@ dependencies = [ "serde", "serde_bare", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -128,6 +128,7 @@ dependencies = [ "agentos-build-support", "agentos-runtime", "agentos-v8-runtime", + "agentos-wasm-abi-generator", "base64 0.22.1", "ciborium", "flume", @@ -146,16 +147,14 @@ name = "agentos-kernel" version = "0.0.1" dependencies = [ "agentos-bridge", - "agentos-runtime", + "agentos-resource", "agentos-vfs-core", "base64 0.22.1", "event-listener", "getrandom 0.2.17", "hickory-proto", - "hickory-resolver", "serde", "serde_json", - "tokio", "web-time", ] @@ -179,6 +178,7 @@ dependencies = [ "agentos-vfs", "agentos-vfs-core", "agentos-vm-config", + "agentos-wasm-abi-generator", "async-trait", "aws-config", "aws-credential-types", @@ -188,6 +188,7 @@ dependencies = [ "command-fds", "ctr", "filetime", + "getrandom 0.2.17", "h2 0.4.15", "hickory-resolver", "hmac 0.12.1", @@ -212,7 +213,7 @@ dependencies = [ "socket2 0.6.4", "tar", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-rustls 0.26.4", "tracing", @@ -264,10 +265,18 @@ dependencies = [ "serde_bare", ] +[[package]] +name = "agentos-resource" +version = "0.0.1" +dependencies = [ + "event-listener", +] + [[package]] name = "agentos-runtime" version = "0.0.1" dependencies = [ + "agentos-resource", "tokio", ] @@ -323,7 +332,7 @@ dependencies = [ "futures", "parking_lot", "scc", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -417,6 +426,16 @@ dependencies = [ "ts-rs", ] +[[package]] +name = "agentos-wasm-abi-generator" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", + "wat", + "witx", +] + [[package]] name = "ahash" version = "0.8.12" @@ -1244,7 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" dependencies = [ "nix 0.31.3", - "thiserror", + "thiserror 2.0.18", "tokio", ] @@ -2027,7 +2046,7 @@ dependencies = [ "ipnet", "jni", "rand", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -2048,7 +2067,7 @@ dependencies = [ "prefix-trie", "rand", "ring 0.17.14", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "url", @@ -2075,7 +2094,7 @@ dependencies = [ "resolv-conf", "smallvec", "system-configuration", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -2505,7 +2524,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -2583,6 +2602,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -3861,7 +3886,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.18", "time", ] @@ -4055,13 +4080,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4241,7 +4286,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -4322,7 +4367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" dependencies = [ "lazy_static", - "thiserror", + "thiserror 2.0.18", "ts-rs-macros", ] @@ -4590,6 +4635,15 @@ dependencies = [ "semver", ] +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + [[package]] name = "wast" version = "252.0.0" @@ -4609,7 +4663,7 @@ version = "1.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" dependencies = [ - "wast", + "wast 252.0.0", ] [[package]] @@ -4875,6 +4929,18 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 6d06f24db1..af7a155ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "crates/native-sidecar", "crates/native-sidecar-browser", "crates/native-sidecar-core", + "crates/resource", "crates/runtime", "crates/sidecar-client", "crates/sidecar-protocol", @@ -22,6 +23,7 @@ members = [ "crates/vfs", "crates/vfs-store", "crates/vm-config", + "crates/wasm-abi-generator", ] # Browser support is intentionally retained in-tree but disabled while the # unified native sidecar reactor lands. Keep these two crates out of ordinary @@ -37,6 +39,7 @@ default-members = [ "crates/native-baseline", "crates/native-sidecar", "crates/native-sidecar-core", + "crates/resource", "crates/runtime", "crates/sidecar-client", "crates/sidecar-protocol", @@ -65,6 +68,7 @@ agentos-native-baseline = { path = "crates/native-baseline", version = "0.0.1" } agentos-native-sidecar = { path = "crates/native-sidecar", version = "0.0.1" } agentos-native-sidecar-browser = { path = "crates/native-sidecar-browser", version = "0.0.1" } agentos-native-sidecar-core = { path = "crates/native-sidecar-core", version = "0.0.1" } +agentos-resource = { path = "crates/resource", version = "0.0.1" } agentos-runtime = { path = "crates/runtime", version = "0.0.1" } agentos-protocol = { path = "crates/agentos-protocol", version = "0.0.1" } agentos-sidecar-client = { path = "crates/sidecar-client", version = "0.0.1" } diff --git a/crates/CLAUDE.md b/crates/CLAUDE.md index fb92d0c360..58c90f0cab 100644 --- a/crates/CLAUDE.md +++ b/crates/CLAUDE.md @@ -64,12 +64,8 @@ These are hard rules with no exceptions: - **The sidecar execution driver's shell-visible builtin commands must mirror the compat runtime surface.** Seed `node` and `wasm` during `create_vm()` and preserve them when refreshing `/__secure_exec/commands/*` in `configure_vm()`, but do not materialize extra `/bin/*` runtime stubs such as `python` unless a lower snapshot or installed software explicitly provides them. Direct `ExecuteRequest { runtime: ... }` dispatch still covers runtimes that are not meant to appear in the guest filesystem. - **Mounted WASM command directories need both command-map refresh and guest `PATH` refresh.** When `crates/sidecar/src/vm.rs` rediscovers `/__secure_exec/commands/*`, update `vm.command_guest_paths` and prepend those parent directories into `vm.guest_env["PATH"]`; `crates/sidecar/src/execution.rs` child-process resolution should search `PATH` entries before falling back to `/bin`, `/usr/bin`, and `/usr/local/bin`, or guest `child_process.spawn("sh")`/`spawn("grep")` misses mounted registry commands. - **Native-sidecar command resolution must use the caller's effective `PATH`, not just `vm.guest_env`.** In `crates/sidecar/src/execution.rs`, path-like guest candidates emitted by shells (for example `/home/agentos/.pi/agent/bin/printf`) should only be treated as executables when the candidate or mapped host file actually exists; if the parent directory came from `PATH` but the file is missing, fall back to the basename so registry command remapping can still find `printf`/`bash`/`which`. -- **Native-sidecar WASM commands see the shadow root, so standard guest directories must be seeded there during VM creation.** In `crates/sidecar/src/vm.rs`, keep `/tmp`, `/var/tmp`, `/bin`, `/usr`, and the rest of the POSIX bootstrap tree materialized in the shadow root before any WASM command runs, or shell redirection and absolute-path checks will disagree with the kernel VFS (`vm.stat("/tmp")` works while `sh -c 'echo hi > /tmp/x'` fails). -- **Host filesystem API writes that should be visible to WASM commands must mirror into the shadow root immediately.** In `crates/sidecar/src/filesystem.rs`, `GuestFilesystemOperation::WriteFile` needs to update both the kernel VFS and the VM shadow tree right away, or `vm.writeFile("/tmp/x")` will succeed while guest `sh`/`cat`/`ls` still miss the file until some later sync path runs. -- **Host filesystem API directory creation must mirror into the shadow root too.** In `crates/sidecar/src/filesystem.rs`, `GuestFilesystemOperation::CreateDir` / `Mkdir` need to create the same directory under the VM shadow tree immediately, or host-backed shells and JavaScript child-process spawns will lose their requested guest cwd even though `vm.stat()` sees the directory in the kernel VFS. -- **Host filesystem API rename/remove operations must keep the shadow root in sync too.** In `crates/sidecar/src/filesystem.rs`, `GuestFilesystemOperation::Rename`, `RemoveFile`, and `RemoveDir` need to move or delete the matching shadow-root path immediately, or the next `exists`/`stat` shadow reconciliation can resurrect stale host-side paths back into the kernel. -- **Guest process writes that land in the sidecar shadow root must be mirrored back into the kernel before process exit completes.** In `crates/sidecar/src/execution.rs`, sync regular files and symlinks from the shadow-root host tree back into the kernel on `ActiveExecutionEvent::Exited`, or commands like `vm.exec("echo hi > /tmp/x && cat /tmp/x")` will succeed while a later `vm.readFile("/tmp/x")` still returns `ENOENT`. -- **VM bootstrap must materialize custom root snapshot files into the shadow root, not just standard directories.** In `crates/sidecar/src/vm.rs`, copy non-bundled snapshot/bootstrap entries into the shadow tree during VM creation, or shell-launched WASM commands like `zip /archive.zip /hello.txt` will create new output files successfully while still missing pre-seeded guest inputs that only exist in the kernel snapshot layer. +- **The kernel VFS is the sole mutable guest-filesystem authority.** Standard directories, custom root snapshots, host API writes, guest writes, rename/remove operations, and process cwd all resolve against live kernel state. Do not restore a mutable executor shadow root, bidirectional reconciliation, or exit-time copying between host and kernel trees. +- **Executors must read guest files through the shared kernel-backed filesystem capability.** This includes standalone-WASM command modules, shell redirection, embedded V8 module resolution, and Python/Pyodide access. A host path is valid only when it is an explicit confined mount/plugin resource; it is never a second copy of ordinary guest state. - **Top-level sidecar executions must forward the resolved guest cwd into the kernel process too.** In `crates/sidecar/src/execution.rs`, the first `kernel.spawn_process(...)` for `execute` requests cannot hardcode `/`, or `vm.exec(..., { cwd })`, ACP adapter `process.cwd()`, and relative shell/tool paths all drift back to the VM root even when the resolved host cwd is correct. - **Bidirectional native sidecar wire frames use signed request IDs.** `request`/`response` frames initiated from TypeScript must keep positive `request_id` values, while sidecar-initiated `sidecar_request`/`sidecar_response` frames must use negative IDs; keep Rust validation, stdio routing, and TS client framing in sync when adding new callback payloads. - **Sidecar callback request IDs must start negative even in default state.** `SharedSidecarRequestClient` should initialize its counter at `-1`; a default `0` request ID causes the first `sidecar_response` to fail protocol validation and leaves callback-driven mounts hanging. @@ -103,9 +99,9 @@ These are hard rules with no exceptions: - **Binding registration bootstrap and binding invocation use different permission paths.** In `crates/native-sidecar/src/bindings.rs`, sidecar-owned `register_host_callbacks(...)` work should temporarily swap the bridge policy to `PermissionsPolicy::allow_all()` while it refreshes `/bin/agentos*` command stubs, then restore the VM policy; actual guest binding execution is separately gated by `binding.invoke` against `:` resources. - **Sidecar request handlers that need rollback must keep fallible mutations inside a `Result`-returning closure.** In handlers like `register_host_callbacks(...)`, a bare block with `?` returns from the whole request handler before rollback runs; wrap the mutation block in `(|| -> Result<_, SidecarError> { ... })()` so restore/fail-closed cleanup still executes on errors. - **Native-sidecar VM bootstrap must keep a temporary static policy installed.** During `create_vm` and `configure_vm`, swap in `PermissionsPolicy::allow_all()` for sidecar-owned `/bin`/command-path reconciliation instead of clearing the stored policy entirely, or the `LocalBridge` fallback will deny internal filesystem checks before the guest-visible policy is restored. -- **Filesystem-side teardown races must fail closed too.** In `crates/sidecar/src/filesystem.rs`, guest filesystem and Python VFS handlers should treat missing VMs or active processes as stale teardown races: log and return a rejection/`Ok(())` instead of `expect(...)`, because dispose can win after a request is queued but before the response is emitted. +- **Filesystem-side teardown races must fail closed too.** Shared filesystem capability handlers should treat missing VMs or active processes as stale teardown races: log and settle the exact request with a typed rejection instead of `expect(...)`, because dispose can win after a request is queued but before the response is emitted. Do not add a language-specific filesystem handler as a workaround. - **Mapped host paths in `crates/sidecar/src/filesystem.rs` must resolve symlinks against the mapping root before touching the real host.** For Python/Pyodide cache and other `AGENT_OS_GUEST_PATH_MAPPINGS` accesses, walk existing path components with `symlink_metadata`, reject targets that leave the mapped root, and filter `readdir` results the same way so guest `stat`/`listdir` calls cannot leak host metadata through escaped symlinks. -- **Mapped host-path metadata and symlink ops must not require a pre-existing kernel mirror.** In `crates/sidecar/src/filesystem.rs`, guest writes under `AGENT_OS_GUEST_PATH_MAPPINGS` land on the host first, so `chmod`, `utimes`, `symlink`, and `readlink` need to operate on the mapped host path even when the kernel shadow entry has not been synced yet; only mirror metadata back into the kernel when the guest path already exists there. +- **Explicit host mappings remain confined mount/plugin resources, not filesystem mirrors.** Metadata and symlink operations must resolve beneath the configured mapping root and enforce kernel process policy without creating or synchronizing a duplicate kernel entry. Ordinary guest paths never fall back to ambient host filesystem operations. - **`host_dir` metadata ops must resolve beneath the mount root, reject symlink leaves, and NOT require read on the target (non-root sidecar).** In `crates/native-sidecar/src/plugins/host_dir.rs`: `chown`/`utimes` resolve the parent via `split_parent`, `reject_symlink_leaf` (an `fstatat` `lstat` through the anchored parent fd → `EPERM` on a symlink), then mutate with `fchownat`/`utimensat(.., AT_SYMLINK_NOFOLLOW)` against `(parent_fd, leaf_name)` — no leaf open, so no read requirement, and `AT_SYMLINK_NOFOLLOW` closes the check→mutate swap race. `stat`/`exists` use `confine::resolve_parent_beneath` + `fstatat`. `chmod` is the deliberate exception: `fchmodat` has no `AT_SYMLINK_NOFOLLOW`, so it keeps `open_metadata_beneath` (leaf `O_RDONLY | O_NOFOLLOW`) + `fchmod` for TOCTOU safety, accepting the read requirement. Do NOT reintroduce `openat2`, an `O_PATH` *metadata/leaf* anchor, `/proc/self/fd` re-opens, or a macOS-specific path — the confinement boundary is the single universal `confine` module (see it and `crates/native-sidecar/CLAUDE.md` for why `openat2` was removed and why the non-root read constraint drives these choices). An `O_PATH` *directory traversal* anchor for search-only intermediate dirs (`confine::open_dir_anchor`, Linux-only, `EACCES` fallback) is allowed and is not a metadata anchor. - **`VirtualStat` schema changes must cross every filesystem boundary together.** When adding stat metadata for mount plugins such as `host_dir`, update the Rust `VirtualStat` constructors (`kernel`, `device_layer`, plugin adapters), sidecar filesystem JSON serialization, JS bridge conversions, and the V8 `Stats` shim in the same change or the extra metadata gets truncated before guest code can observe it. - **Stateful JS crypto sync RPCs in `crates/sidecar/src/execution.rs` are per-process state.** Keep `service_javascript_crypto_sync_rpc` threaded with `&mut ActiveProcess` for cipher/Diffie-Hellman session handlers, and for AES-GCM treat `authTagLength` as auth-tag output sizing instead of calling `Crypter::set_tag_len()` during encryption on the current OpenSSL provider. diff --git a/crates/agentos-sidecar/src/acp/mod.rs b/crates/agentos-sidecar/src/acp/mod.rs index 835a923210..8d574b4b8b 100644 --- a/crates/agentos-sidecar/src/acp/mod.rs +++ b/crates/agentos-sidecar/src/acp/mod.rs @@ -1190,6 +1190,9 @@ fn error_response(error: SidecarError) -> AcpResponse { } fn error_code(error: &SidecarError) -> String { + if let SidecarError::Host(error) = error { + return error.code.clone(); + } let code = match error { SidecarError::ResourceLimit(_) => "resource_limit", SidecarError::InvalidState(message) => message @@ -1211,8 +1214,10 @@ fn error_code(error: &SidecarError) -> String { SidecarError::Kernel(_) => "kernel", SidecarError::Plugin(_) => "plugin", SidecarError::Execution(_) => "execution", + SidecarError::ExecutionEventChannelClosed { .. } => "execution_event_channel_closed", SidecarError::Bridge(_) => "bridge", SidecarError::Io(_) => "io", + SidecarError::Host(_) => unreachable!("handled above"), }; String::from(code) } diff --git a/crates/bridge/bridge-contract.json b/crates/bridge/bridge-contract.json index 7ca79d2912..480b0ab699 100644 --- a/crates/bridge/bridge-contract.json +++ b/crates/bridge/bridge-contract.json @@ -224,6 +224,7 @@ "_processExec", "_processExecFdImageCommit", "_processSignalState", + "_processSignalEnd", "_processTakeSignal", "_processWasmSyncRpc" ] @@ -1237,6 +1238,10 @@ "method": "process.signal_state", "translateArgs": false }, + "_processSignalEnd": { + "method": "process.signal_end", + "translateArgs": false + }, "_processTakeSignal": { "method": "process.take_signal", "translateArgs": false diff --git a/crates/bridge/src/queue_tracker.rs b/crates/bridge/src/queue_tracker.rs index 06ef0ca1f6..c599947b0d 100644 --- a/crates/bridge/src/queue_tracker.rs +++ b/crates/bridge/src/queue_tracker.rs @@ -70,6 +70,8 @@ impl LimitCategory { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TrackedLimit { JavascriptEventChannel, + PendingSyncRpcCalls, + PendingPythonVfsRpcCalls, V8SessionFrames, SidecarStdinFrames, SidecarStdoutFrames, @@ -78,6 +80,10 @@ pub enum TrackedLimit { PendingProcessEventBytes, PendingExecutionEvents, PendingExecutionEventBytes, + PendingChildProcessSyncCount, + PendingChildProcessSyncBytes, + AsyncCompletionCount, + AsyncCompletionBytes, PendingKernelStdinBytes, PendingWasmSignals, PendingSidecarResponses, @@ -92,12 +98,18 @@ pub enum TrackedLimit { VmSocketDatagramQueueLen, VmFilesystemBytes, VmInodes, + VmPreadBytes, + VmFdWriteBytes, + VmProcessArgvBytes, + VmProcessEnvBytes, + VmReaddirEntries, + VmBlockingReadMs, VmRecursiveFsDepth, VmRecursiveFsEntries, V8HeapBytes, V8CpuTimeMs, V8WallClockMs, - WasmFuelMs, + WasmWallClockMs, WasmMemoryBytes, } @@ -107,6 +119,8 @@ impl TrackedLimit { pub fn as_str(self) -> &'static str { match self { TrackedLimit::JavascriptEventChannel => "javascript_event_channel", + TrackedLimit::PendingSyncRpcCalls => "pending_sync_rpc_calls", + TrackedLimit::PendingPythonVfsRpcCalls => "pending_python_vfs_rpc_calls", TrackedLimit::V8SessionFrames => "v8_session_frames", TrackedLimit::SidecarStdinFrames => "sidecar_stdin_frames", TrackedLimit::SidecarStdoutFrames => "sidecar_stdout_frames", @@ -115,6 +129,10 @@ impl TrackedLimit { TrackedLimit::PendingProcessEventBytes => "pending_process_event_bytes", TrackedLimit::PendingExecutionEvents => "pending_execution_events", TrackedLimit::PendingExecutionEventBytes => "pending_execution_event_bytes", + TrackedLimit::PendingChildProcessSyncCount => "pending_child_process_sync_count", + TrackedLimit::PendingChildProcessSyncBytes => "pending_child_process_sync_bytes", + TrackedLimit::AsyncCompletionCount => "async_completion_count", + TrackedLimit::AsyncCompletionBytes => "async_completion_bytes", TrackedLimit::PendingKernelStdinBytes => "pending_kernel_stdin_bytes", TrackedLimit::PendingWasmSignals => "pending_wasm_signals", TrackedLimit::PendingSidecarResponses => "pending_sidecar_responses", @@ -129,12 +147,18 @@ impl TrackedLimit { TrackedLimit::VmSocketDatagramQueueLen => "vm_socket_datagram_queue_len", TrackedLimit::VmFilesystemBytes => "vm_filesystem_bytes", TrackedLimit::VmInodes => "vm_inodes", + TrackedLimit::VmPreadBytes => "vm_pread_bytes", + TrackedLimit::VmFdWriteBytes => "vm_fd_write_bytes", + TrackedLimit::VmProcessArgvBytes => "vm_process_argv_bytes", + TrackedLimit::VmProcessEnvBytes => "vm_process_env_bytes", + TrackedLimit::VmReaddirEntries => "vm_readdir_entries", + TrackedLimit::VmBlockingReadMs => "vm_blocking_read_ms", TrackedLimit::VmRecursiveFsDepth => "vm_recursive_fs_depth", TrackedLimit::VmRecursiveFsEntries => "vm_recursive_fs_entries", TrackedLimit::V8HeapBytes => "v8_heap_bytes", TrackedLimit::V8CpuTimeMs => "v8_cpu_time_ms", TrackedLimit::V8WallClockMs => "v8_wall_clock_ms", - TrackedLimit::WasmFuelMs => "wasm_fuel_ms", + TrackedLimit::WasmWallClockMs => "wasm_wall_clock_ms", TrackedLimit::WasmMemoryBytes => "wasm_memory_bytes", } } @@ -142,6 +166,8 @@ impl TrackedLimit { pub fn category(self) -> LimitCategory { match self { TrackedLimit::JavascriptEventChannel + | TrackedLimit::PendingSyncRpcCalls + | TrackedLimit::PendingPythonVfsRpcCalls | TrackedLimit::V8SessionFrames | TrackedLimit::SidecarStdinFrames | TrackedLimit::SidecarStdoutFrames @@ -150,6 +176,10 @@ impl TrackedLimit { | TrackedLimit::PendingProcessEventBytes | TrackedLimit::PendingExecutionEvents | TrackedLimit::PendingExecutionEventBytes + | TrackedLimit::PendingChildProcessSyncCount + | TrackedLimit::PendingChildProcessSyncBytes + | TrackedLimit::AsyncCompletionCount + | TrackedLimit::AsyncCompletionBytes | TrackedLimit::PendingKernelStdinBytes | TrackedLimit::PendingWasmSignals | TrackedLimit::PendingSidecarResponses @@ -164,12 +194,18 @@ impl TrackedLimit { | TrackedLimit::VmSocketDatagramQueueLen | TrackedLimit::VmFilesystemBytes | TrackedLimit::VmInodes + | TrackedLimit::VmPreadBytes + | TrackedLimit::VmFdWriteBytes + | TrackedLimit::VmProcessArgvBytes + | TrackedLimit::VmProcessEnvBytes + | TrackedLimit::VmReaddirEntries | TrackedLimit::VmRecursiveFsDepth | TrackedLimit::VmRecursiveFsEntries => LimitCategory::Resource, TrackedLimit::V8HeapBytes | TrackedLimit::WasmMemoryBytes => LimitCategory::Memory, - TrackedLimit::V8CpuTimeMs | TrackedLimit::V8WallClockMs | TrackedLimit::WasmFuelMs => { - LimitCategory::Cpu - } + TrackedLimit::VmBlockingReadMs + | TrackedLimit::V8CpuTimeMs + | TrackedLimit::V8WallClockMs + | TrackedLimit::WasmWallClockMs => LimitCategory::Cpu, } } } diff --git a/crates/client/src/agent_os.rs b/crates/client/src/agent_os.rs index 325f52c27d..842edd23e1 100644 --- a/crates/client/src/agent_os.rs +++ b/crates/client/src/agent_os.rs @@ -4041,7 +4041,9 @@ mod tests { wasm: Some(WasmLimits { prewarm_timeout_ms: Some(30_000), runner_heap_limit_mb: Some(2_048), - runner_cpu_time_limit_ms: Some(60_000), + active_cpu_time_limit_ms: Some(60_000), + wall_clock_limit_ms: Some(120_000), + deterministic_fuel: Some(1_000_000), ..Default::default() }), ..Default::default() @@ -4093,6 +4095,8 @@ mod tests { let wasm = limits.wasm.expect("wasm limits"); assert_eq!(wasm.prewarm_timeout_ms, Some(30_000)); assert_eq!(wasm.runner_heap_limit_mb, Some(2_048)); - assert_eq!(wasm.runner_cpu_time_limit_ms, Some(60_000)); + assert_eq!(wasm.active_cpu_time_limit_ms, Some(60_000)); + assert_eq!(wasm.wall_clock_limit_ms, Some(120_000)); + assert_eq!(wasm.deterministic_fuel, Some(1_000_000)); } } diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index f8de322e9a..91e0e96131 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -376,12 +376,6 @@ pub struct ResourceLimits { skip_serializing_if = "Option::is_none" )] pub max_readdir_entries: Option, - #[serde( - default, - rename = "maxWasmFuel", - skip_serializing_if = "Option::is_none" - )] - pub max_wasm_fuel: Option, #[serde( default, rename = "maxWasmMemoryBytes", @@ -716,10 +710,22 @@ pub struct WasmLimits { pub runner_heap_limit_mb: Option, #[serde( default, - rename = "runnerCpuTimeLimitMs", + rename = "activeCpuTimeLimitMs", + skip_serializing_if = "Option::is_none" + )] + pub active_cpu_time_limit_ms: Option, + #[serde( + default, + rename = "wallClockLimitMs", + skip_serializing_if = "Option::is_none" + )] + pub wall_clock_limit_ms: Option, + #[serde( + default, + rename = "deterministicFuel", skip_serializing_if = "Option::is_none" )] - pub runner_cpu_time_limit_ms: Option, + pub deterministic_fuel: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -754,6 +760,18 @@ pub struct ProcessLimits { skip_serializing_if = "Option::is_none" )] pub pending_event_bytes: Option, + #[serde( + default, + rename = "maxPendingChildSyncCount", + skip_serializing_if = "Option::is_none" + )] + pub max_pending_child_sync_count: Option, + #[serde( + default, + rename = "maxPendingChildSyncBytes", + skip_serializing_if = "Option::is_none" + )] + pub max_pending_child_sync_bytes: Option, } // --------------------------------------------------------------------------- diff --git a/crates/client/tests/native_root_mount_e2e.rs b/crates/client/tests/native_root_mount_e2e.rs index a13f9a2817..18b80fb0d4 100644 --- a/crates/client/tests/native_root_mount_e2e.rs +++ b/crates/client/tests/native_root_mount_e2e.rs @@ -164,7 +164,11 @@ impl MemBridgeFs { } "pwrite" => { let path = path()?; - let offset = args.get("offset").and_then(Value::as_u64).unwrap_or(0) as usize; + let offset = args + .get("offset") + .and_then(Value::as_u64) + .ok_or_else(|| "EINVAL missing offset".to_string())? + as usize; let content = args .get("content") .and_then(Value::as_str) @@ -180,7 +184,7 @@ impl MemBridgeFs { } let end = offset .checked_add(content.len()) - .ok_or_else(|| "EFBIG write offset overflow".to_string())?; + .ok_or_else(|| "EFBIG pwrite range overflow".to_string())?; if entry.content.len() < end { entry.content.resize(end, 0); } @@ -449,9 +453,9 @@ impl MemBridgeFs { } "utimes" => { let path = path()?; - if !entries.contains_key(&path) { - return Err(format!("ENOENT no such entry: {path}")); - } + entries + .get(&path) + .ok_or_else(|| format!("ENOENT no such entry: {path}"))?; Ok(None) } "truncate" => { diff --git a/crates/execution/Cargo.toml b/crates/execution/Cargo.toml index c772bb9f63..9b700bb757 100644 --- a/crates/execution/Cargo.toml +++ b/crates/execution/Cargo.toml @@ -36,6 +36,7 @@ tokio = { version = "1", features = ["rt", "sync", "time"] } tracing = "0.1" [dev-dependencies] +agentos-wasm-abi-generator = { path = "../wasm-abi-generator" } tempfile = "3" wat = "1" diff --git a/crates/execution/abi/wasi_snapshot_preview1/UPSTREAM.md b/crates/execution/abi/wasi_snapshot_preview1/UPSTREAM.md new file mode 100644 index 0000000000..ea33245a1b --- /dev/null +++ b/crates/execution/abi/wasi_snapshot_preview1/UPSTREAM.md @@ -0,0 +1,13 @@ +# Pinned WASI Preview1 interface source + +These WITX files are copied verbatim from the WebAssembly/WASI repository at +commit `d4d3df3072b65ce43cb01c1add72b402d69a79d1` (the source +revision embedded by the pinned `witx = 0.9.1` parser): + +- `phases/snapshot/witx/typenames.witx` +- `phases/snapshot/witx/wasi_snapshot_preview1.witx` + +The upstream files are licensed under Apache-2.0 with LLVM exception. They are +the source of truth for generated Preview1 core-WASM signatures and memory +layouts. AgentOS's custom `host_*` ABI remains defined by the same generated +manifest alongside the lowered Preview1 surface. diff --git a/crates/execution/abi/wasi_snapshot_preview1/typenames.witx b/crates/execution/abi/wasi_snapshot_preview1/typenames.witx new file mode 100644 index 0000000000..1905ecf766 --- /dev/null +++ b/crates/execution/abi/wasi_snapshot_preview1/typenames.witx @@ -0,0 +1,749 @@ +;; Type names used by low-level WASI interfaces. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/tree/master/docs/witx.md) +;; for an explanation of what that means. + +(typename $size u32) + +;;; Non-negative file size or length of a region within a file. +(typename $filesize u64) + +;;; Timestamp in nanoseconds. +(typename $timestamp u64) + +;;; Identifiers for clocks. +(typename $clockid + (enum (@witx tag u32) + ;;; The clock measuring real time. Time value zero corresponds with + ;;; 1970-01-01T00:00:00Z. + $realtime + ;;; The store-wide monotonic clock, which is defined as a clock measuring + ;;; real time, whose value cannot be adjusted and which cannot have negative + ;;; clock jumps. The epoch of this clock is undefined. The absolute time + ;;; value of this clock therefore has no meaning. + $monotonic + ;;; The CPU-time clock associated with the current process. + $process_cputime_id + ;;; The CPU-time clock associated with the current thread. + $thread_cputime_id + ) +) + +;;; Error codes returned by functions. +;;; Not all of these error codes are returned by the functions provided by this +;;; API; some are used in higher-level library layers, and others are provided +;;; merely for alignment with POSIX. +(typename $errno + (enum (@witx tag u16) + ;;; No error occurred. System call completed successfully. + $success + ;;; Argument list too long. + $2big + ;;; Permission denied. + $acces + ;;; Address in use. + $addrinuse + ;;; Address not available. + $addrnotavail + ;;; Address family not supported. + $afnosupport + ;;; Resource unavailable, or operation would block. + $again + ;;; Connection already in progress. + $already + ;;; Bad file descriptor. + $badf + ;;; Bad message. + $badmsg + ;;; Device or resource busy. + $busy + ;;; Operation canceled. + $canceled + ;;; No child processes. + $child + ;;; Connection aborted. + $connaborted + ;;; Connection refused. + $connrefused + ;;; Connection reset. + $connreset + ;;; Resource deadlock would occur. + $deadlk + ;;; Destination address required. + $destaddrreq + ;;; Mathematics argument out of domain of function. + $dom + ;;; Reserved. + $dquot + ;;; File exists. + $exist + ;;; Bad address. + $fault + ;;; File too large. + $fbig + ;;; Host is unreachable. + $hostunreach + ;;; Identifier removed. + $idrm + ;;; Illegal byte sequence. + $ilseq + ;;; Operation in progress. + $inprogress + ;;; Interrupted function. + $intr + ;;; Invalid argument. + $inval + ;;; I/O error. + $io + ;;; Socket is connected. + $isconn + ;;; Is a directory. + $isdir + ;;; Too many levels of symbolic links. + $loop + ;;; File descriptor value too large. + $mfile + ;;; Too many links. + $mlink + ;;; Message too large. + $msgsize + ;;; Reserved. + $multihop + ;;; Filename too long. + $nametoolong + ;;; Network is down. + $netdown + ;;; Connection aborted by network. + $netreset + ;;; Network unreachable. + $netunreach + ;;; Too many files open in system. + $nfile + ;;; No buffer space available. + $nobufs + ;;; No such device. + $nodev + ;;; No such file or directory. + $noent + ;;; Executable file format error. + $noexec + ;;; No locks available. + $nolck + ;;; Reserved. + $nolink + ;;; Not enough space. + $nomem + ;;; No message of the desired type. + $nomsg + ;;; Protocol not available. + $noprotoopt + ;;; No space left on device. + $nospc + ;;; Function not supported. + $nosys + ;;; The socket is not connected. + $notconn + ;;; Not a directory or a symbolic link to a directory. + $notdir + ;;; Directory not empty. + $notempty + ;;; State not recoverable. + $notrecoverable + ;;; Not a socket. + $notsock + ;;; Not supported, or operation not supported on socket. + $notsup + ;;; Inappropriate I/O control operation. + $notty + ;;; No such device or address. + $nxio + ;;; Value too large to be stored in data type. + $overflow + ;;; Previous owner died. + $ownerdead + ;;; Operation not permitted. + $perm + ;;; Broken pipe. + $pipe + ;;; Protocol error. + $proto + ;;; Protocol not supported. + $protonosupport + ;;; Protocol wrong type for socket. + $prototype + ;;; Result too large. + $range + ;;; Read-only file system. + $rofs + ;;; Invalid seek. + $spipe + ;;; No such process. + $srch + ;;; Reserved. + $stale + ;;; Connection timed out. + $timedout + ;;; Text file busy. + $txtbsy + ;;; Cross-device link. + $xdev + ;;; Extension: Capabilities insufficient. + $notcapable + ) +) + +;;; File descriptor rights, determining which actions may be performed. +(typename $rights + (flags (@witx repr u64) + ;;; The right to invoke `fd_datasync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::dsync`. + $fd_datasync + ;;; The right to invoke `fd_read` and `sock_recv`. + ;; + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + $fd_read + ;;; The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + $fd_seek + ;;; The right to invoke `fd_fdstat_set_flags`. + $fd_fdstat_set_flags + ;;; The right to invoke `fd_sync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::rsync` and `fdflags::dsync`. + $fd_sync + ;;; The right to invoke `fd_seek` in such a way that the file offset + ;;; remains unaltered (i.e., `whence::cur` with offset zero), or to + ;;; invoke `fd_tell`. + $fd_tell + ;;; The right to invoke `fd_write` and `sock_send`. + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + $fd_write + ;;; The right to invoke `fd_advise`. + $fd_advise + ;;; The right to invoke `fd_allocate`. + $fd_allocate + ;;; The right to invoke `path_create_directory`. + $path_create_directory + ;;; If `path_open` is set, the right to invoke `path_open` with `oflags::creat`. + $path_create_file + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; source directory. + $path_link_source + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; target directory. + $path_link_target + ;;; The right to invoke `path_open`. + $path_open + ;;; The right to invoke `fd_readdir`. + $fd_readdir + ;;; The right to invoke `path_readlink`. + $path_readlink + ;;; The right to invoke `path_rename` with the file descriptor as the source directory. + $path_rename_source + ;;; The right to invoke `path_rename` with the file descriptor as the target directory. + $path_rename_target + ;;; The right to invoke `path_filestat_get`. + $path_filestat_get + ;;; The right to change a file's size (there is no `path_filestat_set_size`). + ;;; If `path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + $path_filestat_set_size + ;;; The right to invoke `path_filestat_set_times`. + $path_filestat_set_times + ;;; The right to invoke `fd_filestat_get`. + $fd_filestat_get + ;;; The right to invoke `fd_filestat_set_size`. + $fd_filestat_set_size + ;;; The right to invoke `fd_filestat_set_times`. + $fd_filestat_set_times + ;;; The right to invoke `path_symlink`. + $path_symlink + ;;; The right to invoke `path_remove_directory`. + $path_remove_directory + ;;; The right to invoke `path_unlink_file`. + $path_unlink_file + ;;; If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + ;;; If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + $poll_fd_readwrite + ;;; The right to invoke `sock_shutdown`. + $sock_shutdown + ) +) + +;;; A file descriptor handle. +(typename $fd (handle)) + +;;; A region of memory for scatter/gather reads. +(typename $iovec + (record + ;;; The address of the buffer to be filled. + (field $buf (@witx pointer u8)) + ;;; The length of the buffer to be filled. + (field $buf_len $size) + ) +) + +;;; A region of memory for scatter/gather writes. +(typename $ciovec + (record + ;;; The address of the buffer to be written. + (field $buf (@witx const_pointer u8)) + ;;; The length of the buffer to be written. + (field $buf_len $size) + ) +) + +(typename $iovec_array (list $iovec)) +(typename $ciovec_array (list $ciovec)) + +;;; Relative offset within a file. +(typename $filedelta s64) + +;;; The position relative to which to set the offset of the file descriptor. +(typename $whence + (enum (@witx tag u8) + ;;; Seek relative to start-of-file. + $set + ;;; Seek relative to current position. + $cur + ;;; Seek relative to end-of-file. + $end + ) +) + +;;; A reference to the offset of a directory entry. +;;; +;;; The value 0 signifies the start of the directory. +(typename $dircookie u64) + +;;; The type for the `dirent::d_namlen` field of `dirent` struct. +(typename $dirnamlen u32) + +;;; File serial number that is unique within its file system. +(typename $inode u64) + +;;; The type of a file descriptor or file. +(typename $filetype + (enum (@witx tag u8) + ;;; The type of the file descriptor or file is unknown or is different from any of the other types specified. + $unknown + ;;; The file descriptor or file refers to a block device inode. + $block_device + ;;; The file descriptor or file refers to a character device inode. + $character_device + ;;; The file descriptor or file refers to a directory inode. + $directory + ;;; The file descriptor or file refers to a regular file inode. + $regular_file + ;;; The file descriptor or file refers to a datagram socket. + $socket_dgram + ;;; The file descriptor or file refers to a byte-stream socket. + $socket_stream + ;;; The file refers to a symbolic link inode. + $symbolic_link + ) +) + +;;; A directory entry. +(typename $dirent + (record + ;;; The offset of the next directory entry stored in this directory. + (field $d_next $dircookie) + ;;; The serial number of the file referred to by this directory entry. + (field $d_ino $inode) + ;;; The length of the name of the directory entry. + (field $d_namlen $dirnamlen) + ;;; The type of the file referred to by this directory entry. + (field $d_type $filetype) + ) +) + +;;; File or memory access pattern advisory information. +(typename $advice + (enum (@witx tag u8) + ;;; The application has no advice to give on its behavior with respect to the specified data. + $normal + ;;; The application expects to access the specified data sequentially from lower offsets to higher offsets. + $sequential + ;;; The application expects to access the specified data in a random order. + $random + ;;; The application expects to access the specified data in the near future. + $willneed + ;;; The application expects that it will not access the specified data in the near future. + $dontneed + ;;; The application expects to access the specified data once and then not reuse it thereafter. + $noreuse + ) +) + +;;; File descriptor flags. +(typename $fdflags + (flags (@witx repr u16) + ;;; Append mode: Data written to the file is always appended to the file's end. + $append + ;;; Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + $dsync + ;;; Non-blocking mode. + $nonblock + ;;; Synchronized read I/O operations. + $rsync + ;;; Write according to synchronized I/O file integrity completion. In + ;;; addition to synchronizing the data stored in the file, the implementation + ;;; may also synchronously update the file's metadata. + $sync + ) +) + +;;; File descriptor attributes. +(typename $fdstat + (record + ;;; File type. + (field $fs_filetype $filetype) + ;;; File descriptor flags. + (field $fs_flags $fdflags) + ;;; Rights that apply to this file descriptor. + (field $fs_rights_base $rights) + ;;; Maximum set of rights that may be installed on new file descriptors that + ;;; are created through this file descriptor, e.g., through `path_open`. + (field $fs_rights_inheriting $rights) + ) +) + +;;; Identifier for a device containing a file system. Can be used in combination +;;; with `inode` to uniquely identify a file or directory in the filesystem. +(typename $device u64) + +;;; Which file time attributes to adjust. +(typename $fstflags + (flags (@witx repr u16) + ;;; Adjust the last data access timestamp to the value stored in `filestat::atim`. + $atim + ;;; Adjust the last data access timestamp to the time of clock `clockid::realtime`. + $atim_now + ;;; Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + $mtim + ;;; Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + $mtim_now + ) +) + +;;; Flags determining the method of how paths are resolved. +(typename $lookupflags + (flags (@witx repr u32) + ;;; As long as the resolved path corresponds to a symbolic link, it is expanded. + $symlink_follow + ) +) + +;;; Open flags used by `path_open`. +(typename $oflags + (flags (@witx repr u16) + ;;; Create file if it does not exist. + $creat + ;;; Fail if not a directory. + $directory + ;;; Fail if file already exists. + $excl + ;;; Truncate file to size 0. + $trunc + ) +) + +;;; Number of hard links to an inode. +(typename $linkcount u64) + +;;; File attributes. +(typename $filestat + (record + ;;; Device ID of device containing the file. + (field $dev $device) + ;;; File serial number. + (field $ino $inode) + ;;; File type. + (field $filetype $filetype) + ;;; Number of hard links to the file. + (field $nlink $linkcount) + ;;; For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + (field $size $filesize) + ;;; Last data access timestamp. + (field $atim $timestamp) + ;;; Last data modification timestamp. + (field $mtim $timestamp) + ;;; Last file status change timestamp. + (field $ctim $timestamp) + ) +) + +;;; User-provided value that may be attached to objects that is retained when +;;; extracted from the implementation. +(typename $userdata u64) + +;;; Type of a subscription to an event or its occurrence. +(typename $eventtype + (enum (@witx tag u8) + ;;; The time value of clock `subscription_clock::id` has + ;;; reached timestamp `subscription_clock::timeout`. + $clock + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has data + ;;; available for reading. This event always triggers for regular files. + $fd_read + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has capacity + ;;; available for writing. This event always triggers for regular files. + $fd_write + ) +) + +;;; The state of the file descriptor subscribed to with +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $eventrwflags + (flags (@witx repr u16) + ;;; The peer of this socket has closed or disconnected. + $fd_readwrite_hangup + ) +) + +;;; The contents of an `event` when type is `eventtype::fd_read` or +;;; `eventtype::fd_write`. +(typename $event_fd_readwrite + (record + ;;; The number of bytes available for reading or writing. + (field $nbytes $filesize) + ;;; The state of the file descriptor. + (field $flags $eventrwflags) + ) +) + +;;; An event that occurred. +(typename $event + (record + ;;; User-provided value that got attached to `subscription::userdata`. + (field $userdata $userdata) + ;;; If non-zero, an error that occurred while processing the subscription request. + (field $error $errno) + ;;; The type of event that occured + (field $type $eventtype) + ;;; The contents of the event, if it is an `eventtype::fd_read` or + ;;; `eventtype::fd_write`. `eventtype::clock` events ignore this field. + (field $fd_readwrite $event_fd_readwrite) + ) +) + +;;; Flags determining how to interpret the timestamp provided in +;;; `subscription_clock::timeout`. +(typename $subclockflags + (flags (@witx repr u16) + ;;; If set, treat the timestamp provided in + ;;; `subscription_clock::timeout` as an absolute timestamp of clock + ;;; `subscription_clock::id`. If clear, treat the timestamp + ;;; provided in `subscription_clock::timeout` relative to the + ;;; current time value of clock `subscription_clock::id`. + $subscription_clock_abstime + ) +) + +;;; The contents of a `subscription` when type is `eventtype::clock`. +(typename $subscription_clock + (record + ;;; The clock against which to compare the timestamp. + (field $id $clockid) + ;;; The absolute or relative timestamp. + (field $timeout $timestamp) + ;;; The amount of time that the implementation may wait additionally + ;;; to coalesce with other events. + (field $precision $timestamp) + ;;; Flags specifying whether the timeout is absolute or relative + (field $flags $subclockflags) + ) +) + +;;; The contents of a `subscription` when type is type is +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $subscription_fd_readwrite + (record + ;;; The file descriptor on which to wait for it to become ready for reading or writing. + (field $file_descriptor $fd) + ) +) + +;;; The contents of a `subscription`. +(typename $subscription_u + (union + (@witx tag $eventtype) + $subscription_clock + $subscription_fd_readwrite + $subscription_fd_readwrite + ) +) + +;;; Subscription to an event. +(typename $subscription + (record + ;;; User-provided value that is attached to the subscription in the + ;;; implementation and returned through `event::userdata`. + (field $userdata $userdata) + ;;; The type of the event to which to subscribe, and its contents + (field $u $subscription_u) + ) +) + +;;; Exit code generated by a process when exiting. +(typename $exitcode u32) + +;;; Signal condition. +(typename $signal + (enum (@witx tag u8) + ;;; No signal. Note that POSIX has special semantics for `kill(pid, 0)`, + ;;; so this value is reserved. + $none + ;;; Hangup. + ;;; Action: Terminates the process. + $hup + ;;; Terminate interrupt signal. + ;;; Action: Terminates the process. + $int + ;;; Terminal quit signal. + ;;; Action: Terminates the process. + $quit + ;;; Illegal instruction. + ;;; Action: Terminates the process. + $ill + ;;; Trace/breakpoint trap. + ;;; Action: Terminates the process. + $trap + ;;; Process abort signal. + ;;; Action: Terminates the process. + $abrt + ;;; Access to an undefined portion of a memory object. + ;;; Action: Terminates the process. + $bus + ;;; Erroneous arithmetic operation. + ;;; Action: Terminates the process. + $fpe + ;;; Kill. + ;;; Action: Terminates the process. + $kill + ;;; User-defined signal 1. + ;;; Action: Terminates the process. + $usr1 + ;;; Invalid memory reference. + ;;; Action: Terminates the process. + $segv + ;;; User-defined signal 2. + ;;; Action: Terminates the process. + $usr2 + ;;; Write on a pipe with no one to read it. + ;;; Action: Ignored. + $pipe + ;;; Alarm clock. + ;;; Action: Terminates the process. + $alrm + ;;; Termination signal. + ;;; Action: Terminates the process. + $term + ;;; Child process terminated, stopped, or continued. + ;;; Action: Ignored. + $chld + ;;; Continue executing, if stopped. + ;;; Action: Continues executing, if stopped. + $cont + ;;; Stop executing. + ;;; Action: Stops executing. + $stop + ;;; Terminal stop signal. + ;;; Action: Stops executing. + $tstp + ;;; Background process attempting read. + ;;; Action: Stops executing. + $ttin + ;;; Background process attempting write. + ;;; Action: Stops executing. + $ttou + ;;; High bandwidth data is available at a socket. + ;;; Action: Ignored. + $urg + ;;; CPU time limit exceeded. + ;;; Action: Terminates the process. + $xcpu + ;;; File size limit exceeded. + ;;; Action: Terminates the process. + $xfsz + ;;; Virtual timer expired. + ;;; Action: Terminates the process. + $vtalrm + ;;; Profiling timer expired. + ;;; Action: Terminates the process. + $prof + ;;; Window changed. + ;;; Action: Ignored. + $winch + ;;; I/O possible. + ;;; Action: Terminates the process. + $poll + ;;; Power failure. + ;;; Action: Terminates the process. + $pwr + ;;; Bad system call. + ;;; Action: Terminates the process. + $sys + ) +) + +;;; Flags provided to `sock_recv`. +(typename $riflags + (flags (@witx repr u16) + ;;; Returns the message without removing it from the socket's receive queue. + $recv_peek + ;;; On byte-stream sockets, block until the full amount of data can be returned. + $recv_waitall + ) +) + +;;; Flags returned by `sock_recv`. +(typename $roflags + (flags (@witx repr u16) + ;;; Returned by `sock_recv`: Message data has been truncated. + $recv_data_truncated + ) +) + +;;; Flags provided to `sock_send`. As there are currently no flags +;;; defined, it must be set to zero. +(typename $siflags u16) + +;;; Which channels on a socket to shut down. +(typename $sdflags + (flags (@witx repr u8) + ;;; Disables further receive operations. + $rd + ;;; Disables further send operations. + $wr + ) +) + +;;; Identifiers for preopened capabilities. +(typename $preopentype + (enum (@witx tag u8) + ;;; A pre-opened directory. + $dir + ) +) + +;;; The contents of a $prestat when type is `preopentype::dir`. +(typename $prestat_dir + (record + ;;; The length of the directory name for use with `fd_prestat_dir_name`. + (field $pr_name_len $size) + ) +) + +;;; Information about a pre-opened capability. +(typename $prestat + (union (@witx tag $preopentype) + $prestat_dir + ) +) + + diff --git a/crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx b/crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx new file mode 100644 index 0000000000..fe1c06255c --- /dev/null +++ b/crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx @@ -0,0 +1,511 @@ +;; WASI Preview. This is an evolution of the API that WASI initially +;; launched with. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/tree/master/docs/witx.md) +;; for an explanation of what that means. + +(use "typenames.witx") + +(module $wasi_snapshot_preview1 + ;;; Linear memory to be accessed by WASI functions that need it. + (import "memory" (memory)) + + ;;; Read command-line argument data. + ;;; The size of the array should match that returned by `args_sizes_get`. + ;;; Each argument is expected to be `\0` terminated. + (@interface func (export "args_get") + (param $argv (@witx pointer (@witx pointer u8))) + (param $argv_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return command-line argument data sizes. + (@interface func (export "args_sizes_get") + ;;; Returns the number of arguments and the size of the argument string + ;;; data, or an error. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Read environment variable data. + ;;; The sizes of the buffers should match that returned by `environ_sizes_get`. + ;;; Key/value pairs are expected to be joined with `=`s, and terminated with `\0`s. + (@interface func (export "environ_get") + (param $environ (@witx pointer (@witx pointer u8))) + (param $environ_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return environment variable data sizes. + (@interface func (export "environ_sizes_get") + ;;; Returns the number of environment variable arguments and the size of the + ;;; environment variable data. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Return the resolution of a clock. + ;;; Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, + ;;; return `errno::inval`. + ;;; Note: This is similar to `clock_getres` in POSIX. + (@interface func (export "clock_res_get") + ;;; The clock for which to return the resolution. + (param $id $clockid) + ;;; The resolution of the clock, or an error if one happened. + (result $error (expected $timestamp (error $errno))) + ) + ;;; Return the time value of a clock. + ;;; Note: This is similar to `clock_gettime` in POSIX. + (@interface func (export "clock_time_get") + ;;; The clock for which to return the time. + (param $id $clockid) + ;;; The maximum lag (exclusive) that the returned time value may have, compared to its actual value. + (param $precision $timestamp) + ;;; The time value of the clock. + (result $error (expected $timestamp (error $errno))) + ) + + ;;; Provide file advisory information on a file descriptor. + ;;; Note: This is similar to `posix_fadvise` in POSIX. + (@interface func (export "fd_advise") + (param $fd $fd) + ;;; The offset within the file to which the advisory applies. + (param $offset $filesize) + ;;; The length of the region to which the advisory applies. + (param $len $filesize) + ;;; The advice. + (param $advice $advice) + (result $error (expected (error $errno))) + ) + + ;;; Force the allocation of space in a file. + ;;; Note: This is similar to `posix_fallocate` in POSIX. + (@interface func (export "fd_allocate") + (param $fd $fd) + ;;; The offset at which to start the allocation. + (param $offset $filesize) + ;;; The length of the area that is allocated. + (param $len $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Close a file descriptor. + ;;; Note: This is similar to `close` in POSIX. + (@interface func (export "fd_close") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Synchronize the data of a file to disk. + ;;; Note: This is similar to `fdatasync` in POSIX. + (@interface func (export "fd_datasync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Get the attributes of a file descriptor. + ;;; Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. + (@interface func (export "fd_fdstat_get") + (param $fd $fd) + ;;; The buffer where the file descriptor's attributes are stored. + (result $error (expected $fdstat (error $errno))) + ) + + ;;; Adjust the flags associated with a file descriptor. + ;;; Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + (@interface func (export "fd_fdstat_set_flags") + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the rights associated with a file descriptor. + ;;; This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights + (@interface func (export "fd_fdstat_set_rights") + (param $fd $fd) + ;;; The desired rights of the file descriptor. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of an open file. + (@interface func (export "fd_filestat_get") + (param $fd $fd) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + ;;; Note: This is similar to `ftruncate` in POSIX. + (@interface func (export "fd_filestat_set_size") + (param $fd $fd) + ;;; The desired file size. + (param $size $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the timestamps of an open file or directory. + ;;; Note: This is similar to `futimens` in POSIX. + (@interface func (export "fd_filestat_set_times") + (param $fd $fd) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Read from a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `preadv` in POSIX. + (@interface func (export "fd_pread") + (param $fd $fd) + ;;; List of scatter/gather vectors in which to store data. + (param $iovs $iovec_array) + ;;; The offset within the file at which to read. + (param $offset $filesize) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_get") + (param $fd $fd) + ;;; The buffer where the description is stored. + (result $error (expected $prestat (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_dir_name") + (param $fd $fd) + ;;; A buffer into which to write the preopened directory name. + (param $path (@witx pointer u8)) + (param $path_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Write to a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `pwritev` in POSIX. + (@interface func (export "fd_pwrite") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + ;;; The offset within the file at which to write. + (param $offset $filesize) + ;;; The number of bytes written. + (result $error (expected $size (error $errno))) + ) + + ;;; Read from a file descriptor. + ;;; Note: This is similar to `readv` in POSIX. + (@interface func (export "fd_read") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $iovs $iovec_array) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Read directory entries from a directory. + ;;; When successful, the contents of the output buffer consist of a sequence of + ;;; directory entries. Each directory entry consists of a `dirent` object, + ;;; followed by `dirent::d_namlen` bytes holding the name of the directory + ;;; entry. + ;; + ;;; This function fills the output buffer as much as possible, potentially + ;;; truncating the last directory entry. This allows the caller to grow its + ;;; read buffer size in case it's too small to fit a single large directory + ;;; entry, or skip the oversized directory entry. + (@interface func (export "fd_readdir") + (param $fd $fd) + ;;; The buffer where directory entries are stored + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The location within the directory to start reading + (param $cookie $dircookie) + ;;; The number of bytes stored in the read buffer. If less than the size of the read buffer, the end of the directory has been reached. + (result $error (expected $size (error $errno))) + ) + + ;;; Atomically replace a file descriptor by renumbering another file descriptor. + ;; + ;;; Due to the strong focus on thread safety, this environment does not provide + ;;; a mechanism to duplicate or renumber a file descriptor to an arbitrary + ;;; number, like `dup2()`. This would be prone to race conditions, as an actual + ;;; file descriptor with the same number could be allocated by a different + ;;; thread at the same time. + ;; + ;;; This function provides a way to atomically renumber file descriptors, which + ;;; would disappear if `dup2()` were to be removed entirely. + (@interface func (export "fd_renumber") + (param $fd $fd) + ;;; The file descriptor to overwrite. + (param $to $fd) + (result $error (expected (error $errno))) + ) + + ;;; Move the offset of a file descriptor. + ;;; Note: This is similar to `lseek` in POSIX. + (@interface func (export "fd_seek") + (param $fd $fd) + ;;; The number of bytes to move. + (param $offset $filedelta) + ;;; The base from which the offset is relative. + (param $whence $whence) + ;;; The new offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Synchronize the data and metadata of a file to disk. + ;;; Note: This is similar to `fsync` in POSIX. + (@interface func (export "fd_sync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Return the current offset of a file descriptor. + ;;; Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + (@interface func (export "fd_tell") + (param $fd $fd) + ;;; The current offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Write to a file descriptor. + ;;; Note: This is similar to `writev` in POSIX. + (@interface func (export "fd_write") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + (result $error (expected $size (error $errno))) + ) + + ;;; Create a directory. + ;;; Note: This is similar to `mkdirat` in POSIX. + (@interface func (export "path_create_directory") + (param $fd $fd) + ;;; The path at which to create the directory. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of a file or directory. + ;;; Note: This is similar to `stat` in POSIX. + (@interface func (export "path_filestat_get") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to inspect. + (param $path string) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the timestamps of a file or directory. + ;;; Note: This is similar to `utimensat` in POSIX. + (@interface func (export "path_filestat_set_times") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to operate on. + (param $path string) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Create a hard link. + ;;; Note: This is similar to `linkat` in POSIX. + (@interface func (export "path_link") + (param $old_fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $old_flags $lookupflags) + ;;; The source path from which to link. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path at which to create the hard link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Open a file or directory. + ;; + ;;; The returned file descriptor is not guaranteed to be the lowest-numbered + ;;; file descriptor not currently open; it is randomized to prevent + ;;; applications from depending on making assumptions about indexes, since this + ;;; is error-prone in multi-threaded contexts. The returned file descriptor is + ;;; guaranteed to be less than 2**31. + ;; + ;;; Note: This is similar to `openat` in POSIX. + (@interface func (export "path_open") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $dirflags $lookupflags) + ;;; The relative path of the file or directory to open, relative to the + ;;; `path_open::fd` directory. + (param $path string) + ;;; The method by which to open the file. + (param $oflags $oflags) + ;;; The initial rights of the newly created file descriptor. The + ;;; implementation is allowed to return a file descriptor with fewer rights + ;;; than specified, if and only if those rights do not apply to the type of + ;;; file being opened. + ;; + ;;; The *base* rights are rights that will apply to operations using the file + ;;; descriptor itself, while the *inheriting* rights are rights that apply to + ;;; file descriptors derived from it. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (param $fdflags $fdflags) + ;;; The file descriptor of the file that has been opened. + (result $error (expected $fd (error $errno))) + ) + + ;;; Read the contents of a symbolic link. + ;;; Note: This is similar to `readlinkat` in POSIX. + (@interface func (export "path_readlink") + (param $fd $fd) + ;;; The path of the symbolic link from which to read. + (param $path string) + ;;; The buffer to which to write the contents of the symbolic link. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The number of bytes placed in the buffer. + (result $error (expected $size (error $errno))) + ) + + ;;; Remove a directory. + ;;; Return `errno::notempty` if the directory is not empty. + ;;; Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + (@interface func (export "path_remove_directory") + (param $fd $fd) + ;;; The path to a directory to remove. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Rename a file or directory. + ;;; Note: This is similar to `renameat` in POSIX. + (@interface func (export "path_rename") + (param $fd $fd) + ;;; The source path of the file or directory to rename. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path to which to rename the file or directory. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Create a symbolic link. + ;;; Note: This is similar to `symlinkat` in POSIX. + (@interface func (export "path_symlink") + ;;; The contents of the symbolic link. + (param $old_path string) + (param $fd $fd) + ;;; The destination path at which to create the symbolic link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + + ;;; Unlink a file. + ;;; Return `errno::isdir` if the path refers to a directory. + ;;; Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + (@interface func (export "path_unlink_file") + (param $fd $fd) + ;;; The path to a file to unlink. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Concurrently poll for the occurrence of a set of events. + (@interface func (export "poll_oneoff") + ;;; The events to which to subscribe. + (param $in (@witx const_pointer $subscription)) + ;;; The events that have occurred. + (param $out (@witx pointer $event)) + ;;; Both the number of subscriptions and events. + (param $nsubscriptions $size) + ;;; The number of events stored. + (result $error (expected $size (error $errno))) + ) + + ;;; Terminate the process normally. An exit code of 0 indicates successful + ;;; termination of the program. The meanings of other values is dependent on + ;;; the environment. + (@interface func (export "proc_exit") + ;;; The exit code returned by the process. + (param $rval $exitcode) + (@witx noreturn) + ) + + ;;; Send a signal to the process of the calling thread. + ;;; Note: This is similar to `raise` in POSIX. + (@interface func (export "proc_raise") + ;;; The signal condition to trigger. + (param $sig $signal) + (result $error (expected (error $errno))) + ) + + ;;; Temporarily yield execution of the calling thread. + ;;; Note: This is similar to `sched_yield` in POSIX. + (@interface func (export "sched_yield") + (result $error (expected (error $errno))) + ) + + ;;; Write high-quality random data into a buffer. + ;;; This function blocks when the implementation is unable to immediately + ;;; provide sufficient high-quality random data. + ;;; This function may execute slowly, so when large mounts of random data are + ;;; required, it's advisable to use this function to seed a pseudo-random + ;;; number generator, rather than to provide the random data directly. + (@interface func (export "random_get") + ;;; The buffer to fill with random data. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Receive a message from a socket. + ;;; Note: This is similar to `recv` in POSIX, though it also supports reading + ;;; the data into multiple buffers in the manner of `readv`. + (@interface func (export "sock_recv") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $ri_data $iovec_array) + ;;; Message flags. + (param $ri_flags $riflags) + ;;; Number of bytes stored in ri_data and message flags. + (result $error (expected (tuple $size $roflags) (error $errno))) + ) + + ;;; Send a message on a socket. + ;;; Note: This is similar to `send` in POSIX, though it also supports writing + ;;; the data from multiple buffers in the manner of `writev`. + (@interface func (export "sock_send") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to retrieve data + (param $si_data $ciovec_array) + ;;; Message flags. + (param $si_flags $siflags) + ;;; Number of bytes transmitted. + (result $error (expected $size (error $errno))) + ) + + ;;; Shut down socket send and receive channels. + ;;; Note: This is similar to `shutdown` in POSIX. + (@interface func (export "sock_shutdown") + (param $fd $fd) + ;;; Which channels on the socket to shut down. + (param $how $sdflags) + (result $error (expected (error $errno))) + ) +) + diff --git a/crates/execution/assets/agentos-wasm-abi.json b/crates/execution/assets/agentos-wasm-abi.json new file mode 100644 index 0000000000..29ecc28612 --- /dev/null +++ b/crates/execution/assets/agentos-wasm-abi.json @@ -0,0 +1,5994 @@ +{ + "schemaVersion": 2, + "abiVersion": "agentos-wasm-host-v1", + "source": { + "preview1Module": "wasi_snapshot_preview1", + "preview1CompatibilityAlias": "wasi_unstable", + "preview1WitxCommit": "d4d3df3072b65ce43cb01c1add72b402d69a79d1", + "preview1Witx": [ + { + "path": "crates/execution/abi/wasi_snapshot_preview1/typenames.witx", + "sha256": "d8144bbc5fca9b88590ac5ff2cc4920c8a4718ead6711edd2ac7086a9fb4cff3" + }, + { + "path": "crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx", + "sha256": "25d7073b323e63171b1430ef380b18e6fc2cc50000b3636a89a4186f9ecab418" + } + ], + "preview1Generator": "agentos-wasm-abi-generator@0.0.1 (witx=0.9.1)", + "wasiLibcCommit": "574b88da481569b65a237cb80daf9a2d5aeaf82d", + "customAbiInventory": "docs/design/wasmtime-phase-0.md" + }, + "moduleAliases": { + "wasi_unstable": "wasi_snapshot_preview1" + }, + "representation": { + "byteOrder": "little", + "pointerBits": 32, + "sizeBits": 32, + "layouts": { + "ciovec": { + "size": 8, + "align": 4, + "fields": { + "buf": 0, + "buf_len": 4 + } + }, + "dirent": { + "size": 24, + "align": 8, + "fields": { + "d_ino": 8, + "d_namlen": 16, + "d_next": 0, + "d_type": 20 + } + }, + "event": { + "size": 32, + "align": 8, + "fields": { + "error": 8, + "fd_readwrite": 16, + "type": 10, + "userdata": 0 + } + }, + "fdstat": { + "size": 24, + "align": 8, + "fields": { + "fs_filetype": 0, + "fs_flags": 2, + "fs_rights_base": 8, + "fs_rights_inheriting": 16 + } + }, + "filestat": { + "size": 64, + "align": 8, + "fields": { + "atim": 40, + "ctim": 56, + "dev": 0, + "filetype": 16, + "ino": 8, + "mtim": 48, + "nlink": 24, + "size": 32 + } + }, + "iovec": { + "size": 8, + "align": 4, + "fields": { + "buf": 0, + "buf_len": 4 + } + }, + "prestat": { + "size": 8, + "align": 4, + "fields": { + "name_len": 4, + "tag": 0 + } + }, + "subscription": { + "size": 48, + "align": 8, + "fields": { + "clock_or_fd": 16, + "type": 8, + "userdata": 0 + } + } + } + }, + "modulePolicy": { + "wasi_snapshot_preview1": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "wasi_unstable": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "host_fs": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "host_user": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "host_tty": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "host_system": [ + "isolated", + "read-only", + "read-write", + "full" + ], + "host_net": [ + "full" + ], + "host_process": [ + "full" + ] + }, + "importPolicyOverrides": { + "host_process.fd_dup_min": [ + "read-only", + "read-write", + "full" + ], + "host_process.fd_flock": [ + "read-only", + "read-write", + "full" + ], + "host_process.fd_getfd": [ + "read-only", + "read-write", + "full" + ], + "host_process.fd_record_lock": [ + "read-only", + "read-write", + "full" + ], + "host_process.fd_setfd": [ + "read-only", + "read-write", + "full" + ], + "host_process.proc_getrlimit": [ + "read-only", + "read-write", + "full" + ], + "host_process.proc_setrlimit": [ + "read-only", + "read-write", + "full" + ], + "host_process.proc_umask": [ + "read-only", + "read-write", + "full" + ], + "host_process.umask": [ + "read-only", + "read-write", + "full" + ] + }, + "coreSignatures": [ + { + "id": "I32I32I32I32I32I64I64I32I32ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i64", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I32I32I32I64I64I32ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i64", + "i64", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I32I32I32I64ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i64" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I32I32I64I32ToI32", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I32I32I64I64I32I32I32I32ToI32", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i64", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I32I64I64I32I32I32I32ToI32", + "params": [ + "i32", + "i32", + "i64", + "i64", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64I32I32ToI32", + "params": [ + "i32", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64I32ToI32", + "params": [ + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64I64I32I32ToI32", + "params": [ + "i32", + "i64", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64I64I32ToI32", + "params": [ + "i32", + "i64", + "i64", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64I64ToI32", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32I64ToI32", + "params": [ + "i32", + "i64" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32ToI32", + "params": [ + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32ToI64", + "params": [ + "i32" + ], + "results": [ + "i64" + ] + }, + { + "id": "I32ToNoResults", + "params": [ + "i32" + ], + "results": [] + }, + { + "id": "I32x10ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x12ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x17ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x21ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x2ToI32", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x3ToI32", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x4ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x4ToI64", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i64" + ] + }, + { + "id": "I32x5ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x6ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x7ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x8ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "I32x9ToI32", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ] + }, + { + "id": "NoParamsToI32", + "params": [], + "results": [ + "i32" + ] + } + ], + "bindings": { + "host_fs.chmod": { + "id": "HostFsChmod", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "MetadataMode", + "decode": "HostFsChmod", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.chown": { + "id": "HostFsChown", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "MetadataOwnership", + "decode": "PathOwnership", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fchmod": { + "id": "HostFsFchmod", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "MetadataMode", + "decode": "HostFsFchmod", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fchown": { + "id": "HostFsFchown", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "MetadataOwnership", + "decode": "DescriptorOwnership", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_blocks": { + "id": "HostFsFdBlocks", + "status": "canonical", + "coreSignature": "I32ToI64", + "handler": "DescriptorMetadata", + "decode": "HostFsFdBlocks", + "encode": "ScalarI64MaxOnError", + "returnKind": "ScalarI64", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_chown": { + "id": "HostFsFdChown", + "status": "compatibility", + "coreSignature": "I32x3ToI32", + "handler": "MetadataOwnership", + "decode": "DescriptorOwnership", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_collapse_range": { + "id": "HostFsFdCollapseRange", + "status": "canonical", + "coreSignature": "I32I64I64ToI32", + "handler": "ExtentRange", + "decode": "HostFsFdCollapseRange", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_fiemap": { + "id": "HostFsFdFiemap", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "HostFsFdFiemap", + "decode": "HostFsFdFiemap", + "encode": "HostFsFdFiemapOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_getxattr": { + "id": "HostFsFdGetxattr", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "DescriptorXattr", + "decode": "HostFsFdGetxattr", + "encode": "HostFsFdGetxattrOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_insert_range": { + "id": "HostFsFdInsertRange", + "status": "canonical", + "coreSignature": "I32I64I64ToI32", + "handler": "ExtentRange", + "decode": "HostFsFdInsertRange", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_link": { + "id": "HostFsFdLink", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "HostFsFdLink", + "decode": "HostFsFdLink", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_listxattr": { + "id": "HostFsFdListxattr", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "DescriptorXattr", + "decode": "HostFsFdListxattr", + "encode": "HostFsFdListxattrOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_mode": { + "id": "HostFsFdMode", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "DescriptorMetadata", + "decode": "HostFsFdMode", + "encode": "ScalarI32ZeroOnError", + "returnKind": "ScalarI32", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_owner": { + "id": "HostFsFdOwner", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "DescriptorMetadata", + "decode": "HostFsFdOwner", + "encode": "HostFsFdOwnerOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_punch_hole": { + "id": "HostFsFdPunchHole", + "status": "canonical", + "coreSignature": "I32I64I64ToI32", + "handler": "ExtentRange", + "decode": "HostFsFdPunchHole", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_removexattr": { + "id": "HostFsFdRemovexattr", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "DescriptorXattr", + "decode": "HostFsFdRemovexattr", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_setxattr": { + "id": "HostFsFdSetxattr", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "DescriptorXattr", + "decode": "HostFsFdSetxattr", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_size": { + "id": "HostFsFdSize", + "status": "canonical", + "coreSignature": "I32ToI64", + "handler": "DescriptorMetadata", + "decode": "HostFsFdSize", + "encode": "ScalarI64MaxOnError", + "returnKind": "ScalarI64", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.fd_zero_range": { + "id": "HostFsFdZeroRange", + "status": "canonical", + "coreSignature": "I32I64I64I32ToI32", + "handler": "ExtentRange", + "decode": "HostFsFdZeroRange", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.ftruncate": { + "id": "HostFsFtruncate", + "status": "compatibility", + "coreSignature": "I32I64ToI32", + "handler": "DescriptorSetLength", + "decode": "FdSetLength", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.open_tmpfile": { + "id": "HostFsOpenTmpfile", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "HostFsOpenTmpfile", + "decode": "HostFsOpenTmpfile", + "encode": "HostFsOpenTmpfileOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_access": { + "id": "HostFsPathAccess", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "HostFsPathAccess", + "decode": "HostFsPathAccess", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_blocks": { + "id": "HostFsPathBlocks", + "status": "canonical", + "coreSignature": "I32x4ToI64", + "handler": "PathMetadata", + "decode": "HostFsPathBlocks", + "encode": "ScalarI64MaxOnError", + "returnKind": "ScalarI64", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_chown": { + "id": "HostFsPathChown", + "status": "compatibility", + "coreSignature": "I32x6ToI32", + "handler": "MetadataOwnership", + "decode": "PathOwnership", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_getxattr": { + "id": "HostFsPathGetxattr", + "status": "canonical", + "coreSignature": "I32x9ToI32", + "handler": "PathXattr", + "decode": "HostFsPathGetxattr", + "encode": "HostFsPathGetxattrOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_listxattr": { + "id": "HostFsPathListxattr", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "PathXattr", + "decode": "HostFsPathListxattr", + "encode": "HostFsPathListxattrOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_mknod": { + "id": "HostFsPathMknod", + "status": "canonical", + "coreSignature": "I32I32I32I32I64ToI32", + "handler": "HostFsPathMknod", + "decode": "HostFsPathMknod", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_mode": { + "id": "HostFsPathMode", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "PathMetadata", + "decode": "HostFsPathMode", + "encode": "ScalarI32ZeroOnError", + "returnKind": "ScalarI32", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_owner": { + "id": "HostFsPathOwner", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "PathMetadata", + "decode": "HostFsPathOwner", + "encode": "HostFsPathOwnerOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_rdev": { + "id": "HostFsPathRdev", + "status": "canonical", + "coreSignature": "I32x4ToI64", + "handler": "PathMetadata", + "decode": "HostFsPathRdev", + "encode": "ScalarI64ZeroOnError", + "returnKind": "ScalarI64", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_removexattr": { + "id": "HostFsPathRemovexattr", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "PathXattr", + "decode": "HostFsPathRemovexattr", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_renameat2": { + "id": "HostFsPathRenameat2", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "PathRename", + "decode": "HostFsPathRenameat2", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_setxattr": { + "id": "HostFsPathSetxattr", + "status": "canonical", + "coreSignature": "I32x9ToI32", + "handler": "PathXattr", + "decode": "HostFsPathSetxattr", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_size": { + "id": "HostFsPathSize", + "status": "canonical", + "coreSignature": "I32x4ToI64", + "handler": "PathMetadata", + "decode": "HostFsPathSize", + "encode": "ScalarI64MaxOnError", + "returnKind": "ScalarI64", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.path_statfs": { + "id": "HostFsPathStatfs", + "status": "canonical", + "coreSignature": "I32x8ToI32", + "handler": "HostFsPathStatfs", + "decode": "HostFsPathStatfs", + "encode": "HostFsPathStatfsOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.remount": { + "id": "HostFsRemount", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "HostFsRemount", + "decode": "HostFsRemount", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.set_open_direct": { + "id": "HostFsSetOpenDirect", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "HostFsSetOpenDirect", + "decode": "HostFsSetOpenDirect", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Local", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_fs.set_open_mode": { + "id": "HostFsSetOpenMode", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "HostFsSetOpenMode", + "decode": "HostFsSetOpenMode", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Local", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_net.net_accept": { + "id": "HostNetNetAccept", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "HostNetNetAccept", + "decode": "HostNetNetAccept", + "encode": "HostNetNetAcceptOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_bind": { + "id": "HostNetNetBind", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostNetNetBind", + "decode": "HostNetNetBind", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_close": { + "id": "HostNetNetClose", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "DescriptorClose", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_connect": { + "id": "HostNetNetConnect", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostNetNetConnect", + "decode": "HostNetNetConnect", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_dns_query_rr_v1": { + "id": "HostNetNetDnsQueryRrV1", + "status": "canonical", + "coreSignature": "I32x8ToI32", + "handler": "HostNetNetDnsQueryRrV1", + "decode": "HostNetNetDnsQueryRrV1", + "encode": "HostNetNetDnsQueryRrV1Output", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_getaddrinfo": { + "id": "HostNetNetGetaddrinfo", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "HostNetNetGetaddrinfo", + "decode": "HostNetNetGetaddrinfo", + "encode": "HostNetNetGetaddrinfoOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_getpeername": { + "id": "HostNetNetGetpeername", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "NetworkAddress", + "decode": "NetworkAddressOutput", + "encode": "NetworkAddressOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_getsockname": { + "id": "HostNetNetGetsockname", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "NetworkAddress", + "decode": "NetworkAddressOutput", + "encode": "NetworkAddressOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_getsockopt": { + "id": "HostNetNetGetsockopt", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "NetworkOption", + "decode": "HostNetNetGetsockopt", + "encode": "HostNetNetGetsockoptOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_listen": { + "id": "HostNetNetListen", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostNetNetListen", + "decode": "HostNetNetListen", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_poll": { + "id": "HostNetNetPoll", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "ProcessPoll", + "decode": "HostNetNetPoll", + "encode": "HostNetNetPollOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_recv": { + "id": "HostNetNetRecv", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "NetworkReceive", + "decode": "HostNetNetRecv", + "encode": "HostNetNetRecvOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_recvfrom": { + "id": "HostNetNetRecvfrom", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "NetworkReceive", + "decode": "HostNetNetRecvfrom", + "encode": "HostNetNetRecvfromOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_send": { + "id": "HostNetNetSend", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "NetworkSend", + "decode": "HostNetNetSend", + "encode": "HostNetNetSendOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_sendto": { + "id": "HostNetNetSendto", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "NetworkSend", + "decode": "HostNetNetSendto", + "encode": "HostNetNetSendtoOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_set_nonblock": { + "id": "HostNetNetSetNonblock", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorStatusFlags", + "decode": "HostNetNetSetNonblock", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_setsockopt": { + "id": "HostNetNetSetsockopt", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "NetworkOption", + "decode": "HostNetNetSetsockopt", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_socket": { + "id": "HostNetNetSocket", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "HostNetNetSocket", + "decode": "HostNetNetSocket", + "encode": "HostNetNetSocketOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_tls_connect": { + "id": "HostNetNetTlsConnect", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostNetNetTlsConnect", + "decode": "HostNetNetTlsConnect", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_validate_accept": { + "id": "HostNetNetValidateAccept", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "NetworkValidate", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_net.net_validate_socket": { + "id": "HostNetNetValidateSocket", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "NetworkValidate", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_dup": { + "id": "HostProcessFdDup", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorDuplicate", + "decode": "HostProcessFdDup", + "encode": "HostProcessFdDupOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_dup_min": { + "id": "HostProcessFdDupMin", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "DescriptorDuplicate", + "decode": "HostProcessFdDupMin", + "encode": "HostProcessFdDupMinOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.fd_dup2": { + "id": "HostProcessFdDup2", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorDuplicate", + "decode": "HostProcessFdDup2", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_flock": { + "id": "HostProcessFdFlock", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorLock", + "decode": "HostProcessFdFlock", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.fd_getfd": { + "id": "HostProcessFdGetfd", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorFlags", + "decode": "HostProcessFdGetfd", + "encode": "HostProcessFdGetfdOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.fd_pipe": { + "id": "HostProcessFdPipe", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostProcessFdPipe", + "decode": "HostProcessFdPipe", + "encode": "HostProcessFdPipeOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_record_lock": { + "id": "HostProcessFdRecordLock", + "status": "canonical", + "coreSignature": "I32I32I32I64I64I32I32I32I32ToI32", + "handler": "DescriptorLock", + "decode": "HostProcessFdRecordLock", + "encode": "HostProcessFdRecordLockOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.fd_recvmsg_rights": { + "id": "HostProcessFdRecvmsgRights", + "status": "canonical", + "coreSignature": "I32x9ToI32", + "handler": "DescriptorRights", + "decode": "HostProcessFdRecvmsgRights", + "encode": "HostProcessFdRecvmsgRightsOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_sendmsg_rights": { + "id": "HostProcessFdSendmsgRights", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "DescriptorRights", + "decode": "HostProcessFdSendmsgRights", + "encode": "HostProcessFdSendmsgRightsOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.fd_setfd": { + "id": "HostProcessFdSetfd", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorFlags", + "decode": "HostProcessFdSetfd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.fd_socketpair": { + "id": "HostProcessFdSocketpair", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "HostProcessFdSocketpair", + "decode": "HostProcessFdSocketpair", + "encode": "HostProcessFdSocketpairOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_closefrom": { + "id": "HostProcessProcClosefrom", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "HostProcessProcClosefrom", + "decode": "HostProcessProcClosefrom", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_exec": { + "id": "HostProcessProcExec", + "status": "canonical", + "coreSignature": "I32x8ToI32", + "handler": "ProcessExec", + "decode": "HostProcessProcExec", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Terminal", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_fexec": { + "id": "HostProcessProcFexec", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "ProcessExec", + "decode": "HostProcessProcFexec", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Terminal", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_getpgid": { + "id": "HostProcessProcGetpgid", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessGroup", + "decode": "HostProcessProcGetpgid", + "encode": "HostProcessProcGetpgidOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_getpid": { + "id": "HostProcessProcGetpid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "HostProcessProcGetpid", + "decode": "HostProcessProcGetpid", + "encode": "HostProcessProcGetpidOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_getppid": { + "id": "HostProcessProcGetppid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "HostProcessProcGetppid", + "decode": "HostProcessProcGetppid", + "encode": "HostProcessProcGetppidOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_getrlimit": { + "id": "HostProcessProcGetrlimit", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostProcessProcGetrlimit", + "decode": "HostProcessProcGetrlimit", + "encode": "HostProcessProcGetrlimitOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.proc_itimer_real": { + "id": "HostProcessProcItimerReal", + "status": "canonical", + "coreSignature": "I32I64I64I32I32ToI32", + "handler": "HostProcessProcItimerReal", + "decode": "HostProcessProcItimerReal", + "encode": "HostProcessProcItimerRealOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_kill": { + "id": "HostProcessProcKill", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostProcessProcKill", + "decode": "HostProcessProcKill", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_ppoll_v1": { + "id": "HostProcessProcPpollV1", + "status": "canonical", + "coreSignature": "I32I32I64I64I32I32I32I32ToI32", + "handler": "ProcessPoll", + "decode": "HostProcessProcPpollV1", + "encode": "HostProcessProcPpollV1Output", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_setpgid": { + "id": "HostProcessProcSetpgid", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessGroup", + "decode": "HostProcessProcSetpgid", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_setrlimit": { + "id": "HostProcessProcSetrlimit", + "status": "canonical", + "coreSignature": "I32I64I64ToI32", + "handler": "HostProcessProcSetrlimit", + "decode": "HostProcessProcSetrlimit", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.proc_sigaction": { + "id": "HostProcessProcSigaction", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "HostProcessProcSigaction", + "decode": "HostProcessProcSigaction", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_signal_mask_v2": { + "id": "HostProcessProcSignalMaskV2", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "HostProcessProcSignalMaskV2", + "decode": "HostProcessProcSignalMaskV2", + "encode": "HostProcessProcSignalMaskV2Output", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_spawn": { + "id": "HostProcessProcSpawn", + "status": "compatibility", + "coreSignature": "I32x10ToI32", + "handler": "ProcessSpawn", + "decode": "HostProcessProcSpawn", + "encode": "ProcessIdOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_spawn_v2": { + "id": "HostProcessProcSpawnV2", + "status": "compatibility", + "coreSignature": "I32x12ToI32", + "handler": "ProcessSpawn", + "decode": "HostProcessProcSpawnV2", + "encode": "ProcessIdOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_spawn_v3": { + "id": "HostProcessProcSpawnV3", + "status": "compatibility", + "coreSignature": "I32x17ToI32", + "handler": "ProcessSpawn", + "decode": "HostProcessProcSpawnV3", + "encode": "ProcessIdOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_spawn_v4": { + "id": "HostProcessProcSpawnV4", + "status": "canonical", + "coreSignature": "I32x21ToI32", + "handler": "ProcessSpawn", + "decode": "HostProcessProcSpawnV4", + "encode": "ProcessIdOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_umask": { + "id": "HostProcessProcUmask", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessUmask", + "decode": "HostProcessProcUmask", + "encode": "HostProcessProcUmaskOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_process.proc_waitpid": { + "id": "HostProcessProcWaitpid", + "status": "compatibility", + "coreSignature": "I32x4ToI32", + "handler": "ProcessWait", + "decode": "HostProcessProcWaitpid", + "encode": "HostProcessProcWaitpidOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_waitpid_v2": { + "id": "HostProcessProcWaitpidV2", + "status": "compatibility", + "coreSignature": "I32x6ToI32", + "handler": "ProcessWait", + "decode": "HostProcessProcWaitpidV2", + "encode": "HostProcessProcWaitpidV2Output", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.proc_waitpid_v3": { + "id": "HostProcessProcWaitpidV3", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "ProcessWait", + "decode": "HostProcessProcWaitpidV3", + "encode": "HostProcessProcWaitpidV3Output", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.pty_open": { + "id": "HostProcessPtyOpen", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostProcessPtyOpen", + "decode": "HostProcessPtyOpen", + "encode": "HostProcessPtyOpenOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "full" + ] + }, + "host_process.sleep_ms": { + "id": "HostProcessSleepMs", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "HostProcessSleepMs", + "decode": "HostProcessSleepMs", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "full" + ] + }, + "host_process.umask": { + "id": "HostProcessUmask", + "status": "compatibility", + "coreSignature": "I32x3ToI32", + "handler": "ProcessUmask", + "decode": "HostProcessUmask", + "encode": "HostProcessUmaskOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "read-only", + "read-write", + "full" + ] + }, + "host_system.get_identity": { + "id": "HostSystemGetIdentity", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostSystemGetIdentity", + "decode": "HostSystemGetIdentity", + "encode": "HostSystemGetIdentityOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.get_attr": { + "id": "HostTtyGetAttr", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "TerminalAttributes", + "decode": "HostTtyGetAttr", + "encode": "HostTtyGetAttrOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.get_pgrp": { + "id": "HostTtyGetPgrp", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "TerminalProcessGroup", + "decode": "TerminalU32Output", + "encode": "TerminalU32Output", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.get_sid": { + "id": "HostTtyGetSid", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostTtyGetSid", + "decode": "TerminalU32Output", + "encode": "TerminalU32Output", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.get_size": { + "id": "HostTtyGetSize", + "status": "compatibility", + "coreSignature": "I32x3ToI32", + "handler": "TerminalSize", + "decode": "HostTtyGetSize", + "encode": "HostTtyGetSizeOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.isatty": { + "id": "HostTtyIsatty", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "TerminalIsatty", + "decode": "Fd", + "encode": "ScalarI32ZeroOnError", + "returnKind": "ScalarI32", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.read": { + "id": "HostTtyRead", + "status": "compatibility", + "coreSignature": "I32x3ToI32", + "handler": "HostTtyRead", + "decode": "HostTtyRead", + "encode": "HostTtyReadOutput", + "returnKind": "ScalarI32", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.set_attr": { + "id": "HostTtySetAttr", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "TerminalAttributes", + "decode": "HostTtySetAttr", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.set_pgrp": { + "id": "HostTtySetPgrp", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "TerminalProcessGroup", + "decode": "HostTtySetPgrp", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.set_raw_mode": { + "id": "HostTtySetRawMode", + "status": "compatibility", + "coreSignature": "I32ToI32", + "handler": "HostTtySetRawMode", + "decode": "HostTtySetRawMode", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_tty.set_size": { + "id": "HostTtySetSize", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "TerminalSize", + "decode": "HostTtySetSize", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getegid": { + "id": "HostUserGetegid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityScalarOutput", + "encode": "IdentityScalarOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.geteuid": { + "id": "HostUserGeteuid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityScalarOutput", + "encode": "IdentityScalarOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getgid": { + "id": "HostUserGetgid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityScalarOutput", + "encode": "IdentityScalarOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getgrent": { + "id": "HostUserGetgrent", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "AccountGroup", + "decode": "AccountByIndex", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getgrgid": { + "id": "HostUserGetgrgid", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "AccountGroup", + "decode": "AccountById", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getgrnam": { + "id": "HostUserGetgrnam", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "AccountGroup", + "decode": "AccountByName", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getgroups": { + "id": "HostUserGetgroups", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "HostUserGetgroups", + "decode": "HostUserGetgroups", + "encode": "HostUserGetgroupsOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getpwent": { + "id": "HostUserGetpwent", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "AccountPassword", + "decode": "AccountByIndex", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getpwnam": { + "id": "HostUserGetpwnam", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "AccountPassword", + "decode": "AccountByName", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getpwuid": { + "id": "HostUserGetpwuid", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "AccountPassword", + "decode": "AccountById", + "encode": "AccountRecordOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getresgid": { + "id": "HostUserGetresgid", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityTripleOutput", + "encode": "IdentityTripleOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getresuid": { + "id": "HostUserGetresuid", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityTripleOutput", + "encode": "IdentityTripleOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.getuid": { + "id": "HostUserGetuid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentitySnapshot", + "decode": "IdentityScalarOutput", + "encode": "IdentityScalarOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.isatty": { + "id": "HostUserIsatty", + "status": "compatibility", + "coreSignature": "I32x2ToI32", + "handler": "TerminalIsatty", + "decode": "HostUserIsatty", + "encode": "HostUserIsattyOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setegid": { + "id": "HostUserSetegid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetOne", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.seteuid": { + "id": "HostUserSeteuid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetOne", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setgid": { + "id": "HostUserSetgid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetOne", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setgroups": { + "id": "HostUserSetgroups", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "HostUserSetgroups", + "decode": "HostUserSetgroups", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setregid": { + "id": "HostUserSetregid", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetTwo", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setresgid": { + "id": "HostUserSetresgid", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetThree", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setresuid": { + "id": "HostUserSetresuid", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetThree", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setreuid": { + "id": "HostUserSetreuid", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetTwo", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "host_user.setuid": { + "id": "HostUserSetuid", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "IdentityCredentials", + "decode": "IdentitySetOne", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.args_get": { + "id": "WasiSnapshotPreview1ArgsGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessArguments", + "decode": "WasiSnapshotPreview1ArgsGet", + "encode": "WasiSnapshotPreview1ArgsGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.args_sizes_get": { + "id": "WasiSnapshotPreview1ArgsSizesGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessArguments", + "decode": "WasiSnapshotPreview1ArgsSizesGet", + "encode": "WasiSnapshotPreview1ArgsSizesGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.clock_res_get": { + "id": "WasiSnapshotPreview1ClockResGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ClockSnapshot", + "decode": "WasiSnapshotPreview1ClockResGet", + "encode": "U64Output", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.clock_time_get": { + "id": "WasiSnapshotPreview1ClockTimeGet", + "status": "canonical", + "coreSignature": "I32I64I32ToI32", + "handler": "ClockSnapshot", + "decode": "WasiSnapshotPreview1ClockTimeGet", + "encode": "U64Output", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.environ_get": { + "id": "WasiSnapshotPreview1EnvironGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessEnvironment", + "decode": "WasiSnapshotPreview1EnvironGet", + "encode": "WasiSnapshotPreview1EnvironGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.environ_sizes_get": { + "id": "WasiSnapshotPreview1EnvironSizesGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "ProcessEnvironment", + "decode": "WasiSnapshotPreview1EnvironSizesGet", + "encode": "WasiSnapshotPreview1EnvironSizesGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_allocate": { + "id": "WasiSnapshotPreview1FdAllocate", + "status": "compatibility", + "coreSignature": "I32I64I64ToI32", + "handler": "ExtentRange", + "decode": "WasiSnapshotPreview1FdAllocate", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_close": { + "id": "WasiSnapshotPreview1FdClose", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "DescriptorClose", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_datasync": { + "id": "WasiSnapshotPreview1FdDatasync", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "DescriptorSync", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_fdstat_get": { + "id": "WasiSnapshotPreview1FdFdstatGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorStatusFlags", + "decode": "WasiSnapshotPreview1FdFdstatGet", + "encode": "WasiSnapshotPreview1FdFdstatGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_fdstat_set_flags": { + "id": "WasiSnapshotPreview1FdFdstatSetFlags", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorStatusFlags", + "decode": "WasiSnapshotPreview1FdFdstatSetFlags", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_filestat_get": { + "id": "WasiSnapshotPreview1FdFilestatGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorMetadata", + "decode": "WasiSnapshotPreview1FdFilestatGet", + "encode": "WasiSnapshotPreview1FdFilestatGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_filestat_set_size": { + "id": "WasiSnapshotPreview1FdFilestatSetSize", + "status": "canonical", + "coreSignature": "I32I64ToI32", + "handler": "DescriptorSetLength", + "decode": "FdSetLength", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_filestat_set_times": { + "id": "WasiSnapshotPreview1FdFilestatSetTimes", + "status": "compatibility", + "coreSignature": "I32I64I64I32ToI32", + "handler": "MetadataSetTimes", + "decode": "WasiSnapshotPreview1FdFilestatSetTimes", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_pread": { + "id": "WasiSnapshotPreview1FdPread", + "status": "canonical", + "coreSignature": "I32I32I32I64I32ToI32", + "handler": "DescriptorRead", + "decode": "WasiSnapshotPreview1FdPread", + "encode": "DescriptorReadOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_prestat_dir_name": { + "id": "WasiSnapshotPreview1FdPrestatDirName", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "Preopen", + "decode": "WasiSnapshotPreview1FdPrestatDirName", + "encode": "WasiSnapshotPreview1FdPrestatDirNameOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_prestat_get": { + "id": "WasiSnapshotPreview1FdPrestatGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "Preopen", + "decode": "WasiSnapshotPreview1FdPrestatGet", + "encode": "WasiSnapshotPreview1FdPrestatGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Bootstrap", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_pwrite": { + "id": "WasiSnapshotPreview1FdPwrite", + "status": "canonical", + "coreSignature": "I32I32I32I64I32ToI32", + "handler": "DescriptorWrite", + "decode": "WasiSnapshotPreview1FdPwrite", + "encode": "DescriptorWriteOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_read": { + "id": "WasiSnapshotPreview1FdRead", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "DescriptorRead", + "decode": "WasiSnapshotPreview1FdRead", + "encode": "DescriptorReadOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_readdir": { + "id": "WasiSnapshotPreview1FdReaddir", + "status": "canonical", + "coreSignature": "I32I32I32I64I32ToI32", + "handler": "WasiSnapshotPreview1FdReaddir", + "decode": "WasiSnapshotPreview1FdReaddir", + "encode": "WasiSnapshotPreview1FdReaddirOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_renumber": { + "id": "WasiSnapshotPreview1FdRenumber", + "status": "compatibility", + "coreSignature": "I32x2ToI32", + "handler": "WasiSnapshotPreview1FdRenumber", + "decode": "WasiSnapshotPreview1FdRenumber", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_seek": { + "id": "WasiSnapshotPreview1FdSeek", + "status": "canonical", + "coreSignature": "I32I64I32I32ToI32", + "handler": "DescriptorSeek", + "decode": "WasiSnapshotPreview1FdSeek", + "encode": "DescriptorOffsetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_sync": { + "id": "WasiSnapshotPreview1FdSync", + "status": "canonical", + "coreSignature": "I32ToI32", + "handler": "DescriptorSync", + "decode": "Fd", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_tell": { + "id": "WasiSnapshotPreview1FdTell", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "DescriptorSeek", + "decode": "WasiSnapshotPreview1FdTell", + "encode": "DescriptorOffsetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.fd_write": { + "id": "WasiSnapshotPreview1FdWrite", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "DescriptorWrite", + "decode": "WasiSnapshotPreview1FdWrite", + "encode": "DescriptorWriteOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_create_directory": { + "id": "WasiSnapshotPreview1PathCreateDirectory", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "WasiSnapshotPreview1PathCreateDirectory", + "decode": "WasiSnapshotPreview1PathCreateDirectory", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_filestat_get": { + "id": "WasiSnapshotPreview1PathFilestatGet", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "PathMetadata", + "decode": "WasiSnapshotPreview1PathFilestatGet", + "encode": "WasiSnapshotPreview1PathFilestatGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_filestat_set_times": { + "id": "WasiSnapshotPreview1PathFilestatSetTimes", + "status": "compatibility", + "coreSignature": "I32I32I32I32I64I64I32ToI32", + "handler": "MetadataSetTimes", + "decode": "WasiSnapshotPreview1PathFilestatSetTimes", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_link": { + "id": "WasiSnapshotPreview1PathLink", + "status": "canonical", + "coreSignature": "I32x7ToI32", + "handler": "WasiSnapshotPreview1PathLink", + "decode": "WasiSnapshotPreview1PathLink", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_open": { + "id": "WasiSnapshotPreview1PathOpen", + "status": "canonical", + "coreSignature": "I32I32I32I32I32I64I64I32I32ToI32", + "handler": "WasiSnapshotPreview1PathOpen", + "decode": "WasiSnapshotPreview1PathOpen", + "encode": "WasiSnapshotPreview1PathOpenOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "SignalRestartable", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_readlink": { + "id": "WasiSnapshotPreview1PathReadlink", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "WasiSnapshotPreview1PathReadlink", + "decode": "WasiSnapshotPreview1PathReadlink", + "encode": "WasiSnapshotPreview1PathReadlinkOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_remove_directory": { + "id": "WasiSnapshotPreview1PathRemoveDirectory", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "PathRemove", + "decode": "WasiSnapshotPreview1PathRemoveDirectory", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_rename": { + "id": "WasiSnapshotPreview1PathRename", + "status": "canonical", + "coreSignature": "I32x6ToI32", + "handler": "PathRename", + "decode": "WasiSnapshotPreview1PathRename", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_symlink": { + "id": "WasiSnapshotPreview1PathSymlink", + "status": "canonical", + "coreSignature": "I32x5ToI32", + "handler": "WasiSnapshotPreview1PathSymlink", + "decode": "WasiSnapshotPreview1PathSymlink", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.path_unlink_file": { + "id": "WasiSnapshotPreview1PathUnlinkFile", + "status": "canonical", + "coreSignature": "I32x3ToI32", + "handler": "PathRemove", + "decode": "WasiSnapshotPreview1PathUnlinkFile", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.poll_oneoff": { + "id": "WasiSnapshotPreview1PollOneoff", + "status": "canonical", + "coreSignature": "I32x4ToI32", + "handler": "ProcessPoll", + "decode": "WasiSnapshotPreview1PollOneoff", + "encode": "WasiSnapshotPreview1PollOneoffOutput", + "returnKind": "WasiErrno", + "executionClass": "Wait", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.proc_exit": { + "id": "WasiSnapshotPreview1ProcExit", + "status": "canonical", + "coreSignature": "I32ToNoResults", + "handler": "WasiSnapshotPreview1ProcExit", + "decode": "WasiSnapshotPreview1ProcExit", + "encode": "Void", + "returnKind": "Void", + "executionClass": "Terminal", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.random_get": { + "id": "WasiSnapshotPreview1RandomGet", + "status": "canonical", + "coreSignature": "I32x2ToI32", + "handler": "WasiSnapshotPreview1RandomGet", + "decode": "WasiSnapshotPreview1RandomGet", + "encode": "WasiSnapshotPreview1RandomGetOutput", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": true, + "prevalidateOutputs": true, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.sched_yield": { + "id": "WasiSnapshotPreview1SchedYield", + "status": "canonical", + "coreSignature": "NoParamsToI32", + "handler": "WasiSnapshotPreview1SchedYield", + "decode": "WasiSnapshotPreview1SchedYield", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Local", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + }, + "wasi_snapshot_preview1.sock_shutdown": { + "id": "WasiSnapshotPreview1SockShutdown", + "status": "compatibility", + "coreSignature": "I32x2ToI32", + "handler": "WasiSnapshotPreview1SockShutdown", + "decode": "WasiSnapshotPreview1SockShutdown", + "encode": "WasiErrno", + "returnKind": "WasiErrno", + "executionClass": "Host", + "restartability": "Never", + "transactional": false, + "prevalidateOutputs": false, + "permissionTiers": [ + "isolated", + "read-only", + "read-write", + "full" + ] + } + }, + "imports": [ + { + "module": "host_fs", + "name": "chmod", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "chown", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fchmod", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fchown", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_blocks", + "params": [ + "i32" + ], + "results": [ + "i64" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_chown", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_fs", + "name": "fd_collapse_range", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_fiemap", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_getxattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_insert_range", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_link", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_listxattr", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_mode", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_owner", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_punch_hole", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_removexattr", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_setxattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_size", + "params": [ + "i32" + ], + "results": [ + "i64" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "fd_zero_range", + "params": [ + "i32", + "i64", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "ftruncate", + "params": [ + "i32", + "i64" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_fs", + "name": "open_tmpfile", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_access", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_blocks", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i64" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_chown", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_fs", + "name": "path_getxattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_listxattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_mknod", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_mode", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_owner", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_rdev", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i64" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_removexattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_renameat2", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_setxattr", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_size", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i64" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "path_statfs", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "remount", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_fs", + "name": "set_open_direct", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_fs", + "name": "set_open_mode", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_net", + "name": "net_accept", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_bind", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_close", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_net", + "name": "net_connect", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_dns_query_rr_v1", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_getaddrinfo", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_getpeername", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_getsockname", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_getsockopt", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_listen", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_poll", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_recv", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_recvfrom", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_send", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_sendto", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_set_nonblock", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_setsockopt", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_socket", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_tls_connect", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_net", + "name": "net_validate_accept", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_net", + "name": "net_validate_socket", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "fd_dup", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_dup_min", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_dup2", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_flock", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_getfd", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_pipe", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_record_lock", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i64", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_recvmsg_rights", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_sendmsg_rights", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_setfd", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "fd_socketpair", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_closefrom", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_exec", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_fexec", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_getpgid", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_getpid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_getppid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_getrlimit", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_itimer_real", + "params": [ + "i32", + "i64", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_kill", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_ppoll_v1", + "params": [ + "i32", + "i32", + "i64", + "i64", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_setpgid", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_setrlimit", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_sigaction", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_signal_mask_v2", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_spawn", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "proc_spawn_v2", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "proc_spawn_v3", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "proc_spawn_v4", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_umask", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "proc_waitpid", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "proc_waitpid_v2", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "proc_waitpid_v3", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "pty_open", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_process", + "name": "sleep_ms", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_process", + "name": "umask", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_system", + "name": "get_identity", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "get_attr", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "get_pgrp", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "get_sid", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "get_size", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_tty", + "name": "isatty", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_tty", + "name": "read", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_tty", + "name": "set_attr", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "set_pgrp", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_tty", + "name": "set_raw_mode", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_tty", + "name": "set_size", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getegid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "geteuid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getgid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getgrent", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getgrgid", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getgrnam", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getgroups", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getpwent", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getpwnam", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getpwuid", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getresgid", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getresuid", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "getuid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "isatty", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "host_user", + "name": "setegid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "seteuid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setgid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setgroups", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setregid", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setresgid", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setresuid", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setreuid", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "host_user", + "name": "setuid", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "args_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "args_sizes_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "clock_res_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "clock_time_get", + "params": [ + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "environ_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "environ_sizes_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_allocate", + "params": [ + "i32", + "i64", + "i64" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_close", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_datasync", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_fdstat_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_fdstat_set_flags", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_filestat_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_filestat_set_size", + "params": [ + "i32", + "i64" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_filestat_set_times", + "params": [ + "i32", + "i64", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_pread", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_prestat_dir_name", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_prestat_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_pwrite", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_read", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_readdir", + "params": [ + "i32", + "i32", + "i32", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_renumber", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_seek", + "params": [ + "i32", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_sync", + "params": [ + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_tell", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "fd_write", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_create_directory", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_filestat_get", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_filestat_set_times", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i64", + "i64", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_link", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_open", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i64", + "i64", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_readlink", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_remove_directory", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_rename", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_symlink", + "params": [ + "i32", + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "path_unlink_file", + "params": [ + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "poll_oneoff", + "params": [ + "i32", + "i32", + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "proc_exit", + "params": [ + "i32" + ], + "results": [], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "random_get", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "sched_yield", + "params": [], + "results": [ + "i32" + ], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "sock_shutdown", + "params": [ + "i32", + "i32" + ], + "results": [ + "i32" + ], + "status": "compatibility" + } + ] +} diff --git a/crates/execution/assets/runners/python-runner.mjs b/crates/execution/assets/runners/python-runner.mjs index ebea35b7f0..5ecda5755c 100644 --- a/crates/execution/assets/runners/python-runner.mjs +++ b/crates/execution/assets/runners/python-runner.mjs @@ -12,6 +12,7 @@ const PYODIDE_INDEX_URL_ENV = 'AGENTOS_PYODIDE_INDEX_URL'; const PYODIDE_PACKAGE_BASE_URL_ENV = 'AGENTOS_PYODIDE_PACKAGE_BASE_URL'; const PYODIDE_PACKAGE_CACHE_DIR_ENV = 'AGENTOS_PYODIDE_PACKAGE_CACHE_DIR'; const PYODIDE_PACKAGE_CACHE_GUEST_ROOT = '/__agentos_pyodide_cache'; +const PYODIDE_PACKAGE_TEMP_GUEST_ROOT = '/__agentos_pyodide_package_tmp'; const PYTHON_CODE_ENV = 'AGENTOS_PYTHON_CODE'; const PYTHON_FILE_ENV = 'AGENTOS_PYTHON_FILE'; const PYTHON_ARGV_ENV = 'AGENTOS_PYTHON_ARGV'; @@ -518,18 +519,16 @@ function rejectPendingRpcRequests(pending, error) { } function normalizePythonBridgeError(error) { - const normalized = error instanceof Error ? error : new Error(String(error)); - const message = normalized.message || String(error); - const separatorIndex = message.indexOf(': '); - if (separatorIndex > 0) { - const code = message.slice(0, separatorIndex); - if (/^(?:ERR_[A-Z0-9_]+|E[A-Z0-9_]+)$/.test(code)) { - normalized.code = code; - normalized.message = message.slice(separatorIndex + 2); - } - } - if (typeof normalized.code !== 'string') { - normalized.code = 'ERR_AGENTOS_PYTHON_VFS_RPC'; + const message = typeof error?.message === 'string' && error.message.length > 0 + ? error.message + : String(error); + const normalized = error instanceof Error ? error : new Error(message); + const structuredCode = typeof error?.code === 'string' && error.code.length > 0 + ? error.code + : 'EIO'; + normalized.code = structuredCode; + if (error?.details !== undefined) { + normalized.details = error.details; } return normalized; } @@ -973,12 +972,30 @@ from js import __agentOSPythonVfsRpc as _agentos_rpc def _agentos_raise_from_error(error): if not isinstance(error, dict): raise RuntimeError(str(error)) + code = str(error.get("code", "") or "EIO") message = str(error.get("message", "secure-exec Python bridge request failed")) - if "EACCES:" in message: - raise PermissionError(message) - if "command not found" in message: - raise FileNotFoundError(message) - raise OSError(message) + details = error.get("details") + if code in ("EACCES", "EPERM"): + exception = PermissionError(message) + elif code == "ENOENT": + exception = FileNotFoundError(message) + else: + exception = OSError(message) + exception.code = code + if details is not None: + exception.details = details + raise exception + +def _agentos_bridge_error(error): + try: + code = str(getattr(error, "code", "") or "") + except Exception: + code = "" + try: + details = getattr(error, "details", None) + except Exception: + details = None + return {"code": code or "EIO", "message": str(error), "details": details} def _agentos_normalize_family(family): if family in (None, 0): @@ -995,7 +1012,7 @@ def _agentos_dns_lookup(hostname, family=None): _agentos_rpc.dnsLookupSync(hostname, _agentos_normalize_family(family)) ) except Exception as error: - _agentos_raise_from_error({"message": str(error)}) + _agentos_raise_from_error(_agentos_bridge_error(error)) addresses = result.get("addresses") or [] if not addresses: raise OSError(f"secure-exec DNS lookup returned no addresses for {hostname}") @@ -1078,7 +1095,7 @@ def _agentos_http_request(url_or_request, data=None): _agentos_rpc.httpRequestSync(url, method, _agentos_json.dumps(headers), body_base64) ) except Exception as error: - _agentos_raise_from_error({"message": str(error)}) + _agentos_raise_from_error(_agentos_bridge_error(error)) response = _SecureExecHttpResponse(payload) if response.status >= 400: raise _agentos_urllib_error.HTTPError( @@ -1107,7 +1124,7 @@ async def _agentos_pyfetch(url, **kwargs): ) ) except Exception as error: - _agentos_raise_from_error({"message": str(error)}) + _agentos_raise_from_error(_agentos_bridge_error(error)) return _SecureExecPyfetchResponse(payload) def _agentos_urlopen(url, data=None, timeout=None, *args, **kwargs): @@ -1162,13 +1179,22 @@ import errno as _agentos_errno _agentos_original_socket_class = _agentos_socket.socket def _agentos_socket_oserror(exc): - # Host errors arrive as "E: message"; recover the errno so Python - # code can catch ConnectionRefusedError/TimeoutError/etc. (OSError picks the - # right subclass from the errno). + # The bridge carries the stable errno name separately from its diagnostic. + # OSError picks the right Python subclass from the numeric errno while the + # original message remains available to the caller. message = str(getattr(exc, "message", None) or exc) - head = message.split(":", 1)[0].strip() - code = getattr(_agentos_errno, head, 0) if head[:1] == "E" and head.isupper() else 0 - return OSError(code or 0, message) + code_name = getattr(exc, "code", None) + errno_value = ( + getattr(_agentos_errno, code_name, _agentos_errno.EIO) + if isinstance(code_name, str) and code_name.startswith("E") + else _agentos_errno.EIO + ) + mapped = OSError(errno_value, message) + mapped.code = code_name if isinstance(code_name, str) and code_name.startswith("E") else "EIO" + details = getattr(exc, "details", None) + if details is not None: + mapped.details = details + return mapped def _agentos_socket_rpc(call): try: @@ -1377,7 +1403,7 @@ class _SecureExecRequestsSession: ) ) except Exception as error: - _agentos_raise_from_error({"message": str(error)}) + _agentos_raise_from_error(_agentos_bridge_error(error)) return _SecureExecRequestsResponse(payload) def get(self, url, **kwargs): @@ -1456,7 +1482,7 @@ def _agentos_subprocess_run(args, *, capture_output=False, check=False, cwd=None ) ) except Exception as error: - _agentos_raise_from_error({"message": str(error)}) + _agentos_raise_from_error(_agentos_bridge_error(error)) stdout_bytes = payload.get("stdout", "").encode("utf-8") stderr_bytes = payload.get("stderr", "").encode("utf-8") if text or encoding is not None: @@ -1746,24 +1772,19 @@ function installPythonWorkspaceFs(pyodide, bridge) { return error; } - const diagnostic = `${error?.code || ''} ${error?.message || ''} ${error?.stack || ''}`; - const message = diagnostic.toLowerCase(); - let errno = ERRNO_CODES.EIO; - if (/permission denied|access denied|denied/.test(message)) { - errno = ERRNO_CODES.EACCES; - } else if (/read-only|erofs/.test(message)) { - errno = ERRNO_CODES.EROFS; - } else if (/not a directory|enotdir/.test(message)) { - errno = ERRNO_CODES.ENOTDIR; - } else if (/is a directory|eisdir/.test(message)) { - errno = ERRNO_CODES.EISDIR; - } else if (/exists|already exists|eexist/.test(message)) { - errno = ERRNO_CODES.EEXIST; - } else if (/not found|no such file|enoent/.test(message)) { - errno = ERRNO_CODES.ENOENT; + const code = typeof error?.code === 'string' ? error.code : ''; + const errno = Number.isInteger(ERRNO_CODES[code]) + ? ERRNO_CODES[code] + : ERRNO_CODES.EIO; + const mapped = new FS.ErrnoError(errno); + mapped.code = code || 'EIO'; + mapped.message = typeof error?.message === 'string' && error.message.length > 0 + ? error.message + : String(error ?? 'filesystem operation failed'); + if (error?.details !== undefined) { + mapped.details = error.details; } - - return new FS.ErrnoError(errno); + return mapped; } function withFsErrors(operation) { @@ -2114,6 +2135,7 @@ function installPythonWorkspaceFs(pyodide, bridge) { 'home', '__agentos_pyodide', '__agentos_pyodide_cache', + '__agentos_pyodide_package_tmp', ]); let rootEntries = []; try { @@ -2151,6 +2173,19 @@ function installPythonWorkspaceFs(pyodide, bridge) { // A path Pyodide owns or cannot shadow — skip it rather than abort boot. } } + // Pyodide keeps `/home` on MEMFS for its own interpreter state, but the + // configured Linux user's home must still be a live kernel-VFS projection. + // Mount that directory specifically so `/home/pyodide` can remain private + // while guest tools share one durable `$HOME` across executor processes. + const configuredHome = readRunnerEnv('HOME'); + if ( + rootEntries.includes('home') && + typeof configuredHome === 'string' && + configuredHome.startsWith('/home/') && + bridge.fsStatSync(configuredHome)?.isDirectory + ) { + mountVfsAt(configuredHome); + } // /workspace stays available for backward compatibility even if the VM root // does not advertise it. if (!rootEntries.includes('workspace')) { @@ -2307,14 +2342,21 @@ function readProgramFromStdin() { // filesystem (via the kernel VFS), so `pip install` survives across separate // `python` invocations and is visible to other processes — just like a real // `site-packages`. It is prepended to `sys.path` on every boot. -const PYTHON_VFS_SITE_PACKAGES = '/root/.agentos/site-packages'; +function pythonVfsSitePackages() { + const configuredHome = readRunnerEnv('HOME'); + const pythonHome = + typeof configuredHome === 'string' && configuredHome.startsWith('/') + ? configuredHome.replace(/\/+$/, '') || '/' + : '/home/agentos'; + return `${pythonHome === '/' ? '' : pythonHome}/.agentos/site-packages`; +} function installPythonVfsSitePackages(pyodide) { if (typeof pyodide?.runPython !== 'function') { return; } try { - pyodide.globals.set('__agentos_vfs_site', PYTHON_VFS_SITE_PACKAGES); + pyodide.globals.set('__agentos_vfs_site', pythonVfsSitePackages()); pyodide.runPython( 'import os as _os, sys as _sys\n' + 'try:\n' + @@ -2326,7 +2368,7 @@ function installPythonVfsSitePackages(pyodide) { // shadow the stdlib. ' _sys.path.append(__agentos_vfs_site)\n' + // Best-effort: if the VFS site-packages can't be created (e.g. a - // read-only `/root`), persistence is simply unavailable — pip still + // read-only home directory), persistence is simply unavailable — pip still // works in-process. Degrade quietly rather than spam stderr. 'except OSError:\n' + ' pass\n' + @@ -2353,7 +2395,7 @@ function installPythonVfsSitePackages(pyodide) { // packages are copied into the persistent VFS site-packages so they survive the // per-process interpreter and can be imported by a later `python` invocation. async function runPythonPip(pyodide) { - pyodide.globals.set('__agentos_vfs_site', PYTHON_VFS_SITE_PACKAGES); + pyodide.globals.set('__agentos_vfs_site', pythonVfsSitePackages()); try { await pyodide.runPythonAsync(` import os, shutil, site, sys @@ -2520,15 +2562,31 @@ try { throw new Error('Pyodide loadPackage() is required to preload Python packages'); } if (canLoadPackages) { - emitWarmupStage('before-load-micropip'); - await pyodide.loadPackage(['micropip']); - emitWarmupStage('after-load-micropip'); - if (preloadPackages.length > 0) { - emitWarmupStage('before-load-preload-packages'); - const packageLoadStarted = realPerformance.now(); - await pyodide.loadPackage(preloadPackages); - packageLoadMs = realPerformance.now() - packageLoadStarted; - emitWarmupStage('after-load-preload-packages'); + pyodide.FS.mkdirTree(PYODIDE_PACKAGE_TEMP_GUEST_ROOT); + pyodide.globals.set('__agentos_pyodide_package_tmp', PYODIDE_PACKAGE_TEMP_GUEST_ROOT); + pyodide.runPython( + 'import tempfile as _agentos_tempfile\n' + + '_agentos_tempfile.tempdir = __agentos_pyodide_package_tmp\n' + + 'del _agentos_tempfile', + ); + try { + emitWarmupStage('before-load-micropip'); + await pyodide.loadPackage(['micropip']); + emitWarmupStage('after-load-micropip'); + if (preloadPackages.length > 0) { + emitWarmupStage('before-load-preload-packages'); + const packageLoadStarted = realPerformance.now(); + await pyodide.loadPackage(preloadPackages); + packageLoadMs = realPerformance.now() - packageLoadStarted; + emitWarmupStage('after-load-preload-packages'); + } + } finally { + pyodide.runPython( + 'import tempfile as _agentos_tempfile\n' + + '_agentos_tempfile.tempdir = None\n' + + 'del _agentos_tempfile', + ); + pyodide.globals.delete('__agentos_pyodide_package_tmp'); } } if (pyodide?._api?.config) { diff --git a/crates/execution/assets/runners/wasi-module.js b/crates/execution/assets/runners/wasi-module.js index 171efbd1dd..52fc521463 100644 --- a/crates/execution/assets/runners/wasi-module.js +++ b/crates/execution/assets/runners/wasi-module.js @@ -31,6 +31,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = ? globalThis.lookupFdHandle : undefined); const __agentOSWasiErrnoSuccess = 0; + const __agentOSWasiErrno2big = 1; const __agentOSWasiErrnoAcces = 2; const __agentOSWasiErrnoAgain = 6; const __agentOSWasiErrnoBadf = 8; @@ -1268,6 +1269,16 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } _statResolvedPath(resolved, follow) { + if ( + this._sidecarManagedProcess() && + typeof resolved?.guestPath === "string" + ) { + return this._measureWasiPhase(follow ? "syncRpcStat" : "syncRpcLstat", () => + __agentOSWasiSyncRpc().callSync(follow ? "fs.statSync" : "fs.lstatSync", [ + resolved.guestPath, + ]) + ); + } if ( typeof globalThis?.__agentOSSyncRpc?.callSync === "function" && typeof resolved?.guestPath === "string" @@ -2263,6 +2274,12 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } _pathOpen(fd, _dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, _fdflags, openedFdPtr) { + // The managed runner replaces path_open with its dirfd-aware kernel RPC. + // Reaching this Node-fs implementation in production is an architecture + // violation, not a reason to open an ambient host descriptor. + if (this._sidecarManagedProcess()) { + return __agentOSWasiErrnoIo; + } try { const entry = this._measureWasiPhase("descriptorEntry", () => this._descriptorEntry(fd)); if ( @@ -2801,9 +2818,17 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = _randomGet(bufPtr, bufLen) { try { const length = Number(bufLen) >>> 0; - const bytes = Buffer.allocUnsafe(length); - __agentOSCrypto().randomFillSync(bytes); - return this._writeBytes(bufPtr, bytes); + const offset = Number(bufPtr) >>> 0; + const memory = this._memoryBytes(); + if (length > __agentOSWasmSyncReadLimitBytes) return __agentOSWasiErrno2big; + if (offset > memory.byteLength - length) return __agentOSWasiErrnoFault; + const chunkBytes = 64 * 1024; + for (let written = 0; written < length; written += chunkBytes) { + __agentOSCrypto().randomFillSync( + memory.subarray(offset + written, offset + Math.min(length, written + chunkBytes)), + ); + } + return __agentOSWasiErrnoSuccess; } catch { return __agentOSWasiErrnoFault; } diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 83319e9f7d..93a525ed10 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -23,6 +23,7 @@ const WASI_ERRNO_AFNOSUPPORT = 5; const WASI_ERRNO_AGAIN = 6; const WASI_ERRNO_ALREADY = 7; const WASI_ERRNO_BADF = 8; +const WASI_ERRNO_BUSY = 10; const WASI_ERRNO_CHILD = 12; const WASI_ERRNO_CONNREFUSED = 14; const WASI_ERRNO_CONNRESET = 15; @@ -103,9 +104,36 @@ const WASM_PAGE_BYTES = 65536; // below that ceiling so ordinary allocation can never collide with the // private 0x40000000 pathname-preopen tag. const LINUX_GUEST_FD_LIMIT = 1 << 20; +// Linux readv(2)/writev(2) reject vectors larger than UIO_MAXIOV. Keep the +// same bound for Preview1 iovec arrays and for poll subscriptions, which are +// likewise attacker-controlled fixed-size guest tables. +const LINUX_IOV_MAX = 1024; +const WASI_POLL_MAX_SUBSCRIPTIONS = 1024; +const XATTR_NAME_MAX = 255; +const XATTR_SIZE_MAX = 64 * 1024; const LINUX_BINPRM_BUF_SIZE = 256; const LINUX_MAX_INTERPRETER_DEPTH = 4; const DEFAULT_WASM_MAX_MODULE_FILE_BYTES = 256 * 1024 * 1024; +const warnedFixedRequestLimits = new Set(); + +function checkFixedRequestLimit(limitName, observed, maximum, errno = WASI_ERRNO_INVAL) { + const numericObserved = Number(observed) >>> 0; + const warningAt = Math.max(1, Math.ceil(maximum * 0.8)); + if ( + numericObserved >= warningAt && + !warnedFixedRequestLimits.has(limitName) + ) { + warnedFixedRequestLimits.add(limitName); + if (typeof process?.stderr?.write === 'function') { + process.stderr.write( + `[agentos] WASM request is near ${limitName} ` + + `(${numericObserved}/${maximum}); split the request if needed\n`, + ); + } + } + return numericObserved > maximum ? errno : WASI_ERRNO_SUCCESS; +} + function boundedWasmSyncRpcReadLength(length) { return Math.min( Number(length) >>> 0, @@ -117,7 +145,7 @@ const POSIX_SPAWN_SETPGROUP = 2; const POSIX_SPAWN_SETSIGDEF = 4; const POSIX_SPAWN_SETSIGMASK = 8; const LINUX_SA_NODEFER = 0x40000000; -const LINUX_SA_RESETHAND = 0x80000000; +const LINUX_SA_RESTART = 0x10000000; const POSIX_SPAWN_SETSCHEDPARAM = 16; const POSIX_SPAWN_SETSCHEDULER = 32; const POSIX_SPAWN_USEVFORK = 64; @@ -154,29 +182,6 @@ function parseVirtualProcessString(value, fallback) { return typeof value === 'string' && value.length > 0 ? value : fallback; } -function parseInitialSignalSet(value, setting) { - if (typeof value !== 'string' || value.length === 0) { - return []; - } - let parsed; - try { - parsed = JSON.parse(value); - } catch (error) { - throw new Error(`${setting} must be a JSON signal-number array: ${error}`); - } - if (!Array.isArray(parsed) || parsed.length > 64) { - throw new Error(`${setting} must contain at most 64 signal numbers`); - } - const signals = new Set(); - for (const value of parsed) { - if (!Number.isInteger(value) || value <= 0 || value > 64) { - throw new Error(`${setting} contains invalid signal ${String(value)}`); - } - signals.add(value); - } - return [...signals]; -} - function parseInitialKernelFdMappings(value, limit) { if (typeof value !== 'string' || value.length === 0) { return new Map(); @@ -513,18 +518,6 @@ function __agentOSWasmEmitPhaseMetrics(reason, extra = {}) { let guestArgv = JSON.parse(process.env.AGENTOS_GUEST_ARGV ?? '[]'); let guestEnv = JSON.parse(process.env.AGENTOS_GUEST_ENV ?? '{}'); -const initialWasmSignalMask = parseInitialSignalSet( - process.env.AGENTOS_WASM_INITIAL_SIGNAL_MASK, - 'AGENTOS_WASM_INITIAL_SIGNAL_MASK', -).filter((signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP); -const initialWasmSignalIgnores = parseInitialSignalSet( - process.env.AGENTOS_WASM_INITIAL_SIGNAL_IGNORES, - 'AGENTOS_WASM_INITIAL_SIGNAL_IGNORES', -); -const initialWasmPendingSignals = parseInitialSignalSet( - process.env.AGENTOS_WASM_INITIAL_PENDING_SIGNALS, - 'AGENTOS_WASM_INITIAL_PENDING_SIGNALS', -); const GUEST_PATH_MAPPINGS = parseGuestPathMappings(process.env.AGENTOS_GUEST_PATH_MAPPINGS); const permissionTier = process.env.AGENTOS_WASM_PERMISSION_TIER ?? 'full'; const prewarmOnly = process.env.AGENTOS_WASM_PREWARM_ONLY === '1'; @@ -559,39 +552,27 @@ const maxStackBytes = Number.isFinite(maxStackBytesValue) && maxStackBytesValue > 0 ? Math.floor(maxStackBytesValue) : null; +const DEFAULT_SYNC_RPC_RESPONSE_LINE_BYTES = 16 * 1024 * 1024; +const maxSyncRpcResponseLineBytesValue = Number( + process.env.AGENTOS_INTERNAL_WASM_SYNC_RPC_RESPONSE_LINE_BYTES, +); +const maxSyncRpcResponseLineBytes = + Number.isSafeInteger(maxSyncRpcResponseLineBytesValue) && maxSyncRpcResponseLineBytesValue > 0 + ? maxSyncRpcResponseLineBytesValue + : DEFAULT_SYNC_RPC_RESPONSE_LINE_BYTES; const maxOpenFdsValue = Number(process.env.AGENTOS_WASM_MAX_OPEN_FDS); const configuredMaxOpenFds = Number.isFinite(maxOpenFdsValue) && maxOpenFdsValue >= 0 ? Math.floor(maxOpenFdsValue) - : 256; -const inheritedNofileHardValue = Number( - process.env.AGENTOS_WASM_RLIMIT_NOFILE_HARD, -); -let rlimitNofileHard = - Number.isSafeInteger(inheritedNofileHardValue) && - inheritedNofileHardValue >= 0 && - inheritedNofileHardValue <= configuredMaxOpenFds - ? inheritedNofileHardValue - : configuredMaxOpenFds; -const inheritedNofileSoftValue = Number( - process.env.AGENTOS_WASM_RLIMIT_NOFILE_SOFT, -); -let rlimitNofileSoft = - Number.isSafeInteger(inheritedNofileSoftValue) && - inheritedNofileSoftValue >= 0 && - inheritedNofileSoftValue <= rlimitNofileHard - ? inheritedNofileSoftValue - : rlimitNofileHard; - -function inheritedNofileBootstrapEnv() { - return { - AGENTOS_WASM_RLIMIT_NOFILE_SOFT: String(rlimitNofileSoft), - AGENTOS_WASM_RLIMIT_NOFILE_HARD: String(rlimitNofileHard), - }; -} + : 1024; const maxSocketsValue = Number(process.env.AGENTOS_WASM_MAX_SOCKETS); const maxSockets = Number.isFinite(maxSocketsValue) && maxSocketsValue >= 0 ? Math.floor(maxSocketsValue) : null; +const SIDECAR_MANAGED_PROCESS = globalThis.__agentOSManagedKernelHost === true; +// Shared with the typed host-dispatch NODE_CWD_FD sentinel. This is an +// executor ABI value, not a real guest descriptor, so path authorization +// must never mistake stdin (fd 0) for the absolute/cwd capability. +const NODE_CWD_FD = 0xffffffff; const initialKernelFdMappings = parseInitialKernelFdMappings( process.env.AGENTOS_WASM_INHERITED_FD_MAPPINGS, configuredMaxOpenFds, @@ -603,8 +584,22 @@ const initialHostNetDescriptions = parseInitialHostNetFds( ); const initialHostNetGuestFds = initialHostNetDescriptions .flatMap((description) => description.guestFds.map((fd) => Number(fd) >>> 0)); +const initialKernelGuestFds = new Set(initialKernelFdMappings.values()); +// Managed host-network metadata describes the same canonical kernel-backed +// descriptor, rather than a second guest descriptor. Legacy/standalone socket +// bootstraps remain disjoint from kernel mappings. +const managedHostNetKernelGuestFds = new Set( + SIDECAR_MANAGED_PROCESS + ? initialHostNetDescriptions + .filter((description) => + typeof description.descriptionId === 'string' && + /^[0-9]+$/.test(description.descriptionId) + ) + .flatMap((description) => description.guestFds.map((fd) => Number(fd) >>> 0)) + : [], +); const initialMappedGuestFds = new Set([ - ...initialKernelFdMappings.values(), + ...initialKernelGuestFds, ...initialHostNetGuestFds, ]); // Keep explicit inherited guest destinations unavailable while bootstrap @@ -612,8 +607,10 @@ const initialMappedGuestFds = new Set([ // this reservation, an earlier unmapped kernel fd can take (for example) // guest fd 7 before a later kernel fd is installed at its required guest fd 7. const pendingInitialKernelGuestFds = new Set(initialKernelFdMappings.values()); -if (initialMappedGuestFds.size !== initialKernelFdMappings.size + initialHostNetGuestFds.length) { - throw new Error('inherited kernel and host-network descriptors overlap in the guest fd table'); +for (const guestFd of initialHostNetGuestFds) { + if (initialKernelGuestFds.has(guestFd) && !managedHostNetKernelGuestFds.has(guestFd)) { + throw new Error('inherited kernel and host-network descriptors overlap in the guest fd table'); + } } if (initialMappedGuestFds.size > configuredMaxOpenFds) { throw new Error( @@ -635,6 +632,20 @@ const maxBlockingReadMs = Number.isFinite(maxBlockingReadMsValue) && maxBlocking : null; const unixConnectTimeoutMs = maxBlockingReadMs ?? 30_000; +function warnNearBlockingReadLimit(operation, startedAt, alreadyWarned, applies = true) { + if ( + !applies || + alreadyWarned || + Date.now() < startedAt + Math.floor(unixConnectTimeoutMs * 0.8) + ) { + return alreadyWarned; + } + process.stderr.write( + `[agentos] ${operation} is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + return true; +} + // A guest can drive WebAssembly into never-returning recursion. V8's default // native stack guard already traps that as a generic `RangeError`, but the // operator-configured typed stack budget was previously @@ -699,22 +710,38 @@ const NODE_SYNC_RPC_ENABLE = process.env.AGENTOS_NODE_SYNC_RPC_ENABLE === '1'; const NODE_SYNC_RPC_REQUEST_FD = parseControlPipeFd(process.env.AGENTOS_NODE_SYNC_RPC_REQUEST_FD); const NODE_SYNC_RPC_RESPONSE_FD = parseControlPipeFd(process.env.AGENTOS_NODE_SYNC_RPC_RESPONSE_FD); const KERNEL_STDIO_SYNC_RPC = process.env.AGENTOS_WASI_STDIO_SYNC_RPC === '1'; -const SIDECAR_MANAGED_PROCESS = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; const SIDECAR_EXEC_COMMIT_RPC = process.env.AGENTOS_WASM_EXEC_COMMIT_RPC === '1'; let nextSyncRpcId = 1; -let syncRpcResponseBuffer = ''; -const spawnedChildren = new Map(); -const spawnedChildrenById = new Map(); +let syncRpcResponseBuffer = Buffer.alloc(0); +let warnedSyncRpcResponseLineBytes = false; +// Managed execution keeps only bounded transport correlations here. Child +// lifecycle, process groups, and wait status remain kernel-authoritative. +const childCorrelationsByPid = new Map(); +const childCorrelationsById = new Map(); let nextBlockingChildPumpIndex = 0; +let nextManagedCorrelationCollectionAtMs = 0; let nextSyntheticChildPid = 0x40000000; -const syntheticFdEntries = new Map(); -const runnerCloexecFds = new Set(); -const delegateManagedFdRefCounts = new Map(); +// Managed entries are projections of live kernel descriptions. Standalone +// entries emulate Node-WASI descriptors and may own runner-local semantics. +const managedKernelFdProjections = new Map(); +const standaloneSyntheticFdEntries = new Map(); +const activeFdProjections = SIDECAR_MANAGED_PROCESS + ? managedKernelFdProjections + : standaloneSyntheticFdEntries; + +function setActiveFdProjection(fd, handle) { + if (SIDECAR_MANAGED_PROCESS && handle?.kind !== 'kernel-fd') { + throw new Error('managed descriptor projections must reference a kernel fd'); + } + activeFdProjections.set(Number(fd) >>> 0, handle); +} + +// Only the standalone Node-WASI fallback lacks kernel descriptor flags. +const standaloneCloexecFds = new Set(); +const standaloneDelegateFdRefCounts = new Map(); const closedPassthroughFds = new Set(); globalThis.__agentOSWasiDelegateFdRefCount = (fd) => - delegateManagedFdRefCounts.get(Number(fd) >>> 0) ?? 0; + standaloneDelegateFdRefCounts.get(Number(fd) >>> 0) ?? 0; const passthroughHandles = new Map([ [0, { kind: 'passthrough', targetFd: 0, displayFd: 0, refCount: 0, open: true }], [1, { kind: 'passthrough', targetFd: 1, displayFd: 1, refCount: 0, open: true }], @@ -838,6 +865,49 @@ function readExecutableFdBytes(targetFd, stats, subject, projectedExecutable = f } function readExecutablePathBytes(command) { + if (SIDECAR_MANAGED_PROCESS) { + const subject = String(command); + const image = callSyncRpc('process.exec_image_open', [subject]); + const handle = String(image?.handle ?? ''); + try { + const projectedExecutable = isProjectedCommandGuestPath(subject); + if (!projectedExecutable && (Number(image?.mode) & 0o111) === 0) { + throw execError('EACCES', `${subject} does not have an executable mode bit`); + } + const size = Number(image?.size); + if (!Number.isSafeInteger(size) || size < 0) { + throw execError('EFBIG', `${subject} has an invalid executable image size`); + } + if (size > maxModuleFileBytes) { + throw execError( + 'EFBIG', + `${subject} is ${size} bytes, exceeding limits.wasm.maxModuleFileBytes (${maxModuleFileBytes}); raise limits.wasm.maxModuleFileBytes if needed`, + ); + } + + const bytes = Buffer.alloc(size); + let offset = 0; + while (offset < size) { + const requested = boundedWasmSyncRpcReadLength(size - offset); + const chunk = Buffer.from(callSyncRpc('process.exec_image_read', [ + handle, + String(offset), + requested, + ]) ?? []); + if (chunk.byteLength === 0 || chunk.byteLength > requested) { + throw execError('EIO', `${subject} changed while its executable image was read`); + } + chunk.copy(bytes, offset); + offset += chunk.byteLength; + } + return bytes; + } finally { + if (handle.length > 0) { + callSyncRpc('process.exec_image_close', [handle]); + } + } + } + const hostPath = resolveExecModulePath(command); const stats = fsModule.statSync(hostPath); const size = validateExecutableStat( @@ -864,21 +934,14 @@ function projectedCommandImageBytes(command) { mapping?.guestPath !== '/opt/agentos/bin' ) continue; const guestCandidate = path.posix.join(mapping.guestPath, name); - const hostCandidate = resolveExecModulePath(guestCandidate); try { - const stats = fsModule.statSync(hostCandidate); - const size = validateExecutableStat(stats, guestCandidate, true); - const bytes = fsModule.readFileSync(hostCandidate); + const bytes = readExecutablePathBytes(guestCandidate); traceHostProcess('projected-command-image', { command, guestCandidate, - hostCandidate, byteLength: bytes.byteLength, magic: Array.from(bytes.subarray(0, 4)), }); - if (bytes.byteLength > size || bytes.byteLength > maxModuleFileBytes) { - throw execError('EFBIG', `${guestCandidate} grew beyond limits.wasm.maxModuleFileBytes while being read`); - } return bytes; } catch (error) { if (error?.code !== 'ENOENT') throw error; @@ -985,12 +1048,62 @@ function executableTargetForHandle(handle) { function loadExecImageFromFd(fd, argv, closeFds) { const descriptor = Number(fd) >>> 0; + const scriptRef = `/proc/self/fd/${descriptor}`; + if (SIDECAR_MANAGED_PROCESS) { + const image = callSyncRpc('process.exec_image_open_fd', [ + canonicalKernelFdForSpawnAction(descriptor), + ]); + const imageHandle = String(image?.handle ?? ''); + let bytes; + try { + const projectedExecutable = isProjectedCommandGuestPath(image?.canonicalPath ?? ''); + if (!projectedExecutable && (Number(image?.mode) & 0o111) === 0) { + throw execError('EACCES', `${scriptRef} does not have an executable mode bit`); + } + const size = Number(image?.size); + if (!Number.isSafeInteger(size) || size < 0) { + throw execError('EFBIG', `${scriptRef} has an invalid executable image size`); + } + if (size > maxModuleFileBytes) { + throw execError( + 'EFBIG', + `${scriptRef} is ${size} bytes, exceeding limits.wasm.maxModuleFileBytes (${maxModuleFileBytes}); raise limits.wasm.maxModuleFileBytes if needed`, + ); + } + bytes = Buffer.alloc(size); + let offset = 0; + while (offset < size) { + const requested = boundedWasmSyncRpcReadLength(size - offset); + const chunk = Buffer.from(callSyncRpc('process.exec_image_read', [ + imageHandle, + String(offset), + requested, + ]) ?? []); + if (chunk.byteLength === 0 || chunk.byteLength > requested) { + throw execError('EIO', `${scriptRef} changed while its executable image was read`); + } + chunk.copy(bytes, offset); + offset += chunk.byteLength; + } + } finally { + if (imageHandle.length > 0) { + callSyncRpc('process.exec_image_close', [imageHandle]); + } + } + if (parseLinuxShebang(bytes) && closeFds.includes(descriptor)) { + throw execError('ENOENT', `${scriptRef} will be closed before its interpreter opens it`); + } + return { + ...compileExecImage(bytes, scriptRef, argv), + scriptRef, + }; + } + const handle = lookupFdHandle(descriptor); const targetFd = executableTargetForHandle(handle); if (targetFd === null) { throw execError('EBADF', `fexecve descriptor ${descriptor} is not an open file`); } - const scriptRef = `/proc/self/fd/${descriptor}`; const bytes = readExecutableFdBytes( targetFd, fsModule.fstatSync(targetFd), @@ -1095,24 +1208,6 @@ const FULL_PREOPEN_RIGHTS_BASE = WASI_RIGHT_PATH_UNLINK_FILE; const FULL_PREOPEN_RIGHTS_INHERITING = READ_WRITE_PREOPEN_RIGHTS_INHERITING; -function kernelFdRightsBase(flags) { - const accessMode = Number(flags) & (KERNEL_O_WRONLY | KERNEL_O_RDWR); - let rights = - WASI_RIGHT_FD_SEEK | - WASI_RIGHT_FD_TELL | - WASI_RIGHT_FD_FDSTAT_SET_FLAGS | - WASI_RIGHT_FD_FILESTAT_GET | - WASI_RIGHT_FD_SYNC | - WASI_RIGHT_POLL_FD_READWRITE; - if (accessMode !== KERNEL_O_WRONLY) { - rights |= WASI_RIGHT_FD_READ; - } - if (accessMode === KERNEL_O_WRONLY || accessMode === KERNEL_O_RDWR) { - rights |= WASI_RIGHT_FD_WRITE; - } - return rights; -} - function buildPreopenRights() { switch (permissionTier) { case 'read-only': @@ -1368,15 +1463,40 @@ function decodeBase64ToUint8Array(value) { return Buffer.from(value, 'base64'); } -// Memoized kernel-PTY probe for the guest's stdio fds. Rust's +function decodeFsBytesPayload(value, label) { + if (Buffer.isBuffer(value)) return Buffer.from(value); + if (ArrayBuffer.isView(value)) { + return Buffer.from(value.buffer, value.byteOffset, value.byteLength); + } + if (value instanceof ArrayBuffer) return Buffer.from(value); + if (Array.isArray(value) && value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + return Buffer.from(value); + } + if (value && typeof value === 'object') { + if (value.__agentOSType === 'bytes' && typeof value.base64 === 'string') { + return Buffer.from(value.base64, 'base64'); + } + if (value.__type === 'Buffer' && typeof value.data === 'string') { + return Buffer.from(value.data, 'base64'); + } + if (typeof value.base64 === 'string') return Buffer.from(value.base64, 'base64'); + } + const error = new Error(`${label} is not a byte payload`); + error.code = 'EIO'; + throw error; +} + +// Live kernel-PTY probe for any guest fd. Rust's // `stdin().is_terminal()` (and wasi-libc `isatty`) ask `fd_fdstat_get` for a // CHARACTER_DEVICE filetype; the runner-process fds are pipes, so a delegated // answer hides the kernel PTY and interactive guests (e.g. brush's prompt) // believe stdin is not a terminal. Ask the sidecar's kernel instead. -const stdioTtyCache = new Map(); function stdioFdIsKernelTty(fd) { const descriptor = Number(fd) >>> 0; - if (descriptor > 2) return false; + const handle = lookupFdHandle(descriptor); + const kernelFd = handle?.kind === 'kernel-fd' + ? Number(handle.targetFd) >>> 0 + : descriptor; // Even in kernel-stdio sync-RPC mode the answer must come from the kernel: // that mode is on for EVERY sidecar wasm execution, including piped // vm.exec() runs whose stdio is NOT a PTY. Hardcoding true here made @@ -1384,14 +1504,12 @@ function stdioFdIsKernelTty(fd) { // CHARACTER_DEVICE, host_tty.isatty said 1), so they enabled raw mode and // the kernel's truthful "not a PTY end" refusal trapped the guest with // exit 1 after otherwise-successful commands. - if (stdioTtyCache.has(descriptor)) return stdioTtyCache.get(descriptor); let isTty = false; try { - isTty = callSyncRpc('__kernel_isatty', [descriptor]) === true; + isTty = callSyncRpc('__kernel_isatty', [kernelFd]) === true; } catch { isTty = false; } - stdioTtyCache.set(descriptor, isTty); return isTty; } @@ -1402,14 +1520,14 @@ function stdioFdIsKernelTty(fd) { // slice under the 30s guest sync-RPC deadline. const KERNEL_WAIT_SLICE_MS = 10_000; const KERNEL_STDIN_WOULD_BLOCK = Symbol('kernel-stdin-would-block'); -// A WASM descendant shares this runner's synchronous sidecar dispatch path. -// While one is active, return to the child event pump frequently enough to -// preserve the concurrent progress Linux gives separately scheduled processes. +// Standalone descendants share this runner's cooperative child path. Managed +// descendants are scheduled and pumped independently by the sidecar. const SPAWNED_CHILD_WAIT_SLICE_MS = 10; function hasActiveSpawnedChildren() { - for (const record of spawnedChildren.values()) { - if (record && typeof record.exitStatus !== 'number') { + if (SIDECAR_MANAGED_PROCESS) return false; + for (const record of childCorrelationsByPid.values()) { + if (record && !childBridgeTerminal(record)) { return true; } } @@ -1454,21 +1572,70 @@ if (prewarmOnly) { process.exit(0); } -const WASI_PREOPENS = buildPreopens(); const WASI_PREOPEN_FD_BASE = 3; // Patched wasi-libc tags descriptors returned by its absolute/cwd pathname // resolver. The tag separates hidden WASI capability roots from the Linux // guest descriptor namespace, where fd 3 is free to be closed or replaced. const AGENTOS_HIDDEN_PREOPEN_FD_TAG = 0x40000000; const AGENTOS_HIDDEN_PREOPEN_FD_MASK = 0x3fffffff; -const WASI_PREOPEN_ENTRIES = Object.entries(WASI_PREOPENS); +const AMBIENT_WASI_PREOPENS = SIDECAR_MANAGED_PROCESS ? {} : buildPreopens(); +const KERNEL_WASI_PREOPENS = SIDECAR_MANAGED_PROCESS + ? callSyncRpc('process.fd_preopens', []) + : null; +if ( + KERNEL_WASI_PREOPENS !== null && + (!Array.isArray(KERNEL_WASI_PREOPENS) || + KERNEL_WASI_PREOPENS.length > configuredMaxOpenFds) +) { + throw new Error( + `kernel WASI preopens exceed the ${configuredMaxOpenFds}-descriptor runtime limit`, + ); +} +const WASI_PREOPEN_ENTRIES = SIDECAR_MANAGED_PROCESS + ? KERNEL_WASI_PREOPENS.map((entry, index) => { + const kernelFd = Number(entry?.fd); + const fd = WASI_PREOPEN_FD_BASE + index; + const guestPath = entry?.guestPath; + const rightsBase = Number(entry?.rightsBase); + const rightsInheriting = Number(entry?.rightsInheriting); + if ( + !Number.isSafeInteger(kernelFd) || kernelFd < WASI_PREOPEN_FD_BASE || + typeof guestPath !== 'string' || !path.posix.isAbsolute(guestPath) || + !Number.isSafeInteger(rightsBase) || rightsBase < 0 || + !Number.isSafeInteger(rightsInheriting) || rightsInheriting < 0 + ) { + throw new Error('kernel returned invalid WASI preopen metadata'); + } + return { + fd, + kernelFd, + guestPath: path.posix.normalize(guestPath), + preopenSpec: { + readOnly: (BigInt(rightsBase) & WASI_RIGHT_FD_WRITE) === 0n, + rightsBase: BigInt(rightsBase), + rightsInheriting: BigInt(rightsInheriting), + }, + kernelManaged: true, + }; + }) + : Object.entries(AMBIENT_WASI_PREOPENS).map( + ([guestPath, preopenSpec], index) => ({ + fd: WASI_PREOPEN_FD_BASE + index, + kernelFd: WASI_PREOPEN_FD_BASE + index, + guestPath, + preopenSpec, + kernelManaged: false, + }), + ); const hiddenPreopenHandles = new Map(); const wasi = new WASI({ version: 'preview1', args: guestArgv, env: guestEnv, - preopens: WASI_PREOPENS, + // Managed execution must never hand node:wasi an ambient host capability. + // Its capability descriptors and metadata come from the sidecar kernel. + preopens: AMBIENT_WASI_PREOPENS, returnOnExit: true, }); @@ -1484,7 +1651,7 @@ if (typeof wasiImport.sock_shutdown !== 'function') { const handle = lookupFdHandle(numericFd); const kernelFd = handle?.kind === 'kernel-fd' ? Number(handle.targetFd) >>> 0 - : delegateManagedFdRefCounts.has(numericFd) + : standaloneDelegateFdRefCounts.has(numericFd) ? numericFd : null; if (kernelFd == null) return WASI_ERRNO_SUCCESS; @@ -1604,6 +1771,27 @@ function canonicalKernelFdForSpawnAction(fd) { return handle?.kind === 'kernel-fd' ? Number(handle.targetFd) >>> 0 : numericFd; } +function managedKernelFdForDuplicate(fd) { + if (!SIDECAR_MANAGED_PROCESS) return null; + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind === 'kernel-fd') { + return Number(handle.targetFd) >>> 0; + } + // The managed kernel owns canonical stdin/stdout/stderr even though + // node:wasi needs local passthrough handles for the initial 0/1/2 I/O. + // Duplicating those descriptions must therefore allocate in the kernel and + // project the result back into the guest descriptor namespace. + if ( + numericFd <= 2 && + handle?.kind === 'passthrough' && + Number(handle.targetFd) === numericFd + ) { + return numericFd; + } + return null; +} + function kernelCloexecFdsForCommit(closeFds) { return closeFds.flatMap((fd) => { const handle = lookupFdHandle(fd); @@ -1679,7 +1867,7 @@ function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { !actionFdPaths.has(fd) && !actionFdSources.has(fd) && !lookupFdHandle(fd) && - !hostNetSockets.has(fd) + !hasHostNetSocket(fd) ) { throw spawnActionError('EBADF', `posix_spawn close references unopened fd ${fd}`); } @@ -1714,7 +1902,7 @@ function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { !actionFdPaths.has(sourceFd) && !actionFdSources.has(sourceFd) && !lookupFdHandle(sourceFd) && - !hostNetSockets.has(sourceFd) + !hasHostNetSocket(sourceFd) ) { throw spawnActionError( 'EBADF', @@ -1734,7 +1922,7 @@ function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { } else { actionFdPaths.delete(fd); const sourceHandle = lookupFdHandle(sourceFd); - if (typeof sourceHandle?.guestPath === 'string') { + if (sourceHandle?.kind !== 'kernel-fd' && typeof sourceHandle?.guestPath === 'string') { actionFdPaths.set(fd, sourceHandle.guestPath); actionFdSources.delete(fd); } else { @@ -1795,7 +1983,7 @@ function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { !actionFdPaths.has(fd) && !actionFdSources.has(fd) && !lookupFdHandle(fd) && - !hostNetSockets.has(fd) + !hasHostNetSocket(fd) ) { throw spawnActionError('EBADF', `posix_spawn fchdir references unopened fd ${fd}`); } @@ -1818,13 +2006,13 @@ function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { // owned only by older children and Node-WASI's private backing table are // deliberately excluded: neither is visible in the parent's fd table. const inheritedGuestFds = new Set([ - ...syntheticFdEntries.keys(), + ...activeFdProjections.keys(), ...passthroughHandles.keys(), - ...delegateManagedFdRefCounts.keys(), - ...hostNetSockets.keys(), + ...standaloneDelegateFdRefCounts.keys(), + ...hostNetGuestFds(), ...actionFdPaths.keys(), ...actionFdSources.keys(), - ...WASI_PREOPEN_ENTRIES.map((_, index) => WASI_PREOPEN_FD_BASE + index), + ...WASI_PREOPEN_ENTRIES.map((entry) => entry.fd), ]); for (const guestFd of inheritedGuestFds) { if (guestFd >= fd) { @@ -1955,7 +2143,7 @@ function kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, lookupflags, direc } function errorHasCode(error, code) { - return error?.code === code || String(error?.message ?? error ?? '').includes(code); + return error != null && typeof error === 'object' && error.code === code; } function blockingIoDeadline(timeoutMs) { @@ -1969,9 +2157,12 @@ function throwBlockingFifoOpenTimeout(guestPath) { } function openBlockingGuestFifoForPathOpen( + kernelDirFd, + relativePath, guestPath, oflags, rightsBase, + rightsInheriting, fdflags, lookupflags, openedFdPtr, @@ -2005,10 +2196,13 @@ function openBlockingGuestFifoForPathOpen( for (;;) { if (kernelFd === null) { try { - kernelFd = Number(callSyncRpc('process.fd_open', [ - guestPath, + kernelFd = Number(callSyncRpc('process.path_open_at', [ + kernelDirFd, + relativePath, nonblockingFlags, mode, + rightsBase.toString(), + rightsInheriting.toString(), ])) >>> 0; } catch (error) { if (accessMode !== KERNEL_O_WRONLY || !errorHasCode(error, 'ENXIO')) { @@ -2032,8 +2226,6 @@ function openBlockingGuestFifoForPathOpen( Atomics.wait(syntheticWaitArray, 0, 0, 1); } const openedFd = registerKernelDelegateFd(kernelFd); - const handle = lookupFdHandle(openedFd); - if (handle) handle.guestPath = guestPath; const result = writeGuestUint32(openedFdPtr, openedFd); if (result !== WASI_ERRNO_SUCCESS) wasiImport.fd_close(openedFd); return result; @@ -2057,9 +2249,6 @@ function resolvePathOpenGuestPath(fd, pathPtr, pathLen) { } const handle = lookupFdHandle(fd); - if (handle && typeof handle.guestPath === 'string') { - return path.posix.resolve(handle.guestPath, target); - } if (handle?.kind === 'kernel-fd' && SIDECAR_MANAGED_PROCESS) { try { const base = callSyncRpc('process.fd_chdir_path', [Number(handle.targetFd) >>> 0]); @@ -2071,16 +2260,18 @@ function resolvePathOpenGuestPath(fd, pathPtr, pathLen) { return null; } } + if (handle && typeof handle.guestPath === 'string') { + return path.posix.resolve(handle.guestPath, target); + } const numericFd = Number(fd) >>> 0; const preopenFd = (numericFd & AGENTOS_HIDDEN_PREOPEN_FD_TAG) !== 0 ? numericFd & AGENTOS_HIDDEN_PREOPEN_FD_MASK : numericFd; - const preopenIndex = preopenFd - WASI_PREOPEN_FD_BASE; - const preopen = WASI_PREOPEN_ENTRIES[preopenIndex]; + const preopen = WASI_PREOPEN_ENTRIES.find((entry) => entry.fd === preopenFd); if (preopen) { - return path.posix.resolve(guestPathForPreopenKey(preopen[0]), target); + return path.posix.resolve(guestPathForPreopenKey(preopen.guestPath), target); } return null; @@ -2101,9 +2292,17 @@ function resolvedGuestPathIsReadOnly(fd, pathPtr, pathLen) { } } -// Guest path recorded for a managed (path_open passthrough) fd, if known. +// Managed paths stay kernel-authoritative; standalone handles may retain a +// runner-local path because Node-WASI owns that fallback description. function guestPathForManagedFd(fd) { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd' && SIDECAR_MANAGED_PROCESS) { + try { + return String(callSyncRpc('process.fd_path', [Number(handle.targetFd) >>> 0])); + } catch { + return null; + } + } if (typeof handle?.guestPath === 'string') { return handle.guestPath; } @@ -2239,12 +2438,12 @@ function fsOpenFlagForPathOpen(oflags, rightsBase, fdflags) { function runnerFdMappingInUse(fd) { return ( pendingInitialKernelGuestFds.has(fd) || - syntheticFdEntries.has(fd) || + activeFdProjections.has(fd) || passthroughHandles.has(fd) || retainedSpawnOutputHandlesByFd.has(fd) || retainedSyntheticHandlesByDisplayFd.has(fd) || - delegateManagedFdRefCounts.has(fd) || - hostNetSockets.has(fd) + standaloneDelegateFdRefCounts.has(fd) || + hasHostNetSocket(fd) ); } @@ -2252,6 +2451,21 @@ function syntheticFdInUse(fd) { return runnerFdMappingInUse(fd) || wasi?.fdTable?.has?.(fd) === true; } +function currentNofileSoftLimit() { + if (!SIDECAR_MANAGED_PROCESS) return configuredMaxOpenFds; + try { + const limit = callSyncRpc('process.getrlimit', [7]); + const soft = BigInt(limit.soft); + if (soft > BigInt(Number.MAX_SAFE_INTEGER)) return LINUX_GUEST_FD_LIMIT; + return Number(soft); + } catch (error) { + if (error?.code === 'ERR_AGENTOS_WASM_SYNC_RPC_UNAVAILABLE') { + return configuredMaxOpenFds; + } + throw error; + } +} + function allocateSyntheticFd(minFd = nextSyntheticFd, reservedCapacity = false) { if (!reservedCapacity && !hasRunnerOpenFdCapacity(1)) { return null; @@ -2263,7 +2477,7 @@ function allocateSyntheticFd(minFd = nextSyntheticFd, reservedCapacity = false) ) { return null; } - const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, currentNofileSoftLimit()); let fd = Math.max(FIRST_SYNTHETIC_FD, numericMinimum); while ( fd < descriptorLimit && @@ -2285,7 +2499,7 @@ function allocateKernelGuestFd(minFd = 3) { ) { return null; } - const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, currentNofileSoftLimit()); let fd = Math.max(3, numericMinimum); while ( fd < descriptorLimit && @@ -2297,6 +2511,9 @@ function allocateKernelGuestFd(minFd = 3) { } function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdflags, openedFdPtr) { + if (SIDECAR_MANAGED_PROCESS) { + throw execError('EIO', 'managed path_open must use a kernel file descriptor'); + } if (!pathOpenMayCreateTarget(oflags, rightsBase, fdflags)) { return null; } @@ -2324,7 +2541,7 @@ function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdfl 0o666, ); const openedFd = allocateSyntheticFd(nextSyntheticFd, true); - syntheticFdEntries.set(openedFd, { + setActiveFdProjection(openedFd, { kind: 'guest-file', targetFd, displayFd: openedFd, @@ -2347,8 +2564,7 @@ function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedF return WASI_ERRNO_NOENT; } const sourceHandle = lookupFdHandle(sourceFd); - const targetFd = executableTargetForHandle(sourceHandle); - if (targetFd === null) { + if (!sourceHandle) { return WASI_ERRNO_NOENT; } if (((Number(lookupflags) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) === 0) { @@ -2363,10 +2579,32 @@ function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedF if (!hasRunnerOpenFdCapacity(1)) { return WASI_ERRNO_MFILE; } + if (SIDECAR_MANAGED_PROCESS) { + if (sourceHandle.kind !== 'kernel-fd') { + return WASI_ERRNO_NOENT; + } + try { + const openedFd = registerKernelDelegateFd( + callSyncRpc('process.fd_dup', [Number(sourceHandle.targetFd) >>> 0]), + ); + const writeResult = writeGuestUint32(openedFdPtr, openedFd); + if (writeResult !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(openedFd); + } + return writeResult; + } catch (error) { + return mapHostProcessError(error); + } + } + + const targetFd = executableTargetForHandle(sourceHandle); + if (targetFd === null) { + return WASI_ERRNO_NOENT; + } sourceHandle.refCount += 1; const openedFd = allocateSyntheticFd(nextSyntheticFd, true); - syntheticFdEntries.set(openedFd, { + setActiveFdProjection(openedFd, { kind: 'guest-file', targetFd, displayFd: openedFd, @@ -2382,13 +2620,27 @@ function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedF } function kernelProcFdPathForGuestPath(guestPath) { - const match = /^\/proc\/self\/fd\/(\d+)$/u.exec(String(guestPath)); + const match = /^(\/?)proc\/self\/fd\/(\d+)$/u.exec(String(guestPath)); + if (!match) return guestPath; + const guestFd = Number(match[2]); + if (!Number.isSafeInteger(guestFd) || guestFd < 0) return guestPath; + const handle = lookupFdHandle(guestFd); + return handle?.kind === 'kernel-fd' + ? `${match[1]}proc/self/fd/${Number(handle.targetFd) >>> 0}` + : guestPath; +} + +function kernelOpenPathForGuestPath(guestPath) { + const match = /^\/?(?:proc\/self\/fd|dev\/fd)\/(\d+)$/u.exec(String(guestPath)); if (!match) return guestPath; const guestFd = Number(match[1]); if (!Number.isSafeInteger(guestFd) || guestFd < 0) return guestPath; const handle = lookupFdHandle(guestFd); + // Keep the operand relative to its root capability. The kernel recognizes + // the normalized /dev/fd path as an open-description alias and intersects + // the requested rights with the backing descriptor's rights. return handle?.kind === 'kernel-fd' - ? `/proc/self/fd/${Number(handle.targetFd) >>> 0}` + ? `dev/fd/${Number(handle.targetFd) >>> 0}` : guestPath; } @@ -2402,7 +2654,10 @@ function fsOpenNumericFlagsForManagedPath(rightsBase, fdflags) { return flags; } -function openManagedPathIoFd(guestPath, rightsBase, fdflags) { +function openStandalonePathIoFd(guestPath, rightsBase, fdflags) { + if (SIDECAR_MANAGED_PROCESS) { + throw execError('EIO', 'managed path I/O must use the kernel descriptor table'); + } if (typeof guestPath !== 'string' || guestPath === '/dev/null') { return null; } @@ -2419,6 +2674,9 @@ function openManagedPathIoFd(guestPath, rightsBase, fdflags) { } function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags, rightsBase) { + if (SIDECAR_MANAGED_PROCESS) { + throw execError('EIO', 'managed path_open must not retain an ambient WASI descriptor'); + } if (!(instanceMemory instanceof WebAssembly.Memory)) { return WASI_ERRNO_SUCCESS; } @@ -2447,7 +2705,7 @@ function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags, rightsBase) { const append = (Number(fdflags) & WASI_FDFLAGS_APPEND) !== 0; retainDelegateFd(retainedFd); if (retainedFd > 2 && !passthroughHandles.has(retainedFd)) { - const ioFd = openManagedPathIoFd(guestPath, rightsBase, fdflags); + const ioFd = openStandalonePathIoFd(guestPath, rightsBase, fdflags); closedPassthroughFds.delete(retainedFd); passthroughHandles.set(retainedFd, { kind: 'passthrough', @@ -2485,6 +2743,53 @@ function writeGuestUint32(ptr, value) { } } +function guestRangeIsValid(ptr, length) { + if (!(instanceMemory instanceof WebAssembly.Memory)) return false; + const offset = Number(ptr); + const byteLength = Number(length); + return Number.isInteger(offset) && offset >= 0 && + Number.isInteger(byteLength) && byteLength >= 0 && + offset <= instanceMemory.buffer.byteLength - byteLength; +} + +function guestRangesAreValid(...ranges) { + return ranges.every(([ptr, length]) => guestRangeIsValid(ptr, length)); +} + +function validateGuestIovRequest(iovs, iovsLen) { + const count = Number(iovsLen) >>> 0; + const countErrno = checkFixedRequestLimit( + 'wasm.abi.maxIovecs', + count, + LINUX_IOV_MAX, + ); + if (countErrno !== WASI_ERRNO_SUCCESS) { + return { errno: countErrno, entries: [], totalLength: 0 }; + } + if (!guestRangeIsValid(iovs, count * 8)) { + return { errno: WASI_ERRNO_FAULT, entries: [], totalLength: 0 }; + } + + const view = new DataView(instanceMemory.buffer); + const tableOffset = Number(iovs) >>> 0; + const entries = []; + let totalLength = 0; + for (let index = 0; index < count; index += 1) { + const entryOffset = tableOffset + index * 8; + const ptr = view.getUint32(entryOffset, true); + const length = view.getUint32(entryOffset + 4, true); + if (!guestRangeIsValid(ptr, length)) { + return { errno: WASI_ERRNO_FAULT, entries: [], totalLength: 0 }; + } + totalLength += length; + if (!Number.isSafeInteger(totalLength) || totalLength > 0xffffffff) { + return { errno: WASI_ERRNO_INVAL, entries: [], totalLength: 0 }; + } + entries.push({ ptr, length }); + } + return { errno: WASI_ERRNO_SUCCESS, entries, totalLength }; +} + function readGuestUint32(ptr) { if (!(instanceMemory instanceof WebAssembly.Memory)) { throw new Error('WebAssembly memory is unavailable'); @@ -2511,7 +2816,7 @@ function statTimestampNs(value) { } function writeGuestFilestat(ptr, stats, filetype = WASI_FILETYPE_REGULAR_FILE) { - if (!(instanceMemory instanceof WebAssembly.Memory)) { + if (!guestRangeIsValid(ptr, 64)) { return WASI_ERRNO_FAULT; } @@ -2546,7 +2851,7 @@ function wasiFiletypeFromStats(stats) { } function writeGuestFdstat(ptr, filetype, flags, rightsBase, rightsInheriting) { - if (!(instanceMemory instanceof WebAssembly.Memory)) { + if (!guestRangeIsValid(ptr, 24)) { return WASI_ERRNO_FAULT; } @@ -2580,6 +2885,8 @@ function mapSyntheticFsError(error) { return WASI_ERRNO_ROFS; case 'EEXIST': return WASI_ERRNO_EXIST; + case 'EFAULT': + return WASI_ERRNO_FAULT; case 'EISDIR': return WASI_ERRNO_ISDIR; case 'ELOOP': @@ -2626,6 +2933,8 @@ function mapHostProcessError(error) { return WASI_ERRNO_2BIG; case 'EBADF': return WASI_ERRNO_BADF; + case 'EBUSY': + return WASI_ERRNO_BUSY; case 'EACCES': return WASI_ERRNO_ACCES; case 'EADDRINUSE': @@ -2639,6 +2948,10 @@ function mapHostProcessError(error) { return WASI_ERRNO_AGAIN; case 'EALREADY': return WASI_ERRNO_ALREADY; + case 'ECHILD': + return WASI_ERRNO_CHILD; + case 'EFAULT': + return WASI_ERRNO_FAULT; case 'EFBIG': return WASI_ERRNO_FBIG; case 'EEXIST': @@ -2655,6 +2968,8 @@ function mapHostProcessError(error) { return WASI_ERRNO_HOSTUNREACH; case 'EINPROGRESS': return WASI_ERRNO_INPROGRESS; + case 'EINTR': + return WASI_ERRNO_INTR; case 'EIO': return WASI_ERRNO_IO; case 'EILSEQ': @@ -2710,9 +3025,7 @@ function mapHostProcessError(error) { case 'EPROTONOSUPPORT': return WASI_ERRNO_PROTONOSUPPORT; default: - return /command not found:/i.test(String(error?.message ?? error)) - ? WASI_ERRNO_NOENT - : WASI_ERRNO_FAULT; + return WASI_ERRNO_FAULT; } } @@ -2760,8 +3073,11 @@ function createPipeHandle(kind, pipe, displayFd) { } function retainDelegateFd(fd) { + if (SIDECAR_MANAGED_PROCESS) { + throw new Error('managed execution cannot retain a Node-WASI delegate fd'); + } const numericFd = Number(fd) >>> 0; - delegateManagedFdRefCounts.set(numericFd, (delegateManagedFdRefCounts.get(numericFd) ?? 0) + 1); + standaloneDelegateFdRefCounts.set(numericFd, (standaloneDelegateFdRefCounts.get(numericFd) ?? 0) + 1); } function registerKernelDelegateFd( @@ -2770,6 +3086,11 @@ function registerKernelDelegateFd( minimumGuestFd = 3, shadowsInternalPreopen = false, ) { + if (!SIDECAR_MANAGED_PROCESS) { + const error = new Error('kernel descriptor projection requires managed execution'); + error.code = 'EIO'; + throw error; + } const rawKernelFd = Number(fd); if (!Number.isSafeInteger(rawKernelFd) || rawKernelFd < 0 || rawKernelFd > 0xffffffff) { const error = new Error(`kernel returned invalid file descriptor ${fd}`); @@ -2808,14 +3129,14 @@ function registerKernelDelegateFd( bootstrapStdioHandle == null && closedPassthroughFds.has(guestFd) && wasi?.fdTable?.has?.(guestFd) === true && - !syntheticFdEntries.has(guestFd) && + !activeFdProjections.has(guestFd) && !retainedSpawnOutputHandlesByFd.has(guestFd); const shadowsPrivatePreopen = shadowsInternalPreopen || shadowsRetainedBootstrapStdio || (hiddenPreopenHandles.has(guestFd) && (bootstrapStdioHandle == null || bootstrapStdioHandle.internalPreopen === true) && - !syntheticFdEntries.has(guestFd) && + !activeFdProjections.has(guestFd) && !retainedSpawnOutputHandlesByFd.has(guestFd)); replacesBootstrapStdio = guestFd <= 2 && @@ -2844,34 +3165,31 @@ function registerKernelDelegateFd( const handle = { kind: 'kernel-fd', targetFd: kernelFd, - displayFd: guestFd, - refCount: guestFd === kernelFd ? 0 : 1, - open: true, }; closedPassthroughFds.delete(guestFd); if (replacesBootstrapStdio || passthroughHandles.get(guestFd)?.internalPreopen === true) { passthroughHandles.delete(guestFd); - delegateManagedFdRefCounts.delete(guestFd); + standaloneDelegateFdRefCounts.delete(guestFd); } if (guestFd === kernelFd) { passthroughHandles.set(guestFd, handle); } else { - syntheticFdEntries.set(guestFd, handle); + setActiveFdProjection(guestFd, handle); } return guestFd; } function releaseDelegateFd(fd) { const numericFd = Number(fd) >>> 0; - const current = delegateManagedFdRefCounts.get(numericFd); + const current = standaloneDelegateFdRefCounts.get(numericFd); if (current == null) { return false; } if (current <= 1) { - delegateManagedFdRefCounts.delete(numericFd); + standaloneDelegateFdRefCounts.delete(numericFd); return true; } - delegateManagedFdRefCounts.set(numericFd, current - 1); + standaloneDelegateFdRefCounts.set(numericFd, current - 1); return false; } @@ -2881,7 +3199,7 @@ function lookupFdHandle(fd) { return hiddenPreopenHandles.get(numericFd & AGENTOS_HIDDEN_PREOPEN_FD_MASK) ?? null; } return ( - syntheticFdEntries.get(numericFd) ?? + activeFdProjections.get(numericFd) ?? retainedSpawnOutputHandlesByFd.get(numericFd)?.handle ?? passthroughHandles.get(numericFd) ?? null @@ -2890,8 +3208,12 @@ function lookupFdHandle(fd) { function kernelFdMappingsForSpawn() { const mappings = new Map(); - for (const [guestFd, handle] of [...passthroughHandles, ...syntheticFdEntries]) { - if (handle?.kind === 'kernel-fd' && handle.open !== false) { + for (const [guestFd, handle] of [...passthroughHandles, ...activeFdProjections]) { + if ( + handle?.kind === 'kernel-fd' && + handle.open !== false && + handle.internalPreopen !== true + ) { mappings.set(Number(guestFd) >>> 0, Number(handle.targetFd) >>> 0); } } @@ -2905,8 +3227,45 @@ function kernelFdMappingsForSpawn() { return [...mappings.entries()]; } +function guestFdCloseOnExec(fd) { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (SIDECAR_MANAGED_PROCESS && handle?.kind === 'kernel-fd') { + return ( + Number(callSyncRpc('process.fd_getfd', [Number(handle.targetFd) >>> 0])) & 1 + ) !== 0; + } + return standaloneCloexecFds.has(numericFd); +} + function hostNetFdsForSpawn() { - const descriptions = new Set(hostNetSockets.values()); + if (SIDECAR_MANAGED_PROCESS) { + const inherited = [...passthroughHandles, ...activeFdProjections] + .filter(([, handle]) => + handle?.kind === 'kernel-fd' && + typeof handle.hostNetDescriptionId === 'string' + ) + .map(([guestFd, handle]) => ({ + guestFd: Number(guestFd) >>> 0, + descriptionId: handle.hostNetDescriptionId, + closeOnExec: guestFdCloseOnExec(guestFd), + })); + if (inherited.length > configuredMaxOpenFds) { + const error = new Error( + `inherited host-network descriptors exceed limits.resources.maxOpenFds (${configuredMaxOpenFds}); ` + + 'raise limits.resources.maxOpenFds if needed', + ); + error.code = 'EMFILE'; + throw error; + } + return inherited; + } + const entries = [...standaloneHostNetSockets.entries()].map(([guestFd, socket]) => [ + guestFd, + socket, + null, + ]); + const descriptions = new Set(entries.map(([, socket]) => socket)); if (maxSockets != null && descriptions.size > maxSockets) { const error = new Error( `inherited host-network descriptions exceed limits.resources.maxSockets (${maxSockets}); ` + @@ -2915,9 +3274,10 @@ function hostNetFdsForSpawn() { error.code = 'EMFILE'; throw error; } - const inherited = [...hostNetSockets.entries()].map(([guestFd, socket]) => ({ + const inherited = entries.map(([guestFd, socket, descriptionId]) => ({ guestFd: Number(guestFd) >>> 0, - closeOnExec: runnerCloexecFds.has(Number(guestFd) >>> 0), + descriptionId, + closeOnExec: guestFdCloseOnExec(guestFd), socketId: socket.socketId ?? null, serverId: socket.serverId ?? null, udpSocketId: socket.udpSocketId ?? null, @@ -2949,7 +3309,7 @@ function hostNetFdsForSpawn() { function lookupSyntheticHandleByDisplayFd(fd, expectedKind = null) { const numericFd = Number(fd) >>> 0; - for (const handle of syntheticFdEntries.values()) { + for (const handle of activeFdProjections.values()) { if (!handle || handle.displayFd !== numericFd) { continue; } @@ -2995,6 +3355,9 @@ function cloneFdHandle(fd) { if (!handle) { return null; } + if (handle.kind === 'kernel-fd') { + throw new Error('managed kernel descriptors must be duplicated by the kernel'); + } handle.refCount += 1; return handle; } @@ -3060,15 +3423,13 @@ function releaseFdHandle(handle) { } if (handle.kind === 'kernel-fd') { - handle.refCount = Math.max(0, handle.refCount - 1); - if ( - handle.refCount === 0 && - handle.open && - !passthroughHandleHasCanonicalMapping(handle) - ) { - handle.open = false; - callSyncRpc('process.fd_close', [Number(handle.targetFd) >>> 0]); + // Managed preopens are hidden capability roots, not guest-owned Linux + // descriptors. Closing their untagged guest aliases must not destroy the + // backing kernel descriptions used by tagged libc pathname operations. + if (handle.internalPreopen === true) { + return; } + callSyncRpc('process.fd_close', [Number(handle.targetFd) >>> 0]); return; } @@ -3090,7 +3451,7 @@ function releaseFdHandle(handle) { function closeSyntheticFd(fd) { const numericFd = Number(fd) >>> 0; - const handle = syntheticFdEntries.get(numericFd); + const handle = activeFdProjections.get(numericFd); if (!handle) { return false; } @@ -3106,7 +3467,7 @@ function closeSyntheticFd(fd) { // the parent after close(2). Mask the underlying Node-WASI/bootstrap slot as // well; otherwise a later fstat(2) can fall through to a runtime-owned fd // with the same number and make a second close appear to be the first. - syntheticFdEntries.delete(numericFd); + activeFdProjections.delete(numericFd); closedPassthroughFds.add(numericFd); releaseFdHandle(handle); if (shouldRetainMapping) { @@ -3136,16 +3497,14 @@ function forgetSidecarClosedKernelFd(fd) { if (handle?.kind !== 'kernel-fd') { return false; } - if (syntheticFdEntries.get(numericFd) === handle) { - syntheticFdEntries.delete(numericFd); + if (activeFdProjections.get(numericFd) === handle) { + activeFdProjections.delete(numericFd); } if (passthroughHandles.get(numericFd) === handle) { passthroughHandles.delete(numericFd); } - handle.refCount = 0; - handle.open = false; closedPassthroughFds.add(numericFd); - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); traceHostProcess('exec-cloexec-kernel-fd-forgotten', { fd: numericFd, targetFd: Number(handle.targetFd) >>> 0, @@ -3153,6 +3512,34 @@ function forgetSidecarClosedKernelFd(fd) { return true; } +function forgetSidecarClosedKernelTargetFd(targetFd) { + const numericTargetFd = Number(targetFd) >>> 0; + const guestFds = new Set([ + ...activeFdProjections.keys(), + ...passthroughHandles.keys(), + ]); + let forgotten = false; + for (const guestFd of guestFds) { + const handle = lookupFdHandle(guestFd); + if ( + handle?.kind === 'kernel-fd' && + Number(handle.targetFd) >>> 0 === numericTargetFd + ) { + forgotten = forgetSidecarClosedKernelFd(guestFd) || forgotten; + } + } + const passthrough = passthroughHandles.get(numericTargetFd); + if ( + passthrough?.kind === 'passthrough' && + passthrough.internalPreopen !== true && + Number(passthrough.targetFd) >>> 0 === numericTargetFd + ) { + forgotten = closePassthroughFd(numericTargetFd) || forgotten; + standaloneCloexecFds.delete(numericTargetFd); + } + return forgotten; +} + function rejectClosedPassthroughFd(fd) { return closedPassthroughFds.has(Number(fd) >>> 0); } @@ -3171,14 +3558,14 @@ function collectInactivePipeHandles(pipe) { return; } - for (const [fd, handle] of Array.from(syntheticFdEntries.entries())) { + for (const [fd, handle] of Array.from(activeFdProjections.entries())) { if ( (handle.kind === 'pipe-read' || handle.kind === 'pipe-write') && handle.pipe === pipe && !handle.open && handle.refCount === 0 ) { - syntheticFdEntries.delete(fd); + activeFdProjections.delete(fd); } } @@ -3218,7 +3605,7 @@ function spawnStdinFdIsSyntheticPipe(fd) { function spawnFdIsKernelBacked(fd) { const numericFd = Number(fd) >>> 0; return lookupFdHandle(numericFd)?.kind === 'kernel-fd' || - delegateManagedFdRefCounts.has(numericFd); + standaloneDelegateFdRefCounts.has(numericFd); } // Shell input redirects (`cmd < file`) reach proc_spawn as a plain file fd in @@ -3312,62 +3699,43 @@ function releaseSpawnOutputHandles(retainedHandles) { } } -function collectGuestIovBytes(iovs, iovsLen) { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - throw new Error('WebAssembly memory is not available'); +function collectGuestIovBytes(iovs, iovsLen, validatedRequest = null) { + const request = validatedRequest ?? validateGuestIovRequest(iovs, iovsLen); + if (request.errno !== WASI_ERRNO_SUCCESS) { + throw new RangeError(`invalid guest iovec request: WASI errno ${request.errno}`); } - const chunks = []; - let totalLength = 0; - - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const view = new DataView(instanceMemory.buffer); - const entryOffset = (Number(iovs) >>> 0) + index * 8; - const ptr = view.getUint32(entryOffset, true); - const len = view.getUint32(entryOffset + 4, true); - const chunk = readGuestBytes(ptr, len); + for (const { ptr, length } of request.entries) { + const chunk = readGuestBytes(ptr, length); chunks.push(chunk); - totalLength += chunk.length; } - - return Buffer.concat(chunks, totalLength); + return Buffer.concat(chunks, request.totalLength); } -function writeBytesToGuestIovs(iovs, iovsLen, bytes) { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - throw new Error('WebAssembly memory is not available'); +function writeBytesToGuestIovs(iovs, iovsLen, bytes, validatedRequest = null) { + const request = validatedRequest ?? validateGuestIovRequest(iovs, iovsLen); + if (request.errno !== WASI_ERRNO_SUCCESS) { + throw new RangeError(`invalid guest iovec request: WASI errno ${request.errno}`); } - const source = Buffer.from(bytes ?? []); + const memory = new Uint8Array(instanceMemory.buffer); let written = 0; - - for (let index = 0; index < (Number(iovsLen) >>> 0) && written < source.length; index += 1) { - const view = new DataView(instanceMemory.buffer); - const memory = new Uint8Array(instanceMemory.buffer); - const entryOffset = (Number(iovs) >>> 0) + index * 8; - const ptr = view.getUint32(entryOffset, true); - const len = view.getUint32(entryOffset + 4, true); + for (const { ptr, length } of request.entries) { + if (written >= source.length) break; const remaining = source.length - written; - const chunkLength = Math.min(len >>> 0, remaining); - memory.set(source.subarray(written, written + chunkLength), ptr >>> 0); + const chunkLength = Math.min(length, remaining); + memory.set(source.subarray(written, written + chunkLength), ptr); written += chunkLength; } - return written >>> 0; } -function guestIovByteLength(iovs, iovsLen) { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - throw new Error('WebAssembly memory is not available'); - } - - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); +function guestIovByteLength(iovs, iovsLen, validatedRequest = null) { + const request = validatedRequest ?? validateGuestIovRequest(iovs, iovsLen); + if (request.errno !== WASI_ERRNO_SUCCESS) { + throw new RangeError(`invalid guest iovec request: WASI errno ${request.errno}`); } - return total >>> 0; + return request.totalLength; } function writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr) { @@ -3381,20 +3749,64 @@ function writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr) { } } -function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { +function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr, iovRequest) { try { - const requestedLength = guestIovByteLength(iovs, iovsLen); + const requestedLength = guestIovByteLength(iovs, iovsLen, iovRequest); if (requestedLength === 0) { return writeGuestUint32(nreadPtr, 0); } - if (socket.nonblock) { - let queued = dequeueHostNetBytes(socket, requestedLength); - if (queued.length > 0) { - return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); - } - if (socket.lastError) return mapHostProcessError(socket.lastError); - if (socket.readableEnded || socket.closed || !socket.socketId) { + if (socket?.managed === true) { + const targetFd = managedHostNetTargetFd(socket); + const stat = callSyncRpc('process.fd_stat', [targetFd]); + const nonblocking = (Number(stat?.flags) & KERNEL_O_NONBLOCK) !== 0; + const configured = nonblocking ? null : callSyncRpc( + 'process.hostnet_get_option', + [targetFd, 'receive-timeout'], + ); + const configuredTimeout = Number(configured?.durationMs); + const hasConfiguredTimeout = configured?.durationMs != null && + Number.isFinite(configuredTimeout) && configuredTimeout >= 0; + const waitLimit = nonblocking + ? 0 + : hasConfiguredTimeout ? configuredTimeout : unixConnectTimeoutMs; + const startedAt = Date.now(); + for (;;) { + if (!nonblocking) { + const remaining = Math.max(0, waitLimit - (Date.now() - startedAt)); + const readable = waitManagedHostNetReadable(socket, remaining, true); + if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; + if (!readable) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + const result = callSyncRpc('process.hostnet_recv', [ + targetFd, + requestedLength, + 0, + 0, + ]); + if (result != null && !isHostNetWouldBlock(result)) { + const bytes = result?.type === 'message' + ? hostNetDatagramBytes(result) + : decodeFsBytesPayload(result, 'managed host-network read'); + return writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr); + } + if (result == null) return writeGuestUint32(nreadPtr, 0); + if (nonblocking) return WASI_ERRNO_AGAIN; + if (Date.now() - startedAt >= waitLimit) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + } + + if (socket.nonblock) { + let queued = dequeueHostNetBytes(socket, requestedLength); + if (queued.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); + } + if (socket.lastError) return mapHostProcessError(socket.lastError); + if (socket.readableEnded || socket.closed || !socket.socketId) { return writeGuestUint32(nreadPtr, 0); } const result = readReadyHostNetSocket(socket, requestedLength, false, 0); @@ -3416,10 +3828,9 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { ? null : startedAt + Math.max(0, socket.recvTimeoutMs); const safeguardDeadline = startedAt + unixConnectTimeoutMs; - const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); let warnedNearLimit = false; while (true) { - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; const queued = dequeueHostNetBytes(socket, requestedLength); if (queued.length > 0) { return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); @@ -3433,12 +3844,11 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { if (receiveDeadline != null && now >= receiveDeadline) { return WASI_ERRNO_AGAIN; } - if (!warnedNearLimit && now >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] blocking socket read is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLimit = warnNearBlockingReadLimit( + 'blocking socket read', + startedAt, + warnedNearLimit, + ); if (now >= safeguardDeadline) { process.stderr.write( `[agentos] blocking socket read exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, @@ -3457,13 +3867,13 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { ? 0 : Math.max(0, Math.min(50, nextDeadline - now)); const result = readReadyHostNetSocket(socket, requestedLength, false, pollWaitMs); - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; if (result?.kind === 'data' && result.bytes.length > 0) { return writeHostNetBytesToGuestIovs(iovs, iovsLen, result.bytes, nreadPtr); } if (pumpsLocalChildren) { pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; } if (receiveDeadline != null && Date.now() >= receiveDeadline) { return WASI_ERRNO_AGAIN; @@ -3474,14 +3884,14 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { } } -function writeHostNetSocketFromGuestIovs(socket, iovs, iovsLen, nwrittenPtr) { - if (!socket?.socketId || socket.closed) { +function writeHostNetSocketFromGuestIovs(socket, iovs, iovsLen, nwrittenPtr, iovRequest) { + if (!socket || (socket.managed !== true && (!socket.socketId || socket.closed))) { return WASI_ERRNO_BADF; } let bytes; try { - bytes = collectGuestIovBytes(iovs, iovsLen); + bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest); } catch { return WASI_ERRNO_FAULT; } @@ -3490,9 +3900,15 @@ function writeHostNetSocketFromGuestIovs(socket, iovs, iovsLen, nwrittenPtr) { } try { - const written = Number( - callSyncRpc('net.write', [socket.socketId, bytes, socket.nonblock === true]), - ) >>> 0; + const written = Number(socket.managed === true + ? callSyncRpc('process.hostnet_send', [ + managedHostNetTargetFd(socket), + bytes, + 0, + null, + unixConnectTimeoutMs, + ]) + : callSyncRpc('net.write', [socket.socketId, bytes, socket.nonblock === true])) >>> 0; return writeGuestUint32(nwrittenPtr, written); } catch (error) { return mapHostProcessError(error); @@ -3629,7 +4045,7 @@ function registerPipeConsumer(fd, childId, stream) { } handle.pipe.consumers.set(`${childId}:${stream}`, { childId, stream }); const shouldDeferInitialDelivery = - stream === 'stdin' && !spawnedChildrenById.has(childId); + stream === 'stdin' && !childCorrelationsById.has(childId); traceHostProcess('register-consumer', { fd: Number(fd) >>> 0, childId, @@ -3677,7 +4093,7 @@ function flushPipeConsumers(pipe) { }); flushed = true; } catch (error) { - if (spawnedChildrenById.has(consumer?.childId) && isChildProcessGoneError(error)) { + if (childCorrelationsById.has(consumer?.childId) && isChildProcessGoneError(error)) { shouldRetryChunk = true; continue; } @@ -3712,7 +4128,7 @@ function closePipeConsumers(pipe) { }); closed = true; } catch (error) { - if (spawnedChildrenById.has(consumer?.childId) && isChildProcessGoneError(error)) { + if (childCorrelationsById.has(consumer?.childId) && isChildProcessGoneError(error)) { continue; } traceHostProcess('close-consumer-stdin-failed', { @@ -3751,7 +4167,12 @@ function parseInitialHostNetFds(value, fdLimit, socketLimit) { } const ids = [entry.socketId, entry.serverId, entry.udpSocketId] .filter((id) => typeof id === 'string' && id.length > 0); - if (ids.length !== 1) { + const managedPending = + SIDECAR_MANAGED_PROCESS && + ids.length === 0 && + typeof entry.descriptionId === 'string' && + /^[0-9]+$/.test(entry.descriptionId); + if (ids.length !== 1 && !managedPending) { throw new Error('inherited host-network entries require exactly one sidecar resource id'); } if (entry.guestFds.length === 0) { @@ -3929,6 +4350,21 @@ function routeChunkToDelegateFd(fd, bytes) { } function finalizeChildExit(record, exitCode, signal, coreDumped = false) { + if (SIDECAR_MANAGED_PROCESS) { + // This flag is only the bridge's "stdout/stderr EOF delivered" gate. The + // kernel remains the sole source of terminal kind, status, and reaping. + record.terminalEventObserved = true; + record.terminalEventObservedAtMs = Date.now(); + for (const fd of record.delegateRetainedFds ?? []) { + if (releaseDelegateFd(fd) && typeof delegateManagedFdClose === 'function') { + delegateManagedFdClose(fd); + } + } + releaseSpawnOutputHandles(record.retainedSpawnOutputHandles); + unregisterChildPipeProducers(record); + unregisterChildPipeConsumers(record); + return 0; + } const signalNumber = signal == null ? 0 : signalNumberFromName(signal) & 0x7f; const rawExitCode = signalNumber === 0 ? Number(exitCode ?? 1) & 0xff : 0; const status = signalNumber === 0 ? rawExitCode : 128 + signalNumber; @@ -3951,6 +4387,54 @@ function finalizeChildExit(record, exitCode, signal, coreDumped = false) { return status; } +function takeManagedWaitTransition(selector, options, _blocking) { + const numericSelector = Number(selector) | 0; + const normalizedOptions = Number(options) >>> 0; + for (;;) { + try { + const transition = callSyncRpc('process.waitpid', [numericSelector, normalizedOptions]); + dispatchPendingWasmSignals(); + return transition; + } catch (error) { + const mustInterrupt = dispatchPendingWasmSignals(true); + if (error?.code === 'EINTR' && !mustInterrupt) { + // The caught handlers all requested SA_RESTART. They have run on the + // guest thread, so reissue the kernel-authoritative blocking wait. + continue; + } + throw error; + } + } +} + +function reapManagedChildCorrelation(transition) { + if (transition?.event !== 'exit') return; + reapSpawnedChild(childCorrelationsByPid.get(Number(transition.pid) >>> 0)); +} + +function collectStaleManagedChildCorrelations() { + if (!SIDECAR_MANAGED_PROCESS) return; + const now = Date.now(); + if (now < nextManagedCorrelationCollectionAtMs) return; + nextManagedCorrelationCollectionAtMs = now + 1000; + for (const record of childCorrelationsByPid.values()) { + if (record?.terminalEventObserved !== true) continue; + try { + callSyncRpc('process.getpgid', [record.pid]); + } catch (error) { + if (error?.code === 'ESRCH' || error?.code === 'ECHILD') { + reapSpawnedChild(record); + } else { + traceHostProcess('managed-child-correlation-collection-fault', { + pid: record.pid, + code: error?.code ?? null, + message: error instanceof Error ? error.message : String(error), + }); + } + } + } +} + function pollChildEvent(record, waitMs) { if (Array.isArray(record?.pendingEvents) && record.pendingEvents.length > 0) { return record.pendingEvents.shift() ?? null; @@ -3962,12 +4446,7 @@ function pollChildEvent(record, waitMs) { } function isChildProcessGoneError(error) { - return ( - (error instanceof Error && error.code === 'ECHILD') || - (error instanceof Error && - typeof error.message === 'string' && - error.message.startsWith('ECHILD:')) - ); + return error != null && typeof error === 'object' && error.code === 'ECHILD'; } function resolveSyntheticGuestPath(value, fromGuestDir = '/') { @@ -4015,6 +4494,7 @@ function chmodMappedGuestPath(guestPath, hostPath, mode) { } function maybeCreateSyntheticCommandResult(command, args, cwd) { + if (SIDECAR_MANAGED_PROCESS) return null; const basename = path.posix.basename(String(command || '')); if (basename === 'chmod') { @@ -4131,9 +4611,9 @@ function reapSpawnedChild(record) { return; } - spawnedChildren.delete(record.pid); + childCorrelationsByPid.delete(record.pid); if (typeof record.childId === 'string' && record.childId.length > 0) { - spawnedChildrenById.delete(record.childId); + childCorrelationsById.delete(record.childId); } } @@ -4144,6 +4624,14 @@ function returnWaitedChild( retPidPtr, retCoreDumpedPtr, ) { + if (!guestRangesAreValid( + [retExitCodePtr, 4], + [retSignalPtr, 4], + [retPidPtr, 4], + [retCoreDumpedPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } // A successful wait may reap the child that generated SIGCHLD. Linux runs // the caught handler before returning the status without rewriting that // successful wait result to EINTR. @@ -4165,6 +4653,9 @@ function returnWaitedChild( } function returnLegacyWaitedChild(record, retStatusPtr, retPidPtr) { + if (!guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])) { + return WASI_ERRNO_FAULT; + } dispatchPendingWasmSignals(); if (writeGuestUint32(retStatusPtr, record.exitStatus ?? 0) !== WASI_ERRNO_SUCCESS) { return WASI_ERRNO_FAULT; @@ -4177,6 +4668,9 @@ function returnLegacyWaitedChild(record, retStatusPtr, retPidPtr) { } function returnRawWaitedChild(record, retStatusPtr, retPidPtr) { + if (!guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])) { + return WASI_ERRNO_FAULT; + } dispatchPendingWasmSignals(); if (writeGuestUint32(retStatusPtr, record.rawWaitStatus ?? 0) !== WASI_ERRNO_SUCCESS) { return WASI_ERRNO_FAULT; @@ -4238,15 +4732,21 @@ function processChildEvent(record, event) { return false; } +function childBridgeTerminal(record) { + return SIDECAR_MANAGED_PROCESS + ? record?.terminalEventObserved === true + : typeof record?.exitStatus === 'number'; +} + function pumpPipeProducers(pipe, waitMs) { let processed = false; for (const [producerKey, producer] of Array.from(pipe.producers.entries())) { - const record = spawnedChildrenById.get(producer.childId); + const record = childCorrelationsById.get(producer.childId); if (!record) { unregisterPipeProducer(pipe, producerKey); continue; } - if (typeof record.exitStatus === 'number') { + if (childBridgeTerminal(record)) { unregisterPipeProducer(pipe, producerKey); continue; } @@ -4324,8 +4824,16 @@ function pumpChildInputPipe(record, waitMs) { } function pumpSpawnedChildren(waitMs) { - const records = Array.from(spawnedChildren.values()).filter( - (record) => record && typeof record.exitStatus !== 'number', + if (SIDECAR_MANAGED_PROCESS) { + // Managed child lifecycle and stream routing are sidecar-owned. Pulling + // child events here races the global process pump and deadlocks when this + // guest is itself parked in a direct host call. Collection is safe here: + // it only queries kernel-owned lifecycle state from the guest thread. + collectStaleManagedChildCorrelations(); + return false; + } + const records = Array.from(childCorrelationsByPid.values()).filter( + (record) => record && !childBridgeTerminal(record), ); if (records.length === 0) { return false; @@ -4363,6 +4871,10 @@ function pumpSpawnedChildren(waitMs) { function pumpSpawnedChildrenOrWait(waitMs) { const boundedWaitMs = Math.max(1, Number(waitMs) >>> 0); + if (SIDECAR_MANAGED_PROCESS) { + callSyncRpc('process.sleep', [boundedWaitMs]); + return false; + } const progressed = pumpSpawnedChildren(boundedWaitMs); if (!progressed) { Atomics.wait(syntheticWaitArray, 0, 0, boundedWaitMs); @@ -4370,17 +4882,38 @@ function pumpSpawnedChildrenOrWait(waitMs) { return progressed; } +function pumpSpawnedChildrenOrWaitRestartable(waitMs) { + try { + pumpSpawnedChildrenOrWait(waitMs); + return false; + } catch (error) { + if (error?.code !== 'EINTR') throw error; + // The sidecar wakes managed sleeps with typed EINTR. Run the handler on + // the guest thread, then tell the caller whether its syscall must return or + // may be reissued under SA_RESTART. + return dispatchPendingWasmSignals(true); + } +} + function encodeGuestBytes(value) { return new TextEncoder().encode(String(value)); } function readGuestBytes(ptr, len) { if (!(instanceMemory instanceof WebAssembly.Memory)) { - throw new Error('WebAssembly memory is not available'); + throw execError('EFAULT', 'WebAssembly memory is not available'); } const start = Number(ptr) >>> 0; const length = Number(len) >>> 0; + const end = start + length; + if ( + !Number.isSafeInteger(end) || + start > instanceMemory.buffer.byteLength || + end > instanceMemory.buffer.byteLength + ) { + throw execError('EFAULT', 'guest byte range is outside WebAssembly memory'); + } return Buffer.from(new Uint8Array(instanceMemory.buffer, start, length)); } @@ -4489,32 +5022,109 @@ function decodeSyncRpcValue(value) { return value; } +// AGENTOS_SYNC_RPC_RESPONSE_LIMIT_HELPERS_BEGIN +function syncRpcResponseLineLimitError(limit, observed) { + const limitName = 'limits.reactor.maxBridgeResponseBytes'; + const error = new Error( + `WASM sync RPC response line exceeds ${limitName} (${limit} encoded bytes); ` + + `raise ${limitName} if larger host replies are required`, + ); + error.code = 'ERR_AGENTOS_RESOURCE_LIMIT'; + error.details = { + limitName, + limit, + observed, + resource: 'syncRpcResponseLineBytes', + }; + return error; +} + +function appendSyncRpcResponseChunk(buffer, chunk, limit) { + const newlineIndex = chunk.indexOf(0x0a); + const lineChunkBytes = newlineIndex >= 0 ? newlineIndex : chunk.byteLength; + const observedLineBytes = buffer.byteLength + lineChunkBytes; + if (observedLineBytes > limit) { + throw syncRpcResponseLineLimitError(limit, observedLineBytes); + } + + if (newlineIndex < 0) { + return { + buffer: Buffer.concat([buffer, chunk], observedLineBytes), + line: null, + observedLineBytes, + }; + } + + const suffix = chunk.subarray(newlineIndex + 1); + if (suffix.byteLength > limit) { + throw syncRpcResponseLineLimitError(limit, suffix.byteLength); + } + const line = buffer.byteLength === 0 + ? chunk.subarray(0, newlineIndex).toString('utf8') + : Buffer.concat([buffer, chunk.subarray(0, newlineIndex)], observedLineBytes).toString('utf8'); + return { + buffer: Buffer.from(suffix), + line, + observedLineBytes, + }; +} +// AGENTOS_SYNC_RPC_RESPONSE_LIMIT_HELPERS_END + +function observeSyncRpcResponseLineBytes(observed) { + const warnAt = Math.max(1, Math.ceil(maxSyncRpcResponseLineBytes * 0.8)); + if (warnedSyncRpcResponseLineBytes || observed < warnAt) return; + warnedSyncRpcResponseLineBytes = true; + if (typeof process?.stderr?.write === 'function') { + process.stderr.write( + `[agentos] WASM sync RPC response line usage ${observed}/${maxSyncRpcResponseLineBytes} ` + + 'encoded bytes is near limits.reactor.maxBridgeResponseBytes; ' + + 'raise limits.reactor.maxBridgeResponseBytes if larger host replies are required\n', + ); + } +} + function readSyncRpcLine() { while (true) { - const newlineIndex = syncRpcResponseBuffer.indexOf('\n'); + const newlineIndex = syncRpcResponseBuffer.indexOf(0x0a); if (newlineIndex >= 0) { - const line = syncRpcResponseBuffer.slice(0, newlineIndex); - syncRpcResponseBuffer = syncRpcResponseBuffer.slice(newlineIndex + 1); + observeSyncRpcResponseLineBytes(newlineIndex); + const line = syncRpcResponseBuffer.subarray(0, newlineIndex).toString('utf8'); + syncRpcResponseBuffer = Buffer.from(syncRpcResponseBuffer.subarray(newlineIndex + 1)); return line; } - const chunk = Buffer.alloc(4096); + const remaining = maxSyncRpcResponseLineBytes - syncRpcResponseBuffer.byteLength; + const readCapacity = Math.min(4096, remaining > 4095 ? 4096 : remaining + 1); + const chunk = Buffer.alloc(readCapacity); const bytesRead = readSync(NODE_SYNC_RPC_RESPONSE_FD, chunk, 0, chunk.length, null); if (bytesRead === 0) { throw new Error('secure-exec WASM sync RPC response channel closed unexpectedly'); } - syncRpcResponseBuffer += chunk.subarray(0, bytesRead).toString('utf8'); + const appended = appendSyncRpcResponseChunk( + syncRpcResponseBuffer, + chunk.subarray(0, bytesRead), + maxSyncRpcResponseLineBytes, + ); + observeSyncRpcResponseLineBytes(appended.observedLineBytes); + syncRpcResponseBuffer = appended.buffer; + if (appended.line !== null) return appended.line; } } -// Standard (non-realtime) Linux signals coalesce while pending. A Set both -// matches that behavior and bounds guest-local pending state to the finite -// signal-number domain even if the host dispatch hook is spammed. -const pendingWasmSignals = new Set(initialWasmPendingSignals); -const wasmSignalRegistrations = new Map(); -const wasmBlockedSignals = new Set(initialWasmSignalMask); let activeSpawnCallContext = null; +function currentKernelSignalMask() { + try { + const response = callSyncRpc('process.signal_mask', [3, []]); + return Array.isArray(response?.signals) ? response.signals : []; + } catch (error) { + if (error?.code === 'ERR_AGENTOS_WASM_SYNC_RPC_UNAVAILABLE') { + return []; + } + throw error; + } +} + function callSyncRpc(method, args = []) { if ( globalThis.__agentOSSyncRpc && @@ -4522,7 +5132,13 @@ function callSyncRpc(method, args = []) { ) { const startedNs = __agentOSWasmNowNs(); try { - return decodeSyncRpcValue(globalThis.__agentOSSyncRpc.callSync(method, args)); + // Give the V8 bridge the same explicit, engine-neutral byte projection + // used by the pipe fallback. Passing a Node Buffer directly makes its + // serialization depend on the bridge serializer and previously lost + // binary arguments for newly typed syscall methods such as sendmsg. + return decodeSyncRpcValue( + globalThis.__agentOSSyncRpc.callSync(method, encodeSyncRpcValue(args)), + ); } finally { __agentOSWasiRecordSyncRpc(method, 'glue', startedNs); } @@ -4554,13 +5170,26 @@ function callSyncRpc(method, args = []) { if (typeof response?.error?.code === 'string') { error.code = response.error.code; } + if (response?.error?.details !== undefined && response.error.details !== null) { + error.details = decodeSyncRpcValue(response.error.details); + } throw error; } finally { __agentOSWasiRecordSyncRpc(method, 'pipe', startedNs); } } -const hostNetSockets = new Map(); +// Standalone Node execution has no kernel descriptor table, so its compatibility +// fallback remains fd-keyed. Managed execution never consults this map: kernel +// file descriptions own allocation, aliasing, inheritance, and close lifetime. +const standaloneHostNetSockets = new Map(); +const initialManagedHostNetDescriptionIds = new Set( + SIDECAR_MANAGED_PROCESS + ? initialHostNetDescriptions + .map((entry) => String(entry?.descriptionId ?? '')) + .filter((descriptionId) => /^[0-9]+$/.test(descriptionId)) + : [], +); for (const inherited of initialHostNetDescriptions) { const metadata = inherited.metadata && typeof inherited.metadata === 'object' ? inherited.metadata @@ -4588,8 +5217,10 @@ for (const inherited of initialHostNetDescriptions) { lastError: null, nonblock: metadata.nonblocking === true, }; - for (const rawFd of inherited.guestFds) { - hostNetSockets.set(Number(rawFd) >>> 0, socket); + if (!SIDECAR_MANAGED_PROCESS) { + for (const rawFd of inherited.guestFds) { + standaloneHostNetSockets.set(Number(rawFd) >>> 0, socket); + } } } let warnedAboutOpenFdLimit = false; @@ -4597,12 +5228,12 @@ let warnedAboutOpenFdLimit = false; function runnerOpenFdSet() { const openFds = new Set([0, 1, 2]); for (const table of [ - syntheticFdEntries, + activeFdProjections, passthroughHandles, retainedSpawnOutputHandlesByFd, retainedSyntheticHandlesByDisplayFd, - delegateManagedFdRefCounts, - hostNetSockets, + standaloneDelegateFdRefCounts, + standaloneHostNetSockets, wasi?.fdTable, ]) { if (!table || typeof table.keys !== 'function') continue; @@ -4613,14 +5244,15 @@ function runnerOpenFdSet() { function hasRunnerOpenFdCapacity(additionalFds) { const openCount = runnerOpenFdSet().size; - const warnAt = Math.max(1, Math.floor(rlimitNofileSoft * 0.9)); + const nofileSoft = currentNofileSoftLimit(); + const warnAt = Math.max(1, Math.floor(nofileSoft * 0.9)); if (!warnedAboutOpenFdLimit && openCount >= warnAt) { warnedAboutOpenFdLimit = true; process.stderr.write( - `[agentos] WASM open fd usage ${openCount}/${rlimitNofileSoft} is near RLIMIT_NOFILE; raise the soft limit or limits.resources.maxOpenFds if needed\n`, + `[agentos] WASM open fd usage ${openCount}/${nofileSoft} is near RLIMIT_NOFILE; raise the soft limit or limits.resources.maxOpenFds if needed\n`, ); } - return openCount + Math.max(0, Number(additionalFds) >>> 0) <= rlimitNofileSoft; + return openCount + Math.max(0, Number(additionalFds) >>> 0) <= nofileSoft; } // Host-net socket fds must stay BELOW the guests' FD_SETSIZE (1024 in the // wasi-libc sysroot): libcurl's select-based Curl_poll / curl_multi_fdset @@ -4635,27 +5267,180 @@ const HOST_NET_MSG_PEEK = 0x0002; const HOST_NET_MSG_DONTWAIT = 0x0040; const HOST_NET_MSG_TRUNC = 0x0020; +function isHostNetWouldBlock(value) { + return value === HOST_NET_TIMEOUT_SENTINEL || ( + value != null && + typeof value === 'object' && + value.kind === 'wouldBlock' + ); +} + +function waitManagedHostNetReadable(socket, timeoutMs, restartableOperation) { + const numericTimeoutMs = Number(timeoutMs); + const deadline = timeoutMs == null || numericTimeoutMs < 0 + ? null + : Date.now() + (Number.isFinite(numericTimeoutMs) ? numericTimeoutMs : 0); + for (;;) { + if (dispatchPendingWasmSignals(restartableOperation === true)) { + return WASI_ERRNO_INTR; + } + const remaining = deadline == null + ? SPAWNED_CHILD_WAIT_SLICE_MS + : Math.max(0, deadline - Date.now()); + if (deadline == null || remaining > 0) { + // Keep runner-side signal and child-lifecycle checkpoints between + // sidecar waits. Managed child execution stays sidecar-owned; this pump + // performs only the collection work appropriate to the current mode. + pumpSpawnedChildren(0); + } + const waitMs = deadline == null + ? SPAWNED_CHILD_WAIT_SLICE_MS + : Math.min(SPAWNED_CHILD_WAIT_SLICE_MS, Math.max(0, deadline - Date.now())); + try { + const response = callSyncRpc('process.posix_poll', [[{ + fd: managedHostNetTargetFd(socket), + events: 0x001 | 0x040, + }], waitMs, null]); + if (Number(response?.readyCount) > 0) return true; + } catch (error) { + if (error?.code !== 'EINTR') throw error; + const mustInterrupt = dispatchPendingWasmSignals(restartableOperation === true); + if (restartableOperation !== true || mustInterrupt) return WASI_ERRNO_INTR; + } + if (deadline != null && Date.now() >= deadline) return false; + } +} + function getHostNetSocket(fd) { - return hostNetSockets.get(Number(fd) >>> 0) ?? null; + const numericFd = Number(fd) >>> 0; + if (!SIDECAR_MANAGED_PROCESS) { + return standaloneHostNetSockets.get(numericFd) ?? null; + } + const handle = lookupFdHandle(numericFd); + if ( + handle?.kind !== 'kernel-fd' || + typeof handle.hostNetDescriptionId !== 'string' + ) return null; + return { + managed: true, + guestFd: numericFd, + targetFd: Number(handle.targetFd) >>> 0, + }; +} + +function hasHostNetSocket(fd) { + return getHostNetSocket(fd) != null; +} + +function attachManagedHostNetDescription(guestFd, descriptionId) { + const normalized = String(descriptionId ?? ''); + if (!/^[0-9]+$/.test(normalized)) { + const error = new Error('host-network fd does not match a managed kernel description'); + error.code = 'EIO'; + throw error; + } + const handle = lookupFdHandle(guestFd); + if (handle?.kind !== 'kernel-fd') { + const error = new Error('managed host-network fd is not kernel-backed'); + error.code = 'EIO'; + throw error; + } + handle.hostNetDescriptionId = normalized; + return guestFd; +} + +function copyManagedHostNetDescription(sourceFd, targetFd) { + if (!SIDECAR_MANAGED_PROCESS) return; + const source = lookupFdHandle(Number(sourceFd) >>> 0); + if (typeof source?.hostNetDescriptionId !== 'string') return; + const target = lookupFdHandle(Number(targetFd) >>> 0); + if (target?.kind === 'kernel-fd') { + target.hostNetDescriptionId = source.hostNetDescriptionId; + } +} + +function hostNetGuestFds() { + if (!SIDECAR_MANAGED_PROCESS) return [...standaloneHostNetSockets.keys()]; + return [...passthroughHandles, ...activeFdProjections] + .filter(([, handle]) => + handle?.kind === 'kernel-fd' && + typeof handle.hostNetDescriptionId === 'string' + ) + .map(([guestFd]) => Number(guestFd) >>> 0); +} + +function registerNewHostNetSocket(socket) { + if (!SIDECAR_MANAGED_PROCESS) { + const fd = allocateHostNetSocketFd(); + if (fd == null) return null; + standaloneHostNetSockets.set(fd, socket); + return fd; + } + const socketType = Number(socket.sockType) >>> 0; + const result = callSyncRpc('process.hostnet_fd_open', [ + Number(socket.domain) >>> 0, + socketType & HOST_NET_SOCKET_TYPE_MASK, + socket.nonblock === true, + (socketType & HOST_NET_SOCK_CLOEXEC) !== 0, + ]); + const descriptionId = String(result?.descriptionId ?? ''); + if (!/^[0-9]+$/.test(descriptionId)) { + const error = new Error('kernel returned an invalid host-network description identity'); + error.code = 'EIO'; + throw error; + } + let guestFd; + try { + guestFd = registerKernelDelegateFd(result?.fd, null, FIRST_SYNTHETIC_FD); + if (guestFd > HOST_NET_SOCKET_FD_MAX) { + const error = new Error('no host-network descriptor is available below FD_SETSIZE'); + error.code = 'EMFILE'; + throw error; + } + attachManagedHostNetDescription(guestFd, descriptionId); + if (!SIDECAR_MANAGED_PROCESS && (socketType & HOST_NET_SOCK_CLOEXEC) !== 0) { + standaloneCloexecFds.add(guestFd); + } + return guestFd; + } catch (error) { + if (guestFd != null) wasiImport.fd_close(guestFd); + throw error; + } } -function validateHostNetSocketDescriptor(fd) { +function validateHostNetSocketDescriptor(fd, requireListening = false) { const numericFd = Number(fd) >>> 0; - const socket = hostNetSockets.get(numericFd); - if (socket && !socket.closed) return WASI_ERRNO_SUCCESS; - if (lookupFdHandle(numericFd) || delegateManagedFdRefCounts.has(numericFd)) { + if (SIDECAR_MANAGED_PROCESS) { + try { + callSyncRpc('process.hostnet_validate', [ + canonicalKernelFdForSpawnAction(numericFd), + requireListening === true, + ]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } + const socket = getHostNetSocket(numericFd); + if (socket && !socket.closed) { + if (!requireListening || (socket.serverId && socket.listening === true)) { + return WASI_ERRNO_SUCCESS; + } + return WASI_ERRNO_INVAL; + } + if (lookupFdHandle(numericFd) || standaloneDelegateFdRefCounts.has(numericFd)) { return WASI_ERRNO_NOTSOCK; } return WASI_ERRNO_BADF; } function allocateHostNetSocketFd() { - if (maxSockets != null && hostNetSockets.size >= maxSockets) { + if (maxSockets != null && new Set(standaloneHostNetSockets.values()).size >= maxSockets) { return null; } if (!hasRunnerOpenFdCapacity(1)) return null; const openFds = runnerOpenFdSet(); - const descriptorLimit = Math.min(HOST_NET_SOCKET_FD_MAX + 1, rlimitNofileSoft); + const descriptorLimit = Math.min(HOST_NET_SOCKET_FD_MAX + 1, currentNofileSoftLimit()); for (let fd = FIRST_SYNTHETIC_FD; fd < descriptorLimit; fd += 1) { if (!openFds.has(fd)) { return fd; @@ -4671,7 +5456,7 @@ function allocateHostNetDuplicateFd(minimumFd = 0) { } if (!hasRunnerOpenFdCapacity(1)) return null; const openFds = runnerOpenFdSet(); - const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, currentNofileSoftLimit()); for (let fd = minimum; fd < descriptorLimit; fd += 1) { if (!openFds.has(fd)) return fd; } @@ -4734,14 +5519,11 @@ function decodeHostNetSocketReadResult(result) { return { kind: 'end' }; } - if (result === HOST_NET_TIMEOUT_SENTINEL) { + if (isHostNetWouldBlock(result)) { return { kind: 'timeout' }; } if (typeof result === 'string') { - if (result === HOST_NET_TIMEOUT_SENTINEL) { - return { kind: 'timeout' }; - } return { kind: 'data', bytes: Buffer.from(result, 'base64') }; } @@ -4752,7 +5534,7 @@ function decodeHostNetSocketReadResult(result) { if (decoded == null) { return { kind: 'end' }; } - if (decoded === HOST_NET_TIMEOUT_SENTINEL) { + if (isHostNetWouldBlock(decoded)) { return { kind: 'timeout' }; } return { kind: 'timeout' }; @@ -4920,6 +5702,39 @@ function formatHostNetUnixAddress(address) { return 'unix-unnamed'; } +function managedHostNetAddressValue(raw) { + const unix = parseHostNetUnixAddress(raw); + if (unix?.autobind === true) return { type: 'unix-autobind' }; + if (typeof unix?.abstractPathHex === 'string') { + return { type: 'unix-abstract', hex: unix.abstractPathHex }; + } + if (typeof unix?.path === 'string') { + return { type: 'unix-path', path: unix.path }; + } + const inet = parseHostNetAddress(raw); + return { type: 'inet', host: inet.host, port: inet.port }; +} + +function managedHostNetTargetFd(socket) { + if (socket?.managed !== true || !Number.isInteger(socket.targetFd)) { + const error = new Error('managed host-network socket projection is invalid'); + error.code = 'EBADF'; + throw error; + } + return Number(socket.targetFd) >>> 0; +} + +function managedHostNetAddressText(value) { + if (typeof value?.address === 'string' && Number.isInteger(Number(value?.port))) { + return formatHostNetAddressInfo(value); + } + if (typeof value?.abstractPathHex === 'string') { + return `unix-abstract:${value.abstractPathHex}`; + } + if (typeof value?.path === 'string') return `unix:${value.path}`; + return 'unix-unnamed'; +} + function hostNetUnixNodePath(address) { if (typeof address?.abstractPathHex === 'string') { return `\0${Buffer.from(address.abstractPathHex, 'hex').toString('utf8')}`; @@ -4997,6 +5812,7 @@ const POSIX_SOCK_DGRAM = 2; // wasi-libc : SOCK_NONBLOCK / SOCK_CLOEXEC bits OR'd into the // socket(2) type argument (Linux-style socket(..., SOCK_STREAM | SOCK_NONBLOCK)). const HOST_NET_SOCK_NONBLOCK = 0x4000; +const HOST_NET_SOCK_CLOEXEC = 0x2000; const HOST_NET_SOL_SOCKET = 1; const HOST_NET_WASI_SOL_SOCKET = 0x7fffffff; const HOST_NET_SO_ERROR = 4; @@ -5238,6 +6054,8 @@ const LINUX_SIGNAL_NAMES = [ 'SIGSYS', ]; const LINUX_MAX_SIGNAL_NUMBER = 64; +const MAX_SUPPLEMENTARY_GROUPS = 64; +const MAX_ACCOUNT_RECORD_BYTES = 4096; function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { if (!(instanceMemory instanceof WebAssembly.Memory)) { @@ -5246,8 +6064,11 @@ function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { try { const requestedLength = Number(maxLen) >>> 0; - const memory = new Uint8Array(instanceMemory.buffer); const written = Math.min(requestedLength, bytes.byteLength); + if (!guestRangesAreValid([ptr, written], [actualLenPtr, 4])) { + return WASI_ERRNO_FAULT; + } + const memory = new Uint8Array(instanceMemory.buffer); memory.set(bytes.subarray(0, written), Number(ptr)); return writeGuestUint32(actualLenPtr, written); } catch { @@ -5255,6 +6076,38 @@ function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { } } +// Account lookup records follow the reentrant libc contract: publish the +// required size and return ERANGE without a partial record when the caller's +// buffer is short. Validate both output ranges before the first write. +function writeGuestAccountRecord(ptr, maxLen, bytes, actualLenPtr) { + if (!(instanceMemory instanceof WebAssembly.Memory)) { + return WASI_ERRNO_FAULT; + } + const memoryLength = instanceMemory.buffer.byteLength; + const outputOffset = Number(ptr) >>> 0; + const required = bytes.byteLength >>> 0; + const capacity = Number(maxLen) >>> 0; + const lengthOffset = Number(actualLenPtr) >>> 0; + if (lengthOffset > memoryLength - 4) { + return WASI_ERRNO_FAULT; + } + if (capacity >= required && outputOffset > memoryLength - required) { + return WASI_ERRNO_FAULT; + } + if (writeGuestUint32(lengthOffset, required) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + if (capacity < required) { + return WASI_ERRNO_RANGE; + } + try { + new Uint8Array(instanceMemory.buffer).set(bytes, outputOffset); + return WASI_ERRNO_SUCCESS; + } catch { + return WASI_ERRNO_FAULT; + } +} + // Perform a single NON-BLOCKING accept on a listening host_net socket. On success it // registers the accepted connection as a new host_net socket and returns // { acceptedFd, address } (address is a Buffer: "host:port" for TCP, the peer path for @@ -5264,7 +6117,7 @@ function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { // server never blocks inside accept() and starves already-connected clients. function tryHostNetAcceptOnce(socket) { let result = callSyncRpc('net.server_accept', [socket.serverId]); - if (!result || result === HOST_NET_TIMEOUT_SENTINEL) { + if (!result || isHostNetWouldBlock(result)) { return null; } if (typeof result === 'string') { @@ -5274,19 +6127,16 @@ function tryHostNetAcceptOnce(socket) { return null; } - const acceptedFd = allocateHostNetSocketFd(); - if (acceptedFd == null) { - callSyncRpc('net.destroy', [result.socketId]); - return { error: WASI_ERRNO_MFILE }; - } const localUnix = unixAddressFromSidecarInfo(result.info, 'local') ?? ((socket.bindOptions?.path != null || socket.bindOptions?.abstractPathHex != null) ? socket.bindOptions : null); const remoteUnix = unixAddressFromSidecarInfo(result.info, 'remote'); - hostNetSockets.set(acceptedFd, { + const acceptedSocket = { domain: socket.domain, - sockType: socket.sockType, + // Linux accept(2) does not inherit SOCK_CLOEXEC or O_NONBLOCK from the + // listener; accept4(2) supplies those flags explicitly. + sockType: Number(socket.sockType) & HOST_NET_SOCKET_TYPE_MASK, protocol: socket.protocol, bindOptions: null, localInfo: normalizeHostNetAddressInfo(result.info?.localAddress, result.info?.localPort), @@ -5304,7 +6154,18 @@ function tryHostNetAcceptOnce(socket) { readableEnded: false, closed: false, lastError: null, - }); + }; + let registeredAcceptedFd; + try { + registeredAcceptedFd = registerNewHostNetSocket(acceptedSocket); + } catch (error) { + callSyncRpc('net.destroy', [result.socketId]); + return { error: mapHostProcessError(error) }; + } + if (registeredAcceptedFd == null) { + callSyncRpc('net.destroy', [result.socketId]); + return { error: WASI_ERRNO_MFILE }; + } let address; if (result.info?.remoteAddress != null && result.info?.remotePort != null) { @@ -5315,24 +6176,67 @@ function tryHostNetAcceptOnce(socket) { } else { address = Buffer.from(remoteUnix ? formatHostNetUnixAddress(remoteUnix) : 'unix-unnamed', 'utf8'); } - return { acceptedFd, address }; + return { acceptedFd: registeredAcceptedFd, address }; } function cleanupAcceptedHostNetSocket(accepted, reason) { const acceptedFd = Number(accepted?.acceptedFd); if (!Number.isInteger(acceptedFd)) return null; - const acceptedSocket = hostNetSockets.get(acceptedFd); - hostNetSockets.delete(acceptedFd); + const acceptedSocket = getHostNetSocket(acceptedFd); if (!acceptedSocket?.socketId) return null; + const result = wasiImport.fd_close(acceptedFd); + if (result === WASI_ERRNO_SUCCESS) return null; try { - callSyncRpc('net.destroy', [acceptedSocket.socketId]); - return null; - } catch (error) { + const error = new Error(`close returned WASI errno ${result}`); + error.code = 'EIO'; process.stderr.write( `[agentos] failed to destroy accepted socket during ${reason}: ${error instanceof Error ? error.message : String(error)}\n`, ); return error; + } catch (error) { + return error; + } +} + +function cleanupDetachedHostNetSocket(numericFd, socket) { + let firstError = null; + const cleanup = (label, operation) => { + try { + operation(); + } catch (error) { + if (firstError == null) firstError = error; + process.stderr.write( + `[agentos] failed to ${label} while closing host_net fd ${numericFd}: ${error instanceof Error ? error.message : String(error)}\n`, + ); + } + }; + if (Array.isArray(socket.pendingAccepts)) { + for (const accepted of socket.pendingAccepts.splice(0)) { + const error = cleanupAcceptedHostNetSocket(accepted, 'listener close'); + if (firstError == null && error != null) firstError = error; + } + } + if (socket.localReservation != null) { + cleanup('release TCP port reservation', () => { + callSyncRpc('net.release_tcp_port', [socket.localReservation]); + }); + } + if (socket.socketId && !socket.closed) { + cleanup('destroy connected socket', () => { + callSyncRpc('net.destroy', [socket.socketId]); + }); } + if (socket.serverId) { + cleanup('close listener', () => { + callSyncRpc('net.server_close', [socket.serverId]); + }); + } + if (socket.udpSocketId) { + cleanup('close datagram socket', () => { + callSyncRpc('dgram.close', [socket.udpSocketId]); + }); + } + return firstError == null ? WASI_ERRNO_SUCCESS : mapHostProcessError(firstError); } const hostNetImport = { @@ -5341,9 +6245,19 @@ const hostNetImport = { // only when a connection is actually pending (a buffered non-blocking accept), so the // server's WaitForSomething does not spin forever inside a blocking accept(). // POLLOUT is always writable. - net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr) { + net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr, temporarySignalMask = null) { const n = Number(nfds) >>> 0; const base0 = Number(fdsPtr) >>> 0; + const pollFdLimit = Math.min(LINUX_IOV_MAX, currentNofileSoftLimit()); + const countErrno = checkFixedRequestLimit( + 'wasm.abi.maxPollFds', + n, + pollFdLimit, + ); + if (countErrno !== WASI_ERRNO_SUCCESS) return countErrno; + if (!guestRangesAreValid([base0, n * 8], [retReadyPtr, 4])) { + return WASI_ERRNO_FAULT; + } // Match Linux's public poll(2) ABI in the owned sysroot exactly. const POLLIN = 0x001; const POLLOUT = 0x004; @@ -5355,25 +6269,110 @@ const hostNetImport = { const NORMAL_READ_EVENTS = POLLIN | POLLRDNORM; const NORMAL_WRITE_EVENTS = POLLOUT | POLLWRNORM; const t = Number(timeoutMs) | 0; - const startedAt = Date.now(); - const deadline = t < 0 ? null : startedAt + Math.max(0, t); - const safeguardDeadline = startedAt + unixConnectTimeoutMs; - const safeguardApplies = deadline == null || safeguardDeadline < deadline; - const effectiveDeadline = safeguardApplies ? safeguardDeadline : deadline; - const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); - let warnedNearLimit = false; - const kernelManagedStdio = - KERNEL_STDIO_SYNC_RPC || - (typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && - process.env.AGENTOS_SANDBOX_ROOT.length > 0); - try { - while (true) { - if (safeguardApplies && !warnedNearLimit && Date.now() >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] blocking poll is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); + if (SIDECAR_MANAGED_PROCESS) { + // One sidecar-owned wait snapshots kernel fds and managed TCP/Unix/UDP + // sockets/listeners together. The optional ppoll mask travels in this + // same RPC; the guest never owns a mask-scope token and never pumps a + // managed socket or child while parked. + const view = new DataView(instanceMemory.buffer); + const targets = []; + const targetEntries = []; + let ready = 0; + for (let i = 0; i < n; i++) { + const base = base0 + i * 8; + const fd = view.getInt32(base, true); + const events = view.getUint16(base + 4, true); + let revents = 0; + const socket = getHostNetSocket(fd); + const handle = fd >= 0 ? lookupFdHandle(fd >>> 0) : undefined; + let targetFd = null; + if (socket?.managed === true) { + targetFd = managedHostNetTargetFd(socket); + } else if (handle?.kind === 'kernel-fd' || handle?.kind === 'passthrough') { + targetFd = Number(handle.targetFd) >>> 0; + } else if (handle) { + revents = events & (NORMAL_READ_EVENTS | NORMAL_WRITE_EVENTS); + } else if (fd >= 0 && fd <= 2) { + targetFd = fd >>> 0; + } else if (fd >= 0) { + revents = POLLNVAL; + } + view.setUint16(base + 6, revents, true); + if (revents !== 0) ready++; + if (targetFd != null) { + targets.push({ fd: targetFd, events }); + targetEntries.push({ base, events }); + } + } + + if (ready > 0) { + if (writeGuestUint32(retReadyPtr, ready) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } + if (targets.length > 0 || temporarySignalMask != null) { + const requestedWait = t < 0 ? null : t; + try { + const response = callSyncRpc('process.posix_poll', [ + targets, + requestedWait, + temporarySignalMask, + ]); + const observed = Array.isArray(response?.fds) ? response.fds : []; + for (let i = 0; i < targetEntries.length; i++) { + const revents = Number(observed[i]?.revents) & 0xffff; + new DataView(instanceMemory.buffer).setUint16( + targetEntries[i].base + 6, + revents, + true, + ); + if (revents !== 0) ready++; + } + // A signal released only when ppoll restored the caller's mask is + // dispatched before returning to guest code without rewriting an + // already-observed successful readiness result. + dispatchPendingWasmSignals(); + if (writeGuestUint32(retReadyPtr, ready) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + if (error?.code === 'EINTR') { + dispatchPendingWasmSignals(); + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_INTR; + } + return mapHostProcessError(error); } + } + if (t === 0) { + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } + // With no waitable descriptor, retain the runner's bounded timer loop + // below. It is the only path that can emit the 80%-of-timeout warning + // while preserving poll(2)'s zero-fd sleep semantics. + } + const startedAt = Date.now(); + const deadline = t < 0 ? null : startedAt + Math.max(0, t); + const safeguardDeadline = startedAt + unixConnectTimeoutMs; + const safeguardApplies = deadline == null || safeguardDeadline < deadline; + const effectiveDeadline = safeguardApplies ? safeguardDeadline : deadline; + let warnedNearLimit = false; + const kernelManagedStdio = KERNEL_STDIO_SYNC_RPC || SIDECAR_MANAGED_PROCESS; + try { + while (true) { + warnedNearLimit = warnNearBlockingReadLimit( + 'blocking poll', + startedAt, + warnedNearLimit, + safeguardApplies, + ); if (dispatchPendingWasmSignals()) { if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { return WASI_ERRNO_FAULT; @@ -5407,7 +6406,17 @@ const hostNetImport = { let revents = 0; const socket = getHostNetSocket(fd); const handle = fd >= 0 ? lookupFdHandle(fd >>> 0) : undefined; - if (socket && !socket.closed) { + if (socket?.managed === true) { + hasHostNetWaitTarget = true; + const response = callSyncRpc('process.hostnet_poll', [[{ + fd: managedHostNetTargetFd(socket), + readable: (events & NORMAL_READ_EVENTS) !== 0, + writable: (events & NORMAL_WRITE_EVENTS) !== 0, + }], 0]); + const observed = Array.isArray(response?.ready) ? response.ready[0] : null; + if (observed?.readable === true) revents |= events & NORMAL_READ_EVENTS; + if (observed?.writable === true) revents |= events & NORMAL_WRITE_EVENTS; + } else if (socket && !socket.closed) { hasHostNetWaitTarget = true; if (socket.serverId) { if (events & NORMAL_READ_EVENTS) { @@ -5585,7 +6594,14 @@ const hostNetImport = { for (let i = 0; i < n; i++) { const fd = v2.getInt32(base0 + i * 8, true); const s = getHostNetSocket(fd); - if (s && !s.serverId) { + if (s?.managed === true) { + callSyncRpc('process.hostnet_poll', [[{ + fd: managedHostNetTargetFd(s), + readable: true, + writable: false, + }], 10]); + pumpedSocket = true; + } else if (s && !s.serverId) { if (s.socketId) { pollHostNetSocket(s, 10); pumpedSocket = true; @@ -5609,6 +6625,7 @@ const hostNetImport = { }, net_socket(domain, sockType, protocol, retFdPtr) { try { + if (!guestRangeIsValid(retFdPtr, 4)) return WASI_ERRNO_FAULT; const numericDomain = Number(domain) >>> 0; // Rust's WASI networking compatibility path uses POSIX socket type // values (1/2), while the owned wasi-libc ABI and sidecar transfer @@ -5623,11 +6640,7 @@ const hostNetImport = { return WASI_ERRNO_NOTSUP; } - const fd = allocateHostNetSocketFd(); - if (fd == null) { - return WASI_ERRNO_MFILE; - } - hostNetSockets.set(fd, { + const socket = { domain: numericDomain, sockType: numericType, protocol: numericProtocol, @@ -5654,14 +6667,16 @@ const hostNetImport = { // early server response mid-upload, and a blocking recv() waits on a // server that is itself waiting for the rest of the request body. nonblock: (numericType & HOST_NET_SOCK_NONBLOCK) !== 0, - }); + }; + const fd = registerNewHostNetSocket(socket); + if (fd == null) return WASI_ERRNO_MFILE; const copyout = writeGuestUint32(retFdPtr, fd); if (copyout !== WASI_ERRNO_SUCCESS) { - hostNetSockets.delete(fd); + wasiImport.fd_close(fd); } return copyout; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, // Mark a host_net socket non-blocking (O_NONBLOCK). The patched wasi-libc fcntl cannot reach @@ -5669,8 +6684,23 @@ const hostNetImport = { net_set_nonblock(fd, enable) { const socket = getHostNetSocket(fd); if (!socket) return WASI_ERRNO_BADF; - socket.nonblock = (Number(enable) >>> 0) !== 0; - return WASI_ERRNO_SUCCESS; + const nonblocking = (Number(enable) >>> 0) !== 0; + try { + if (SIDECAR_MANAGED_PROCESS) { + const handle = lookupFdHandle(Number(fd) >>> 0); + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + const stat = callSyncRpc('process.fd_stat', [Number(handle.targetFd) >>> 0]); + const currentFlags = Number(stat?.flags) >>> 0; + const nextFlags = nonblocking + ? currentFlags | KERNEL_O_NONBLOCK + : currentFlags & ~KERNEL_O_NONBLOCK; + callSyncRpc('process.fd_set_flags', [Number(handle.targetFd) >>> 0, nextFlags]); + } + if (socket.managed !== true) socket.nonblock = nonblocking; + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } }, net_connect(fd, addrPtr, addrLen) { const socket = getHostNetSocket(fd); @@ -5690,6 +6720,14 @@ const hostNetImport = { // padding; cut at the first NUL so the unix path is clean before classification. const nulAt = rawAddr.indexOf(String.fromCharCode(0)); if (nulAt >= 0) rawAddr = rawAddr.slice(0, nulAt); + if (socket.managed === true) { + callSyncRpc('process.hostnet_connect', [ + managedHostNetTargetFd(socket), + managedHostNetAddressValue(rawAddr), + unixConnectTimeoutMs, + ]); + return WASI_ERRNO_SUCCESS; + } // AF_UNIX addresses use an explicit wire prefix so relative paths and paths containing ':' // cannot be mistaken for TCP host:port strings. const unixAddress = parseHostNetUnixAddress(rawAddr); @@ -5702,31 +6740,36 @@ const hostNetImport = { if (socket.serverId) { request.boundServerId = socket.serverId; } - const deadline = Date.now() + unixConnectTimeoutMs; - const warningAt = Date.now() + Math.floor(unixConnectTimeoutMs * 0.8); + const startedAt = Date.now(); + const deadline = startedAt + unixConnectTimeoutMs; let warnedNearLimit = false; let result; for (;;) { - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; try { result = callSyncRpc('net.connect', [request]); break; } catch (error) { if (mapHostProcessError(error) !== WASI_ERRNO_AGAIN) throw error; if (socket.nonblock) return WASI_ERRNO_AGAIN; - if (!warnedNearLimit && Date.now() >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] blocking AF_UNIX connect is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLimit = warnNearBlockingReadLimit( + 'blocking AF_UNIX connect', + startedAt, + warnedNearLimit, + ); if (Date.now() >= deadline) { process.stderr.write( `[agentos] blocking AF_UNIX connect exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, ); return WASI_ERRNO_TIMEDOUT; } - pumpSpawnedChildrenOrWait(Math.min(10, Math.max(1, deadline - Date.now()))); + if ( + pumpSpawnedChildrenOrWaitRestartable( + Math.min(10, Math.max(1, deadline - Date.now())), + ) + ) { + return WASI_ERRNO_INTR; + } } } if (!result || typeof result.socketId !== 'string') { @@ -5791,6 +6834,9 @@ const hostNetImport = { }, net_getaddrinfo(hostPtr, hostLen, portPtr, portLen, family, retAddrPtr, retAddrLenPtr) { try { + if (!guestRangeIsValid(retAddrLenPtr, 4)) return WASI_ERRNO_FAULT; + const outputCapacity = readGuestUint32(retAddrLenPtr); + if (!guestRangeIsValid(retAddrPtr, outputCapacity)) return WASI_ERRNO_FAULT; const hostname = readGuestString(hostPtr, hostLen); const numericFamily = Number(family) >>> 0; const lookupOptions = { hostname, all: true }; @@ -5819,7 +6865,7 @@ const hostNetImport = { const encoded = Buffer.from(JSON.stringify(payload), 'utf8'); return writeGuestBytes( retAddrPtr, - readGuestUint32(retAddrLenPtr), + outputCapacity, encoded, retAddrLenPtr, ); @@ -5838,6 +6884,15 @@ const hostNetImport = { retFlagsPtr, ) { try { + const outputCapacity = Number(outCap) >>> 0; + if (!guestRangesAreValid( + [outPtr, outputCapacity], + [retLenPtr, 4], + [retTtlPtr, 4], + [retFlagsPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } const numericType = Number(rrtype) >>> 0; const requestedType = numericType === 12 ? 'PTR' @@ -5898,7 +6953,7 @@ const hostNetImport = { if (writeGuestUint32(retLenPtr, payloadLength) !== WASI_ERRNO_SUCCESS) { return WASI_ERRNO_FAULT; } - if ((Number(outCap) >>> 0) < payloadLength) { + if (outputCapacity < payloadLength) { return WASI_ERRNO_NOBUFS; } @@ -5936,6 +6991,13 @@ const hostNetImport = { } try { + if (socket.managed === true) { + callSyncRpc('process.hostnet_bind', [ + managedHostNetTargetFd(socket), + managedHostNetAddressValue(readGuestString(addrPtr, addrLen)), + ]); + return WASI_ERRNO_SUCCESS; + } if (socket.bindOptions != null || socket.serverId != null) { return WASI_ERRNO_INVAL; } @@ -6033,14 +7095,21 @@ const hostNetImport = { if (!socket || socket.closed) { return validateHostNetSocketDescriptor(fd); } - if (socket.socketId != null) { + if (socket.managed !== true && socket.socketId != null) { return WASI_ERRNO_INVAL; } - if (!socket.bindOptions) { + if (socket.managed !== true && !socket.bindOptions) { return WASI_ERRNO_INVAL; } try { + if (socket.managed === true) { + callSyncRpc('process.hostnet_listen', [ + managedHostNetTargetFd(socket), + Math.max(0, Number(backlog) >>> 0), + ]); + return WASI_ERRNO_SUCCESS; + } const request = { backlog: Math.max(0, Number(backlog) >>> 0), }; @@ -6073,11 +7142,78 @@ const hostNetImport = { }, net_accept(fd, retFdPtr, retAddrPtr, retAddrLenPtr) { const socket = getHostNetSocket(fd); - const validation = validateHostNetSocketDescriptor(fd); + const validation = validateHostNetSocketDescriptor(fd, true); if (validation !== WASI_ERRNO_SUCCESS) return validation; + if (socket?.managed === true) { + if (!guestRangesAreValid([retFdPtr, 4], [retAddrLenPtr, 4])) { + return WASI_ERRNO_FAULT; + } + const addressCapacity = readGuestUint32(retAddrLenPtr); + if (!guestRangeIsValid(retAddrPtr, addressCapacity)) return WASI_ERRNO_FAULT; + let acceptedGuestFd = null; + try { + const listenerStat = callSyncRpc('process.fd_stat', [ + managedHostNetTargetFd(socket), + ]); + const listenerNonblocking = + (Number(listenerStat?.flags) & KERNEL_O_NONBLOCK) !== 0; + const startedAt = Date.now(); + let result = callSyncRpc('process.hostnet_accept', [ + managedHostNetTargetFd(socket), + false, + false, + 0, + ]); + while (result == null || isHostNetWouldBlock(result)) { + if (listenerNonblocking) return WASI_ERRNO_AGAIN; + const remaining = Math.max(0, unixConnectTimeoutMs - (Date.now() - startedAt)); + const readable = waitManagedHostNetReadable(socket, remaining, true); + if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; + if (!readable) return WASI_ERRNO_TIMEDOUT; + result = callSyncRpc('process.hostnet_accept', [ + managedHostNetTargetFd(socket), + false, + false, + 0, + ]); + if (result == null || isHostNetWouldBlock(result)) { + if (Date.now() - startedAt >= unixConnectTimeoutMs) return WASI_ERRNO_TIMEDOUT; + } + } + acceptedGuestFd = registerKernelDelegateFd(result.fd, null, FIRST_SYNTHETIC_FD); + attachManagedHostNetDescription(acceptedGuestFd, result.descriptionId); + const info = result.info ?? {}; + const address = Buffer.from(managedHostNetAddressText({ + address: info.remoteAddress, + port: info.remotePort, + path: info.remotePath, + abstractPathHex: info.remoteAbstractPathHex, + }), 'utf8'); + if (writeGuestUint32(retFdPtr, acceptedGuestFd) !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(acceptedGuestFd); + return WASI_ERRNO_FAULT; + } + const copied = writeGuestBytes( + retAddrPtr, + addressCapacity, + address, + retAddrLenPtr, + ); + if (copied !== WASI_ERRNO_SUCCESS) wasiImport.fd_close(acceptedGuestFd); + return copied; + } catch (error) { + if (acceptedGuestFd != null) wasiImport.fd_close(acceptedGuestFd); + return mapHostProcessError(error); + } + } if (!socket.serverId || socket.listening !== true) { return WASI_ERRNO_INVAL; } + if (!guestRangesAreValid([retFdPtr, 4], [retAddrLenPtr, 4])) { + return WASI_ERRNO_FAULT; + } + const addressCapacity = readGuestUint32(retAddrLenPtr); + if (!guestRangeIsValid(retAddrPtr, addressCapacity)) return WASI_ERRNO_FAULT; let accepted = null; try { @@ -6094,10 +7230,9 @@ const hostNetImport = { const receiveDeadline = Number.isFinite(receiveTimeoutMs) && receiveTimeoutMs > 0 ? startedAt + receiveTimeoutMs : null; - const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); let warnedNearLimit = false; while (!accepted) { - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; accepted = tryHostNetAcceptOnce(socket); if (!accepted && socket.nonblock) return WASI_ERRNO_AGAIN; if (!accepted) { @@ -6105,12 +7240,11 @@ const hostNetImport = { if (receiveDeadline != null && now >= receiveDeadline) { return WASI_ERRNO_AGAIN; } - if (!warnedNearLimit && now >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] blocking accept is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLimit = warnNearBlockingReadLimit( + 'blocking accept', + startedAt, + warnedNearLimit, + ); if (now >= safeguardDeadline) { process.stderr.write( `[agentos] blocking accept exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, @@ -6120,7 +7254,13 @@ const hostNetImport = { const nextDeadline = receiveDeadline == null ? safeguardDeadline : Math.min(receiveDeadline, safeguardDeadline); - pumpSpawnedChildrenOrWait(Math.min(10, Math.max(1, nextDeadline - now))); + if ( + pumpSpawnedChildrenOrWaitRestartable( + Math.min(10, Math.max(1, nextDeadline - now)), + ) + ) { + return WASI_ERRNO_INTR; + } } } if (accepted.error != null) { @@ -6132,7 +7272,7 @@ const hostNetImport = { } const addressCopyout = writeGuestBytes( retAddrPtr, - readGuestUint32(retAddrLenPtr), + addressCapacity, accepted.address, retAddrLenPtr, ); @@ -6149,12 +7289,7 @@ const hostNetImport = { return validateHostNetSocketDescriptor(fd); }, net_validate_accept(fd) { - const validation = validateHostNetSocketDescriptor(fd); - if (validation !== WASI_ERRNO_SUCCESS) return validation; - const socket = getHostNetSocket(fd); - return socket?.serverId && socket.listening === true - ? WASI_ERRNO_SUCCESS - : WASI_ERRNO_INVAL; + return validateHostNetSocketDescriptor(fd, true); }, net_getsockname(fd, addrPtr, addrLenPtr) { const socket = getHostNetSocket(fd); @@ -6162,16 +7297,31 @@ const hostNetImport = { return validateHostNetSocketDescriptor(fd); } try { + if (!guestRangeIsValid(addrLenPtr, 4)) return WASI_ERRNO_FAULT; + const addressCapacity = readGuestUint32(addrLenPtr); + if (!guestRangeIsValid(addrPtr, addressCapacity)) return WASI_ERRNO_FAULT; + if (socket.managed === true) { + const value = callSyncRpc('process.hostnet_local_address', [ + managedHostNetTargetFd(socket), + ]); + if (value == null) return WASI_ERRNO_INVAL; + return writeGuestBytes( + addrPtr, + addressCapacity, + Buffer.from(managedHostNetAddressText(value), 'utf8'), + addrLenPtr, + ); + } refreshHostNetUnixSocketInfo(socket); if (socket.localUnixAddress != null) { const address = Buffer.from(socket.localUnixAddress, 'utf8'); - return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + return writeGuestBytes(addrPtr, addressCapacity, address, addrLenPtr); } if (!socket.localInfo) { return WASI_ERRNO_INVAL; } const address = Buffer.from(formatHostNetAddressInfo(socket.localInfo), 'utf8'); - return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + return writeGuestBytes(addrPtr, addressCapacity, address, addrLenPtr); } catch (error) { return mapHostProcessError(error); } @@ -6182,24 +7332,41 @@ const hostNetImport = { return validateHostNetSocketDescriptor(fd); } try { + if (!guestRangeIsValid(addrLenPtr, 4)) return WASI_ERRNO_FAULT; + const addressCapacity = readGuestUint32(addrLenPtr); + if (!guestRangeIsValid(addrPtr, addressCapacity)) return WASI_ERRNO_FAULT; + if (socket.managed === true) { + const value = callSyncRpc('process.hostnet_peer_address', [ + managedHostNetTargetFd(socket), + ]); + return writeGuestBytes( + addrPtr, + addressCapacity, + Buffer.from(managedHostNetAddressText(value), 'utf8'), + addrLenPtr, + ); + } refreshHostNetUnixSocketInfo(socket); if (socket.remoteUnixAddress != null) { const address = Buffer.from(socket.remoteUnixAddress, 'utf8'); - return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + return writeGuestBytes(addrPtr, addressCapacity, address, addrLenPtr); } if (!socket.remoteInfo) { return WASI_ERRNO_NOTCONN; } const address = Buffer.from(formatHostNetAddressInfo(socket.remoteInfo), 'utf8'); - return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + return writeGuestBytes(addrPtr, addressCapacity, address, addrLenPtr); } catch (error) { return mapHostProcessError(error); } }, net_send(fd, bufPtr, bufLen, flags, retSentPtr) { + if (!guestRangesAreValid([bufPtr, Number(bufLen) >>> 0], [retSentPtr, 4])) { + return WASI_ERRNO_FAULT; + } const socket = getHostNetSocket(fd); const handle = lookupFdHandle(Number(fd) >>> 0); - if (handle?.kind === 'kernel-fd') { + if (!socket && handle?.kind === 'kernel-fd') { try { const chunk = readGuestBytes(bufPtr, bufLen); const written = Number(callSyncRpc('process.fd_sendmsg_rights', [ @@ -6213,12 +7380,22 @@ const hostNetImport = { return mapHostProcessError(error); } } - if (!socket?.socketId || socket.closed) { + if (!socket || (socket.managed !== true && (!socket.socketId || socket.closed))) { return WASI_ERRNO_BADF; } try { const chunk = readGuestBytes(bufPtr, bufLen); + if (socket.managed === true) { + const written = Number(callSyncRpc('process.hostnet_send', [ + managedHostNetTargetFd(socket), + chunk, + Number(flags) >>> 0, + null, + unixConnectTimeoutMs, + ])) >>> 0; + return writeGuestUint32(retSentPtr, written); + } if ((Number(flags) >>> 0) !== 0) { // Non-zero send flags are currently ignored in the WASM host_net shim. } @@ -6236,9 +7413,12 @@ const hostNetImport = { } }, net_recv(fd, bufPtr, bufLen, flags, retReceivedPtr) { + if (!guestRangesAreValid([bufPtr, Number(bufLen) >>> 0], [retReceivedPtr, 4])) { + return WASI_ERRNO_FAULT; + } const socket = getHostNetSocket(fd); const handle = lookupFdHandle(Number(fd) >>> 0); - if (handle?.kind === 'kernel-fd') { + if (!socket && handle?.kind === 'kernel-fd') { try { const recvFlags = Number(flags) >>> 0; const result = callSyncRpc('process.fd_recvmsg_rights', [ @@ -6267,6 +7447,52 @@ const hostNetImport = { try { const recvFlags = Number(flags) >>> 0; + if (socket.managed === true) { + const nonblocking = (recvFlags & HOST_NET_MSG_DONTWAIT) !== 0; + const configured = nonblocking ? null : callSyncRpc( + 'process.hostnet_get_option', + [managedHostNetTargetFd(socket), 'receive-timeout'], + ); + const configuredTimeout = Number(configured?.durationMs); + const hasConfiguredTimeout = configured?.durationMs != null + && Number.isFinite(configuredTimeout) && configuredTimeout >= 0; + const waitLimit = nonblocking + ? 0 + : hasConfiguredTimeout ? configuredTimeout : unixConnectTimeoutMs; + const startedAt = Date.now(); + let result; + for (;;) { + if (!nonblocking) { + const remaining = Math.max(0, waitLimit - (Date.now() - startedAt)); + const readable = waitManagedHostNetReadable(socket, remaining, true); + if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; + if (!readable) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + result = callSyncRpc('process.hostnet_recv', [ + managedHostNetTargetFd(socket), + Number(bufLen) >>> 0, + recvFlags, + 0, + ]); + if (!isHostNetWouldBlock(result)) break; + if (nonblocking) return WASI_ERRNO_AGAIN; + if (Date.now() - startedAt >= waitLimit) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + if (result == null) return writeGuestUint32(retReceivedPtr, 0); + const bytes = result?.type === 'message' + ? hostNetDatagramBytes(result) + : decodeFsBytesPayload(result, 'managed host-network receive'); + const write = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); + if (write !== WASI_ERRNO_SUCCESS) return write; + if ((recvFlags & HOST_NET_MSG_TRUNC) !== 0 && bytes.length > (Number(bufLen) >>> 0)) { + return writeGuestUint32(retReceivedPtr, bytes.length); + } + return WASI_ERRNO_SUCCESS; + } if (hostNetSocketBaseType(socket) === HOST_NET_SOCK_DGRAM) { const supportedFlags = HOST_NET_MSG_PEEK | HOST_NET_MSG_DONTWAIT | HOST_NET_MSG_TRUNC; if ((recvFlags & ~supportedFlags) !== 0) { @@ -6329,10 +7555,9 @@ const hostNetImport = { ? null : startedAt + Math.max(0, socket.recvTimeoutMs); const safeguardDeadline = startedAt + unixConnectTimeoutMs; - const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); let warnedNearLimit = false; while (true) { - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; const queued = peek ? peekHostNetBytes(socket, bufLen) : dequeueHostNetBytes(socket, bufLen); if (queued.length > 0) { return writeGuestBytes(bufPtr, bufLen, queued, retReceivedPtr); @@ -6350,12 +7575,11 @@ const hostNetImport = { if (receiveDeadline != null && now >= receiveDeadline) { return WASI_ERRNO_AGAIN; } - if (!warnedNearLimit && now >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] blocking socket receive is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLimit = warnNearBlockingReadLimit( + 'blocking socket receive', + startedAt, + warnedNearLimit, + ); if (now >= safeguardDeadline) { process.stderr.write( `[agentos] blocking socket receive exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, @@ -6373,13 +7597,13 @@ const hostNetImport = { ? 0 : Math.max(0, Math.min(50, nextDeadline - now)); const result = readReadyHostNetSocket(socket, bufLen, peek, pollWaitMs); - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; if (result?.kind === 'data' && result.bytes.length > 0) { return writeGuestBytes(bufPtr, bufLen, result.bytes, retReceivedPtr); } if (pumpsLocalChildren) { pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; } if (receiveDeadline != null && Date.now() >= receiveDeadline) { return WASI_ERRNO_AGAIN; @@ -6390,6 +7614,13 @@ const hostNetImport = { } }, net_sendto(fd, bufPtr, bufLen, flags, addrPtr, addrLen, retSentPtr) { + if (!guestRangesAreValid( + [bufPtr, Number(bufLen) >>> 0], + [addrPtr, Number(addrLen) >>> 0], + [retSentPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } const socket = getHostNetSocket(fd); if (!socket || socket.closed) { return WASI_ERRNO_BADF; @@ -6399,13 +7630,23 @@ const hostNetImport = { if ((Number(flags) >>> 0) !== 0) { return WASI_ERRNO_INVAL; } + const { host, port } = parseHostNetAddress(readGuestString(addrPtr, addrLen)); + const chunk = readGuestBytes(bufPtr, bufLen); + if (socket.managed === true) { + const result = callSyncRpc('process.hostnet_send', [ + managedHostNetTargetFd(socket), + chunk, + Number(flags) >>> 0, + { type: 'inet', host, port }, + unixConnectTimeoutMs, + ]); + const written = Number(result?.bytes ?? result) >>> 0; + return writeGuestUint32(retSentPtr, written); + } const udpSocketId = ensureHostNetUdpSocket(socket); if (!udpSocketId) { return WASI_ERRNO_FAULT; } - - const { host, port } = parseHostNetAddress(readGuestString(addrPtr, addrLen)); - const chunk = readGuestBytes(bufPtr, bufLen); const result = callSyncRpc('dgram.send', [ udpSocketId, chunk, @@ -6414,8 +7655,8 @@ const hostNetImport = { socket.localInfo = normalizeHostNetAddressInfo(result?.localAddress, result?.localPort); const written = Number(result?.bytes) >>> 0; return writeGuestUint32(retSentPtr, written); - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_recvfrom(fd, bufPtr, bufLen, flags, retReceivedPtr, retAddrPtr, retAddrLenPtr) { @@ -6425,11 +7666,69 @@ const hostNetImport = { } try { + if (!guestRangesAreValid( + [bufPtr, Number(bufLen) >>> 0], + [retReceivedPtr, 4], + [retAddrLenPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } + const addressCapacity = readGuestUint32(retAddrLenPtr); + if (!guestRangeIsValid(retAddrPtr, addressCapacity)) return WASI_ERRNO_FAULT; const recvFlags = Number(flags) >>> 0; const supportedFlags = HOST_NET_MSG_PEEK | HOST_NET_MSG_DONTWAIT | HOST_NET_MSG_TRUNC; if ((recvFlags & ~supportedFlags) !== 0) { return WASI_ERRNO_INVAL; } + if (socket.managed === true) { + const nonblocking = (recvFlags & HOST_NET_MSG_DONTWAIT) !== 0; + const configured = nonblocking ? null : callSyncRpc( + 'process.hostnet_get_option', + [managedHostNetTargetFd(socket), 'receive-timeout'], + ); + const configuredTimeout = Number(configured?.durationMs); + const hasConfiguredTimeout = configured?.durationMs != null + && Number.isFinite(configuredTimeout) && configuredTimeout >= 0; + const waitLimit = nonblocking + ? 0 + : hasConfiguredTimeout ? configuredTimeout : unixConnectTimeoutMs; + const startedAt = Date.now(); + let event; + for (;;) { + if (!nonblocking) { + const remaining = Math.max(0, waitLimit - (Date.now() - startedAt)); + const readable = waitManagedHostNetReadable(socket, remaining, true); + if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; + if (!readable) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + event = callSyncRpc('process.hostnet_recv', [ + managedHostNetTargetFd(socket), + Number(bufLen) >>> 0, + recvFlags, + 0, + ]); + if (event != null && !isHostNetWouldBlock(event)) break; + if (nonblocking) return WASI_ERRNO_AGAIN; + if (Date.now() - startedAt >= waitLimit) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + } + const bytes = hostNetDatagramBytes(event); + const dataResult = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); + if (dataResult !== WASI_ERRNO_SUCCESS) return dataResult; + const address = Buffer.from(formatHostNetAddressInfo({ + address: event.remoteAddress, + port: event.remotePort, + }), 'utf8'); + const addressResult = writeGuestBytes(retAddrPtr, addressCapacity, address, retAddrLenPtr); + if (addressResult !== WASI_ERRNO_SUCCESS) return addressResult; + if ((recvFlags & HOST_NET_MSG_TRUNC) !== 0 && bytes.length > (Number(bufLen) >>> 0)) { + return writeGuestUint32(retReceivedPtr, bytes.length); + } + return WASI_ERRNO_SUCCESS; + } const udpSocketId = ensureHostNetUdpSocket(socket); if (!udpSocketId) { return WASI_ERRNO_FAULT; @@ -6455,12 +7754,6 @@ const hostNetImport = { } catch { return WASI_ERRNO_INVAL; } - let addressCapacity; - try { - addressCapacity = readGuestUint32(retAddrLenPtr); - } catch { - return WASI_ERRNO_FAULT; - } const addressResult = writeGuestBytes(retAddrPtr, addressCapacity, address, retAddrLenPtr); if (addressResult !== WASI_ERRNO_SUCCESS) { return addressResult; @@ -6469,8 +7762,8 @@ const hostNetImport = { return writeGuestUint32(retReceivedPtr, bytes.length); } return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_setsockopt(fd, level, optname, optvalPtr, optvalLen) { @@ -6491,7 +7784,15 @@ const hostNetImport = { return WASI_ERRNO_INVAL; } if (sockoptKind === 'recv-timeout') { - socket.recvTimeoutMs = timeoutMs; + if (socket.managed === true) { + callSyncRpc('process.hostnet_set_option', [ + managedHostNetTargetFd(socket), + 'receive-timeout', + { durationMs: timeoutMs }, + ]); + } else { + socket.recvTimeoutMs = timeoutMs; + } } } catch { return WASI_ERRNO_FAULT; @@ -6505,7 +7806,9 @@ const hostNetImport = { } try { + if (!guestRangeIsValid(optvalLenPtr, 4)) return WASI_ERRNO_FAULT; const optvalLen = readGuestUint32(optvalLenPtr); + if (!guestRangeIsValid(optvalPtr, optvalLen)) return WASI_ERRNO_FAULT; const normalizedLevel = Number(level) >>> 0; const normalizedOptname = Number(optname) >>> 0; if ( @@ -6516,7 +7819,17 @@ const hostNetImport = { if (optvalLen < 4) { return WASI_ERRNO_INVAL; } - new DataView(instanceMemory.buffer).setInt32(Number(optvalPtr) >>> 0, 0, true); + const socketError = socket.managed === true + ? Number(callSyncRpc('process.hostnet_get_option', [ + managedHostNetTargetFd(socket), + 'error', + ])) | 0 + : 0; + new DataView(instanceMemory.buffer).setInt32( + Number(optvalPtr) >>> 0, + socketError, + true, + ); return writeGuestUint32(optvalLenPtr, 4); } return WASI_ERRNO_INVAL; @@ -6526,65 +7839,55 @@ const hostNetImport = { }, net_close(fd) { const numericFd = Number(fd) >>> 0; - const socket = hostNetSockets.get(numericFd); + const socket = getHostNetSocket(numericFd); if (!socket) { return WASI_ERRNO_BADF; } - - hostNetSockets.delete(numericFd); - // dup/dup2 and inherited descriptors are aliases of one Linux open-file - // description. Closing one descriptor must not destroy the sidecar socket - // while another descriptor in this process still refers to it. - if ([...hostNetSockets.values()].some((candidate) => candidate === socket)) { - return WASI_ERRNO_SUCCESS; - } - let firstError = null; - const cleanup = (label, operation) => { + if (SIDECAR_MANAGED_PROCESS) { + const handle = lookupFdHandle(numericFd); + if (handle?.kind !== 'kernel-fd') { + return WASI_ERRNO_BADF; + } try { - operation(); + const closed = activeFdProjections.get(numericFd) === handle + ? closeSyntheticFd(numericFd) + : closePassthroughFd(numericFd); + if (!closed) return WASI_ERRNO_BADF; + return WASI_ERRNO_SUCCESS; } catch (error) { - if (firstError == null) firstError = error; - process.stderr.write( - `[agentos] failed to ${label} while closing host_net fd ${numericFd}: ${error instanceof Error ? error.message : String(error)}\n`, - ); + return mapHostProcessError(error); } - }; - if (Array.isArray(socket.pendingAccepts)) { - for (const accepted of socket.pendingAccepts.splice(0)) { - const error = cleanupAcceptedHostNetSocket(accepted, 'listener close'); - if (firstError == null && error != null) firstError = error; + } else { + standaloneHostNetSockets.delete(numericFd); + // dup/dup2 aliases share one compatibility object when no kernel exists. + if ([...standaloneHostNetSockets.values()].some((candidate) => candidate === socket)) { + return WASI_ERRNO_SUCCESS; } } - if (socket.localReservation != null) { - cleanup('release TCP port reservation', () => { - callSyncRpc('net.release_tcp_port', [socket.localReservation]); - }); - } - if (socket.socketId && !socket.closed) { - cleanup('destroy connected socket', () => { - callSyncRpc('net.destroy', [socket.socketId]); - }); - } - if (socket.serverId) { - cleanup('close listener', () => { - callSyncRpc('net.server_close', [socket.serverId]); - }); - } - if (socket.udpSocketId) { - cleanup('close datagram socket', () => { - callSyncRpc('dgram.close', [socket.udpSocketId]); - }); - } - return firstError == null ? WASI_ERRNO_SUCCESS : mapHostProcessError(firstError); + return SIDECAR_MANAGED_PROCESS + ? WASI_ERRNO_SUCCESS + : cleanupDetachedHostNetSocket(numericFd, socket); }, net_tls_connect(fd, hostnamePtr, hostnameLen, flags = 0) { const socket = getHostNetSocket(fd); - if (!socket?.socketId || socket.closed) { + if (!socket || (socket.managed !== true && (!socket.socketId || socket.closed))) { return WASI_ERRNO_BADF; } try { const servername = readGuestString(hostnamePtr, hostnameLen); + if (socket.managed === true) { + if ((Number(flags) & 1) === 1 || guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') { + return WASI_ERRNO_NOTSUP; + } + callSyncRpc('process.hostnet_tls_connect', [ + managedHostNetTargetFd(socket), + servername, + [], + unixConnectTimeoutMs, + ]); + return WASI_ERRNO_SUCCESS; + } const tlsOptions = { servername }; if ((Number(flags) & 1) === 1 || guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') { tlsOptions.rejectUnauthorized = false; @@ -6613,6 +7916,7 @@ const hostProcessImport = { cwdLen, retPidPtr, ) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; // Legacy ABI used by checked-in command modules. In this contract the // executable is argv[0]; newer callers use proc_spawn_v2 so argv[0] // can differ from the executable path. @@ -6662,6 +7966,7 @@ const hostProcessImport = { pgroup, retPidPtr, ) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; const flags = Number(attrFlags) >>> 0; if ((flags & ~SUPPORTED_POSIX_SPAWN_FLAGS) !== 0) { return WASI_ERRNO_NOTSUP; @@ -6680,17 +7985,7 @@ const hostProcessImport = { const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter( (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, ); - const inheritedIgnores = [...wasmSignalRegistrations.entries()] - .filter( - ([signal, registration]) => - registration.action === 'ignore' && !defaultSignals.includes(signal), - ) - .map(([signal]) => signal); activeSpawnCallContext = { - internalBootstrapEnv: { - AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify(signalMask), - AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), - }, attrFlags: flags, // v3 implements posix_spawn, not posix_spawnp. Preserve the // caller's executable path and argv exactly instead of routing @@ -6746,6 +8041,7 @@ const hostProcessImport = { schedPriority, retPidPtr, ) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; const flags = Number(attrFlags) >>> 0; if ((flags & ~SUPPORTED_POSIX_SPAWN_FLAGS) !== 0) { return WASI_ERRNO_NOTSUP; @@ -6797,17 +8093,7 @@ const hostProcessImport = { const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter( (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, ); - const inheritedIgnores = [...wasmSignalRegistrations.entries()] - .filter( - ([signal, registration]) => - registration.action === 'ignore' && !defaultSignals.includes(signal), - ) - .map(([signal]) => signal); activeSpawnCallContext = { - internalBootstrapEnv: { - AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify(signalMask), - AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), - }, attrFlags: flags, exactExecPath: searchPath === null, searchPath, @@ -6854,6 +8140,7 @@ const hostProcessImport = { retPidPtr, resolvedCwdOverride, ) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; if (permissionTier !== 'full') { return WASI_ERRNO_FAULT; } @@ -6900,8 +8187,8 @@ const hostProcessImport = { record.processGroup = Number( callSyncRpc('process.getpgid', [VIRTUAL_PID]), ) >>> 0; - spawnedChildren.set(record.pid, record); - spawnedChildrenById.set(record.childId, record); + childCorrelationsByPid.set(record.pid, record); + childCorrelationsById.set(record.childId, record); traceHostProcess('proc-spawn-synthetic', { command, childId: record.childId, @@ -6923,10 +8210,8 @@ const hostProcessImport = { stdoutTarget, stderrTarget, kernelFdMappings: kernelFdMappingsForSpawn(), - internalBootstrapEnv: { - ...(activeSpawnCallContext?.internalBootstrapEnv ?? {}), - ...inheritedNofileBootstrapEnv(), - }, + internalBootstrapEnv: + activeSpawnCallContext?.internalBootstrapEnv ?? {}, spawnAttrFlags: activeSpawnCallContext?.attrFlags ?? 0, spawnPgroup: activeSpawnCallContext?.pgroup ?? null, }); @@ -6954,10 +8239,8 @@ const hostProcessImport = { argv0, cwd, env, - internalBootstrapEnv: { - ...(activeSpawnCallContext?.internalBootstrapEnv ?? {}), - ...inheritedNofileBootstrapEnv(), - }, + internalBootstrapEnv: + activeSpawnCallContext?.internalBootstrapEnv ?? {}, spawnAttrFlags: activeSpawnCallContext?.attrFlags ?? 0, spawnExactPath: activeSpawnCallContext?.exactExecPath ?? false, spawnSearchPath: activeSpawnCallContext?.searchPath, @@ -6994,9 +8277,12 @@ const hostProcessImport = { if (!Number.isInteger(pid) || pid === 0 || typeof result?.childId !== 'string') { return WASI_ERRNO_FAULT; } - let processGroup = Number(result?.pgid) >>> 0; - if (processGroup === 0) { - processGroup = Number(callSyncRpc('process.getpgid', [pid])) >>> 0; + let processGroup = 0; + if (!SIDECAR_MANAGED_PROCESS) { + processGroup = Number(result?.pgid) >>> 0; + if (processGroup === 0) { + processGroup = Number(callSyncRpc('process.getpgid', [pid])) >>> 0; + } } const directPosixStdin = @@ -7040,7 +8326,7 @@ const hostProcessImport = { (fd, index, values) => fd != null && fd > 2 && - delegateManagedFdRefCounts.has(fd) && + standaloneDelegateFdRefCounts.has(fd) && values.indexOf(fd) === index, ); for (const fd of delegateRetainedFds) { @@ -7063,10 +8349,10 @@ const hostProcessImport = { exitSignal: null, exitStatus: null, rawWaitStatus: null, - processGroup, + ...(SIDECAR_MANAGED_PROCESS ? {} : { processGroup }), }; - spawnedChildren.set(pid, record); - spawnedChildrenById.set(result.childId, record); + childCorrelationsByPid.set(pid, record); + childCorrelationsById.set(result.childId, record); traceHostProcess('proc-spawn-ready', { command, childId: result.childId, @@ -7121,9 +8407,6 @@ const hostProcessImport = { // an otherwise valid non-WASM image is prepared and committed // by the sidecar without ever resuming this image. if (SIDECAR_EXEC_COMMIT_RPC && loadError?.code === 'ENOEXEC') { - const inheritedIgnores = [...wasmSignalRegistrations.entries()] - .filter(([, registration]) => registration.action === 'ignore') - .map(([signal]) => signal); callSyncRpc('process.exec', [{ command, args: argv.slice(1), @@ -7133,12 +8416,7 @@ const hostProcessImport = { shell: false, cloexecFds: kernelCloexecFdsForCommit(closeFds), localReplacement: false, - internalBootstrapEnv: { - AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify([...wasmBlockedSignals]), - AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), - AGENTOS_WASM_INITIAL_PENDING_SIGNALS: JSON.stringify([...pendingWasmSignals]), - ...inheritedNofileBootstrapEnv(), - }, + internalBootstrapEnv: {}, }, }]); const returned = new Error( @@ -7160,7 +8438,7 @@ const hostProcessImport = { shell: false, cloexecFds: kernelCloexecFdsForCommit(closeFds), localReplacement: true, - internalBootstrapEnv: inheritedNofileBootstrapEnv(), + internalBootstrapEnv: {}, }, }]); if (result?.committed !== true) { @@ -7220,7 +8498,7 @@ const hostProcessImport = { cloexecFds: kernelCloexecFdsForCommit(closeFds), localReplacement: true, executableFd: canonicalKernelFdForSpawnAction(descriptor), - internalBootstrapEnv: inheritedNofileBootstrapEnv(), + internalBootstrapEnv: {}, }, }]); if (result?.committed !== true) { @@ -7253,15 +8531,46 @@ const hostProcessImport = { } }, proc_waitpid(pid, options, retStatusPtr, retPidPtr) { + if (!guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])) { + return WASI_ERRNO_FAULT; + } const requestedPid = Number(pid) >>> 0; if (permissionTier !== 'full') { return WASI_ERRNO_CHILD; } + if (SIDECAR_MANAGED_PROCESS) { + try { + const normalizedOptions = Number(options) >>> 0; + if ((normalizedOptions & ~1) !== 0) return WASI_ERRNO_INVAL; + const transition = takeManagedWaitTransition( + Number(pid) | 0, + normalizedOptions, + (normalizedOptions & 1) === 0, + ); + if (!transition) { + if (writeGuestUint32(retStatusPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(retPidPtr, 0); + } + if ( + writeGuestUint32(retStatusPtr, Number(transition.status) >>> 0) !== + WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + const result = writeGuestUint32(retPidPtr, Number(transition.pid) >>> 0); + if (result === WASI_ERRNO_SUCCESS) reapManagedChildCorrelation(transition); + return result; + } catch (error) { + return mapHostProcessError(error); + } + } const waitAny = requestedPid === 0xffffffff; - if (!waitAny && !spawnedChildren.has(requestedPid)) { + if (!waitAny && !childCorrelationsByPid.has(requestedPid)) { return WASI_ERRNO_CHILD; } - if (spawnedChildren.size === 0) { + if (childCorrelationsByPid.size === 0) { return WASI_ERRNO_CHILD; } @@ -7273,8 +8582,8 @@ const hostProcessImport = { } while (true) { const records = waitAny - ? Array.from(spawnedChildren.values()) - : [spawnedChildren.get(requestedPid)].filter(Boolean); + ? Array.from(childCorrelationsByPid.values()) + : [childCorrelationsByPid.get(requestedPid)].filter(Boolean); if (records.length === 0) { return WASI_ERRNO_CHILD; } @@ -7324,7 +8633,7 @@ const hostProcessImport = { // A matching status wins over a simultaneously delivered // signal. Otherwise a caught signal (including SIGCHLD from a // non-selected sibling) interrupts blocking waitpid on Linux. - if (dispatchPendingWasmSignals()) { + if (dispatchPendingWasmSignals(true)) { return WASI_ERRNO_INTR; } } @@ -7344,18 +8653,52 @@ const hostProcessImport = { retPidPtr, retCoreDumpedPtr, ) { + if (!guestRangesAreValid( + [retExitCodePtr, 4], + [retSignalPtr, 4], + [retPidPtr, 4], + [retCoreDumpedPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } const requestedPid = Number(pid) >>> 0; if (permissionTier !== 'full') { return WASI_ERRNO_CHILD; } - const waitAny = requestedPid === 0xffffffff; - if (!waitAny && !spawnedChildren.has(requestedPid)) { - // Linux waitpid reports ECHILD when pid does not name a child of - // this process. ESRCH is reserved for operations such as kill(2). - return WASI_ERRNO_CHILD; - } - if (spawnedChildren.size === 0) { - return WASI_ERRNO_CHILD; + if (SIDECAR_MANAGED_PROCESS) { + try { + const normalizedOptions = Number(options) >>> 0; + if ((normalizedOptions & ~1) !== 0) return WASI_ERRNO_INVAL; + const transition = takeManagedWaitTransition( + Number(pid) | 0, + normalizedOptions, + (normalizedOptions & 1) === 0, + ); + if (!transition) return writeGuestUint32(retPidPtr, 0); + for (const [ptr, value] of [ + [retExitCodePtr, transition.exitCode], + [retSignalPtr, transition.signal], + [retCoreDumpedPtr, transition.coreDumped ? 1 : 0], + [retPidPtr, transition.pid], + ]) { + if (writeGuestUint32(ptr, Number(value) >>> 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + } + reapManagedChildCorrelation(transition); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } + const waitAny = requestedPid === 0xffffffff; + if (!waitAny && !childCorrelationsByPid.has(requestedPid)) { + // Linux waitpid reports ECHILD when pid does not name a child of + // this process. ESRCH is reserved for operations such as kill(2). + return WASI_ERRNO_CHILD; + } + if (childCorrelationsByPid.size === 0) { + return WASI_ERRNO_CHILD; } try { @@ -7371,8 +8714,8 @@ const hostProcessImport = { while (true) { const records = waitAny - ? Array.from(spawnedChildren.values()) - : [spawnedChildren.get(requestedPid)].filter(Boolean); + ? Array.from(childCorrelationsByPid.values()) + : [childCorrelationsByPid.get(requestedPid)].filter(Boolean); if (records.length === 0) { return WASI_ERRNO_CHILD; } @@ -7449,7 +8792,7 @@ const hostProcessImport = { retCoreDumpedPtr, ); } - if (dispatchPendingWasmSignals()) { + if (dispatchPendingWasmSignals(true)) { return WASI_ERRNO_INTR; } } @@ -7462,6 +8805,9 @@ const hostProcessImport = { } }, proc_waitpid_v3(pid, options, retStatusPtr, retPidPtr) { + if (!guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])) { + return WASI_ERRNO_FAULT; + } const requestedPid = Number(pid) | 0; if (permissionTier !== 'full') { return WASI_ERRNO_CHILD; @@ -7475,20 +8821,42 @@ const hostProcessImport = { } try { + if (SIDECAR_MANAGED_PROCESS) { + const transition = takeManagedWaitTransition( + requestedPid, + normalizedOptions, + !waitNoHang, + ); + if (!transition) { + if (writeGuestUint32(retStatusPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(retPidPtr, 0); + } + if ( + writeGuestUint32(retStatusPtr, Number(transition.rawStatus) >>> 0) !== + WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + const result = writeGuestUint32(retPidPtr, Number(transition.pid) >>> 0); + if (result === WASI_ERRNO_SUCCESS) reapManagedChildCorrelation(transition); + return result; + } const callerProcessGroup = requestedPid === 0 ? Number(callSyncRpc('process.getpgid', [VIRTUAL_PID])) >>> 0 : 0; const matchingRecords = () => { if (requestedPid > 0) { - return [spawnedChildren.get(requestedPid)].filter(Boolean); + return [childCorrelationsByPid.get(requestedPid)].filter(Boolean); } if (requestedPid === -1) { - return Array.from(spawnedChildren.values()); + return Array.from(childCorrelationsByPid.values()); } const selectedGroup = requestedPid === 0 ? callerProcessGroup : Math.abs(requestedPid) >>> 0; - return Array.from(spawnedChildren.values()).filter( + return Array.from(childCorrelationsByPid.values()).filter( (record) => record.processGroup === selectedGroup, ); }; @@ -7512,7 +8880,7 @@ const hostProcessImport = { normalizedOptions, ]); if (transition && typeof transition.pid === 'number') { - const transitionedRecord = spawnedChildren.get( + const transitionedRecord = childCorrelationsByPid.get( Number(transition.pid) >>> 0, ); if (transitionedRecord && records.includes(transitionedRecord)) { @@ -7572,7 +8940,7 @@ const hostProcessImport = { normalizedOptions, ]); if (transition && typeof transition.pid === 'number') { - const transitionedRecord = spawnedChildren.get( + const transitionedRecord = childCorrelationsByPid.get( Number(transition.pid) >>> 0, ); if (transitionedRecord && records.includes(transitionedRecord)) { @@ -7593,7 +8961,7 @@ const hostProcessImport = { if (readyRecord) { return returnRawWaitedChild(readyRecord, retStatusPtr, retPidPtr); } - if (dispatchPendingWasmSignals()) { + if (dispatchPendingWasmSignals(true)) { return WASI_ERRNO_INTR; } } @@ -7609,96 +8977,89 @@ const hostProcessImport = { if (permissionTier !== 'full') { return WASI_ERRNO_SRCH; } - const targetPid = Number(pid) >>> 0; + const targetPid = Number(pid) | 0; const numericSignal = Number(signal) >>> 0; - const signalName = signalNameFromNumber(numericSignal); + if (numericSignal > 31) return WASI_ERRNO_INVAL; + const signalName = numericSignal === 0 ? '0' : signalNameFromNumber(numericSignal); try { if (targetPid === VIRTUAL_PID) { - // Signal zero only probes existence and permissions. Default - // dispositions must be enforced by the sidecar so termination, - // stop/continue, and wait status remain kernel-owned. A caught - // self-signal stays local so blocking, coalescing, sa_mask, - // SA_NODEFER, and SA_RESETHAND all use the same delivery path as - // externally delivered WASM signals. - if (numericSignal === 0) { - callSyncRpc('process.kill', [VIRTUAL_PID, signalName]); - return WASI_ERRNO_SUCCESS; - } - const registration = wasmSignalRegistrations.get(numericSignal); - if (registration?.action === 'ignore') { - return WASI_ERRNO_SUCCESS; - } - if (registration?.action === 'user') { - if (wasmBlockedSignals.has(numericSignal)) { - pendingWasmSignals.add(numericSignal); - } else { - dispatchWasmSignal(numericSignal); - } - return WASI_ERRNO_SUCCESS; - } + // Self-signals take the same kernel path as external and + // kernel-generated signals. The kernel decides ignore/default/ + // caught behavior and retains blocked standard signals. callSyncRpc('process.kill', [VIRTUAL_PID, signalName]); + if (numericSignal !== 0) { + dispatchPendingWasmSignals(); + } return WASI_ERRNO_SUCCESS; } - const record = spawnedChildren.get(targetPid); - if (record) { - callSyncRpc('child_process.kill', [record.childId, signalName]); - return WASI_ERRNO_SUCCESS; - } - + // The sidecar authorizes direct children from the kernel process + // table and delivers through the child's runtime endpoint. The + // runner's childId map is transport correlation only. callSyncRpc('process.kill', [targetPid, signalName]); return WASI_ERRNO_SUCCESS; } catch (error) { - if (error?.code === 'ESRCH') { - return WASI_ERRNO_SRCH; - } - return WASI_ERRNO_FAULT; + return mapHostProcessError(error); } }, proc_getpid(retPidPtr) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retPidPtr, VIRTUAL_PID); }, proc_getppid(retPidPtr) { + if (!guestRangeIsValid(retPidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retPidPtr, VIRTUAL_PPID); }, proc_getrlimit(resource, retSoftPtr, retHardPtr) { - // Linux RLIMIT_NOFILE is resource 7. The typed per-execution value - // originates at limits.resources.maxOpenFds and is already the - // enforcement cap used by this runner's descriptor tables. - if ((Number(resource) >>> 0) !== 7) { - return WASI_ERRNO_NOTSUP; + if (!guestRangesAreValid([retSoftPtr, 8], [retHardPtr, 8])) { + return WASI_ERRNO_FAULT; + } + const resourceKind = Number(resource) >>> 0; + if (resourceKind > 9) return WASI_ERRNO_INVAL; + if (!SIDECAR_MANAGED_PROCESS && resourceKind === 7) { + const softResult = writeGuestUint64(retSoftPtr, configuredMaxOpenFds); + if (softResult !== WASI_ERRNO_SUCCESS) return softResult; + return writeGuestUint64(retHardPtr, configuredMaxOpenFds); } - const softResult = writeGuestUint64(retSoftPtr, rlimitNofileSoft); - if (softResult !== WASI_ERRNO_SUCCESS) { - return softResult; + try { + const limit = callSyncRpc('process.getrlimit', [resourceKind]); + const softResult = writeGuestUint64(retSoftPtr, limit.soft); + if (softResult !== WASI_ERRNO_SUCCESS) { + return softResult; + } + return writeGuestUint64(retHardPtr, limit.hard); + } catch (error) { + if ( + error?.code === 'ERR_AGENTOS_WASM_SYNC_RPC_UNAVAILABLE' && + resourceKind === 7 + ) { + const softResult = writeGuestUint64(retSoftPtr, configuredMaxOpenFds); + if (softResult !== WASI_ERRNO_SUCCESS) return softResult; + return writeGuestUint64(retHardPtr, configuredMaxOpenFds); + } + return mapHostProcessError(error); } - return writeGuestUint64(retHardPtr, rlimitNofileHard); }, proc_setrlimit(resource, soft, hard) { - if ((Number(resource) >>> 0) !== 7) { - return WASI_ERRNO_NOTSUP; - } + const resourceKind = Number(resource) >>> 0; + if (resourceKind > 9) return WASI_ERRNO_INVAL; const requestedSoft = BigInt.asUintN(64, BigInt(soft)); const requestedHard = BigInt.asUintN(64, BigInt(hard)); - if (requestedSoft > requestedHard) { - return WASI_ERRNO_INVAL; - } - if (requestedHard > BigInt(rlimitNofileHard)) { - return WASI_ERRNO_PERM; - } - if ( - requestedSoft > BigInt(Number.MAX_SAFE_INTEGER) || - requestedHard > BigInt(Number.MAX_SAFE_INTEGER) - ) { - return WASI_ERRNO_INVAL; + try { + callSyncRpc('process.setrlimit', [ + resourceKind, + requestedSoft.toString(), + requestedHard.toString(), + ]); + warnedAboutOpenFdLimit = false; + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); } - rlimitNofileSoft = Number(requestedSoft); - rlimitNofileHard = Number(requestedHard); - warnedAboutOpenFdLimit = false; - return WASI_ERRNO_SUCCESS; }, proc_umask(mask, retPreviousPtr) { + if (!guestRangeIsValid(retPreviousPtr, 4)) return WASI_ERRNO_FAULT; try { const previous = Number( callSyncRpc('process.umask', [Number(mask) & 0o777]), @@ -7709,6 +9070,7 @@ const hostProcessImport = { } }, umask(mask, setMask, retPreviousPtr) { + if (!guestRangeIsValid(retPreviousPtr, 4)) return WASI_ERRNO_FAULT; try { const args = Number(setMask) !== 0 ? [Number(mask) & 0o777] : []; const previous = Number(callSyncRpc('process.umask', args)) >>> 0; @@ -7718,6 +9080,9 @@ const hostProcessImport = { } }, proc_itimer_real(operation, valueUs, intervalUs, retRemainingUsPtr, retIntervalUsPtr) { + if (!guestRangesAreValid([retRemainingUsPtr, 8], [retIntervalUsPtr, 8])) { + return WASI_ERRNO_FAULT; + } try { const numericOperation = Number(operation) >>> 0; if (numericOperation > 1) { @@ -7755,6 +9120,7 @@ const hostProcessImport = { } }, proc_getpgid(pid, retPgidPtr) { + if (!guestRangeIsValid(retPgidPtr, 4)) return WASI_ERRNO_FAULT; if (permissionTier !== 'full') { return WASI_ERRNO_SRCH; } @@ -7791,6 +9157,9 @@ const hostProcessImport = { } }, fd_pipe(retReadFdPtr, retWriteFdPtr) { + if (!guestRangesAreValid([retReadFdPtr, 4], [retWriteFdPtr, 4])) { + return WASI_ERRNO_FAULT; + } let readFd = null; let writeFd = null; try { @@ -7807,12 +9176,12 @@ const hostProcessImport = { readFd = allocateSyntheticFd(nextSyntheticFd, true); writeFd = allocateSyntheticFd(nextSyntheticFd, true); if (readFd == null || writeFd == null) return WASI_ERRNO_MFILE; - syntheticFdEntries.set(readFd, createPipeHandle('pipe-read', pipe, readFd)); - syntheticFdEntries.set(writeFd, createPipeHandle('pipe-write', pipe, writeFd)); + setActiveFdProjection(readFd, createPipeHandle('pipe-read', pipe, readFd)); + setActiveFdProjection(writeFd, createPipeHandle('pipe-write', pipe, writeFd)); } else { - const result = callSyncRpc('process.fd_pipe'); - readFd = registerKernelDelegateFd(result?.readFd); - writeFd = registerKernelDelegateFd(result?.writeFd); + const result = callSyncRpc('process.fd_pipe'); + readFd = registerKernelDelegateFd(result?.readFd); + writeFd = registerKernelDelegateFd(result?.writeFd); } if (writeGuestUint32(retReadFdPtr, readFd) !== WASI_ERRNO_SUCCESS) { wasiImport.fd_close(readFd); @@ -7832,31 +9201,34 @@ const hostProcessImport = { } }, fd_dup(fd, retNewFdPtr) { + if (!guestRangeIsValid(retNewFdPtr, 4)) return WASI_ERRNO_FAULT; try { - const hostNetSource = hostNetSockets.get(Number(fd) >>> 0); - if (hostNetSource) { + const sourceFd = Number(fd) >>> 0; + const hostNetSource = getHostNetSocket(sourceFd); + if (!SIDECAR_MANAGED_PROCESS && hostNetSource) { const duplicatedFd = allocateHostNetDuplicateFd(0); if (duplicatedFd == null) return WASI_ERRNO_MFILE; - hostNetSockets.set(duplicatedFd, hostNetSource); - runnerCloexecFds.delete(duplicatedFd); + standaloneHostNetSockets.set(duplicatedFd, hostNetSource); + standaloneCloexecFds.delete(duplicatedFd); if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { hostNetImport.net_close(duplicatedFd); return WASI_ERRNO_FAULT; } return WASI_ERRNO_SUCCESS; } - const source = lookupFdHandle(fd); - if (source?.kind === 'kernel-fd') { + const kernelSourceFd = managedKernelFdForDuplicate(sourceFd); + if (kernelSourceFd != null) { const duplicatedFd = registerKernelDelegateFd( - callSyncRpc('process.fd_dup', [Number(source.targetFd) >>> 0]), + callSyncRpc('process.fd_dup', [kernelSourceFd]), ); + copyManagedHostNetDescription(sourceFd, duplicatedFd); if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { wasiImport.fd_close(duplicatedFd); return WASI_ERRNO_FAULT; } return WASI_ERRNO_SUCCESS; } - const handle = cloneFdHandle(fd); + const handle = cloneFdHandle(sourceFd); if (!handle) { return WASI_ERRNO_BADF; } @@ -7865,7 +9237,7 @@ const hostProcessImport = { releaseFdHandle(handle); return WASI_ERRNO_MFILE; } - syntheticFdEntries.set(duplicatedFd, handle); + setActiveFdProjection(duplicatedFd, handle); traceHostProcess('fd-dup', { fd: Number(fd) >>> 0, duplicatedFd, @@ -7886,7 +9258,7 @@ const hostProcessImport = { return WASI_ERRNO_BADF; } if (sourceFd === targetFd) { - if (!lookupFdHandle(sourceFd) && !hostNetSockets.has(sourceFd)) { + if (!lookupFdHandle(sourceFd) && !hasHostNetSocket(sourceFd)) { return WASI_ERRNO_BADF; } traceHostProcess('fd-dup2-same-fd', { @@ -7895,12 +9267,12 @@ const hostProcessImport = { }); return WASI_ERRNO_SUCCESS; } - if (targetFd >= rlimitNofileSoft) { + if (targetFd >= currentNofileSoftLimit()) { return WASI_ERRNO_BADF; } - const hostNetSource = hostNetSockets.get(sourceFd); - if (hostNetSource) { + const hostNetSource = getHostNetSocket(sourceFd); + if (!SIDECAR_MANAGED_PROCESS && hostNetSource) { const targetWasOpen = runnerOpenFdSet().has(targetFd); if (!targetWasOpen && !hasRunnerOpenFdCapacity(1)) { return WASI_ERRNO_MFILE; @@ -7909,15 +9281,15 @@ const hostProcessImport = { if (closeResult !== WASI_ERRNO_SUCCESS && closeResult !== WASI_ERRNO_BADF) { return closeResult; } - hostNetSockets.set(targetFd, hostNetSource); + standaloneHostNetSockets.set(targetFd, hostNetSource); // dup2(2) always clears FD_CLOEXEC on the replacement descriptor. - runnerCloexecFds.delete(targetFd); + standaloneCloexecFds.delete(targetFd); closedPassthroughFds.delete(targetFd); return WASI_ERRNO_SUCCESS; } - const kernelSource = lookupFdHandle(sourceFd); - if (kernelSource?.kind === 'kernel-fd') { + const kernelSourceFd = managedKernelFdForDuplicate(sourceFd); + if (kernelSourceFd != null) { const targetHandle = lookupFdHandle(targetFd); const targetIsInternalPreopen = targetHandle?.internalPreopen === true; const shadowsInternalPreopen = hiddenPreopenHandles.has(targetFd); @@ -7938,7 +9310,7 @@ const hostProcessImport = { // kernel fd targetFd shadows it; retain fdTable/backing // state for future path resolution. passthroughHandles.delete(targetFd); - delegateManagedFdRefCounts.delete(targetFd); + standaloneDelegateFdRefCounts.delete(targetFd); return WASI_ERRNO_SUCCESS; })() : wasiImport.fd_close(targetFd); @@ -7963,9 +9335,15 @@ const hostProcessImport = { // destination; using the raw guest number as a kernel dup2 // target can alias the source after preopen collisions. const duplicatedKernelFd = callSyncRpc('process.fd_dup', [ - Number(kernelSource.targetFd) >>> 0, + kernelSourceFd, ]); - registerKernelDelegateFd(duplicatedKernelFd, targetFd, 3, shadowsInternalPreopen); + const duplicatedGuestFd = registerKernelDelegateFd( + duplicatedKernelFd, + targetFd, + 3, + shadowsInternalPreopen, + ); + copyManagedHostNetDescription(sourceFd, duplicatedGuestFd); return WASI_ERRNO_SUCCESS; } @@ -7984,10 +9362,10 @@ const hostProcessImport = { sourceKind: sourceHandle.kind, sourceTargetFd: sourceHandle.targetFd ?? null, sourceDisplayFd: sourceHandle.displayFd ?? null, - existingKind: syntheticFdEntries.get(targetFd)?.kind ?? passthroughHandles.get(targetFd)?.kind ?? null, + existingKind: activeFdProjections.get(targetFd)?.kind ?? passthroughHandles.get(targetFd)?.kind ?? null, }); - if (hostNetSockets.has(targetFd)) { + if (hasHostNetSocket(targetFd)) { const closeResult = hostNetImport.net_close(targetFd); if (closeResult !== WASI_ERRNO_SUCCESS) { releaseFdHandle(sourceHandle); @@ -7996,7 +9374,7 @@ const hostProcessImport = { } closeSyntheticFd(targetFd); closePassthroughFd(targetFd); - syntheticFdEntries.set(targetFd, sourceHandle); + setActiveFdProjection(targetFd, sourceHandle); closedPassthroughFds.delete(targetFd); traceHostProcess('fd-dup2-installed', { oldFd: sourceFd, @@ -8015,6 +9393,7 @@ const hostProcessImport = { } }, fd_dup_min(fd, minFd, retNewFdPtr) { + if (!guestRangeIsValid(retNewFdPtr, 4)) return WASI_ERRNO_FAULT; try { const sourceFd = Number(fd); const minimumFdNumber = Number(minFd); @@ -8027,16 +9406,16 @@ const hostProcessImport = { if (minimumFdNumber >= LINUX_GUEST_FD_LIMIT) { return WASI_ERRNO_INVAL; } - if (minimumFdNumber >= rlimitNofileSoft) { + if (minimumFdNumber >= currentNofileSoftLimit()) { return WASI_ERRNO_INVAL; } - const hostNetSource = hostNetSockets.get(sourceFd); - if (hostNetSource) { + const hostNetSource = getHostNetSocket(sourceFd); + if (!SIDECAR_MANAGED_PROCESS && hostNetSource) { const duplicatedFd = allocateHostNetDuplicateFd(minimumFdNumber); if (duplicatedFd == null) return WASI_ERRNO_MFILE; - hostNetSockets.set(duplicatedFd, hostNetSource); - runnerCloexecFds.delete(duplicatedFd); + standaloneHostNetSockets.set(duplicatedFd, hostNetSource); + standaloneCloexecFds.delete(duplicatedFd); if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { hostNetImport.net_close(duplicatedFd); return WASI_ERRNO_FAULT; @@ -8044,17 +9423,18 @@ const hostProcessImport = { return WASI_ERRNO_SUCCESS; } - const kernelSource = lookupFdHandle(sourceFd); - if (kernelSource?.kind === 'kernel-fd') { - // F_DUPFD's lower bound belongs to the guest descriptor table, - // not the sidecar kernel's private backing-fd namespace. Asking - // the kernel for fd >= minFd can exceed its bounded table for a - // perfectly valid guest request such as F_DUPFD(512). + const kernelSourceFd = managedKernelFdForDuplicate(sourceFd); + if (kernelSourceFd != null) { + // Kernel and guest allocation share the authoritative + // RLIMIT_NOFILE. The guest projection still chooses its own + // lowest visible alias because hidden preopens can occupy a + // different numeric slot than the backing descriptor. const duplicatedFd = registerKernelDelegateFd( - callSyncRpc('process.fd_dup', [Number(kernelSource.targetFd) >>> 0]), + callSyncRpc('process.fd_dup_min', [kernelSourceFd, minimumFdNumber]), null, minimumFdNumber, ); + copyManagedHostNetDescription(sourceFd, duplicatedFd); if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { wasiImport.fd_close(duplicatedFd); return WASI_ERRNO_FAULT; @@ -8074,7 +9454,7 @@ const hostProcessImport = { return WASI_ERRNO_MFILE; } - syntheticFdEntries.set(duplicatedFd, handle); + setActiveFdProjection(duplicatedFd, handle); traceHostProcess('fd-dup-min', { fd: sourceFd >>> 0, minimumFd: minimumFdNumber >>> 0, @@ -8095,14 +9475,15 @@ const hostProcessImport = { } }, fd_getfd(fd, retFlagsPtr) { + if (!guestRangeIsValid(retFlagsPtr, 4)) return WASI_ERRNO_FAULT; try { const numericFd = Number(fd) >>> 0; const handle = lookupFdHandle(numericFd); let flags; if (handle?.kind === 'kernel-fd') { flags = Number(callSyncRpc('process.fd_getfd', [Number(handle.targetFd) >>> 0])); - } else if (handle || hostNetSockets.has(numericFd)) { - flags = runnerCloexecFds.has(numericFd) ? 1 : 0; + } else if (handle || hasHostNetSocket(numericFd)) { + flags = standaloneCloexecFds.has(numericFd) ? 1 : 0; } else { return WASI_ERRNO_BADF; } @@ -8124,13 +9505,14 @@ const hostProcessImport = { Number(handle.targetFd) >>> 0, normalizedFlags, ]); - } else if (!handle && !hostNetSockets.has(numericFd)) { + } else if (!handle && !hasHostNetSocket(numericFd)) { return WASI_ERRNO_BADF; - } - if ((normalizedFlags & 1) !== 0) { - runnerCloexecFds.add(numericFd); } else { - runnerCloexecFds.delete(numericFd); + if ((normalizedFlags & 1) !== 0) { + standaloneCloexecFds.add(numericFd); + } else { + standaloneCloexecFds.delete(numericFd); + } } return WASI_ERRNO_SUCCESS; } catch (error) { @@ -8145,7 +9527,7 @@ const hostProcessImport = { const normalizedOperation = Number(operation) >>> 0; const handle = lookupFdHandle(numericFd); if (handle?.kind !== 'kernel-fd') { - return handle || hostNetSockets.has(numericFd) + return handle || hasHostNetSocket(numericFd) ? WASI_ERRNO_NOTSUP : WASI_ERRNO_BADF; } @@ -8158,7 +9540,6 @@ const hostProcessImport = { : normalizedOperation; const startedAt = Date.now(); const deadline = startedAt + unixConnectTimeoutMs; - const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); let warnedNearLimit = false; while (true) { try { @@ -8175,23 +9556,24 @@ const hostProcessImport = { return mapHostProcessError(error); } const now = Date.now(); - if (!warnedNearLimit && now >= warningAt) { - warnedNearLimit = true; - process.stderr.write( - `[agentos] flock is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLimit = warnNearBlockingReadLimit( + 'flock', + startedAt, + warnedNearLimit, + ); if (now >= deadline) { process.stderr.write( `[agentos] flock exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, ); return WASI_ERRNO_TIMEDOUT; } - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; // Keep the sidecar dispatcher free so the lock owner can run // and unlock while this guest observes blocking flock semantics. - pumpSpawnedChildrenOrWait(SPAWNED_CHILD_WAIT_SLICE_MS); - if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (pumpSpawnedChildrenOrWaitRestartable(SPAWNED_CHILD_WAIT_SLICE_MS)) { + return WASI_ERRNO_INTR; + } + if (dispatchPendingWasmSignals(true)) return WASI_ERRNO_INTR; } } } catch (error) { @@ -8210,6 +9592,17 @@ const hostProcessImport = { retLengthPtr, ) { const numericCommand = Number(command) >>> 0; + if ( + numericCommand === 12 && + !guestRangesAreValid( + [retTypePtr, 4], + [retPidPtr, 4], + [retStartPtr, 8], + [retLengthPtr, 8], + ) + ) { + return WASI_ERRNO_FAULT; + } let blockingWaitRegistered = false; const cancelBlockingLockWait = () => { callSyncRpc('process.fd_record_lock_cancel', []); @@ -8222,7 +9615,7 @@ const hostProcessImport = { // A runner-local or host-network descriptor has no stable VFS // inode identity. Never report a lock that the kernel cannot // enforce against other VM processes. - return handle || hostNetSockets.has(numericFd) + return handle || hasHostNetSocket(numericFd) ? WASI_ERRNO_NOTSUP : WASI_ERRNO_BADF; } @@ -8233,8 +9626,6 @@ const hostProcessImport = { } const lockWaitStartedAt = Date.now(); const lockWaitDeadline = lockWaitStartedAt + unixConnectTimeoutMs; - const lockWaitWarningAt = - lockWaitStartedAt + Math.floor(unixConnectTimeoutMs * 0.8); let warnedNearLockWaitLimit = false; let response; while (true) { @@ -8257,12 +9648,11 @@ const hostProcessImport = { } blockingWaitRegistered = true; const now = Date.now(); - if (!warnedNearLockWaitLimit && now >= lockWaitWarningAt) { - warnedNearLockWaitLimit = true; - process.stderr.write( - `[agentos] F_SETLKW is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, - ); - } + warnedNearLockWaitLimit = warnNearBlockingReadLimit( + 'F_SETLKW', + lockWaitStartedAt, + warnedNearLockWaitLimit, + ); if (now >= lockWaitDeadline) { cancelBlockingLockWait(); process.stderr.write( @@ -8274,12 +9664,15 @@ const hostProcessImport = { // another VM process can reach close/unlock. Suspend in the // runner instead, advancing descendants and yielding briefly // to independently scheduled processes between retries. - if (dispatchPendingWasmSignals()) { + if (dispatchPendingWasmSignals(true)) { + cancelBlockingLockWait(); + return WASI_ERRNO_INTR; + } + if (pumpSpawnedChildrenOrWaitRestartable(SPAWNED_CHILD_WAIT_SLICE_MS)) { cancelBlockingLockWait(); return WASI_ERRNO_INTR; } - pumpSpawnedChildrenOrWait(SPAWNED_CHILD_WAIT_SLICE_MS); - if (dispatchPendingWasmSignals()) { + if (dispatchPendingWasmSignals(true)) { cancelBlockingLockWait(); return WASI_ERRNO_INTR; } @@ -8314,14 +9707,89 @@ const hostProcessImport = { proc_closefrom(lowFd) { const minimumFd = Number(lowFd) >>> 0; const openVirtualFds = new Set([ - ...syntheticFdEntries.keys(), + ...activeFdProjections.keys(), ...passthroughHandles.keys(), ...retainedSpawnOutputHandlesByFd.keys(), ...retainedSyntheticHandlesByDisplayFd.keys(), - ...hostNetSockets.keys(), - ...delegateManagedFdRefCounts.keys(), + ...hostNetGuestFds(), + ...standaloneDelegateFdRefCounts.keys(), ...(wasi?.fdTable?.keys?.() ?? []), ]); + if (SIDECAR_MANAGED_PROCESS) { + const exactKernelFds = new Set(); + for (const fd of openVirtualFds) { + if (fd < minimumFd) continue; + const handle = lookupFdHandle(fd); + if ( + handle?.internalPreopen === true || + hiddenPreopenHandles.has(fd) + ) { + continue; + } + if (handle?.kind === 'kernel-fd') { + exactKernelFds.add(Number(handle.targetFd) >>> 0); + } else if ( + fd <= 2 && + handle?.kind === 'passthrough' && + Number(handle.targetFd) === fd + ) { + exactKernelFds.add(fd); + } + } + let response; + try { + response = callSyncRpc('process.fd_closefrom', [ + minimumFd, + [...exactKernelFds], + ]); + } catch (error) { + return mapHostProcessError(error); + } + if (!Array.isArray(response?.closedFds)) { + return WASI_ERRNO_IO; + } + const closedKernelFds = new Set( + response.closedFds.map((fd) => Number(fd) >>> 0), + ); + for (const fd of closedKernelFds) { + forgetSidecarClosedKernelTargetFd(fd); + } + + let firstError = WASI_ERRNO_SUCCESS; + for (const fd of [...openVirtualFds].sort((left, right) => left - right)) { + if (fd < minimumFd) continue; + const handle = lookupFdHandle(fd); + // Preopens remain private capability roots after closefrom. Only + // their ordinary, untagged Linux aliases disappear here. + if ( + handle?.internalPreopen === true || + hiddenPreopenHandles.has(fd) + ) { + activeFdProjections.delete(fd); + passthroughHandles.delete(fd); + closedPassthroughFds.add(fd); + standaloneCloexecFds.delete(fd); + continue; + } + if (handle?.kind === 'kernel-fd') { + // Bulk kernel retirement already removed every canonical + // descriptor in its range; never issue a second per-fd close. + if (closedKernelFds.has(Number(handle.targetFd) >>> 0)) { + forgetSidecarClosedKernelFd(fd); + } + continue; + } + const result = wasiImport.fd_close(fd); + if ( + result !== WASI_ERRNO_SUCCESS && + result !== WASI_ERRNO_BADF && + firstError === WASI_ERRNO_SUCCESS + ) { + firstError = result; + } + } + return firstError; + } let firstError = WASI_ERRNO_SUCCESS; for (const fd of [...openVirtualFds].sort((left, right) => left - right)) { if (fd < minimumFd) { @@ -8336,7 +9804,7 @@ const hostProcessImport = { ) { passthroughHandles.delete(fd); closedPassthroughFds.add(fd); - runnerCloexecFds.delete(fd); + standaloneCloexecFds.delete(fd); continue; } const result = wasiImport.fd_close(fd); @@ -8354,6 +9822,9 @@ const hostProcessImport = { let firstFd = null; let secondFd = null; try { + if (!guestRangeIsValid(retFirstPtr, 4) || !guestRangeIsValid(retSecondPtr, 4)) { + return WASI_ERRNO_FAULT; + } if (!hasRunnerOpenFdCapacity(2)) return WASI_ERRNO_MFILE; const result = callSyncRpc('process.fd_socketpair', [ Number(socketKind) >>> 0, @@ -8377,6 +9848,7 @@ const hostProcessImport = { } }, fd_sendmsg_rights(socketFd, dataPtr, dataLen, rightsPtr, rightsLen, flags, retSentPtr) { + if (!guestRangeIsValid(retSentPtr, 4)) return WASI_ERRNO_FAULT; try { if (!(instanceMemory instanceof WebAssembly.Memory)) return WASI_ERRNO_FAULT; const byteLength = Number(dataLen) >>> 0; @@ -8401,32 +9873,21 @@ const hostProcessImport = { const rights = []; for (let index = 0; index < rightsLength; index += 1) { const guestFd = view.getUint32(rightsOffset + index * 4, true); - if (hostNetSockets.has(guestFd)) { - const socket = hostNetSockets.get(guestFd); + if (hasHostNetSocket(guestFd)) { + const handle = lookupFdHandle(guestFd); rights.push({ kind: 'hostNet', - socketId: socket.socketId ?? null, - serverId: socket.serverId ?? null, - udpSocketId: socket.udpSocketId ?? null, - domain: Number(socket.domain) >>> 0, - socketType: Number(socket.sockType) >>> 0, - protocol: Number(socket.protocol) >>> 0, - nonblocking: socket.nonblock === true, - recvTimeoutMs: socket.recvTimeoutMs ?? null, - bindOptions: socket.bindOptions ?? null, - localInfo: socket.localInfo ?? null, - localUnixAddress: socket.localUnixAddress ?? null, - localReservation: socket.localReservation ?? null, - remoteInfo: socket.remoteInfo ?? null, - remoteUnixAddress: socket.remoteUnixAddress ?? null, - listening: socket.listening === true, + fd: handle?.kind === 'kernel-fd' + ? Number(handle.targetFd) >>> 0 + : null, + descriptionId: handle?.hostNetDescriptionId ?? null, }); continue; } const handle = lookupFdHandle(guestFd); const kernelFd = handle?.kind === 'kernel-fd' ? Number(handle.targetFd) >>> 0 - : delegateManagedFdRefCounts.has(guestFd) + : standaloneDelegateFdRefCounts.has(guestFd) ? guestFd : null; if (kernelFd == null) { @@ -8532,7 +9993,7 @@ const hostProcessImport = { } const progressed = pumpSpawnedChildren(10); dispatchPendingWasmSignals(); - if (!progressed && spawnedChildren.size === 0) { + if (!progressed && childCorrelationsByPid.size === 0) { Atomics.wait(syntheticWaitArray, 0, 0, 1); } } @@ -8557,7 +10018,48 @@ const hostProcessImport = { if (received?.kind === 'kernel') { fd = registerKernelDelegateFd(received.fd); } else if (received?.kind === 'hostNet') { - fd = allocateHostNetSocketFd(); + if (SIDECAR_MANAGED_PROCESS) { + const descriptionId = String(received.descriptionId ?? ''); + try { + fd = registerKernelDelegateFd(received.fd); + attachManagedHostNetDescription(fd, descriptionId); + } catch (error) { + try { + callSyncRpc('process.fd_close', [Number(received.fd) >>> 0]); + } catch (closeError) { + process.stderr.write( + `[agentos] failed to roll back received managed fd: ${closeError instanceof Error ? closeError.message : String(closeError)}\n`, + ); + } + throw error; + } + } else { + const socket = { + domain: Number(received.domain) >>> 0, + sockType: Number(received.socketType) >>> 0, + protocol: Number(received.protocol) >>> 0, + bindOptions: received.bindOptions ?? null, + localInfo: received.localInfo ?? null, + localUnixAddress: received.localUnixAddress ?? null, + localReservation: received.localReservation ?? null, + remoteInfo: received.remoteInfo ?? null, + remoteUnixAddress: received.remoteUnixAddress ?? null, + listening: received.listening === true, + serverId: received.serverId ?? null, + socketId: received.socketId ?? null, + udpSocketId: received.udpSocketId ?? null, + pendingDatagram: null, + recvTimeoutMs: received.recvTimeoutMs ?? null, + readChunks: [], + pendingAccepts: [], + readableEnded: false, + closed: false, + lastError: null, + nonblock: received.nonblocking === true, + }; + fd = allocateHostNetSocketFd(); + if (fd != null) standaloneHostNetSockets.set(fd, socket); + } if (fd == null) { localControlTruncated = true; if (typeof received.socketId === 'string') { @@ -8574,38 +10076,12 @@ const hostProcessImport = { } continue; } - hostNetSockets.set(fd, { - domain: Number(received.domain) >>> 0, - sockType: Number(received.socketType) >>> 0, - protocol: Number(received.protocol) >>> 0, - bindOptions: received.bindOptions ?? null, - localInfo: received.localInfo ?? normalizeHostNetAddressInfo( - received.localAddress, - received.localPort, - ), - localUnixAddress: received.localUnixAddress ?? null, - localReservation: received.localReservation ?? null, - remoteInfo: received.remoteInfo ?? normalizeHostNetAddressInfo( - received.remoteAddress, - received.remotePort, - ), - remoteUnixAddress: received.remoteUnixAddress ?? null, - listening: received.listening === true, - serverId: received.serverId ?? null, - socketId: received.socketId ?? null, - udpSocketId: received.udpSocketId ?? null, - pendingDatagram: null, - recvTimeoutMs: received.recvTimeoutMs ?? null, - readChunks: [], - pendingAccepts: [], - readableEnded: false, - closed: false, - lastError: null, - nonblock: received.nonblocking === true, - }); } else { return WASI_ERRNO_FAULT; } + if ((numericFlags & 0x40000000) !== 0) { + if (!SIDECAR_MANAGED_PROCESS) standaloneCloexecFds.add(fd); + } installedFds.push(fd); view.setUint32(rightsOffset + installedCount * 4, fd, true); installedCount += 1; @@ -8630,47 +10106,69 @@ const hostProcessImport = { } }, sleep_ms(milliseconds) { + const durationMs = Number(milliseconds) >>> 0; + if (!SIDECAR_MANAGED_PROCESS) { + Atomics.wait(syntheticWaitArray, 0, 0, durationMs); + return WASI_ERRNO_SUCCESS; + } try { - const waitArray = new Int32Array(new SharedArrayBuffer(4)); - const deadline = Date.now() + (Number(milliseconds) >>> 0); - while (Date.now() < deadline) { - // Keep guest sleeps interruptible by V8 termination during SIGTERM, - // SIGKILL, and VM disposal. Also drain handled Wasm signals at - // syscall boundaries so cooperative handlers run during sleeps. - dispatchPendingWasmSignals(); - Atomics.wait(waitArray, 0, 0, Math.max(1, Math.min(10, deadline - Date.now()))); - } + callSyncRpc('process.sleep', [durationMs]); dispatchPendingWasmSignals(); return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + dispatchPendingWasmSignals(); + return mapHostProcessError(error); } }, pty_open(retMasterFdPtr, retSlaveFdPtr) { - return WASI_ERRNO_FAULT; + let masterFd = null; + let slaveFd = null; + try { + if (!guestRangeIsValid(retMasterFdPtr, 4) || !guestRangeIsValid(retSlaveFdPtr, 4)) { + return WASI_ERRNO_FAULT; + } + if (!hasRunnerOpenFdCapacity(2)) return WASI_ERRNO_MFILE; + const result = callSyncRpc('process.pty_open', []); + masterFd = registerKernelDelegateFd(result?.masterFd); + slaveFd = registerKernelDelegateFd(result?.slaveFd); + writeGuestUint32(retMasterFdPtr, masterFd); + writeGuestUint32(retSlaveFdPtr, slaveFd); + return WASI_ERRNO_SUCCESS; + } catch (error) { + if (masterFd != null) wasiImport.fd_close(masterFd); + if (slaveFd != null) wasiImport.fd_close(slaveFd); + return mapHostProcessError(error); + } }, proc_sigaction(signal, action, maskLo, maskHi, flags) { if (permissionTier !== 'full') { return WASI_ERRNO_FAULT; } + const numericSignal = Number(signal) >>> 0; + if (numericSignal === 0 || numericSignal > 64) { + return WASI_ERRNO_INVAL; + } try { const registration = { action: action === 0 ? 'default' : action === 1 ? 'ignore' : 'user', mask: decodeSignalMask(maskLo, maskHi), flags: Number(flags) >>> 0, }; + if ( + registration.action === 'user' && + typeof instance?.exports?.__wasi_signal_trampoline !== 'function' + ) { + // Accepting this registration would let the kernel publish a + // caught delivery that this image can never run. Reject it at + // registration time instead of silently consuming the token. + return WASI_ERRNO_NOTSUP; + } callSyncRpc('process.signal_state', [ - Number(signal) >>> 0, + numericSignal, registration.action, JSON.stringify(registration.mask), registration.flags, ]); - const numericSignal = Number(signal) >>> 0; - if (registration.action === 'default') { - wasmSignalRegistrations.delete(numericSignal); - } else { - wasmSignalRegistrations.set(numericSignal, registration); - } traceHostProcess('proc-sigaction', { signal: numericSignal, action: registration.action, @@ -8683,37 +10181,24 @@ const hostProcessImport = { } }, proc_signal_mask_v2(how, setLo, setHi, retOldLoPtr, retOldHiPtr) { + if (!guestRangesAreValid([retOldLoPtr, 4], [retOldHiPtr, 4])) { + return WASI_ERRNO_FAULT; + } if (permissionTier !== 'full') { return WASI_ERRNO_FAULT; } try { - const previous = encodeSignalMask(wasmBlockedSignals); - writeGuestUint32(retOldLoPtr, previous.lo); - writeGuestUint32(retOldHiPtr, previous.hi); const operation = Number(how) >>> 0; - if (operation === 3) { - return WASI_ERRNO_SUCCESS; - } - if (operation > 2) { + if (operation > 3) { return WASI_ERRNO_INVAL; } const requested = decodeSignalMask(setLo, setHi).filter( (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, ); - if (operation === 0) { - for (const signal of requested) { - wasmBlockedSignals.add(signal); - } - } else if (operation === 1) { - for (const signal of requested) { - wasmBlockedSignals.delete(signal); - } - } else { - wasmBlockedSignals.clear(); - for (const signal of requested) { - wasmBlockedSignals.add(signal); - } - } + const response = callSyncRpc('process.signal_mask', [operation, requested]); + const previous = encodeSignalMask(response?.signals ?? []); + writeGuestUint32(retOldLoPtr, previous.lo); + writeGuestUint32(retOldHiPtr, previous.hi); dispatchPendingWasmSignals(); return WASI_ERRNO_SUCCESS; } catch { @@ -8730,10 +10215,10 @@ const hostProcessImport = { hasSigmask, retReadyPtr, ) { + if (!guestRangeIsValid(retReadyPtr, 4)) return WASI_ERRNO_FAULT; if (permissionTier !== 'full') { return WASI_ERRNO_PERM; } - const previousMask = new Set(wasmBlockedSignals); try { const seconds = BigInt(timeoutSec); const nanoseconds = BigInt(timeoutNsec); @@ -8745,30 +10230,20 @@ const hostProcessImport = { const milliseconds = seconds * 1000n + (nanoseconds + 999_999n) / 1_000_000n; timeoutMs = Number(milliseconds > 2_147_483_647n ? 2_147_483_647n : milliseconds); } - if ((Number(hasSigmask) >>> 0) !== 0) { - wasmBlockedSignals.clear(); - for (const signal of decodeSignalMask(sigmaskLo, sigmaskHi)) { - if (signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP) { - wasmBlockedSignals.add(signal); - } - } - } - // No guest code runs between the mask swap and the first poll - // boundary. That boundary drains pending signals and returns - // EINTR when it invokes an unblocked caught handler. - return hostNetImport.net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr); + const temporaryMask = (Number(hasSigmask) >>> 0) !== 0 + ? decodeSignalMask(sigmaskLo, sigmaskHi).filter( + (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, + ) + : null; + return hostNetImport.net_poll( + fdsPtr, + nfds, + timeoutMs, + retReadyPtr, + temporaryMask, + ); } catch { return WASI_ERRNO_FAULT; - } finally { - wasmBlockedSignals.clear(); - for (const signal of previousMask) { - wasmBlockedSignals.add(signal); - } - // A signal may have arrived while blocked only by ppoll's - // temporary mask. Linux runs that now-unblocked handler before - // returning to user code without rewriting a successful poll - // result, so drain after restoration rather than dropping it. - dispatchPendingWasmSignals(); } }, }; @@ -8785,20 +10260,33 @@ const limitedHostProcessImport = { umask: hostProcessImport.umask, }; -function hostUserLookup(rpcMethod, args, bufPtr, bufLen, retLenPtr) { +function hostUserLookup(lookup, bufPtr, bufLen, retLenPtr) { try { - const entry = String(callSyncRpc(rpcMethod, args)); - return writeGuestBytes(bufPtr, bufLen, encodeGuestBytes(entry), retLenPtr); + const capacity = Number(bufLen) >>> 0; + if ( + !guestRangeIsValid(retLenPtr, 4) || + !guestRangeIsValid(bufPtr, capacity) + ) { + return WASI_ERRNO_FAULT; + } + const entry = String(lookup()); + return writeGuestAccountRecord(bufPtr, capacity, encodeGuestBytes(entry), retLenPtr); } catch (error) { return mapSyntheticFsError(error); } } -function hostUserNameLookup(rpcMethod, namePtr, nameLen, bufPtr, bufLen, retLenPtr) { +function hostUserNameLookup(lookup, namePtr, nameLen, bufPtr, bufLen, retLenPtr) { try { + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxAccountNameBytes', + nameLen, + MAX_ACCOUNT_RECORD_BYTES, + WASI_ERRNO_NAMETOOLONG, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; return hostUserLookup( - rpcMethod, - [readGuestString(namePtr, nameLen)], + () => lookup(readGuestString(namePtr, nameLen)), bufPtr, bufLen, retLenPtr, @@ -8816,6 +10304,7 @@ function hostUserOptionalId(value) { const hostUserImport = { getuid(retUidPtr) { try { + if (!guestRangeIsValid(retUidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retUidPtr, callSyncRpc('process.getuid', [])); } catch (error) { return mapSyntheticFsError(error); @@ -8823,6 +10312,7 @@ const hostUserImport = { }, getgid(retGidPtr) { try { + if (!guestRangeIsValid(retGidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retGidPtr, callSyncRpc('process.getgid', [])); } catch (error) { return mapSyntheticFsError(error); @@ -8830,6 +10320,7 @@ const hostUserImport = { }, geteuid(retUidPtr) { try { + if (!guestRangeIsValid(retUidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retUidPtr, callSyncRpc('process.geteuid', [])); } catch (error) { return mapSyntheticFsError(error); @@ -8837,6 +10328,7 @@ const hostUserImport = { }, getegid(retGidPtr) { try { + if (!guestRangeIsValid(retGidPtr, 4)) return WASI_ERRNO_FAULT; return writeGuestUint32(retGidPtr, callSyncRpc('process.getegid', [])); } catch (error) { return mapSyntheticFsError(error); @@ -8844,6 +10336,13 @@ const hostUserImport = { }, getresuid(retUidPtr, retEuidPtr, retSuidPtr) { try { + if ( + !guestRangeIsValid(retUidPtr, 4) || + !guestRangeIsValid(retEuidPtr, 4) || + !guestRangeIsValid(retSuidPtr, 4) + ) { + return WASI_ERRNO_FAULT; + } const [uid, euid, suid] = callSyncRpc('process.getresuid', []); return writeGuestUint32(retUidPtr, uid) || writeGuestUint32(retEuidPtr, euid) || @@ -8854,6 +10353,13 @@ const hostUserImport = { }, getresgid(retGidPtr, retEgidPtr, retSgidPtr) { try { + if ( + !guestRangeIsValid(retGidPtr, 4) || + !guestRangeIsValid(retEgidPtr, 4) || + !guestRangeIsValid(retSgidPtr, 4) + ) { + return WASI_ERRNO_FAULT; + } const [gid, egid, sgid] = callSyncRpc('process.getresgid', []); return writeGuestUint32(retGidPtr, gid) || writeGuestUint32(retEgidPtr, egid) || @@ -8936,11 +10442,26 @@ const hostUserImport = { }, getgroups(size, groupsPtr, retCountPtr) { try { - const groups = callSyncRpc('process.getgroups', []); + if (!guestRangeIsValid(retCountPtr, 4)) { + return WASI_ERRNO_FAULT; + } const capacity = Number(size) >>> 0; + if ( + capacity !== 0 && + !guestRangeIsValid(groupsPtr, Math.min(capacity, MAX_SUPPLEMENTARY_GROUPS) * 4) + ) { + return WASI_ERRNO_FAULT; + } + const groups = callSyncRpc('process.getgroups', []); + if (!Array.isArray(groups) || groups.length > MAX_SUPPLEMENTARY_GROUPS) { + return WASI_ERRNO_INVAL; + } if (capacity !== 0 && capacity < groups.length) { return WASI_ERRNO_INVAL; } + if (capacity !== 0 && !guestRangeIsValid(groupsPtr, groups.length * 4)) { + return WASI_ERRNO_FAULT; + } if (capacity !== 0) { for (let index = 0; index < groups.length; index += 1) { const errno = writeGuestUint32(Number(groupsPtr) + index * 4, groups[index]); @@ -8954,8 +10475,18 @@ const hostUserImport = { }, setgroups(count, groupsPtr) { try { + const groupCount = Number(count) >>> 0; + const countErrno = checkFixedRequestLimit( + 'wasm.abi.maxSupplementaryGroups', + groupCount, + MAX_SUPPLEMENTARY_GROUPS, + ); + if (countErrno !== WASI_ERRNO_SUCCESS) return countErrno; + if (!guestRangeIsValid(groupsPtr, groupCount * 4)) { + return WASI_ERRNO_FAULT; + } const groups = []; - for (let index = 0; index < (Number(count) >>> 0); index += 1) { + for (let index = 0; index < groupCount; index += 1) { groups.push(readGuestUint32(Number(groupsPtr) + index * 4)); } callSyncRpc('process.setgroups', [groups]); @@ -8966,13 +10497,12 @@ const hostUserImport = { }, isatty(fd, retBoolPtr) { const descriptor = Number(fd) >>> 0; - const isTerminal = descriptor <= 2 && stdioFdIsKernelTty(descriptor) ? 1 : 0; + const isTerminal = stdioFdIsKernelTty(descriptor) ? 1 : 0; return writeGuestUint32(retBoolPtr, isTerminal); }, getpwuid(uid, bufPtr, bufLen, retLenPtr) { return hostUserLookup( - 'process.getpwuid', - [Number(uid) >>> 0], + () => callSyncRpc('process.getpwuid', [Number(uid) >>> 0]), bufPtr, bufLen, retLenPtr, @@ -8980,7 +10510,7 @@ const hostUserImport = { }, getpwnam(namePtr, nameLen, bufPtr, bufLen, retLenPtr) { return hostUserNameLookup( - 'process.getpwnam', + (name) => callSyncRpc('process.getpwnam', [name]), namePtr, nameLen, bufPtr, @@ -8990,8 +10520,7 @@ const hostUserImport = { }, getpwent(index, bufPtr, bufLen, retLenPtr) { return hostUserLookup( - 'process.getpwent', - [Number(index) >>> 0], + () => callSyncRpc('process.getpwent', [Number(index) >>> 0]), bufPtr, bufLen, retLenPtr, @@ -8999,8 +10528,7 @@ const hostUserImport = { }, getgrgid(gid, bufPtr, bufLen, retLenPtr) { return hostUserLookup( - 'process.getgrgid', - [Number(gid) >>> 0], + () => callSyncRpc('process.getgrgid', [Number(gid) >>> 0]), bufPtr, bufLen, retLenPtr, @@ -9008,7 +10536,7 @@ const hostUserImport = { }, getgrnam(namePtr, nameLen, bufPtr, bufLen, retLenPtr) { return hostUserNameLookup( - 'process.getgrnam', + (name) => callSyncRpc('process.getgrnam', [name]), namePtr, nameLen, bufPtr, @@ -9018,8 +10546,7 @@ const hostUserImport = { }, getgrent(index, bufPtr, bufLen, retLenPtr) { return hostUserLookup( - 'process.getgrent', - [Number(index) >>> 0], + () => callSyncRpc('process.getgrent', [Number(index) >>> 0]), bufPtr, bufLen, retLenPtr, @@ -9035,24 +10562,23 @@ const HOST_FS_GUEST_CWD = ? path.posix.normalize(guestEnv.PWD) : '/'; -for (let index = 0; index < WASI_PREOPEN_ENTRIES.length; index += 1) { - const fd = WASI_PREOPEN_FD_BASE + index; - const [guestPath, preopenSpec] = WASI_PREOPEN_ENTRIES[index]; +for (const preopen of WASI_PREOPEN_ENTRIES) { + const { fd, kernelFd, guestPath, preopenSpec, kernelManaged } = preopen; const preopenHandle = { - kind: 'passthrough', - targetFd: fd, + kind: kernelManaged ? 'kernel-fd' : 'passthrough', + targetFd: kernelFd, displayFd: fd, refCount: 0, open: true, guestPath: guestPathForPreopenKey(guestPath), readOnly: preopenSpec?.readOnly === true, internalPreopen: true, + rightsBase: BigInt(preopenSpec?.rightsBase ?? 0), + rightsInheriting: BigInt(preopenSpec?.rightsInheriting ?? 0), }; - // node:wasi always owns this capability descriptor, even when the Linux - // guest namespace starts with the same descriptor closed or inherited from - // the kernel. Patched libc reaches that private descriptor through the - // tagged alias; only expose the untagged descriptor when it is actually free - // in the guest descriptor table. + // Patched libc reaches capability roots through the tagged alias. Managed + // execution points that alias at the kernel fd; standalone execution keeps + // node:wasi's private descriptor behind the same bookkeeping layer. hiddenPreopenHandles.set(fd, preopenHandle); if (initialClosedGuestFds.has(fd)) { // Keep the private tagged capability for libc path resolution, but make @@ -9063,7 +10589,7 @@ for (let index = 0; index < WASI_PREOPEN_ENTRIES.length; index += 1) { !initialMappedGuestFds.has(fd) && !passthroughHandles.has(fd) ) { - retainDelegateFd(fd); + if (!kernelManaged) retainDelegateFd(fd); closedPassthroughFds.delete(fd); passthroughHandles.set(fd, preopenHandle); } @@ -9077,11 +10603,22 @@ if (SIDECAR_MANAGED_PROCESS) { ); } inheritedEntries.sort((left, right) => Number(left?.fd) - Number(right?.fd)); + const projectedPreopenKernelFds = new Set( + WASI_PREOPEN_ENTRIES + .filter((entry) => entry.kernelManaged) + .map((entry) => Number(entry.kernelFd) >>> 0), + ); for (const entry of inheritedEntries) { const kernelFd = Number(entry?.fd); if (!Number.isSafeInteger(kernelFd) || kernelFd < 0 || kernelFd > 0xffffffff) { throw new Error(`kernel descriptor snapshot contains invalid fd ${String(entry?.fd)}`); } + // The WASI adapter already projected capability roots into the stable + // guest preopen range beginning at fd 3. Kernel allocation order is an + // implementation detail and must not create a second Linux-visible alias. + if (projectedPreopenKernelFds.has(kernelFd >>> 0)) { + continue; + } const mappedGuestFd = initialKernelFdMappings.get(kernelFd); // The kernel always has canonical stdio entries. Leave an unmapped entry // on Node's bootstrap handle, but do not discard a POSIX-spawn dup2 that @@ -9093,67 +10630,27 @@ if (SIDECAR_MANAGED_PROCESS) { if (mappedGuestFd != null) { pendingInitialKernelGuestFds.delete(mappedGuestFd); } + const existingHandle = lookupFdHandle(kernelFd); + if ( + existingHandle?.kind === 'kernel-fd' && + Number(existingHandle.targetFd) === kernelFd && + mappedGuestFd == null + ) { + const descriptionId = String(entry?.descriptionId ?? ''); + if (initialManagedHostNetDescriptionIds.has(descriptionId)) { + existingHandle.hostNetDescriptionId = descriptionId; + } + continue; + } const guestFd = registerKernelDelegateFd( kernelFd, mappedGuestFd ?? null, ); - if ((Number(entry?.fdFlags) & 1) !== 0) { - runnerCloexecFds.add(guestFd); - } - } -} - -function hostFsModeFromStat(stat) { - const mode = Number(stat?.mode); - return Number.isInteger(mode) && mode > 0 ? mode >>> 0 : 0; -} - -const hostFsSizeByGuestPath = new Map(); -// Bound the per-path size cache so a guest truncating many distinct paths cannot -// grow it without limit. Entries are insertion-ordered, so evicting the oldest -// key is a cheap LRU-ish bound. -const HOST_FS_SIZE_CACHE_MAX_ENTRIES = 4096; -let hostFsSizeCacheEvictionWarned = false; - -function forgetHostFsSize(guestPath) { - if (typeof guestPath !== 'string') { - return; - } - hostFsSizeByGuestPath.delete(path.posix.normalize(guestPath)); -} - -function rememberHostFsSize(guestPath, size) { - if (typeof guestPath !== 'string') { - return; - } - const normalized = path.posix.normalize(guestPath); - if (!Number.isFinite(size) || size < 0) { - hostFsSizeByGuestPath.delete(normalized); - return; - } - if ( - !hostFsSizeByGuestPath.has(normalized) && - hostFsSizeByGuestPath.size >= HOST_FS_SIZE_CACHE_MAX_ENTRIES - ) { - const oldest = hostFsSizeByGuestPath.keys().next().value; - if (oldest !== undefined) { - hostFsSizeByGuestPath.delete(oldest); - } - if (!hostFsSizeCacheEvictionWarned) { - hostFsSizeCacheEvictionWarned = true; - traceHostProcess('host-fs-size-cache-evict', { - max: HOST_FS_SIZE_CACHE_MAX_ENTRIES, - }); + const descriptionId = String(entry?.descriptionId ?? ''); + if (initialManagedHostNetDescriptionIds.has(descriptionId)) { + attachManagedHostNetDescription(guestFd, descriptionId); } } - hostFsSizeByGuestPath.set(normalized, BigInt(Math.trunc(size))); -} - -function rememberedHostFsSize(guestPath) { - if (typeof guestPath !== 'string') { - return null; - } - return hostFsSizeByGuestPath.get(path.posix.normalize(guestPath)) ?? null; } function resolveHostFsPath(value, fromGuestDir = HOST_FS_GUEST_CWD) { @@ -9190,13 +10687,30 @@ function mutateGuestFileRange(fd, offset, length, method, extraArgs = []) { ) { return WASI_ERRNO_INVAL; } - callSyncRpc(method, [ + const args = [ Number(handle.targetFd) >>> 0, rangeOffset, rangeLength, ...extraArgs, - ]); - forgetHostFsSize(handle.guestPath); + ]; + switch (method) { + case 'fs.fallocateSync': + callSyncRpc('fs.fallocateSync', args); + break; + case 'fs.zeroRangeSync': + callSyncRpc('fs.zeroRangeSync', args); + break; + case 'fs.insertRangeSync': + callSyncRpc('fs.insertRangeSync', args); + break; + case 'fs.collapseRangeSync': + callSyncRpc('fs.collapseRangeSync', args); + break; + default: + throw Object.assign(new Error(`unsupported file-range method ${method}`), { + code: 'EINVAL', + }); + } return WASI_ERRNO_SUCCESS; } catch (error) { traceHostProcess('guest-file-range-error', { @@ -9212,6 +10726,7 @@ function mutateGuestFileRange(fd, offset, length, method, extraArgs = []) { const hostFsImport = { open_tmpfile(dirFd, pathPtr, pathLen, flags, mode, retFdPtr) { try { + if (!guestRangeIsValid(retFdPtr, 4)) return WASI_ERRNO_FAULT; const directory = resolvePathOpenGuestPath(dirFd, pathPtr, pathLen); if (typeof directory !== 'string') return WASI_ERRNO_BADF; if (isWorkspaceReadOnly() || guestPathIsReadOnly(directory)) return WASI_ERRNO_ROFS; @@ -9259,7 +10774,6 @@ const hostFsImport = { return WASI_ERRNO_ROFS; } callSyncRpc('fs.linkFdSync', [Number(handle.targetFd) >>> 0, destination]); - handle.guestPath = destination; return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -9353,6 +10867,15 @@ const hostFsImport = { ) { let target; try { + if (!guestRangesAreValid( + [retTotalBytesPtr, 8], + [retUsedBytesPtr, 8], + [retAvailableBytesPtr, 8], + [retTotalInodesPtr, 8], + [retFreeInodesPtr, 8], + )) { + return WASI_ERRNO_FAULT; + } const rawTarget = readGuestString(pathPtr, pathLen); target = Number(fd) >>> 0 === 0xffffffff ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) @@ -9379,12 +10902,17 @@ const hostFsImport = { }, fd_fiemap(fd, index, retStartPtr, retEndPtr, retFlagsPtr) { try { + if (!guestRangesAreValid([retStartPtr, 8], [retEndPtr, 8], [retFlagsPtr, 4])) { + return WASI_ERRNO_FAULT; + } const handle = lookupFdHandle(Number(fd) >>> 0); if (handle?.kind !== 'guest-file' && handle?.kind !== 'kernel-fd' || typeof handle.targetFd !== 'number') { return WASI_ERRNO_BADF; } - const ranges = callSyncRpc('fs.fiemapSync', [Number(handle.targetFd) >>> 0]); - const range = Array.isArray(ranges) ? ranges[Number(index) >>> 0] : null; + const range = callSyncRpc('fs.fiemapAtSync', [ + Number(handle.targetFd) >>> 0, + Number(index) >>> 0, + ]); if (!range) { return WASI_ERRNO_NODATA; } @@ -9451,17 +10979,22 @@ const hostFsImport = { }, path_owner(fd, pathPtr, pathLen, followSymlinks, retUidPtr, retGidPtr) { try { - const rawTarget = readGuestString(pathPtr, pathLen); - const target = Number(fd) >>> 0 === 0xffffffff - ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) - : resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { + if (!guestRangesAreValid([retUidPtr, 4], [retGidPtr, 4])) { + return WASI_ERRNO_FAULT; + } + const numericFd = Number(fd) >>> 0; + const rawTarget = readGuestString(pathPtr, pathLen); + const operand = numericFd === NODE_CWD_FD + ? { dirFd: NODE_CWD_FD, path: path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) } + : kernelPathOperand(numericFd, pathPtr, pathLen); + if (!operand) { return WASI_ERRNO_BADF; } - const stat = callSyncRpc( - Number(followSymlinks) === 0 ? 'fs.lstatSync' : 'fs.statSync', - [target], - ); + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + Number(followSymlinks) !== 0, + ]); return writeGuestUint32(retUidPtr, stat.uid) || writeGuestUint32(retGidPtr, stat.gid); } catch (error) { return mapSyntheticFsError(error); @@ -9469,6 +11002,9 @@ const hostFsImport = { }, fd_owner(fd, retUidPtr, retGidPtr) { try { + if (!guestRangesAreValid([retUidPtr, 4], [retGidPtr, 4])) { + return WASI_ERRNO_FAULT; + } const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); @@ -9509,21 +11045,22 @@ const hostFsImport = { } }, path_chown(fd, pathPtr, pathLen, uid, gid, followSymlinks) { - let rawTarget; - let target; try { - rawTarget = readGuestString(pathPtr, pathLen); - target = Number(fd) >>> 0 === 0xffffffff - ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) - : resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { + const numericFd = Number(fd) >>> 0; + const rawTarget = readGuestString(pathPtr, pathLen); + const operand = numericFd === NODE_CWD_FD + ? { dirFd: NODE_CWD_FD, path: path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) } + : kernelPathOperand(numericFd, pathPtr, pathLen); + if (!operand) { return WASI_ERRNO_BADF; } - if (Number(followSymlinks) === 0) { - callSyncRpc('fs.lchownSync', [target, Number(uid) >>> 0, Number(gid) >>> 0]); - } else { - callSyncRpc('fs.chownSync', [target, Number(uid) >>> 0, Number(gid) >>> 0]); - } + callSyncRpc('process.path_chown_at', [ + operand.dirFd, + operand.path, + Number(uid) >>> 0, + Number(gid) >>> 0, + Number(followSymlinks) !== 0, + ]); return WASI_ERRNO_SUCCESS; } catch (error) { traceHostProcess('host-fs-path-chown-error', { @@ -9531,19 +11068,21 @@ const hostFsImport = { message: error?.message, followSymlinks: Number(followSymlinks) !== 0, fd: Number(fd) >>> 0, - rawTarget, - target, }); return mapSyntheticFsError(error); } }, fd_chown(fd, uid, gid) { try { - const target = guestPathForManagedFd(fd); - if (typeof target !== 'string') { + const handle = lookupFdHandle(fd); + if (handle?.kind !== 'kernel-fd') { return WASI_ERRNO_BADF; } - callSyncRpc('fs.chownSync', [target, Number(uid) >>> 0, Number(gid) >>> 0]); + callSyncRpc('process.fd_chown', [ + Number(handle.targetFd) >>> 0, + Number(uid) >>> 0, + Number(gid) >>> 0, + ]); return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -9561,6 +11100,17 @@ const hostFsImport = { retSizePtr, ) { try { + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; + const capacity = Number(size) >>> 0; + if (!guestRangesAreValid([retSizePtr, 4], [valuePtr, capacity])) { + return WASI_ERRNO_FAULT; + } const rawTarget = readGuestString(pathPtr, pathLen); const target = Number(fd) >>> 0 === 0xffffffff ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) @@ -9574,7 +11124,6 @@ const hostFsImport = { const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value ?? []); const sizeResult = writeGuestUint32(retSizePtr, bytes.byteLength); if (sizeResult !== WASI_ERRNO_SUCCESS) return sizeResult; - const capacity = Number(size) >>> 0; if (capacity === 0) return WASI_ERRNO_SUCCESS; if (capacity < bytes.byteLength) return WASI_ERRNO_RANGE; new Uint8Array(instanceMemory.buffer).set(bytes, Number(valuePtr) >>> 0); @@ -9585,17 +11134,26 @@ const hostFsImport = { }, fd_getxattr(fd, namePtr, nameLen, valuePtr, size, retSizePtr) { try { - const target = guestPathForManagedFd(fd); - if (typeof target !== 'string') return WASI_ERRNO_BADF; - const value = callSyncRpc('fs.getxattrSync', [ - target, + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; + const capacity = Number(size) >>> 0; + if (!guestRangesAreValid([retSizePtr, 4], [valuePtr, capacity])) { + return WASI_ERRNO_FAULT; + } + const handle = lookupFdHandle(fd); + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + const value = callSyncRpc('fs.fgetxattrSync', [ + Number(handle.targetFd) >>> 0, readGuestString(namePtr, nameLen), - true, ]); const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value ?? []); const sizeResult = writeGuestUint32(retSizePtr, bytes.byteLength); if (sizeResult !== WASI_ERRNO_SUCCESS) return sizeResult; - const capacity = Number(size) >>> 0; if (capacity === 0) return WASI_ERRNO_SUCCESS; if (capacity < bytes.byteLength) return WASI_ERRNO_RANGE; new Uint8Array(instanceMemory.buffer).set(bytes, Number(valuePtr) >>> 0); @@ -9606,6 +11164,10 @@ const hostFsImport = { }, path_listxattr(fd, pathPtr, pathLen, listPtr, size, followSymlinks, retSizePtr) { try { + const capacity = Number(size) >>> 0; + if (!guestRangesAreValid([retSizePtr, 4], [listPtr, capacity])) { + return WASI_ERRNO_FAULT; + } const rawTarget = readGuestString(pathPtr, pathLen); const target = Number(fd) >>> 0 === 0xffffffff ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) @@ -9615,7 +11177,6 @@ const hostFsImport = { const bytes = Buffer.from((Array.isArray(names) ? names : []).map((name) => `${name}\0`).join('')); const sizeResult = writeGuestUint32(retSizePtr, bytes.byteLength); if (sizeResult !== WASI_ERRNO_SUCCESS) return sizeResult; - const capacity = Number(size) >>> 0; if (capacity === 0) return WASI_ERRNO_SUCCESS; if (capacity < bytes.byteLength) return WASI_ERRNO_RANGE; new Uint8Array(instanceMemory.buffer).set(bytes, Number(listPtr) >>> 0); @@ -9626,13 +11187,16 @@ const hostFsImport = { }, fd_listxattr(fd, listPtr, size, retSizePtr) { try { - const target = guestPathForManagedFd(fd); - if (typeof target !== 'string') return WASI_ERRNO_BADF; - const names = callSyncRpc('fs.listxattrSync', [target, true]); + const capacity = Number(size) >>> 0; + if (!guestRangesAreValid([retSizePtr, 4], [listPtr, capacity])) { + return WASI_ERRNO_FAULT; + } + const handle = lookupFdHandle(fd); + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + const names = callSyncRpc('fs.flistxattrSync', [Number(handle.targetFd) >>> 0]); const bytes = Buffer.from((Array.isArray(names) ? names : []).map((name) => `${name}\0`).join('')); const sizeResult = writeGuestUint32(retSizePtr, bytes.byteLength); if (sizeResult !== WASI_ERRNO_SUCCESS) return sizeResult; - const capacity = Number(size) >>> 0; if (capacity === 0) return WASI_ERRNO_SUCCESS; if (capacity < bytes.byteLength) return WASI_ERRNO_RANGE; new Uint8Array(instanceMemory.buffer).set(bytes, Number(listPtr) >>> 0); @@ -9654,6 +11218,20 @@ const hostFsImport = { ) { let target; try { + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; + const valueErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrValueBytes', + size, + XATTR_SIZE_MAX, + WASI_ERRNO_2BIG, + ); + if (valueErrno !== WASI_ERRNO_SUCCESS) return valueErrno; const rawTarget = readGuestString(pathPtr, pathLen); target = Number(fd) >>> 0 === 0xffffffff ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) @@ -9678,14 +11256,27 @@ const hostFsImport = { }, fd_setxattr(fd, namePtr, nameLen, valuePtr, size, flags) { try { - const target = guestPathForManagedFd(fd); - if (typeof target !== 'string') return WASI_ERRNO_BADF; - callSyncRpc('fs.setxattrSync', [ - target, + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; + const valueErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrValueBytes', + size, + XATTR_SIZE_MAX, + WASI_ERRNO_2BIG, + ); + if (valueErrno !== WASI_ERRNO_SUCCESS) return valueErrno; + const handle = lookupFdHandle(fd); + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + callSyncRpc('fs.fsetxattrSync', [ + Number(handle.targetFd) >>> 0, readGuestString(namePtr, nameLen), readGuestBytes(valuePtr, size), Number(flags) >>> 0, - true, ]); return WASI_ERRNO_SUCCESS; } catch (error) { @@ -9694,6 +11285,13 @@ const hostFsImport = { }, path_removexattr(fd, pathPtr, pathLen, namePtr, nameLen, followSymlinks) { try { + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const rawTarget = readGuestString(pathPtr, pathLen); const target = Number(fd) >>> 0 === 0xffffffff ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) @@ -9711,9 +11309,19 @@ const hostFsImport = { }, fd_removexattr(fd, namePtr, nameLen) { try { - const target = guestPathForManagedFd(fd); - if (typeof target !== 'string') return WASI_ERRNO_BADF; - callSyncRpc('fs.removexattrSync', [target, readGuestString(namePtr, nameLen), true]); + const nameErrno = checkFixedRequestLimit( + 'wasm.abi.maxXattrNameBytes', + nameLen, + XATTR_NAME_MAX, + WASI_ERRNO_RANGE, + ); + if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; + const handle = lookupFdHandle(fd); + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + callSyncRpc('fs.fremovexattrSync', [ + Number(handle.targetFd) >>> 0, + readGuestString(namePtr, nameLen), + ]); return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -9739,44 +11347,15 @@ const hostFsImport = { return HOST_FS_MODE_CHARACTER; } - try { - const targetFd = - typeof handle?.ioFd === 'number' - ? Number(handle.ioFd) >>> 0 - : typeof handle?.targetFd === 'number' - ? Number(handle.targetFd) >>> 0 - : descriptor; - return hostFsModeFromStat(fsModule.fstatSync(targetFd)) || HOST_FS_MODE_REGULAR; - } catch { - return HOST_FS_MODE_REGULAR; - } + return 0; }, fd_size(fd) { const descriptor = Number(fd) >>> 0; try { const handle = lookupFdHandle(descriptor); - if (handle?.kind === 'kernel-fd') { - const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); - return BigInt(stat?.size ?? -1); - } - const rememberedSize = rememberedHostFsSize(handle?.guestPath); - if (rememberedSize != null) { - return rememberedSize; - } - if (typeof handle?.ioFd === 'number') { - return BigInt(fsModule.fstatSync(Number(handle.ioFd) >>> 0).size ?? -1); - } - if (typeof handle?.guestPath === 'string') { - const hostPath = resolveHostFsPath(handle.guestPath); - if (typeof hostPath === 'string') { - return BigInt(fsModule.statSync(hostPath).size ?? -1); - } - return BigInt(fsModule.statSync(handle.guestPath).size ?? -1); - } - const targetFd = typeof handle?.targetFd === 'number' - ? Number(handle.targetFd) >>> 0 - : descriptor; - return BigInt(fsModule.fstatSync(targetFd).size ?? -1); + if (handle?.kind !== 'kernel-fd') return (1n << 64n) - 1n; + const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); + return BigInt(stat?.size ?? -1); } catch { return (1n << 64n) - 1n; } @@ -9785,99 +11364,65 @@ const hostFsImport = { const descriptor = Number(fd) >>> 0; try { const handle = lookupFdHandle(descriptor); - if (handle?.kind === 'kernel-fd') { - const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); - return BigInt(stat?.blocks ?? -1); - } - if (typeof handle?.ioFd === 'number') { - return BigInt(fsModule.fstatSync(Number(handle.ioFd) >>> 0).blocks ?? -1); - } - if (typeof handle?.guestPath === 'string') { - const hostPath = resolveHostFsPath(handle.guestPath); - const stat = fsModule.statSync( - typeof hostPath === 'string' ? hostPath : handle.guestPath, - ); - return BigInt(stat?.blocks ?? -1); - } - return BigInt(fsModule.fstatSync(descriptor).blocks ?? -1); + if (handle?.kind !== 'kernel-fd') return (1n << 64n) - 1n; + const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); + return BigInt(stat?.blocks ?? -1); } catch { return (1n << 64n) - 1n; } }, path_mode(fd, pathPtr, pathLen, followSymlinks) { try { - const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { - return 0; - } - const stat = callSyncRpc( - Number(followSymlinks) === 0 ? 'fs.lstatSync' : 'fs.statSync', - [target], - ); - const mode = hostFsModeFromStat(stat); - traceHostProcess('host-fs-path-mode', { - target, - followSymlinks: Number(followSymlinks) >>> 0, - mode, - }); - return mode; + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) return 0; + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + Number(followSymlinks) !== 0, + ]); + return Number(stat?.mode) >>> 0; } catch { traceHostProcess('host-fs-path-mode-fault', {}); return 0; } }, path_size(fd, pathPtr, pathLen, followSymlinks) { - const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { - return (1n << 64n) - 1n; - } - const rememberedSize = rememberedHostFsSize(target); - if (rememberedSize != null) { - return rememberedSize; - } - try { - const hostPath = resolveHostFsPath(target); - if (typeof hostPath === 'string') { - const stat = - Number(followSymlinks) === 0 - ? fsModule.lstatSync(hostPath) - : fsModule.statSync(hostPath); - return BigInt(stat?.size ?? -1); - } - const guestStat = - Number(followSymlinks) === 0 - ? fsModule.lstatSync(target) - : fsModule.statSync(target); - return BigInt(guestStat?.size ?? -1); + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) return (1n << 64n) - 1n; + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + Number(followSymlinks) !== 0, + ]); + return BigInt(stat?.size ?? -1); } catch { return (1n << 64n) - 1n; } }, path_blocks(fd, pathPtr, pathLen, followSymlinks) { - const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') return (1n << 64n) - 1n; try { - const stat = callSyncRpc( - Number(followSymlinks) === 0 ? 'fs.lstatSync' : 'fs.statSync', - [target], - ); + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) return (1n << 64n) - 1n; + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + Number(followSymlinks) !== 0, + ]); return BigInt(stat?.blocks ?? -1); } catch { return (1n << 64n) - 1n; } }, path_rdev(fd, pathPtr, pathLen, followSymlinks) { - const rawTarget = readGuestString(pathPtr, pathLen); - const target = Number(fd) >>> 0 === 0xffffffff - ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) - : resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') return 0n; - try { - const stat = callSyncRpc( - Number(followSymlinks) === 0 ? 'fs.lstatSync' : 'fs.statSync', - [target], - ); + try { + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) return 0n; + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + Number(followSymlinks) !== 0, + ]); return BigInt(stat?.rdev ?? 0); } catch { return 0n; @@ -9885,24 +11430,14 @@ const hostFsImport = { }, chmod(fd, pathPtr, pathLen, mode) { try { - const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { - return WASI_ERRNO_NOENT; - } - const mapping = resolveHostFsMapping(target); - if (!mapping || typeof mapping.hostPath !== 'string') { - return WASI_ERRNO_NOENT; - } - if (mapping.readOnly) { - return WASI_ERRNO_ROFS; - } - traceHostProcess('host-fs-chmod', { - target, - hostPath: mapping.hostPath, - mode: Number(mode) >>> 0, - }); - chmodMappedGuestPath(target, mapping.hostPath, Number(mode) >>> 0); - return 0; + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_chmod_at', [ + operand.dirFd, + operand.path, + Number(mode) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; } catch (error) { traceHostProcess('host-fs-chmod-fault', { message: error instanceof Error ? error.message : String(error), @@ -9914,31 +11449,12 @@ const hostFsImport = { try { const descriptor = Number(fd) >>> 0; const handle = lookupFdHandle(descriptor); - if (handle?.kind === 'kernel-fd') { - callSyncRpc('process.fd_chmod', [ - Number(handle.targetFd) >>> 0, - Number(mode) >>> 0, - ]); - return WASI_ERRNO_SUCCESS; - } - if (handle?.readOnly === true) { - return WASI_ERRNO_ROFS; - } - if (typeof handle?.guestPath === 'string') { - const mapping = resolveHostFsMapping(handle.guestPath); - if (!mapping || typeof mapping.hostPath !== 'string') { - return WASI_ERRNO_NOENT; - } - if (mapping.readOnly) { - return WASI_ERRNO_ROFS; - } - chmodMappedGuestPath(handle.guestPath, mapping.hostPath, Number(mode) >>> 0); - return 0; - } - const targetFd = - typeof handle?.targetFd === 'number' ? Number(handle.targetFd) >>> 0 : descriptor; - fsModule.fchmodSync(targetFd, Number(mode) >>> 0); - return 0; + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + callSyncRpc('process.fd_chmod', [ + Number(handle.targetFd) >>> 0, + Number(mode) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; } catch (error) { traceHostProcess('host-fs-fchmod-fault', { message: error instanceof Error ? error.message : String(error), @@ -9992,49 +11508,42 @@ const hostFsImport = { const descriptor = Number(fd) >>> 0; const nextSize = Number(length); if (!Number.isFinite(nextSize) || nextSize < 0) { - return 1; + return WASI_ERRNO_INVAL; } const handle = lookupFdHandle(descriptor); - if (handle?.kind === 'kernel-fd') { - callSyncRpc('process.fd_truncate', [ - Number(handle.targetFd) >>> 0, - BigInt(nextSize).toString(), - ]); - return WASI_ERRNO_SUCCESS; - } - if (handle?.readOnly === true) { - return 1; - } - if (typeof handle?.ioFd === 'number') { - fsModule.ftruncateSync(handle.ioFd, nextSize); - if ((handle.position ?? 0) > nextSize) { - handle.position = nextSize; - } - rememberHostFsSize(handle.guestPath, nextSize); - return 0; - } - if (typeof handle?.guestPath === 'string') { - const pathFd = fsModule.openSync(handle.guestPath, 0o1, 0o666); - try { - fsModule.ftruncateSync(pathFd, nextSize); - if ((handle.position ?? 0) > nextSize) { - handle.position = nextSize; - } - rememberHostFsSize(handle.guestPath, nextSize); - } finally { - fsModule.closeSync(pathFd); - } - return 0; - } - return 1; - } catch { - return 1; + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + callSyncRpc('process.fd_truncate', [ + Number(handle.targetFd) >>> 0, + BigInt(nextSize).toString(), + ]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapSyntheticFsError(error); } }, }; wasiImport.clock_time_get = (clockId, precision, resultPtr) => { + if (!guestRangeIsValid(resultPtr, 8)) return WASI_ERRNO_FAULT; const numericClockId = Number(clockId) >>> 0; + if (SIDECAR_MANAGED_PROCESS) { + try { + const deterministicRealtime = numericClockId === 0 ? frozenTimeNs.toString() : null; + const value = callSyncRpc('process.clock_time', [ + numericClockId, + BigInt(precision).toString(), + deterministicRealtime, + ]); + const nanoseconds = BigInt(value); + if (nanoseconds < 0n || nanoseconds > 0xffffffffffffffffn) { + return WASI_ERRNO_OVERFLOW; + } + new DataView(instanceMemory.buffer).setBigUint64(Number(resultPtr), nanoseconds, true); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (numericClockId !== 0 && delegateClockTimeGet) { return delegateClockTimeGet(clockId, precision, resultPtr); } @@ -10054,7 +11563,21 @@ wasiImport.clock_time_get = (clockId, precision, resultPtr) => { }; wasiImport.clock_res_get = (clockId, resultPtr) => { + if (!guestRangeIsValid(resultPtr, 8)) return WASI_ERRNO_FAULT; const numericClockId = Number(clockId) >>> 0; + if (SIDECAR_MANAGED_PROCESS) { + try { + const value = callSyncRpc('process.clock_resolution', [numericClockId]); + const nanoseconds = BigInt(value); + if (nanoseconds < 0n || nanoseconds > 0xffffffffffffffffn) { + return WASI_ERRNO_OVERFLOW; + } + new DataView(instanceMemory.buffer).setBigUint64(Number(resultPtr), nanoseconds, true); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (numericClockId !== 0 && delegateClockResGet) { return delegateClockResGet(clockId, resultPtr); } @@ -10073,6 +11596,32 @@ wasiImport.clock_res_get = (clockId, resultPtr) => { } }; +// Managed VMs use the shared sidecar entropy source. Validate the complete +// guest output range and configured total limit before the first host request +// or write, then use bounded response chunks so neither bridge lane nor host +// allocation scales with an attacker-selected linear-memory length. +if (SIDECAR_MANAGED_PROCESS) { + wasiImport.random_get = (bufferPtr, bufferLength) => { + const length = Number(bufferLength) >>> 0; + if (!guestRangeIsValid(bufferPtr, length)) return WASI_ERRNO_FAULT; + if (length > __agentOSWasmEntropyLimitBytes) return WASI_ERRNO_2BIG; + const output = new Uint8Array(instanceMemory.buffer); + const base = Number(bufferPtr) >>> 0; + const chunkBytes = 64 * 1024; + try { + for (let offset = 0; offset < length; offset += chunkBytes) { + const requested = Math.min(chunkBytes, length - offset); + const bytes = Buffer.from(callSyncRpc('process.random_get', [requested]) ?? []); + if (bytes.byteLength !== requested) return WASI_ERRNO_IO; + output.set(bytes, base + offset); + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }; +} + if (delegatePathOpen) { wasiImport.path_open = ( fd, @@ -10085,6 +11634,9 @@ if (delegatePathOpen) { fdflags, openedFdPtr, ) => { + if (!guestRangesAreValid([pathPtr, Number(pathLen) >>> 0], [openedFdPtr, 4])) { + return WASI_ERRNO_FAULT; + } const requestedCreateMode = pendingOpenCreateMode; pendingOpenCreateMode = null; const requestedDirect = pendingOpenDirect; @@ -10110,6 +11662,12 @@ if (delegatePathOpen) { if (!passthroughDirHandle && rejectClosedPassthroughFd(fd)) { return WASI_ERRNO_BADF; } + if (SIDECAR_MANAGED_PROCESS && passthroughDirHandle?.kind !== 'kernel-fd') { + return WASI_ERRNO_BADF; + } + const managedOpenPath = SIDECAR_MANAGED_PROCESS + ? kernelOpenPathForGuestPath(readGuestString(pathPtr, pathLen)) + : null; const delegateDirFd = passthroughDirHandle?.kind === 'passthrough' @@ -10138,55 +11696,51 @@ if (delegatePathOpen) { if (guestReadOnlyDenied) { return denyReadOnlyMutation(); } - const procFdResult = openProcSelfFdAlias( - guestPath, - oflags, - rightsBase, - dirflags, - openedFdPtr, - ); - if (procFdResult !== null) { - return procFdResult; - } - if (SIDECAR_MANAGED_PROCESS) { - try { - const fifoResult = openBlockingGuestFifoForPathOpen( + const procFdResult = SIDECAR_MANAGED_PROCESS + ? null + : openProcSelfFdAlias( guestPath, oflags, rightsBase, - fdflags, dirflags, openedFdPtr, - requestedCreateMode ?? 0o666, - requestedDirect, ); - if (fifoResult !== null) return fifoResult; + if (procFdResult !== null) { + return procFdResult; + } + if (SIDECAR_MANAGED_PROCESS) { + try { + if (typeof guestPath === 'string') { + const fifoResult = openBlockingGuestFifoForPathOpen( + Number(passthroughDirHandle.targetFd) >>> 0, + managedOpenPath, + guestPath, + oflags, + rightsBase, + rightsInheriting, + fdflags, + dirflags, + openedFdPtr, + requestedCreateMode ?? 0o666, + requestedDirect, + ); + if (fifoResult !== null) return fifoResult; + } } catch (error) { return mapHostProcessError(error); } } if (SIDECAR_MANAGED_PROCESS) { - if (typeof guestPath !== 'string') { - return WASI_ERRNO_BADF; - } - if (!hasRunnerOpenFdCapacity(1)) { - return WASI_ERRNO_MFILE; - } + if (!hasRunnerOpenFdCapacity(1)) return WASI_ERRNO_MFILE; try { - const kernelFd = Number( - passthroughDirHandle?.kind === 'kernel-fd' - ? callSyncRpc('process.path_open_at', [ - Number(passthroughDirHandle.targetFd) >>> 0, - readGuestString(pathPtr, pathLen), - kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags, requestedDirect), - requestedCreateMode ?? 0o666, - ]) - : callSyncRpc('process.fd_open', [ - guestPath, - kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags, requestedDirect), - requestedCreateMode ?? 0o666, - ]) - ) >>> 0; + const kernelFd = Number(callSyncRpc('process.path_open_at', [ + Number(passthroughDirHandle.targetFd) >>> 0, + managedOpenPath, + kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags, requestedDirect), + requestedCreateMode ?? 0o666, + rightsBase.toString(), + rightsInheriting.toString(), + ])) >>> 0; if ((Number(oflags) & WASI_OFLAGS_DIRECTORY) !== 0) { const stat = callSyncRpc('process.fd_stat', [kernelFd]); if ((Number(stat?.filetype) >>> 0) !== WASI_FILETYPE_DIRECTORY) { @@ -10197,8 +11751,6 @@ if (delegatePathOpen) { } } const openedFd = registerKernelDelegateFd(kernelFd); - const openedHandle = lookupFdHandle(openedFd); - if (openedHandle) openedHandle.guestPath = guestPath; const writeResult = writeGuestUint32(openedFdPtr, openedFd); if (writeResult !== WASI_ERRNO_SUCCESS) { wasiImport.fd_close(openedFd); @@ -10348,6 +11900,9 @@ const kernelPathOperationHandlers = { return WASI_ERRNO_SUCCESS; }, path_filestat_get(args) { + if (!guestRangesAreValid([args[2], Number(args[3]) >>> 0], [args[4], 64])) { + return WASI_ERRNO_FAULT; + } const operand = kernelPathOperand(args[0], args[2], args[3]); if (!operand) return WASI_ERRNO_BADF; const stat = callSyncRpc('process.path_stat_at', [ @@ -10632,10 +12187,19 @@ const KERNEL_POLLERR = 0x0008; const KERNEL_POLLHUP = 0x0010; wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { + const iovRequest = validateGuestIovRequest(iovs, iovsLen); + if (iovRequest.errno !== WASI_ERRNO_SUCCESS) return iovRequest.errno; + if (!guestRangeIsValid(nreadPtr, 4)) return WASI_ERRNO_FAULT; const numericFd = Number(fd) >>> 0; const hostNetSocket = getHostNetSocket(numericFd); if (hostNetSocket) { - return readHostNetSocketToGuestIovs(hostNetSocket, iovs, iovsLen, nreadPtr); + return readHostNetSocketToGuestIovs( + hostNetSocket, + iovs, + iovsLen, + nreadPtr, + iovRequest, + ); } const handle = __agentOSWasiMeasurePhase('fd_read', 'lookup_handle', () => @@ -10644,7 +12208,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { if (handle?.kind === 'kernel-fd') { try { const requestedLength = boundedWasmSyncRpcReadLength( - guestIovByteLength(iovs, iovsLen), + guestIovByteLength(iovs, iovsLen, iovRequest), ); const kernelFd = Number(handle.targetFd) >>> 0; let bytes; @@ -10694,7 +12258,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } } } - const written = writeBytesToGuestIovs(iovs, iovsLen, bytes); + const written = writeBytesToGuestIovs(iovs, iovsLen, bytes, iovRequest); return writeGuestUint32(nreadPtr, written); } catch (error) { return mapHostProcessError(error); @@ -10702,18 +12266,11 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } if (handle?.kind === 'pipe-read') { try { - const requestedLength = __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - return 0; - } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; - }); + const requestedLength = __agentOSWasiMeasurePhase( + 'fd_read', + 'iov_scan', + () => iovRequest.totalLength, + ); const pipeClosed = __agentOSWasiMeasurePhase('fd_read', 'pipe_wait', () => { while (handle.pipe.chunks.length === 0) { @@ -10738,7 +12295,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { dequeuePipeBytes(handle.pipe, requestedLength) ); const written = __agentOSWasiMeasurePhase('fd_read', 'guest_iov_write', () => - writeBytesToGuestIovs(iovs, iovsLen, chunk) + writeBytesToGuestIovs(iovs, iovsLen, chunk, iovRequest) ); return __agentOSWasiMeasurePhase('fd_read', 'result_marshal', () => writeGuestUint32(nreadPtr, written) @@ -10751,18 +12308,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { if (handle?.kind === 'guest-file') { try { const requestedLength = boundedWasmSyncRpcReadLength( - __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - return 0; - } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; - }), + __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => iovRequest.totalLength), ); const buffer = Buffer.alloc(requestedLength); const bytesRead = __agentOSWasiMeasurePhase('fd_read', 'host_io', () => @@ -10776,7 +12322,12 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { ); handle.position = (handle.position ?? 0) + bytesRead; const written = __agentOSWasiMeasurePhase('fd_read', 'guest_iov_write', () => - writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)) + writeBytesToGuestIovs( + iovs, + iovsLen, + buffer.subarray(0, bytesRead), + iovRequest, + ) ); return __agentOSWasiMeasurePhase('fd_read', 'result_marshal', () => writeGuestUint32(nreadPtr, written) @@ -10789,18 +12340,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { if (handle?.kind === 'passthrough' && typeof handle.ioFd === 'number') { try { const requestedLength = boundedWasmSyncRpcReadLength( - __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - return 0; - } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; - }), + __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => iovRequest.totalLength), ); const buffer = Buffer.alloc(requestedLength); const bytesRead = __agentOSWasiMeasurePhase('fd_read', 'host_io', () => @@ -10814,7 +12354,12 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { ); handle.position = (handle.position ?? 0) + bytesRead; const written = __agentOSWasiMeasurePhase('fd_read', 'guest_iov_write', () => - writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)) + writeBytesToGuestIovs( + iovs, + iovsLen, + buffer.subarray(0, bytesRead), + iovRequest, + ) ); return __agentOSWasiMeasurePhase('fd_read', 'result_marshal', () => writeGuestUint32(nreadPtr, written) @@ -10833,23 +12378,14 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { // not the runner process's unrelated host stdin. OpenSSH duplicates stdin // before its poll/read loop, so splitting these paths loses pipe EOF. // https://man7.org/linux/man-pages/man2/dup.2.html - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const sidecarManagedProcess = SIDECAR_MANAGED_PROCESS; if (sidecarManagedProcess || KERNEL_STDIO_SYNC_RPC) { try { - const requestedLength = __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - return 0; - } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; - }); + const requestedLength = __agentOSWasiMeasurePhase( + 'fd_read', + 'iov_scan', + () => iovRequest.totalLength, + ); const nonblocking = ((delegatedWasiFdFlags.get(numericFd) ?? 0) & WASI_FDFLAGS_NONBLOCK) !== 0; const chunk = __agentOSWasiMeasurePhase('fd_read', 'kernel_stdin_read', () => @@ -10864,7 +12400,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { ); } const written = __agentOSWasiMeasurePhase('fd_read', 'guest_iov_write', () => - writeBytesToGuestIovs(iovs, iovsLen, chunk) + writeBytesToGuestIovs(iovs, iovsLen, chunk, iovRequest) ); return __agentOSWasiMeasurePhase('fd_read', 'result_marshal', () => writeGuestUint32(nreadPtr, written) @@ -10901,22 +12437,15 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { }; wasiImport.fd_readdir = (fd, bufPtr, bufLen, cookie, bufUsedPtr) => { + const bufferLength = Number(bufLen) >>> 0; + if (!guestRangesAreValid([bufPtr, bufferLength], [bufUsedPtr, 4])) { + return WASI_ERRNO_FAULT; + } const numericFd = Number(fd) >>> 0; const handle = lookupFdHandle(numericFd); if (handle?.kind === 'kernel-fd') { - if (!(instanceMemory instanceof WebAssembly.Memory)) { - return WASI_ERRNO_FAULT; - } - const bufferOffset = Number(bufPtr) >>> 0; - const bufferLength = Number(bufLen) >>> 0; const memoryBytes = new Uint8Array(instanceMemory.buffer); - if ( - bufferOffset > memoryBytes.length || - bufferLength > memoryBytes.length - bufferOffset - ) { - return WASI_ERRNO_FAULT; - } const zeroResult = writeGuestUint32(bufUsedPtr, 0); if (zeroResult !== WASI_ERRNO_SUCCESS || bufferLength === 0) { return zeroResult; @@ -10985,36 +12514,39 @@ wasiImport.fd_readdir = (fd, bufPtr, bufLen, cookie, bufUsedPtr) => { }; wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { + const iovRequest = validateGuestIovRequest(iovs, iovsLen); + if (iovRequest.errno !== WASI_ERRNO_SUCCESS) return iovRequest.errno; + if (!guestRangeIsValid(nreadPtr, 4)) return WASI_ERRNO_FAULT; const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { const requestedLength = boundedWasmSyncRpcReadLength( - guestIovByteLength(iovs, iovsLen), + guestIovByteLength(iovs, iovsLen, iovRequest), ); const bytes = Buffer.from(callSyncRpc('process.fd_pread', [ Number(handle.targetFd) >>> 0, requestedLength, BigInt(offset).toString(), ]) ?? []); - return writeGuestUint32(nreadPtr, writeBytesToGuestIovs(iovs, iovsLen, bytes)); + return writeGuestUint32( + nreadPtr, + writeBytesToGuestIovs(iovs, iovsLen, bytes, iovRequest), + ); } catch (error) { return mapHostProcessError(error); } } - if (handle?.kind === 'guest-file') { + // Managed guests must never fall through to Node-WASI's process-local file + // descriptors. All production positioned I/O is kernel descriptor I/O. + if (SIDECAR_MANAGED_PROCESS) return WASI_ERRNO_BADF; + if (!SIDECAR_MANAGED_PROCESS && handle?.kind === 'guest-file') { try { const requestedLength = boundedWasmSyncRpcReadLength( (() => { if (!(instanceMemory instanceof WebAssembly.Memory)) { return 0; } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; + return iovRequest.totalLength; })(), ); const buffer = Buffer.alloc(requestedLength); @@ -11025,7 +12557,12 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { requestedLength, Number(offset), ); - const written = writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + const written = writeBytesToGuestIovs( + iovs, + iovsLen, + buffer.subarray(0, bytesRead), + iovRequest, + ); return writeGuestUint32(nreadPtr, written); } catch { return WASI_ERRNO_FAULT; @@ -11033,20 +12570,14 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { } if (handle?.kind === 'passthrough') { - if (typeof handle.ioFd === 'number') { + if (!SIDECAR_MANAGED_PROCESS && typeof handle.ioFd === 'number') { try { const requestedLength = boundedWasmSyncRpcReadLength( (() => { if (!(instanceMemory instanceof WebAssembly.Memory)) { return 0; } - const view = new DataView(instanceMemory.buffer); - let total = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - total += view.getUint32(entryOffset + 4, true); - } - return total >>> 0; + return iovRequest.totalLength; })(), ); const buffer = Buffer.alloc(requestedLength); @@ -11057,7 +12588,12 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { requestedLength, Number(offset), ); - const written = writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + const written = writeBytesToGuestIovs( + iovs, + iovsLen, + buffer.subarray(0, bytesRead), + iovRequest, + ); return writeGuestUint32(nreadPtr, written); } catch (error) { return mapSyntheticFsError(error); @@ -11079,10 +12615,13 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { }; wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { + const iovRequest = validateGuestIovRequest(iovs, iovsLen); + if (iovRequest.errno !== WASI_ERRNO_SUCCESS) return iovRequest.errno; + if (!guestRangeIsValid(nwrittenPtr, 4)) return WASI_ERRNO_FAULT; const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { - const bytes = collectGuestIovBytes(iovs, iovsLen); + const bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest); const written = Number(callSyncRpc('process.fd_pwrite', [ Number(handle.targetFd) >>> 0, bytes, @@ -11096,12 +12635,15 @@ wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { return mapHostProcessError(error); } } - if (handle?.kind === 'guest-file') { + // Managed guests must never fall through to Node-WASI's process-local file + // descriptors. All production positioned I/O is kernel descriptor I/O. + if (SIDECAR_MANAGED_PROCESS) return WASI_ERRNO_BADF; + if (!SIDECAR_MANAGED_PROCESS && handle?.kind === 'guest-file') { if (handle.readOnly === true) { return WASI_ERRNO_ROFS; } try { - const bytes = collectGuestIovBytes(iovs, iovsLen); + const bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest); const written = fsModule.writeSync( handle.targetFd, bytes, @@ -11119,9 +12661,9 @@ wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { if (handle.readOnly === true) { return WASI_ERRNO_ROFS; } - if (typeof handle.ioFd === 'number') { + if (!SIDECAR_MANAGED_PROCESS && typeof handle.ioFd === 'number') { try { - const bytes = collectGuestIovBytes(iovs, iovsLen); + const bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest); const written = fsModule.writeSync( handle.ioFd, bytes, @@ -11132,7 +12674,6 @@ wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { // A positioned write can grow the file past a size remembered from a // prior truncate; drop the stale entry so fd_size/path_size fall // through to the authoritative fstat. - forgetHostFsSize(handle.guestPath); return writeGuestUint32(nwrittenPtr, written); } catch (error) { return mapSyntheticFsError(error); @@ -11208,6 +12749,7 @@ wasiImport.fd_datasync = (fd) => { }; wasiImport.fd_seek = (fd, offset, whence, newOffsetPtr) => { + if (!guestRangeIsValid(newOffsetPtr, 8)) return WASI_ERRNO_FAULT; const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { @@ -11264,6 +12806,7 @@ wasiImport.fd_seek = (fd, offset, whence, newOffsetPtr) => { }; wasiImport.fd_tell = (fd, offsetPtr) => { + if (!guestRangeIsValid(offsetPtr, 8)) return WASI_ERRNO_FAULT; const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { @@ -11304,13 +12847,34 @@ wasiImport.fd_tell = (fd, offsetPtr) => { }; wasiImport.fd_fdstat_get = (fd, statPtr) => { + if (!guestRangeIsValid(statPtr, 24)) return WASI_ERRNO_FAULT; + const handle = __agentOSWasiMeasurePhase('fd_fdstat_get', 'lookup_handle', () => + lookupFdHandle(fd) + ); + if (handle?.kind === 'kernel-fd') { + try { + const stat = callSyncRpc('process.fd_stat', [Number(handle.targetFd) >>> 0]); + const kernelFlags = Number(stat?.flags) >>> 0; + const wasiFlags = (kernelFlags & KERNEL_O_APPEND ? WASI_FDFLAGS_APPEND : 0) + | (kernelFlags & KERNEL_O_NONBLOCK ? WASI_FDFLAGS_NONBLOCK : 0); + return writeGuestFdstat( + statPtr, + Number(stat?.filetype) >>> 0, + wasiFlags, + BigInt(stat?.rightsBase ?? 0), + BigInt(stat?.rightsInheriting ?? 0), + ); + } catch (error) { + return mapHostProcessError(error); + } + } // Host-net sockets (curl/wget/git TLS transports): report a stream-socket // fdstat with the current O_NONBLOCK state so guest fcntl(F_GETFL) works. // Without this, fcntl-based non-blocking setup fails with EBADF and guests // that expect EAGAIN semantics (libcurl mid-upload reads) block forever. { const hostNetSocket = getHostNetSocket(fd); - if (hostNetSocket && !hostNetSocket.closed) { + if (!SIDECAR_MANAGED_PROCESS && hostNetSocket && !hostNetSocket.closed) { return writeGuestFdstat( statPtr, WASI_FILETYPE_SOCKET_STREAM, @@ -11324,34 +12888,13 @@ wasiImport.fd_fdstat_get = (fd, statPtr) => { ); } } - const handle = __agentOSWasiMeasurePhase('fd_fdstat_get', 'lookup_handle', () => - lookupFdHandle(fd) - ); - if (handle?.kind === 'kernel-fd') { - try { - const stat = callSyncRpc('process.fd_stat', [Number(handle.targetFd) >>> 0]); - const kernelFlags = Number(stat?.flags) >>> 0; - const wasiFlags = (kernelFlags & KERNEL_O_APPEND ? WASI_FDFLAGS_APPEND : 0) - | (kernelFlags & KERNEL_O_NONBLOCK ? WASI_FDFLAGS_NONBLOCK : 0); - return writeGuestFdstat( - statPtr, - Number(stat?.filetype) >>> 0, - wasiFlags, - kernelFdRightsBase(kernelFlags), - 0n, - ); - } catch (error) { - return mapHostProcessError(error); - } - } // Kernel-PTY stdio must report CHARACTER_DEVICE so guest is_terminal()/ // isatty() see the TTY (the runner-process fds behind the delegate are // pipes). Resolve dup'd passthrough handles to their target fd first. { const stdioFd = handle?.kind === 'passthrough' ? Number(handle.targetFd) >>> 0 : Number(fd) >>> 0; - if ((handle == null || handle.kind === 'passthrough') && stdioFd <= 2 && - stdioFdIsKernelTty(stdioFd)) { + if ((handle == null || handle.kind === 'passthrough') && stdioFdIsKernelTty(stdioFd)) { return __agentOSWasiMeasurePhase( 'fd_fdstat_get', 'marshal_fdstat', @@ -11466,16 +13009,6 @@ wasiImport.fd_fdstat_get = (fd, statPtr) => { }; wasiImport.fd_fdstat_set_flags = (fd, flags) => { - // Host-net sockets: honor O_NONBLOCK (guest fcntl F_SETFL). net_recv/net_send - // consult `socket.nonblock` to return EAGAIN instead of blocking, which - // non-blocking clients like libcurl rely on to interleave send/recv. - { - const hostNetSocket = getHostNetSocket(fd); - if (hostNetSocket && !hostNetSocket.closed) { - hostNetSocket.nonblock = (Number(flags) & WASI_FDFLAGS_NONBLOCK) !== 0; - return WASI_ERRNO_SUCCESS; - } - } const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { @@ -11483,11 +13016,25 @@ wasiImport.fd_fdstat_set_flags = (fd, flags) => { const kernelFlags = (wasiFlags & WASI_FDFLAGS_APPEND ? KERNEL_O_APPEND : 0) | (wasiFlags & WASI_FDFLAGS_NONBLOCK ? KERNEL_O_NONBLOCK : 0); callSyncRpc('process.fd_set_flags', [Number(handle.targetFd) >>> 0, kernelFlags]); + const hostNetSocket = getHostNetSocket(fd); + if (hostNetSocket && !hostNetSocket.closed) { + hostNetSocket.nonblock = (wasiFlags & WASI_FDFLAGS_NONBLOCK) !== 0; + } return WASI_ERRNO_SUCCESS; } catch (error) { return mapHostProcessError(error); } } + // Host-net sockets: honor O_NONBLOCK (guest fcntl F_SETFL). net_recv/net_send + // consult `socket.nonblock` to return EAGAIN instead of blocking, which + // non-blocking clients like libcurl rely on to interleave send/recv. + { + const hostNetSocket = getHostNetSocket(fd); + if (!SIDECAR_MANAGED_PROCESS && hostNetSocket && !hostNetSocket.closed) { + hostNetSocket.nonblock = (Number(flags) & WASI_FDFLAGS_NONBLOCK) !== 0; + return WASI_ERRNO_SUCCESS; + } + } if (handle && handle.kind !== 'passthrough') { return WASI_ERRNO_BADF; } @@ -11516,6 +13063,7 @@ wasiImport.fd_fdstat_set_flags = (fd, flags) => { }; wasiImport.fd_filestat_get = (fd, statPtr) => { + if (!guestRangeIsValid(statPtr, 64)) return WASI_ERRNO_FAULT; const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { @@ -11593,7 +13141,6 @@ wasiImport.fd_filestat_set_size = (fd, size) => { if ((handle.position ?? 0) > nextSize) { handle.position = nextSize; } - rememberHostFsSize(handle.guestPath, nextSize); return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -11611,7 +13158,6 @@ wasiImport.fd_filestat_set_size = (fd, size) => { if ((handle.position ?? 0) > nextSize) { handle.position = nextSize; } - rememberHostFsSize(handle.guestPath, nextSize); return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -11626,7 +13172,6 @@ wasiImport.fd_filestat_set_size = (fd, size) => { if ((handle.position ?? 0) > nextSize) { handle.position = nextSize; } - rememberHostFsSize(handle.guestPath, nextSize); } finally { fsModule.closeSync(pathFd); } @@ -11650,7 +13195,28 @@ wasiImport.fd_filestat_set_size = (fd, size) => { }; wasiImport.fd_prestat_get = (fd, prestatPtr) => { + if (!guestRangeIsValid(prestatPtr, 8)) { + return WASI_ERRNO_FAULT; + } const handle = lookupFdHandle(fd); + if (SIDECAR_MANAGED_PROCESS) { + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + try { + const preopen = callSyncRpc('process.fd_preopen', [ + Number(handle.targetFd) >>> 0, + ]); + if (!preopen || typeof preopen.guestPath !== 'string') { + return WASI_ERRNO_BADF; + } + const view = new DataView(instanceMemory.buffer); + const offset = Number(prestatPtr) >>> 0; + view.setUint8(offset, 0); + view.setUint32(offset + 4, Buffer.byteLength(preopen.guestPath), true); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle && handle.kind !== 'passthrough') { return WASI_ERRNO_BADF; } @@ -11671,7 +13237,28 @@ wasiImport.fd_prestat_get = (fd, prestatPtr) => { }; wasiImport.fd_prestat_dir_name = (fd, pathPtr, pathLen) => { + const outputLength = Number(pathLen) >>> 0; + if (!guestRangeIsValid(pathPtr, outputLength)) { + return WASI_ERRNO_FAULT; + } const handle = lookupFdHandle(fd); + if (SIDECAR_MANAGED_PROCESS) { + if (handle?.kind !== 'kernel-fd') return WASI_ERRNO_BADF; + try { + const preopen = callSyncRpc('process.fd_preopen', [ + Number(handle.targetFd) >>> 0, + ]); + if (!preopen || typeof preopen.guestPath !== 'string') { + return WASI_ERRNO_BADF; + } + const bytes = Buffer.from(preopen.guestPath, 'utf8'); + if (outputLength < bytes.length) return WASI_ERRNO_NAMETOOLONG; + new Uint8Array(instanceMemory.buffer, Number(pathPtr), bytes.length).set(bytes); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle && handle.kind !== 'passthrough') { return WASI_ERRNO_BADF; } @@ -11717,11 +13304,66 @@ function writeKernelFdCooperatively(targetFd, bytes) { } } +function kernelFdStdioStream(targetFd, descriptorPath) { + if (descriptorPath === '/dev/stdout') return 'stdout'; + if (descriptorPath === '/dev/stderr') return 'stderr'; + + // PTY descriptions retain their /dev/pts/... path. Match a duplicated + // terminal descriptor against canonical fd 1/2 by the kernel's open-file- + // description identity; isatty alone would incorrectly classify an + // unrelated PTY opened by the guest as host stdio. + let isTty = false; + try { + isTty = callSyncRpc('__kernel_isatty', [targetFd]) === true; + } catch (error) { + // Anonymous kernel objects (notably socketpairs) have no terminal + // identity. Treat the kernel's "not a terminal/path-backed fd" answers as + // a negative probe; the descriptor remains a valid ordinary I/O target. + if ( + error?.code !== 'EINVAL' + && error?.code !== 'ENOTTY' + && error?.code !== 'ENOTSUP' + ) { + throw error; + } + } + if (!isTty) return null; + const targetDescription = String( + callSyncRpc('process.fd_description_identity', [targetFd])?.descriptionId ?? '', + ); + for (const [stdioFd, stream] of [[1, 'stdout'], [2, 'stderr']]) { + try { + const stdioDescription = String( + callSyncRpc('process.fd_description_identity', [stdioFd])?.descriptionId ?? '', + ); + if (targetDescription !== '' && targetDescription === stdioDescription) { + return stream; + } + } catch (error) { + if (error?.code !== 'EBADF') throw error; + } + } + return null; +} + wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { + const iovRequest = validateGuestIovRequest(iovs, iovsLen); + if (iovRequest.errno !== WASI_ERRNO_SUCCESS) { + return iovRequest.errno; + } + if (!guestRangeIsValid(nwrittenPtr, 4)) { + return WASI_ERRNO_FAULT; + } const numericFd = Number(fd) >>> 0; const hostNetSocket = getHostNetSocket(numericFd); if (hostNetSocket) { - return writeHostNetSocketFromGuestIovs(hostNetSocket, iovs, iovsLen, nwrittenPtr); + return writeHostNetSocketFromGuestIovs( + hostNetSocket, + iovs, + iovsLen, + nwrittenPtr, + iovRequest, + ); } const handle = __agentOSWasiMeasurePhase('fd_write', 'lookup_handle', () => @@ -11729,11 +13371,24 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { ); if (handle?.kind === 'kernel-fd') { try { - const bytes = collectGuestIovBytes(iovs, iovsLen); - const written = writeKernelFdCooperatively( - Number(handle.targetFd) >>> 0, - bytes, - ); + const bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest); + const kernelFd = Number(handle.targetFd) >>> 0; + // The kernel description, not the guest fd number, decides whether an + // alias still targets stdout/stderr. Keep the kernel as source of truth + // so dup/fcntl aliases and 1>&2/2>&1 redirections surface through the same + // ordered output event path as canonical fd 1/2. + let descriptorPath = null; + try { + descriptorPath = String(callSyncRpc('process.fd_path', [kernelFd])); + } catch (error) { + // Anonymous pipes and sockets have no pathname. They are valid write + // targets but cannot be canonical stdout/stderr path aliases. + if (error?.code !== 'EINVAL' && error?.code !== 'ENOTSUP') throw error; + } + const stdioStream = kernelFdStdioStream(kernelFd, descriptorPath); + const written = stdioStream != null + ? Number(callSyncRpc('__kernel_stdio_write', [kernelFd, bytes])) >>> 0 + : writeKernelFdCooperatively(kernelFd, bytes); if (!Number.isSafeInteger(written) || written < 0 || written > bytes.length) { return WASI_ERRNO_FAULT; } @@ -11745,7 +13400,7 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { if (handle?.kind === 'pipe-write') { try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => - collectGuestIovBytes(iovs, iovsLen) + collectGuestIovBytes(iovs, iovsLen, iovRequest) ); if (bytes.length > 0 && !pipeHasReaders(handle.pipe)) { return WASI_ERRNO_PIPE; @@ -11768,7 +13423,7 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { } try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => - collectGuestIovBytes(iovs, iovsLen) + collectGuestIovBytes(iovs, iovsLen, iovRequest) ); const written = __agentOSWasiMeasurePhase('fd_write', 'host_io', () => writeBytesToGuestFileHandle(handle, bytes) @@ -11789,11 +13444,9 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { if (passthroughStdioTarget != null) { try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => - collectGuestIovBytes(iovs, iovsLen) + collectGuestIovBytes(iovs, iovsLen, iovRequest) ); - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const sidecarManagedProcess = SIDECAR_MANAGED_PROCESS; if (sidecarManagedProcess || KERNEL_STDIO_SYNC_RPC) { const written = __agentOSWasiMeasurePhase('fd_write', 'sync_rpc', () => Number(callSyncRpc('__kernel_stdio_write', [passthroughStdioTarget, bytes])) >>> 0 @@ -11820,7 +13473,7 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { if (typeof handle.ioFd === 'number') { try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => - collectGuestIovBytes(iovs, iovsLen) + collectGuestIovBytes(iovs, iovsLen, iovRequest) ); const written = __agentOSWasiMeasurePhase('fd_write', 'host_io', () => writeBytesToGuestFileHandle({ ...handle, targetFd: handle.ioFd }, bytes) @@ -11830,10 +13483,6 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { } else { handle.position = (handle.position ?? 0) + written; } - // The write grew/changed the file; a size remembered from a prior - // truncate is now stale. Drop it so fd_size/path_size fall through to - // the authoritative fstat rather than reporting the old length. - forgetHostFsSize(handle.guestPath); return __agentOSWasiMeasurePhase('fd_write', 'result_marshal', () => writeGuestUint32(nwrittenPtr, written) ); @@ -11867,22 +13516,22 @@ wasiImport.fd_close = (fd) => { const numericFd = Number(fd) >>> 0; traceHostProcess('fd-close-begin', { fd: numericFd, - syntheticKind: syntheticFdEntries.get(numericFd)?.kind ?? null, + syntheticKind: activeFdProjections.get(numericFd)?.kind ?? null, passthroughKind: passthroughHandles.get(numericFd)?.kind ?? null, }); - if (hostNetSockets.has(numericFd)) { + if (hasHostNetSocket(numericFd)) { const result = __agentOSWasiMeasurePhase('fd_close', 'host_socket_close', () => hostNetImport.net_close(numericFd) ); // net_close consumes the runner fd even when a sidecar cleanup RPC fails, // matching close(2)'s no-retry rule. Never let a reused fd inherit stale // FD_CLOEXEC state from the consumed description. - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); return result; } try { if (__agentOSWasiMeasurePhase('fd_close', 'synthetic_close', () => closeSyntheticFd(fd))) { - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); traceHostProcess('fd-close-synthetic', { fd: Number(fd) >>> 0 }); return WASI_ERRNO_SUCCESS; } @@ -11900,7 +13549,7 @@ wasiImport.fd_close = (fd) => { targetFd: handle.targetFd ?? null, }); closePassthroughFd(fd); - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); return WASI_ERRNO_SUCCESS; } catch (error) { return mapHostProcessError(error); @@ -11912,7 +13561,7 @@ wasiImport.fd_close = (fd) => { targetFd: handle.targetFd ?? null, }); __agentOSWasiMeasurePhase('fd_close', 'fd_bookkeeping', () => closePassthroughFd(fd)); - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); return WASI_ERRNO_SUCCESS; } @@ -11924,17 +13573,17 @@ wasiImport.fd_close = (fd) => { return WASI_ERRNO_BADF; } - if (delegateManagedFdRefCounts.has(Number(fd) >>> 0)) { + if (standaloneDelegateFdRefCounts.has(Number(fd) >>> 0)) { const shouldDelegateClose = __agentOSWasiMeasurePhase('fd_close', 'fd_bookkeeping', () => releaseDelegateFd(fd) ); traceHostProcess('fd-close-delegate-tracked', { fd: Number(fd) >>> 0, shouldDelegateClose, - remainingRefs: delegateManagedFdRefCounts.get(Number(fd) >>> 0) ?? 0, + remainingRefs: standaloneDelegateFdRefCounts.get(Number(fd) >>> 0) ?? 0, }); if (!shouldDelegateClose) { - runnerCloexecFds.delete(numericFd); + standaloneCloexecFds.delete(numericFd); return WASI_ERRNO_SUCCESS; } passthroughHandles.delete(Number(fd) >>> 0); @@ -11946,7 +13595,7 @@ wasiImport.fd_close = (fd) => { delegateManagedFdClose(fd) ) : WASI_ERRNO_BADF; - if (result === WASI_ERRNO_SUCCESS) runnerCloexecFds.delete(numericFd); + if (result === WASI_ERRNO_SUCCESS) standaloneCloexecFds.delete(numericFd); return result; }; @@ -11958,12 +13607,48 @@ wasiImport.fd_renumber = (from, to) => { return WASI_ERRNO_BADF; } if (sourceFd === targetFd) { - return lookupFdHandle(sourceFd) || delegateManagedFdRefCounts.has(sourceFd) + return lookupFdHandle(sourceFd) || standaloneDelegateFdRefCounts.has(sourceFd) ? WASI_ERRNO_SUCCESS : WASI_ERRNO_BADF; } - const syntheticHandle = syntheticFdEntries.get(sourceFd); + const managedSource = lookupFdHandle(sourceFd); + if (SIDECAR_MANAGED_PROCESS && managedSource?.kind === 'kernel-fd') { + const sourceHostNetDescriptionId = managedSource.hostNetDescriptionId; + const managedTarget = lookupFdHandle(targetFd); + if (managedTarget && managedTarget.kind !== 'kernel-fd') { + return WASI_ERRNO_BADF; + } + const shadowsInternalPreopen = + managedTarget?.internalPreopen === true && hiddenPreopenHandles.has(targetFd); + const movedKernelFd = callSyncRpc('process.fd_move', [ + Number(managedSource.targetFd) >>> 0, + managedTarget?.kind === 'kernel-fd' && !shadowsInternalPreopen + ? Number(managedTarget.targetFd) >>> 0 + : null, + ]); + // The kernel mutation above is atomic from the guest's perspective. + // Only now may the runner replace its bounded identity projections. + if (managedTarget?.kind === 'kernel-fd' && !shadowsInternalPreopen) { + forgetSidecarClosedKernelFd(targetFd); + } + forgetSidecarClosedKernelFd(sourceFd); + const movedGuestFd = registerKernelDelegateFd( + movedKernelFd, + targetFd, + 3, + shadowsInternalPreopen, + ); + if (typeof sourceHostNetDescriptionId === 'string') { + const movedHandle = lookupFdHandle(movedGuestFd); + if (movedHandle?.kind === 'kernel-fd') { + movedHandle.hostNetDescriptionId = sourceHostNetDescriptionId; + } + } + return WASI_ERRNO_SUCCESS; + } + + const syntheticHandle = activeFdProjections.get(sourceFd); const passthroughHandle = passthroughHandles.get(sourceFd); const retainedSpawnOutputHandle = retainedSpawnOutputHandlesByFd.get(sourceFd); if (!syntheticHandle && !passthroughHandle && !retainedSpawnOutputHandle) { @@ -11976,10 +13661,10 @@ wasiImport.fd_renumber = (from, to) => { } if ( - syntheticFdEntries.has(targetFd) || + activeFdProjections.has(targetFd) || passthroughHandles.has(targetFd) || retainedSpawnOutputHandlesByFd.has(targetFd) || - delegateManagedFdRefCounts.has(targetFd) + standaloneDelegateFdRefCounts.has(targetFd) ) { const closeResult = wasiImport.fd_close(targetFd); if (closeResult !== WASI_ERRNO_SUCCESS) { @@ -11988,9 +13673,9 @@ wasiImport.fd_renumber = (from, to) => { } if (syntheticHandle) { - syntheticFdEntries.delete(sourceFd); + activeFdProjections.delete(sourceFd); syntheticHandle.displayFd = targetFd; - syntheticFdEntries.set(targetFd, syntheticHandle); + setActiveFdProjection(targetFd, syntheticHandle); } else if (passthroughHandle) { passthroughHandles.delete(sourceFd); passthroughHandle.displayFd = targetFd; @@ -12008,9 +13693,9 @@ wasiImport.fd_renumber = (from, to) => { closedPassthroughFds.add(sourceFd); closedPassthroughFds.delete(targetFd); - const sourceWasCloexec = runnerCloexecFds.delete(sourceFd); - runnerCloexecFds.delete(targetFd); - if (sourceWasCloexec) runnerCloexecFds.add(targetFd); + const sourceWasCloexec = standaloneCloexecFds.delete(sourceFd); + standaloneCloexecFds.delete(targetFd); + if (sourceWasCloexec) standaloneCloexecFds.add(targetFd); nextSyntheticFd = Math.max(nextSyntheticFd, targetFd + 1); traceHostProcess('fd-renumber', { @@ -12033,20 +13718,31 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { } const subscriptionCount = Number(nsubscriptions) >>> 0; + const countErrno = checkFixedRequestLimit( + 'wasm.abi.maxPollSubscriptions', + subscriptionCount, + WASI_POLL_MAX_SUBSCRIPTIONS, + ); + if (countErrno !== WASI_ERRNO_SUCCESS) return countErrno; + const subscriptionSize = 48; + const eventSize = 32; + if (!guestRangesAreValid( + [inPtr, subscriptionCount * subscriptionSize], + [outPtr, subscriptionCount * eventSize], + [neventsPtr, 4], + )) { + return WASI_ERRNO_FAULT; + } if (subscriptionCount === 0) { return writeGuestUint32(neventsPtr, 0); } - const subscriptionSize = 48; - const eventSize = 32; const view = new DataView(instanceMemory.buffer); const memory = new Uint8Array(instanceMemory.buffer); const subscriptions = []; let hasSyntheticSubscription = false; let hasRemappedPassthroughSubscription = false; - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const sidecarManagedProcess = SIDECAR_MANAGED_PROCESS; let timeoutMs = null; for (let index = 0; index < subscriptionCount; index += 1) { @@ -12320,6 +14016,41 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { return writeGuestUint32(neventsPtr, readyEvents.length); }; +// Immutable VM system identity is sidecar/kernel-owned. The complete guest +// destination is checked before the RPC so a bad pointer cannot trigger host +// work and then fail while publishing the result. +const hostSystemIdentityFields = [ + 'hostname', + 'type', + 'release', + 'version', + 'machine', + 'domainName', +]; +const hostSystemImport = { + get_identity(field, bufferPtr, bufferLength) { + const fieldIndex = Number(field) >>> 0; + const capacity = Number(bufferLength) >>> 0; + if (fieldIndex >= hostSystemIdentityFields.length) return WASI_ERRNO_INVAL; + if (!guestRangeIsValid(bufferPtr, capacity)) return WASI_ERRNO_FAULT; + if (capacity === 0) return WASI_ERRNO_NAMETOOLONG; + try { + const identity = callSyncRpc('process.system_identity', []); + const value = identity?.[hostSystemIdentityFields[fieldIndex]]; + if (typeof value !== 'string') return WASI_ERRNO_IO; + const bytes = Buffer.from(value, 'utf8'); + if (bytes.length + 1 > capacity) return WASI_ERRNO_NAMETOOLONG; + const output = new Uint8Array(instanceMemory.buffer); + const base = Number(bufferPtr) >>> 0; + output.set(bytes, base); + output[base + bytes.length] = 0; + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, +}; + // Terminal event source for crossterm-based guests (brush shell, reedline). // The patched crossterm WasiEventSource reads keystrokes through this import: // read(ptr, len, timeout_ms) -> usize @@ -12335,6 +14066,10 @@ const hostTtyImport = { read(ptr, len, timeoutMs) { const cap = Number(len) >>> 0; if (cap === 0) return 0; + // Validate the guest destination before asking the kernel for input. Once + // __kernel_stdin_read returns bytes they have been consumed from the PTY; + // discovering an invalid pointer afterwards would silently lose them. + if (!guestRangeIsValid(ptr, cap)) return 0; const blocking = (timeoutMs >>> 0) === 0xffffffff; const deadline = blocking ? Infinity : Date.now() + (Number(timeoutMs) >>> 0); while (true) { @@ -12358,19 +14093,99 @@ const hostTtyImport = { // `host_tty.isatty(fd)` -> 1 if the guest fd is a kernel PTY, else 0. isatty(fd) { const descriptor = Number(fd) >>> 0; - return descriptor <= 2 && stdioFdIsKernelTty(descriptor) ? 1 : 0; + return stdioFdIsKernelTty(descriptor) ? 1 : 0; }, // `host_tty.get_size(fd, colsPtr, rowsPtr)` -> writes the PTY window size as two // little-endian u16s and returns 0; non-zero (ENOTTY) if fd is not a PTY. get_size(fd, colsPtr, rowsPtr) { - const size = callSyncRpc('__kernel_tty_size', [fd >>> 0]); - if (!size || typeof size.cols !== 'number' || typeof size.rows !== 'number') { - return 25; // ENOTTY + if (!guestRangeIsValid(colsPtr, 2) || !guestRangeIsValid(rowsPtr, 2)) { + return WASI_ERRNO_FAULT; + } + try { + const kernelFd = canonicalKernelFdForSpawnAction(fd); + const size = callSyncRpc('__kernel_tty_size', [kernelFd]); + if (!size || typeof size.cols !== 'number' || typeof size.rows !== 'number') { + return 25; // ENOTTY + } + const view = new DataView(instanceMemory.buffer); + view.setUint16(colsPtr >>> 0, size.cols & 0xffff, true); + view.setUint16(rowsPtr >>> 0, size.rows & 0xffff, true); + return 0; + } catch (error) { + // host_tty is a libc/POSIX extension and returns native errno values, + // not Preview1 errno ordinals. + if (error?.code === 'EBADF') return 9; + if (error?.code === 'ENOTTY') return 25; + return 5; // EIO + } + }, + set_size(fd, cols, rows) { + try { + const numericCols = Number(cols) >>> 0; + const numericRows = Number(rows) >>> 0; + if (numericCols > 0xffff || numericRows > 0xffff) { + return WASI_ERRNO_INVAL; + } + const kernelFd = canonicalKernelFdForSpawnAction(fd); + callSyncRpc('__kernel_tty_set_size', [kernelFd, numericCols, numericRows]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, + get_attr(fd, flagsPtr, ccPtr) { + try { + if (!guestRangeIsValid(flagsPtr, 4) || !guestRangeIsValid(ccPtr, 7)) { + return WASI_ERRNO_FAULT; + } + const kernelFd = canonicalKernelFdForSpawnAction(fd); + const value = callSyncRpc('__kernel_tcgetattr', [kernelFd]); + const cc = Array.isArray(value?.cc) ? value.cc : null; + if (cc == null || cc.length !== 7) return WASI_ERRNO_FAULT; + writeGuestUint32(flagsPtr, Number(value?.flags) >>> 0); + new Uint8Array(instanceMemory.buffer).set(cc.map((byte) => Number(byte) & 0xff), ccPtr >>> 0); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, + set_attr(fd, flags, ccPtr) { + try { + if (!guestRangeIsValid(ccPtr, 7)) return WASI_ERRNO_FAULT; + const cc = Array.from(new Uint8Array(instanceMemory.buffer, ccPtr >>> 0, 7)); + const kernelFd = canonicalKernelFdForSpawnAction(fd); + callSyncRpc('__kernel_tcsetattr', [kernelFd, flags >>> 0, cc]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, + get_pgrp(fd, retPgidPtr) { + try { + if (!guestRangeIsValid(retPgidPtr, 4)) return WASI_ERRNO_FAULT; + const kernelFd = canonicalKernelFdForSpawnAction(fd); + return writeGuestUint32(retPgidPtr, callSyncRpc('__kernel_tcgetpgrp', [kernelFd])); + } catch (error) { + return mapHostProcessError(error); + } + }, + set_pgrp(fd, pgid) { + try { + const kernelFd = canonicalKernelFdForSpawnAction(fd); + callSyncRpc('__kernel_tcsetpgrp', [kernelFd, pgid >>> 0]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, + get_sid(fd, retSidPtr) { + try { + if (!guestRangeIsValid(retSidPtr, 4)) return WASI_ERRNO_FAULT; + const kernelFd = canonicalKernelFdForSpawnAction(fd); + return writeGuestUint32(retSidPtr, callSyncRpc('__kernel_tcgetsid', [kernelFd])); + } catch (error) { + return mapHostProcessError(error); } - const view = new DataView(instanceMemory.buffer); - view.setUint16(colsPtr >>> 0, size.cols & 0xffff, true); - view.setUint16(rowsPtr >>> 0, size.rows & 0xffff, true); - return 0; }, // Toggle terminal raw mode on the guest's PTY. crossterm/pty_probe/vim call this // instead of tcsetattr; route it to the kernel so the guest gets raw keystrokes. @@ -12409,6 +14224,7 @@ function instantiateWasmModule(targetModule) { return __agentOSWasmMeasurePhase('WebAssembly.Instance', () => new WebAssembly.Instance(targetModule, { wasi_snapshot_preview1: wasiImport, wasi_unstable: wasiImport, + host_system: hostSystemImport, host_tty: hostTtyImport, // Read-write commands like DuckDB need fd_dup_min from the patched // wasi-libc surface, but broader host_process capabilities stay @@ -12432,7 +14248,7 @@ if (instance.exports.memory instanceof WebAssembly.Memory) { } function initializeSignalMaskForInstance(targetInstance) { - const mask = encodeSignalMask(wasmBlockedSignals); + const mask = encodeSignalMask(currentKernelSignalMask()); if (mask.lo === 0 && mask.hi === 0) { return; } @@ -12446,140 +14262,63 @@ function initializeSignalMaskForInstance(targetInstance) { } initializeSignalMaskForInstance(instance); -for (const signal of initialWasmSignalIgnores) { - callSyncRpc('process.signal_state', [signal, 'ignore', '[]', 0]); - wasmSignalRegistrations.set(signal, { - action: 'ignore', - mask: [], - flags: 0, - }); -} -function dispatchWasmSignal(signal) { - const numeric = Number(signal) | 0; +function dispatchWasmSignal(delivery) { + const numeric = Number(delivery?.signal) | 0; + const token = Number(delivery?.token); if (numeric <= 0) { return false; } - const registration = wasmSignalRegistrations.get(numeric); - if (registration?.action === 'ignore') { - return false; - } - if (registration?.action !== 'user') { - // The libc trampoline dispatches user handlers only. Default dispositions - // remain sidecar-owned so fatal signals terminate the VM and non-fatal - // defaults (SIGCHLD/SIGCONT/...) follow the kernel signal table. - callSyncRpc('process.kill', [VIRTUAL_PID, signalNameFromNumber(numeric)]); - return false; - } if (typeof instance?.exports?.__wasi_signal_trampoline !== 'function') { - return false; - } - const previousMask = new Set(wasmBlockedSignals); - if (registration?.action === 'user') { - for (const maskedSignal of registration.mask) { - if (maskedSignal !== LINUX_SIGKILL && maskedSignal !== LINUX_SIGSTOP) { - wasmBlockedSignals.add(maskedSignal); - } - } - if ((registration.flags & LINUX_SA_NODEFER) === 0) { - wasmBlockedSignals.add(numeric); - } - if ((registration.flags & LINUX_SA_RESETHAND) !== 0) { - wasmSignalRegistrations.delete(numeric); - callSyncRpc('process.signal_state', [numeric, 'default', '[]', 0]); + if (Number.isSafeInteger(token)) { + callSyncRpc('process.signal_end', [token]); } + return false; } - let caught = false; try { instance.exports.__wasi_signal_trampoline(numeric); - caught = true; + return true; } finally { - wasmBlockedSignals.clear(); - for (const blockedSignal of previousMask) { - wasmBlockedSignals.add(blockedSignal); + if (Number.isSafeInteger(token)) { + callSyncRpc('process.signal_end', [token]); } - caught = dispatchLocallyPendingWasmSignals() || caught; } - return caught; } -function dispatchLocallyPendingWasmSignals() { +// Returns true when the current syscall must observe EINTR. For the documented +// restartable family (blocking fd/socket read/write, accept/connect, waitpid, +// flock, and record locks), a batch whose every caught handler has SA_RESTART +// completes its handler checkpoints and lets the operation continue. +function dispatchPendingWasmSignals(restartableOperation = false) { let caught = false; - for (const signal of [...pendingWasmSignals]) { - // A nested handler may drain another member of this snapshot. Do not - // dispatch that stale snapshot entry a second time. - if (!pendingWasmSignals.has(signal)) { - continue; - } - if (wasmBlockedSignals.has(signal)) { - continue; - } - pendingWasmSignals.delete(signal); - caught = dispatchWasmSignal(signal) || caught; - } - return caught; -} - -function dispatchPendingWasmSignals() { - let caught = dispatchLocallyPendingWasmSignals(); - // Standard signals coalesce, so at most one pending instance of each of the - // 64 supported signals can be transferred from the sidecar per boundary. + let everyCaughtHandlerRestarts = true; + // Standard signals coalesce in the kernel, so 64 iterations are a strict + // bound even when handlers recursively make more signals deliverable. for (let index = 0; index < 64; index += 1) { - let signal; + let delivery; try { - signal = callSyncRpc('process.take_signal', []); + delivery = callSyncRpc('process.take_signal', []); } catch (error) { if (error?.code === 'ERR_AGENTOS_WASM_SYNC_RPC_UNAVAILABLE') { - return caught; + return caught && !(restartableOperation && everyCaughtHandlerRestarts); } throw error; } - if (typeof signal !== 'number') { - return caught; + if (delivery == null || typeof delivery?.signal !== 'number') { + return caught && !(restartableOperation && everyCaughtHandlerRestarts); } - if (wasmBlockedSignals.has(signal)) { - pendingWasmSignals.add(signal); - } else { - caught = dispatchWasmSignal(signal) || caught; - } - } - return caught; -} - -function resetCaughtWasmSignalDispositionsForExec(sidecarCommitted) { - for (const [signal, registration] of wasmSignalRegistrations) { - if (registration.action !== 'user') { - continue; - } - if (!sidecarCommitted) { - try { - callSyncRpc('process.signal_state', [signal, 'default', '[]', 0]); - } catch (error) { - if (typeof process?.stderr?.write === 'function') { - process.stderr.write( - `[agentos] exec committed locally but failed to reset signal ${signal}: ${ - error instanceof Error ? error.message : String(error) - }\n`, - ); - } - } + if ((Number(delivery?.flags) & LINUX_SA_RESTART) === 0) { + everyCaughtHandlerRestarts = false; } - wasmSignalRegistrations.delete(signal); + caught = dispatchWasmSignal(delivery) || caught; } + return caught && !(restartableOperation && everyCaughtHandlerRestarts); } Object.defineProperty(globalThis, '__secureExecWasmSignalDispatch', { configurable: true, writable: true, - value: (_eventType, payload) => { - const signal = - typeof payload?.number === 'number' - ? payload.number - : signalNumberFromName(payload?.signal); - if (signal > 0 && signal <= LINUX_MAX_SIGNAL_NUMBER) { - pendingWasmSignals.add(signal); - } - }, + value: () => dispatchPendingWasmSignals(), }); while (typeof instance.exports._start === 'function') { @@ -12615,7 +14354,6 @@ while (typeof instance.exports._start === 'function') { ); } } - resetCaughtWasmSignalDispositionsForExec(error.image.sidecarCommitted === true); guestArgv = error.image.argv; guestEnv = error.image.env; wasi.args = guestArgv.map((value) => String(value)); diff --git a/crates/execution/assets/wasi-preview1-imports.json b/crates/execution/assets/wasi-preview1-imports.json deleted file mode 100644 index c37d36c801..0000000000 --- a/crates/execution/assets/wasi-preview1-imports.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - "args_get", - "args_sizes_get", - "clock_res_get", - "clock_time_get", - "environ_get", - "environ_sizes_get", - "fd_close", - "fd_datasync", - "fd_fdstat_get", - "fd_fdstat_set_flags", - "fd_filestat_get", - "fd_filestat_set_size", - "fd_pread", - "fd_prestat_dir_name", - "fd_prestat_get", - "fd_pwrite", - "fd_read", - "fd_readdir", - "fd_seek", - "fd_sync", - "fd_tell", - "fd_write", - "path_create_directory", - "path_filestat_get", - "path_link", - "path_open", - "path_readlink", - "path_remove_directory", - "path_rename", - "path_symlink", - "path_unlink_file", - "poll_oneoff", - "proc_exit", - "random_get", - "sched_yield" -] diff --git a/crates/execution/src/abi/generated.rs b/crates/execution/src/abi/generated.rs new file mode 100644 index 0000000000..df8235b589 --- /dev/null +++ b/crates/execution/src/abi/generated.rs @@ -0,0 +1,3938 @@ +// @generated by scripts/generate-wasm-abi-manifest.mjs; do not edit. +// Source: crates/execution/assets/agentos-wasm-abi.json (schema 2). + +#![allow(clippy::too_many_lines)] + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CoreValueType { + I32, + I64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum CoreSignatureId { + I32I32I32I32I32I64I64I32I32ToI32, + I32I32I32I32I64I64I32ToI32, + I32I32I32I32I64ToI32, + I32I32I32I64I32ToI32, + I32I32I32I64I64I32I32I32I32ToI32, + I32I32I64I64I32I32I32I32ToI32, + I32I64I32I32ToI32, + I32I64I32ToI32, + I32I64I64I32I32ToI32, + I32I64I64I32ToI32, + I32I64I64ToI32, + I32I64ToI32, + I32ToI32, + I32ToI64, + I32ToNoResults, + I32x10ToI32, + I32x12ToI32, + I32x17ToI32, + I32x21ToI32, + I32x2ToI32, + I32x3ToI32, + I32x4ToI32, + I32x4ToI64, + I32x5ToI32, + I32x6ToI32, + I32x7ToI32, + I32x8ToI32, + I32x9ToI32, + NoParamsToI32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum ImportId { + HostFsChmod, + HostFsChown, + HostFsFchmod, + HostFsFchown, + HostFsFdBlocks, + HostFsFdChown, + HostFsFdCollapseRange, + HostFsFdFiemap, + HostFsFdGetxattr, + HostFsFdInsertRange, + HostFsFdLink, + HostFsFdListxattr, + HostFsFdMode, + HostFsFdOwner, + HostFsFdPunchHole, + HostFsFdRemovexattr, + HostFsFdSetxattr, + HostFsFdSize, + HostFsFdZeroRange, + HostFsFtruncate, + HostFsOpenTmpfile, + HostFsPathAccess, + HostFsPathBlocks, + HostFsPathChown, + HostFsPathGetxattr, + HostFsPathListxattr, + HostFsPathMknod, + HostFsPathMode, + HostFsPathOwner, + HostFsPathRdev, + HostFsPathRemovexattr, + HostFsPathRenameat2, + HostFsPathSetxattr, + HostFsPathSize, + HostFsPathStatfs, + HostFsRemount, + HostFsSetOpenDirect, + HostFsSetOpenMode, + HostNetNetAccept, + HostNetNetBind, + HostNetNetClose, + HostNetNetConnect, + HostNetNetDnsQueryRrV1, + HostNetNetGetaddrinfo, + HostNetNetGetpeername, + HostNetNetGetsockname, + HostNetNetGetsockopt, + HostNetNetListen, + HostNetNetPoll, + HostNetNetRecv, + HostNetNetRecvfrom, + HostNetNetSend, + HostNetNetSendto, + HostNetNetSetNonblock, + HostNetNetSetsockopt, + HostNetNetSocket, + HostNetNetTlsConnect, + HostNetNetValidateAccept, + HostNetNetValidateSocket, + HostProcessFdDup, + HostProcessFdDupMin, + HostProcessFdDup2, + HostProcessFdFlock, + HostProcessFdGetfd, + HostProcessFdPipe, + HostProcessFdRecordLock, + HostProcessFdRecvmsgRights, + HostProcessFdSendmsgRights, + HostProcessFdSetfd, + HostProcessFdSocketpair, + HostProcessProcClosefrom, + HostProcessProcExec, + HostProcessProcFexec, + HostProcessProcGetpgid, + HostProcessProcGetpid, + HostProcessProcGetppid, + HostProcessProcGetrlimit, + HostProcessProcItimerReal, + HostProcessProcKill, + HostProcessProcPpollV1, + HostProcessProcSetpgid, + HostProcessProcSetrlimit, + HostProcessProcSigaction, + HostProcessProcSignalMaskV2, + HostProcessProcSpawn, + HostProcessProcSpawnV2, + HostProcessProcSpawnV3, + HostProcessProcSpawnV4, + HostProcessProcUmask, + HostProcessProcWaitpid, + HostProcessProcWaitpidV2, + HostProcessProcWaitpidV3, + HostProcessPtyOpen, + HostProcessSleepMs, + HostProcessUmask, + HostSystemGetIdentity, + HostTtyGetAttr, + HostTtyGetPgrp, + HostTtyGetSid, + HostTtyGetSize, + HostTtyIsatty, + HostTtyRead, + HostTtySetAttr, + HostTtySetPgrp, + HostTtySetRawMode, + HostTtySetSize, + HostUserGetegid, + HostUserGeteuid, + HostUserGetgid, + HostUserGetgrent, + HostUserGetgrgid, + HostUserGetgrnam, + HostUserGetgroups, + HostUserGetpwent, + HostUserGetpwnam, + HostUserGetpwuid, + HostUserGetresgid, + HostUserGetresuid, + HostUserGetuid, + HostUserIsatty, + HostUserSetegid, + HostUserSeteuid, + HostUserSetgid, + HostUserSetgroups, + HostUserSetregid, + HostUserSetresgid, + HostUserSetresuid, + HostUserSetreuid, + HostUserSetuid, + WasiSnapshotPreview1ArgsGet, + WasiSnapshotPreview1ArgsSizesGet, + WasiSnapshotPreview1ClockResGet, + WasiSnapshotPreview1ClockTimeGet, + WasiSnapshotPreview1EnvironGet, + WasiSnapshotPreview1EnvironSizesGet, + WasiSnapshotPreview1FdAllocate, + WasiSnapshotPreview1FdClose, + WasiSnapshotPreview1FdDatasync, + WasiSnapshotPreview1FdFdstatGet, + WasiSnapshotPreview1FdFdstatSetFlags, + WasiSnapshotPreview1FdFilestatGet, + WasiSnapshotPreview1FdFilestatSetSize, + WasiSnapshotPreview1FdFilestatSetTimes, + WasiSnapshotPreview1FdPread, + WasiSnapshotPreview1FdPrestatDirName, + WasiSnapshotPreview1FdPrestatGet, + WasiSnapshotPreview1FdPwrite, + WasiSnapshotPreview1FdRead, + WasiSnapshotPreview1FdReaddir, + WasiSnapshotPreview1FdRenumber, + WasiSnapshotPreview1FdSeek, + WasiSnapshotPreview1FdSync, + WasiSnapshotPreview1FdTell, + WasiSnapshotPreview1FdWrite, + WasiSnapshotPreview1PathCreateDirectory, + WasiSnapshotPreview1PathFilestatGet, + WasiSnapshotPreview1PathFilestatSetTimes, + WasiSnapshotPreview1PathLink, + WasiSnapshotPreview1PathOpen, + WasiSnapshotPreview1PathReadlink, + WasiSnapshotPreview1PathRemoveDirectory, + WasiSnapshotPreview1PathRename, + WasiSnapshotPreview1PathSymlink, + WasiSnapshotPreview1PathUnlinkFile, + WasiSnapshotPreview1PollOneoff, + WasiSnapshotPreview1ProcExit, + WasiSnapshotPreview1RandomGet, + WasiSnapshotPreview1SchedYield, + WasiSnapshotPreview1SockShutdown, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum HandlerId { + AccountGroup, + AccountPassword, + ClockSnapshot, + DescriptorClose, + DescriptorDuplicate, + DescriptorFlags, + DescriptorLock, + DescriptorMetadata, + DescriptorRead, + DescriptorRights, + DescriptorSeek, + DescriptorSetLength, + DescriptorStatusFlags, + DescriptorSync, + DescriptorWrite, + DescriptorXattr, + ExtentRange, + HostFsFdFiemap, + HostFsFdLink, + HostFsOpenTmpfile, + HostFsPathAccess, + HostFsPathMknod, + HostFsPathStatfs, + HostFsRemount, + HostFsSetOpenDirect, + HostFsSetOpenMode, + HostNetNetAccept, + HostNetNetBind, + HostNetNetConnect, + HostNetNetDnsQueryRrV1, + HostNetNetGetaddrinfo, + HostNetNetListen, + HostNetNetSocket, + HostNetNetTlsConnect, + HostProcessFdPipe, + HostProcessFdSocketpair, + HostProcessProcClosefrom, + HostProcessProcGetpid, + HostProcessProcGetppid, + HostProcessProcGetrlimit, + HostProcessProcItimerReal, + HostProcessProcKill, + HostProcessProcSetrlimit, + HostProcessProcSigaction, + HostProcessProcSignalMaskV2, + HostProcessPtyOpen, + HostProcessSleepMs, + HostSystemGetIdentity, + HostTtyGetSid, + HostTtyRead, + HostTtySetRawMode, + HostUserGetgroups, + HostUserSetgroups, + IdentityCredentials, + IdentitySnapshot, + MetadataMode, + MetadataOwnership, + MetadataSetTimes, + NetworkAddress, + NetworkOption, + NetworkReceive, + NetworkSend, + NetworkValidate, + PathMetadata, + PathRemove, + PathRename, + PathXattr, + Preopen, + ProcessArguments, + ProcessEnvironment, + ProcessExec, + ProcessGroup, + ProcessPoll, + ProcessSpawn, + ProcessUmask, + ProcessWait, + TerminalAttributes, + TerminalIsatty, + TerminalProcessGroup, + TerminalSize, + WasiSnapshotPreview1FdReaddir, + WasiSnapshotPreview1FdRenumber, + WasiSnapshotPreview1PathCreateDirectory, + WasiSnapshotPreview1PathLink, + WasiSnapshotPreview1PathOpen, + WasiSnapshotPreview1PathReadlink, + WasiSnapshotPreview1PathSymlink, + WasiSnapshotPreview1ProcExit, + WasiSnapshotPreview1RandomGet, + WasiSnapshotPreview1SchedYield, + WasiSnapshotPreview1SockShutdown, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum DecodeId { + AccountById, + AccountByIndex, + AccountByName, + DescriptorOwnership, + Fd, + FdSetLength, + HostFsChmod, + HostFsFchmod, + HostFsFdBlocks, + HostFsFdCollapseRange, + HostFsFdFiemap, + HostFsFdGetxattr, + HostFsFdInsertRange, + HostFsFdLink, + HostFsFdListxattr, + HostFsFdMode, + HostFsFdOwner, + HostFsFdPunchHole, + HostFsFdRemovexattr, + HostFsFdSetxattr, + HostFsFdSize, + HostFsFdZeroRange, + HostFsOpenTmpfile, + HostFsPathAccess, + HostFsPathBlocks, + HostFsPathGetxattr, + HostFsPathListxattr, + HostFsPathMknod, + HostFsPathMode, + HostFsPathOwner, + HostFsPathRdev, + HostFsPathRemovexattr, + HostFsPathRenameat2, + HostFsPathSetxattr, + HostFsPathSize, + HostFsPathStatfs, + HostFsRemount, + HostFsSetOpenDirect, + HostFsSetOpenMode, + HostNetNetAccept, + HostNetNetBind, + HostNetNetConnect, + HostNetNetDnsQueryRrV1, + HostNetNetGetaddrinfo, + HostNetNetGetsockopt, + HostNetNetListen, + HostNetNetPoll, + HostNetNetRecv, + HostNetNetRecvfrom, + HostNetNetSend, + HostNetNetSendto, + HostNetNetSetNonblock, + HostNetNetSetsockopt, + HostNetNetSocket, + HostNetNetTlsConnect, + HostProcessFdDup, + HostProcessFdDup2, + HostProcessFdDupMin, + HostProcessFdFlock, + HostProcessFdGetfd, + HostProcessFdPipe, + HostProcessFdRecordLock, + HostProcessFdRecvmsgRights, + HostProcessFdSendmsgRights, + HostProcessFdSetfd, + HostProcessFdSocketpair, + HostProcessProcClosefrom, + HostProcessProcExec, + HostProcessProcFexec, + HostProcessProcGetpgid, + HostProcessProcGetpid, + HostProcessProcGetppid, + HostProcessProcGetrlimit, + HostProcessProcItimerReal, + HostProcessProcKill, + HostProcessProcPpollV1, + HostProcessProcSetpgid, + HostProcessProcSetrlimit, + HostProcessProcSigaction, + HostProcessProcSignalMaskV2, + HostProcessProcSpawn, + HostProcessProcSpawnV2, + HostProcessProcSpawnV3, + HostProcessProcSpawnV4, + HostProcessProcUmask, + HostProcessProcWaitpid, + HostProcessProcWaitpidV2, + HostProcessProcWaitpidV3, + HostProcessPtyOpen, + HostProcessSleepMs, + HostProcessUmask, + HostSystemGetIdentity, + HostTtyGetAttr, + HostTtyGetSize, + HostTtyRead, + HostTtySetAttr, + HostTtySetPgrp, + HostTtySetRawMode, + HostTtySetSize, + HostUserGetgroups, + HostUserIsatty, + HostUserSetgroups, + IdentityScalarOutput, + IdentitySetOne, + IdentitySetThree, + IdentitySetTwo, + IdentityTripleOutput, + NetworkAddressOutput, + PathOwnership, + TerminalU32Output, + WasiSnapshotPreview1ArgsGet, + WasiSnapshotPreview1ArgsSizesGet, + WasiSnapshotPreview1ClockResGet, + WasiSnapshotPreview1ClockTimeGet, + WasiSnapshotPreview1EnvironGet, + WasiSnapshotPreview1EnvironSizesGet, + WasiSnapshotPreview1FdAllocate, + WasiSnapshotPreview1FdFdstatGet, + WasiSnapshotPreview1FdFdstatSetFlags, + WasiSnapshotPreview1FdFilestatGet, + WasiSnapshotPreview1FdFilestatSetTimes, + WasiSnapshotPreview1FdPread, + WasiSnapshotPreview1FdPrestatDirName, + WasiSnapshotPreview1FdPrestatGet, + WasiSnapshotPreview1FdPwrite, + WasiSnapshotPreview1FdRead, + WasiSnapshotPreview1FdReaddir, + WasiSnapshotPreview1FdRenumber, + WasiSnapshotPreview1FdSeek, + WasiSnapshotPreview1FdTell, + WasiSnapshotPreview1FdWrite, + WasiSnapshotPreview1PathCreateDirectory, + WasiSnapshotPreview1PathFilestatGet, + WasiSnapshotPreview1PathFilestatSetTimes, + WasiSnapshotPreview1PathLink, + WasiSnapshotPreview1PathOpen, + WasiSnapshotPreview1PathReadlink, + WasiSnapshotPreview1PathRemoveDirectory, + WasiSnapshotPreview1PathRename, + WasiSnapshotPreview1PathSymlink, + WasiSnapshotPreview1PathUnlinkFile, + WasiSnapshotPreview1PollOneoff, + WasiSnapshotPreview1ProcExit, + WasiSnapshotPreview1RandomGet, + WasiSnapshotPreview1SchedYield, + WasiSnapshotPreview1SockShutdown, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum EncodeId { + AccountRecordOutput, + DescriptorOffsetOutput, + DescriptorReadOutput, + DescriptorWriteOutput, + HostFsFdFiemapOutput, + HostFsFdGetxattrOutput, + HostFsFdListxattrOutput, + HostFsFdOwnerOutput, + HostFsOpenTmpfileOutput, + HostFsPathGetxattrOutput, + HostFsPathListxattrOutput, + HostFsPathOwnerOutput, + HostFsPathStatfsOutput, + HostNetNetAcceptOutput, + HostNetNetDnsQueryRrV1Output, + HostNetNetGetaddrinfoOutput, + HostNetNetGetsockoptOutput, + HostNetNetPollOutput, + HostNetNetRecvOutput, + HostNetNetRecvfromOutput, + HostNetNetSendOutput, + HostNetNetSendtoOutput, + HostNetNetSocketOutput, + HostProcessFdDupMinOutput, + HostProcessFdDupOutput, + HostProcessFdGetfdOutput, + HostProcessFdPipeOutput, + HostProcessFdRecordLockOutput, + HostProcessFdRecvmsgRightsOutput, + HostProcessFdSendmsgRightsOutput, + HostProcessFdSocketpairOutput, + HostProcessProcGetpgidOutput, + HostProcessProcGetpidOutput, + HostProcessProcGetppidOutput, + HostProcessProcGetrlimitOutput, + HostProcessProcItimerRealOutput, + HostProcessProcPpollV1Output, + HostProcessProcSignalMaskV2Output, + HostProcessProcUmaskOutput, + HostProcessProcWaitpidOutput, + HostProcessProcWaitpidV2Output, + HostProcessProcWaitpidV3Output, + HostProcessPtyOpenOutput, + HostProcessUmaskOutput, + HostSystemGetIdentityOutput, + HostTtyGetAttrOutput, + HostTtyGetSizeOutput, + HostTtyReadOutput, + HostUserGetgroupsOutput, + HostUserIsattyOutput, + IdentityScalarOutput, + IdentityTripleOutput, + NetworkAddressOutput, + ProcessIdOutput, + ScalarI32ZeroOnError, + ScalarI64MaxOnError, + ScalarI64ZeroOnError, + TerminalU32Output, + U64Output, + Void, + WasiErrno, + WasiSnapshotPreview1ArgsGetOutput, + WasiSnapshotPreview1ArgsSizesGetOutput, + WasiSnapshotPreview1EnvironGetOutput, + WasiSnapshotPreview1EnvironSizesGetOutput, + WasiSnapshotPreview1FdFdstatGetOutput, + WasiSnapshotPreview1FdFilestatGetOutput, + WasiSnapshotPreview1FdPrestatDirNameOutput, + WasiSnapshotPreview1FdPrestatGetOutput, + WasiSnapshotPreview1FdReaddirOutput, + WasiSnapshotPreview1PathFilestatGetOutput, + WasiSnapshotPreview1PathOpenOutput, + WasiSnapshotPreview1PathReadlinkOutput, + WasiSnapshotPreview1PollOneoffOutput, + WasiSnapshotPreview1RandomGetOutput, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ImportStatus { + Canonical, + Compatibility, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ReturnKind { + WasiErrno, + ScalarI32, + ScalarI64, + Void, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ExecutionClass { + Bootstrap, + Host, + Wait, + Local, + Terminal, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Restartability { + Never, + SignalRestartable, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PermissionTier { + Isolated, + ReadOnly, + ReadWrite, + Full, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct PermissionTiers(u8); + +impl PermissionTiers { + pub const fn from_bits(bits: u8) -> Self { + Self(bits) + } + + pub const fn bits(self) -> u8 { + self.0 + } + + pub const fn contains(self, tier: PermissionTier) -> bool { + let bit = match tier { + PermissionTier::Isolated => 1 << 0, + PermissionTier::ReadOnly => 1 << 1, + PermissionTier::ReadWrite => 1 << 2, + PermissionTier::Full => 1 << 3, + }; + self.0 & bit != 0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CoreSignature { + pub id: CoreSignatureId, + pub params: &'static [CoreValueType], + pub results: &'static [CoreValueType], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AbiBinding { + pub id: ImportId, + pub module: &'static str, + pub name: &'static str, + pub signature: CoreSignatureId, + pub status: ImportStatus, + pub handler: HandlerId, + pub decode: DecodeId, + pub encode: EncodeId, + pub return_kind: ReturnKind, + pub execution_class: ExecutionClass, + pub restartability: Restartability, + /// Submit the semantic action as one shared host operation; do not decompose it into adapter-side check/mutate steps. + pub transactional: bool, + /// Validate every guest output range before submitting the host operation. + pub prevalidate_outputs: bool, + pub permission_tiers: PermissionTiers, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AliasBinding { + pub alias_module: &'static str, + pub canonical_module: &'static str, + pub import: ImportId, + pub permission_tiers: PermissionTiers, +} + +pub const ABI_SCHEMA_VERSION: u32 = 2; +pub const ABI_VERSION: &str = "agentos-wasm-host-v1"; + +pub const CORE_SIGNATURES: &[CoreSignature] = &[ + CoreSignature { + id: CoreSignatureId::I32I32I32I32I32I64I64I32I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I32I32I32I64I64I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I32I32I32I64ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I32I32I64I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I32I32I64I64I32I32I32I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I32I64I64I32I32I32I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64I32I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64I32ToI32, + params: &[CoreValueType::I32, CoreValueType::I64, CoreValueType::I32], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64I64I32I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64I64I32ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I64, + CoreValueType::I64, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64I64ToI32, + params: &[CoreValueType::I32, CoreValueType::I64, CoreValueType::I64], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32I64ToI32, + params: &[CoreValueType::I32, CoreValueType::I64], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32ToI32, + params: &[CoreValueType::I32], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32ToI64, + params: &[CoreValueType::I32], + results: &[CoreValueType::I64], + }, + CoreSignature { + id: CoreSignatureId::I32ToNoResults, + params: &[CoreValueType::I32], + results: &[], + }, + CoreSignature { + id: CoreSignatureId::I32x10ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x12ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x17ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x21ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x2ToI32, + params: &[CoreValueType::I32, CoreValueType::I32], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x3ToI32, + params: &[CoreValueType::I32, CoreValueType::I32, CoreValueType::I32], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x4ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x4ToI64, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I64], + }, + CoreSignature { + id: CoreSignatureId::I32x5ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x6ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x7ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x8ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::I32x9ToI32, + params: &[ + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + CoreValueType::I32, + ], + results: &[CoreValueType::I32], + }, + CoreSignature { + id: CoreSignatureId::NoParamsToI32, + params: &[], + results: &[CoreValueType::I32], + }, +]; + +pub const ABI_BINDINGS: &[AbiBinding] = &[ + AbiBinding { + id: ImportId::HostFsChmod, + module: "host_fs", + name: "chmod", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::MetadataMode, + decode: DecodeId::HostFsChmod, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsChown, + module: "host_fs", + name: "chown", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::MetadataOwnership, + decode: DecodeId::PathOwnership, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFchmod, + module: "host_fs", + name: "fchmod", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::MetadataMode, + decode: DecodeId::HostFsFchmod, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFchown, + module: "host_fs", + name: "fchown", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::MetadataOwnership, + decode: DecodeId::DescriptorOwnership, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdBlocks, + module: "host_fs", + name: "fd_blocks", + signature: CoreSignatureId::I32ToI64, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorMetadata, + decode: DecodeId::HostFsFdBlocks, + encode: EncodeId::ScalarI64MaxOnError, + return_kind: ReturnKind::ScalarI64, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdChown, + module: "host_fs", + name: "fd_chown", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::MetadataOwnership, + decode: DecodeId::DescriptorOwnership, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdCollapseRange, + module: "host_fs", + name: "fd_collapse_range", + signature: CoreSignatureId::I32I64I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ExtentRange, + decode: DecodeId::HostFsFdCollapseRange, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdFiemap, + module: "host_fs", + name: "fd_fiemap", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsFdFiemap, + decode: DecodeId::HostFsFdFiemap, + encode: EncodeId::HostFsFdFiemapOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdGetxattr, + module: "host_fs", + name: "fd_getxattr", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorXattr, + decode: DecodeId::HostFsFdGetxattr, + encode: EncodeId::HostFsFdGetxattrOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdInsertRange, + module: "host_fs", + name: "fd_insert_range", + signature: CoreSignatureId::I32I64I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ExtentRange, + decode: DecodeId::HostFsFdInsertRange, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdLink, + module: "host_fs", + name: "fd_link", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsFdLink, + decode: DecodeId::HostFsFdLink, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdListxattr, + module: "host_fs", + name: "fd_listxattr", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorXattr, + decode: DecodeId::HostFsFdListxattr, + encode: EncodeId::HostFsFdListxattrOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdMode, + module: "host_fs", + name: "fd_mode", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorMetadata, + decode: DecodeId::HostFsFdMode, + encode: EncodeId::ScalarI32ZeroOnError, + return_kind: ReturnKind::ScalarI32, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdOwner, + module: "host_fs", + name: "fd_owner", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorMetadata, + decode: DecodeId::HostFsFdOwner, + encode: EncodeId::HostFsFdOwnerOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdPunchHole, + module: "host_fs", + name: "fd_punch_hole", + signature: CoreSignatureId::I32I64I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ExtentRange, + decode: DecodeId::HostFsFdPunchHole, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdRemovexattr, + module: "host_fs", + name: "fd_removexattr", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorXattr, + decode: DecodeId::HostFsFdRemovexattr, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdSetxattr, + module: "host_fs", + name: "fd_setxattr", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorXattr, + decode: DecodeId::HostFsFdSetxattr, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdSize, + module: "host_fs", + name: "fd_size", + signature: CoreSignatureId::I32ToI64, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorMetadata, + decode: DecodeId::HostFsFdSize, + encode: EncodeId::ScalarI64MaxOnError, + return_kind: ReturnKind::ScalarI64, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFdZeroRange, + module: "host_fs", + name: "fd_zero_range", + signature: CoreSignatureId::I32I64I64I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ExtentRange, + decode: DecodeId::HostFsFdZeroRange, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsFtruncate, + module: "host_fs", + name: "ftruncate", + signature: CoreSignatureId::I32I64ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::DescriptorSetLength, + decode: DecodeId::FdSetLength, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsOpenTmpfile, + module: "host_fs", + name: "open_tmpfile", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsOpenTmpfile, + decode: DecodeId::HostFsOpenTmpfile, + encode: EncodeId::HostFsOpenTmpfileOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathAccess, + module: "host_fs", + name: "path_access", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsPathAccess, + decode: DecodeId::HostFsPathAccess, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathBlocks, + module: "host_fs", + name: "path_blocks", + signature: CoreSignatureId::I32x4ToI64, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::HostFsPathBlocks, + encode: EncodeId::ScalarI64MaxOnError, + return_kind: ReturnKind::ScalarI64, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathChown, + module: "host_fs", + name: "path_chown", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::MetadataOwnership, + decode: DecodeId::PathOwnership, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathGetxattr, + module: "host_fs", + name: "path_getxattr", + signature: CoreSignatureId::I32x9ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathXattr, + decode: DecodeId::HostFsPathGetxattr, + encode: EncodeId::HostFsPathGetxattrOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathListxattr, + module: "host_fs", + name: "path_listxattr", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathXattr, + decode: DecodeId::HostFsPathListxattr, + encode: EncodeId::HostFsPathListxattrOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathMknod, + module: "host_fs", + name: "path_mknod", + signature: CoreSignatureId::I32I32I32I32I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsPathMknod, + decode: DecodeId::HostFsPathMknod, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathMode, + module: "host_fs", + name: "path_mode", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::HostFsPathMode, + encode: EncodeId::ScalarI32ZeroOnError, + return_kind: ReturnKind::ScalarI32, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathOwner, + module: "host_fs", + name: "path_owner", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::HostFsPathOwner, + encode: EncodeId::HostFsPathOwnerOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathRdev, + module: "host_fs", + name: "path_rdev", + signature: CoreSignatureId::I32x4ToI64, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::HostFsPathRdev, + encode: EncodeId::ScalarI64ZeroOnError, + return_kind: ReturnKind::ScalarI64, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathRemovexattr, + module: "host_fs", + name: "path_removexattr", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathXattr, + decode: DecodeId::HostFsPathRemovexattr, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathRenameat2, + module: "host_fs", + name: "path_renameat2", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathRename, + decode: DecodeId::HostFsPathRenameat2, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathSetxattr, + module: "host_fs", + name: "path_setxattr", + signature: CoreSignatureId::I32x9ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathXattr, + decode: DecodeId::HostFsPathSetxattr, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathSize, + module: "host_fs", + name: "path_size", + signature: CoreSignatureId::I32x4ToI64, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::HostFsPathSize, + encode: EncodeId::ScalarI64MaxOnError, + return_kind: ReturnKind::ScalarI64, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsPathStatfs, + module: "host_fs", + name: "path_statfs", + signature: CoreSignatureId::I32x8ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsPathStatfs, + decode: DecodeId::HostFsPathStatfs, + encode: EncodeId::HostFsPathStatfsOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsRemount, + module: "host_fs", + name: "remount", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostFsRemount, + decode: DecodeId::HostFsRemount, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsSetOpenDirect, + module: "host_fs", + name: "set_open_direct", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::HostFsSetOpenDirect, + decode: DecodeId::HostFsSetOpenDirect, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Local, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostFsSetOpenMode, + module: "host_fs", + name: "set_open_mode", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::HostFsSetOpenMode, + decode: DecodeId::HostFsSetOpenMode, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Local, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostNetNetAccept, + module: "host_net", + name: "net_accept", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetAccept, + decode: DecodeId::HostNetNetAccept, + encode: EncodeId::HostNetNetAcceptOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetBind, + module: "host_net", + name: "net_bind", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetBind, + decode: DecodeId::HostNetNetBind, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetClose, + module: "host_net", + name: "net_close", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::DescriptorClose, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetConnect, + module: "host_net", + name: "net_connect", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetConnect, + decode: DecodeId::HostNetNetConnect, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetDnsQueryRrV1, + module: "host_net", + name: "net_dns_query_rr_v1", + signature: CoreSignatureId::I32x8ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetDnsQueryRrV1, + decode: DecodeId::HostNetNetDnsQueryRrV1, + encode: EncodeId::HostNetNetDnsQueryRrV1Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetGetaddrinfo, + module: "host_net", + name: "net_getaddrinfo", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetGetaddrinfo, + decode: DecodeId::HostNetNetGetaddrinfo, + encode: EncodeId::HostNetNetGetaddrinfoOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetGetpeername, + module: "host_net", + name: "net_getpeername", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkAddress, + decode: DecodeId::NetworkAddressOutput, + encode: EncodeId::NetworkAddressOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetGetsockname, + module: "host_net", + name: "net_getsockname", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkAddress, + decode: DecodeId::NetworkAddressOutput, + encode: EncodeId::NetworkAddressOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetGetsockopt, + module: "host_net", + name: "net_getsockopt", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkOption, + decode: DecodeId::HostNetNetGetsockopt, + encode: EncodeId::HostNetNetGetsockoptOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetListen, + module: "host_net", + name: "net_listen", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetListen, + decode: DecodeId::HostNetNetListen, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetPoll, + module: "host_net", + name: "net_poll", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessPoll, + decode: DecodeId::HostNetNetPoll, + encode: EncodeId::HostNetNetPollOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetRecv, + module: "host_net", + name: "net_recv", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkReceive, + decode: DecodeId::HostNetNetRecv, + encode: EncodeId::HostNetNetRecvOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetRecvfrom, + module: "host_net", + name: "net_recvfrom", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkReceive, + decode: DecodeId::HostNetNetRecvfrom, + encode: EncodeId::HostNetNetRecvfromOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetSend, + module: "host_net", + name: "net_send", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkSend, + decode: DecodeId::HostNetNetSend, + encode: EncodeId::HostNetNetSendOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetSendto, + module: "host_net", + name: "net_sendto", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkSend, + decode: DecodeId::HostNetNetSendto, + encode: EncodeId::HostNetNetSendtoOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetSetNonblock, + module: "host_net", + name: "net_set_nonblock", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorStatusFlags, + decode: DecodeId::HostNetNetSetNonblock, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetSetsockopt, + module: "host_net", + name: "net_setsockopt", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::NetworkOption, + decode: DecodeId::HostNetNetSetsockopt, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetSocket, + module: "host_net", + name: "net_socket", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetSocket, + decode: DecodeId::HostNetNetSocket, + encode: EncodeId::HostNetNetSocketOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetTlsConnect, + module: "host_net", + name: "net_tls_connect", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostNetNetTlsConnect, + decode: DecodeId::HostNetNetTlsConnect, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetValidateAccept, + module: "host_net", + name: "net_validate_accept", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::NetworkValidate, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostNetNetValidateSocket, + module: "host_net", + name: "net_validate_socket", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::NetworkValidate, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdDup, + module: "host_process", + name: "fd_dup", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorDuplicate, + decode: DecodeId::HostProcessFdDup, + encode: EncodeId::HostProcessFdDupOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdDupMin, + module: "host_process", + name: "fd_dup_min", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorDuplicate, + decode: DecodeId::HostProcessFdDupMin, + encode: EncodeId::HostProcessFdDupMinOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessFdDup2, + module: "host_process", + name: "fd_dup2", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorDuplicate, + decode: DecodeId::HostProcessFdDup2, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdFlock, + module: "host_process", + name: "fd_flock", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorLock, + decode: DecodeId::HostProcessFdFlock, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessFdGetfd, + module: "host_process", + name: "fd_getfd", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorFlags, + decode: DecodeId::HostProcessFdGetfd, + encode: EncodeId::HostProcessFdGetfdOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessFdPipe, + module: "host_process", + name: "fd_pipe", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessFdPipe, + decode: DecodeId::HostProcessFdPipe, + encode: EncodeId::HostProcessFdPipeOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdRecordLock, + module: "host_process", + name: "fd_record_lock", + signature: CoreSignatureId::I32I32I32I64I64I32I32I32I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorLock, + decode: DecodeId::HostProcessFdRecordLock, + encode: EncodeId::HostProcessFdRecordLockOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessFdRecvmsgRights, + module: "host_process", + name: "fd_recvmsg_rights", + signature: CoreSignatureId::I32x9ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorRights, + decode: DecodeId::HostProcessFdRecvmsgRights, + encode: EncodeId::HostProcessFdRecvmsgRightsOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdSendmsgRights, + module: "host_process", + name: "fd_sendmsg_rights", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorRights, + decode: DecodeId::HostProcessFdSendmsgRights, + encode: EncodeId::HostProcessFdSendmsgRightsOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessFdSetfd, + module: "host_process", + name: "fd_setfd", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorFlags, + decode: DecodeId::HostProcessFdSetfd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessFdSocketpair, + module: "host_process", + name: "fd_socketpair", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessFdSocketpair, + decode: DecodeId::HostProcessFdSocketpair, + encode: EncodeId::HostProcessFdSocketpairOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcClosefrom, + module: "host_process", + name: "proc_closefrom", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcClosefrom, + decode: DecodeId::HostProcessProcClosefrom, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcExec, + module: "host_process", + name: "proc_exec", + signature: CoreSignatureId::I32x8ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessExec, + decode: DecodeId::HostProcessProcExec, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Terminal, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcFexec, + module: "host_process", + name: "proc_fexec", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessExec, + decode: DecodeId::HostProcessProcFexec, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Terminal, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcGetpgid, + module: "host_process", + name: "proc_getpgid", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessGroup, + decode: DecodeId::HostProcessProcGetpgid, + encode: EncodeId::HostProcessProcGetpgidOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcGetpid, + module: "host_process", + name: "proc_getpid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcGetpid, + decode: DecodeId::HostProcessProcGetpid, + encode: EncodeId::HostProcessProcGetpidOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcGetppid, + module: "host_process", + name: "proc_getppid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcGetppid, + decode: DecodeId::HostProcessProcGetppid, + encode: EncodeId::HostProcessProcGetppidOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcGetrlimit, + module: "host_process", + name: "proc_getrlimit", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcGetrlimit, + decode: DecodeId::HostProcessProcGetrlimit, + encode: EncodeId::HostProcessProcGetrlimitOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessProcItimerReal, + module: "host_process", + name: "proc_itimer_real", + signature: CoreSignatureId::I32I64I64I32I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcItimerReal, + decode: DecodeId::HostProcessProcItimerReal, + encode: EncodeId::HostProcessProcItimerRealOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcKill, + module: "host_process", + name: "proc_kill", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcKill, + decode: DecodeId::HostProcessProcKill, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcPpollV1, + module: "host_process", + name: "proc_ppoll_v1", + signature: CoreSignatureId::I32I32I64I64I32I32I32I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessPoll, + decode: DecodeId::HostProcessProcPpollV1, + encode: EncodeId::HostProcessProcPpollV1Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSetpgid, + module: "host_process", + name: "proc_setpgid", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessGroup, + decode: DecodeId::HostProcessProcSetpgid, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSetrlimit, + module: "host_process", + name: "proc_setrlimit", + signature: CoreSignatureId::I32I64I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcSetrlimit, + decode: DecodeId::HostProcessProcSetrlimit, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessProcSigaction, + module: "host_process", + name: "proc_sigaction", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcSigaction, + decode: DecodeId::HostProcessProcSigaction, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSignalMaskV2, + module: "host_process", + name: "proc_signal_mask_v2", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessProcSignalMaskV2, + decode: DecodeId::HostProcessProcSignalMaskV2, + encode: EncodeId::HostProcessProcSignalMaskV2Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSpawn, + module: "host_process", + name: "proc_spawn", + signature: CoreSignatureId::I32x10ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessSpawn, + decode: DecodeId::HostProcessProcSpawn, + encode: EncodeId::ProcessIdOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSpawnV2, + module: "host_process", + name: "proc_spawn_v2", + signature: CoreSignatureId::I32x12ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessSpawn, + decode: DecodeId::HostProcessProcSpawnV2, + encode: EncodeId::ProcessIdOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSpawnV3, + module: "host_process", + name: "proc_spawn_v3", + signature: CoreSignatureId::I32x17ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessSpawn, + decode: DecodeId::HostProcessProcSpawnV3, + encode: EncodeId::ProcessIdOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcSpawnV4, + module: "host_process", + name: "proc_spawn_v4", + signature: CoreSignatureId::I32x21ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessSpawn, + decode: DecodeId::HostProcessProcSpawnV4, + encode: EncodeId::ProcessIdOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcUmask, + module: "host_process", + name: "proc_umask", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessUmask, + decode: DecodeId::HostProcessProcUmask, + encode: EncodeId::HostProcessProcUmaskOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostProcessProcWaitpid, + module: "host_process", + name: "proc_waitpid", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessWait, + decode: DecodeId::HostProcessProcWaitpid, + encode: EncodeId::HostProcessProcWaitpidOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcWaitpidV2, + module: "host_process", + name: "proc_waitpid_v2", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessWait, + decode: DecodeId::HostProcessProcWaitpidV2, + encode: EncodeId::HostProcessProcWaitpidV2Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessProcWaitpidV3, + module: "host_process", + name: "proc_waitpid_v3", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessWait, + decode: DecodeId::HostProcessProcWaitpidV3, + encode: EncodeId::HostProcessProcWaitpidV3Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessPtyOpen, + module: "host_process", + name: "pty_open", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostProcessPtyOpen, + decode: DecodeId::HostProcessPtyOpen, + encode: EncodeId::HostProcessPtyOpenOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessSleepMs, + module: "host_process", + name: "sleep_ms", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::HostProcessSleepMs, + decode: DecodeId::HostProcessSleepMs, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(8), + }, + AbiBinding { + id: ImportId::HostProcessUmask, + module: "host_process", + name: "umask", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ProcessUmask, + decode: DecodeId::HostProcessUmask, + encode: EncodeId::HostProcessUmaskOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(14), + }, + AbiBinding { + id: ImportId::HostSystemGetIdentity, + module: "host_system", + name: "get_identity", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostSystemGetIdentity, + decode: DecodeId::HostSystemGetIdentity, + encode: EncodeId::HostSystemGetIdentityOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyGetAttr, + module: "host_tty", + name: "get_attr", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::TerminalAttributes, + decode: DecodeId::HostTtyGetAttr, + encode: EncodeId::HostTtyGetAttrOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyGetPgrp, + module: "host_tty", + name: "get_pgrp", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::TerminalProcessGroup, + decode: DecodeId::TerminalU32Output, + encode: EncodeId::TerminalU32Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyGetSid, + module: "host_tty", + name: "get_sid", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostTtyGetSid, + decode: DecodeId::TerminalU32Output, + encode: EncodeId::TerminalU32Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyGetSize, + module: "host_tty", + name: "get_size", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::TerminalSize, + decode: DecodeId::HostTtyGetSize, + encode: EncodeId::HostTtyGetSizeOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyIsatty, + module: "host_tty", + name: "isatty", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::TerminalIsatty, + decode: DecodeId::Fd, + encode: EncodeId::ScalarI32ZeroOnError, + return_kind: ReturnKind::ScalarI32, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtyRead, + module: "host_tty", + name: "read", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::HostTtyRead, + decode: DecodeId::HostTtyRead, + encode: EncodeId::HostTtyReadOutput, + return_kind: ReturnKind::ScalarI32, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtySetAttr, + module: "host_tty", + name: "set_attr", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::TerminalAttributes, + decode: DecodeId::HostTtySetAttr, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtySetPgrp, + module: "host_tty", + name: "set_pgrp", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::TerminalProcessGroup, + decode: DecodeId::HostTtySetPgrp, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtySetRawMode, + module: "host_tty", + name: "set_raw_mode", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::HostTtySetRawMode, + decode: DecodeId::HostTtySetRawMode, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostTtySetSize, + module: "host_tty", + name: "set_size", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::TerminalSize, + decode: DecodeId::HostTtySetSize, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetegid, + module: "host_user", + name: "getegid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityScalarOutput, + encode: EncodeId::IdentityScalarOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGeteuid, + module: "host_user", + name: "geteuid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityScalarOutput, + encode: EncodeId::IdentityScalarOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetgid, + module: "host_user", + name: "getgid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityScalarOutput, + encode: EncodeId::IdentityScalarOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetgrent, + module: "host_user", + name: "getgrent", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountGroup, + decode: DecodeId::AccountByIndex, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetgrgid, + module: "host_user", + name: "getgrgid", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountGroup, + decode: DecodeId::AccountById, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetgrnam, + module: "host_user", + name: "getgrnam", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountGroup, + decode: DecodeId::AccountByName, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetgroups, + module: "host_user", + name: "getgroups", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostUserGetgroups, + decode: DecodeId::HostUserGetgroups, + encode: EncodeId::HostUserGetgroupsOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetpwent, + module: "host_user", + name: "getpwent", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountPassword, + decode: DecodeId::AccountByIndex, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetpwnam, + module: "host_user", + name: "getpwnam", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountPassword, + decode: DecodeId::AccountByName, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetpwuid, + module: "host_user", + name: "getpwuid", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::AccountPassword, + decode: DecodeId::AccountById, + encode: EncodeId::AccountRecordOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetresgid, + module: "host_user", + name: "getresgid", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityTripleOutput, + encode: EncodeId::IdentityTripleOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetresuid, + module: "host_user", + name: "getresuid", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityTripleOutput, + encode: EncodeId::IdentityTripleOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserGetuid, + module: "host_user", + name: "getuid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentitySnapshot, + decode: DecodeId::IdentityScalarOutput, + encode: EncodeId::IdentityScalarOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserIsatty, + module: "host_user", + name: "isatty", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::TerminalIsatty, + decode: DecodeId::HostUserIsatty, + encode: EncodeId::HostUserIsattyOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetegid, + module: "host_user", + name: "setegid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetOne, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSeteuid, + module: "host_user", + name: "seteuid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetOne, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetgid, + module: "host_user", + name: "setgid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetOne, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetgroups, + module: "host_user", + name: "setgroups", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::HostUserSetgroups, + decode: DecodeId::HostUserSetgroups, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetregid, + module: "host_user", + name: "setregid", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetTwo, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetresgid, + module: "host_user", + name: "setresgid", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetThree, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetresuid, + module: "host_user", + name: "setresuid", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetThree, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetreuid, + module: "host_user", + name: "setreuid", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetTwo, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::HostUserSetuid, + module: "host_user", + name: "setuid", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::IdentityCredentials, + decode: DecodeId::IdentitySetOne, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1ArgsGet, + module: "wasi_snapshot_preview1", + name: "args_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessArguments, + decode: DecodeId::WasiSnapshotPreview1ArgsGet, + encode: EncodeId::WasiSnapshotPreview1ArgsGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1ArgsSizesGet, + module: "wasi_snapshot_preview1", + name: "args_sizes_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessArguments, + decode: DecodeId::WasiSnapshotPreview1ArgsSizesGet, + encode: EncodeId::WasiSnapshotPreview1ArgsSizesGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1ClockResGet, + module: "wasi_snapshot_preview1", + name: "clock_res_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ClockSnapshot, + decode: DecodeId::WasiSnapshotPreview1ClockResGet, + encode: EncodeId::U64Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1ClockTimeGet, + module: "wasi_snapshot_preview1", + name: "clock_time_get", + signature: CoreSignatureId::I32I64I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ClockSnapshot, + decode: DecodeId::WasiSnapshotPreview1ClockTimeGet, + encode: EncodeId::U64Output, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1EnvironGet, + module: "wasi_snapshot_preview1", + name: "environ_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessEnvironment, + decode: DecodeId::WasiSnapshotPreview1EnvironGet, + encode: EncodeId::WasiSnapshotPreview1EnvironGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1EnvironSizesGet, + module: "wasi_snapshot_preview1", + name: "environ_sizes_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessEnvironment, + decode: DecodeId::WasiSnapshotPreview1EnvironSizesGet, + encode: EncodeId::WasiSnapshotPreview1EnvironSizesGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdAllocate, + module: "wasi_snapshot_preview1", + name: "fd_allocate", + signature: CoreSignatureId::I32I64I64ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::ExtentRange, + decode: DecodeId::WasiSnapshotPreview1FdAllocate, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdClose, + module: "wasi_snapshot_preview1", + name: "fd_close", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorClose, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdDatasync, + module: "wasi_snapshot_preview1", + name: "fd_datasync", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorSync, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdFdstatGet, + module: "wasi_snapshot_preview1", + name: "fd_fdstat_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorStatusFlags, + decode: DecodeId::WasiSnapshotPreview1FdFdstatGet, + encode: EncodeId::WasiSnapshotPreview1FdFdstatGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdFdstatSetFlags, + module: "wasi_snapshot_preview1", + name: "fd_fdstat_set_flags", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorStatusFlags, + decode: DecodeId::WasiSnapshotPreview1FdFdstatSetFlags, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdFilestatGet, + module: "wasi_snapshot_preview1", + name: "fd_filestat_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorMetadata, + decode: DecodeId::WasiSnapshotPreview1FdFilestatGet, + encode: EncodeId::WasiSnapshotPreview1FdFilestatGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdFilestatSetSize, + module: "wasi_snapshot_preview1", + name: "fd_filestat_set_size", + signature: CoreSignatureId::I32I64ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorSetLength, + decode: DecodeId::FdSetLength, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdFilestatSetTimes, + module: "wasi_snapshot_preview1", + name: "fd_filestat_set_times", + signature: CoreSignatureId::I32I64I64I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::MetadataSetTimes, + decode: DecodeId::WasiSnapshotPreview1FdFilestatSetTimes, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdPread, + module: "wasi_snapshot_preview1", + name: "fd_pread", + signature: CoreSignatureId::I32I32I32I64I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorRead, + decode: DecodeId::WasiSnapshotPreview1FdPread, + encode: EncodeId::DescriptorReadOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdPrestatDirName, + module: "wasi_snapshot_preview1", + name: "fd_prestat_dir_name", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::Preopen, + decode: DecodeId::WasiSnapshotPreview1FdPrestatDirName, + encode: EncodeId::WasiSnapshotPreview1FdPrestatDirNameOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdPrestatGet, + module: "wasi_snapshot_preview1", + name: "fd_prestat_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::Preopen, + decode: DecodeId::WasiSnapshotPreview1FdPrestatGet, + encode: EncodeId::WasiSnapshotPreview1FdPrestatGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Bootstrap, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdPwrite, + module: "wasi_snapshot_preview1", + name: "fd_pwrite", + signature: CoreSignatureId::I32I32I32I64I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorWrite, + decode: DecodeId::WasiSnapshotPreview1FdPwrite, + encode: EncodeId::DescriptorWriteOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdRead, + module: "wasi_snapshot_preview1", + name: "fd_read", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorRead, + decode: DecodeId::WasiSnapshotPreview1FdRead, + encode: EncodeId::DescriptorReadOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdReaddir, + module: "wasi_snapshot_preview1", + name: "fd_readdir", + signature: CoreSignatureId::I32I32I32I64I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1FdReaddir, + decode: DecodeId::WasiSnapshotPreview1FdReaddir, + encode: EncodeId::WasiSnapshotPreview1FdReaddirOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdRenumber, + module: "wasi_snapshot_preview1", + name: "fd_renumber", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::WasiSnapshotPreview1FdRenumber, + decode: DecodeId::WasiSnapshotPreview1FdRenumber, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdSeek, + module: "wasi_snapshot_preview1", + name: "fd_seek", + signature: CoreSignatureId::I32I64I32I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorSeek, + decode: DecodeId::WasiSnapshotPreview1FdSeek, + encode: EncodeId::DescriptorOffsetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdSync, + module: "wasi_snapshot_preview1", + name: "fd_sync", + signature: CoreSignatureId::I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorSync, + decode: DecodeId::Fd, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdTell, + module: "wasi_snapshot_preview1", + name: "fd_tell", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorSeek, + decode: DecodeId::WasiSnapshotPreview1FdTell, + encode: EncodeId::DescriptorOffsetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1FdWrite, + module: "wasi_snapshot_preview1", + name: "fd_write", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::DescriptorWrite, + decode: DecodeId::WasiSnapshotPreview1FdWrite, + encode: EncodeId::DescriptorWriteOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathCreateDirectory, + module: "wasi_snapshot_preview1", + name: "path_create_directory", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1PathCreateDirectory, + decode: DecodeId::WasiSnapshotPreview1PathCreateDirectory, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathFilestatGet, + module: "wasi_snapshot_preview1", + name: "path_filestat_get", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathMetadata, + decode: DecodeId::WasiSnapshotPreview1PathFilestatGet, + encode: EncodeId::WasiSnapshotPreview1PathFilestatGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathFilestatSetTimes, + module: "wasi_snapshot_preview1", + name: "path_filestat_set_times", + signature: CoreSignatureId::I32I32I32I32I64I64I32ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::MetadataSetTimes, + decode: DecodeId::WasiSnapshotPreview1PathFilestatSetTimes, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathLink, + module: "wasi_snapshot_preview1", + name: "path_link", + signature: CoreSignatureId::I32x7ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1PathLink, + decode: DecodeId::WasiSnapshotPreview1PathLink, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathOpen, + module: "wasi_snapshot_preview1", + name: "path_open", + signature: CoreSignatureId::I32I32I32I32I32I64I64I32I32ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1PathOpen, + decode: DecodeId::WasiSnapshotPreview1PathOpen, + encode: EncodeId::WasiSnapshotPreview1PathOpenOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::SignalRestartable, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathReadlink, + module: "wasi_snapshot_preview1", + name: "path_readlink", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1PathReadlink, + decode: DecodeId::WasiSnapshotPreview1PathReadlink, + encode: EncodeId::WasiSnapshotPreview1PathReadlinkOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathRemoveDirectory, + module: "wasi_snapshot_preview1", + name: "path_remove_directory", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathRemove, + decode: DecodeId::WasiSnapshotPreview1PathRemoveDirectory, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathRename, + module: "wasi_snapshot_preview1", + name: "path_rename", + signature: CoreSignatureId::I32x6ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathRename, + decode: DecodeId::WasiSnapshotPreview1PathRename, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathSymlink, + module: "wasi_snapshot_preview1", + name: "path_symlink", + signature: CoreSignatureId::I32x5ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1PathSymlink, + decode: DecodeId::WasiSnapshotPreview1PathSymlink, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PathUnlinkFile, + module: "wasi_snapshot_preview1", + name: "path_unlink_file", + signature: CoreSignatureId::I32x3ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::PathRemove, + decode: DecodeId::WasiSnapshotPreview1PathUnlinkFile, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1PollOneoff, + module: "wasi_snapshot_preview1", + name: "poll_oneoff", + signature: CoreSignatureId::I32x4ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::ProcessPoll, + decode: DecodeId::WasiSnapshotPreview1PollOneoff, + encode: EncodeId::WasiSnapshotPreview1PollOneoffOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Wait, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1ProcExit, + module: "wasi_snapshot_preview1", + name: "proc_exit", + signature: CoreSignatureId::I32ToNoResults, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1ProcExit, + decode: DecodeId::WasiSnapshotPreview1ProcExit, + encode: EncodeId::Void, + return_kind: ReturnKind::Void, + execution_class: ExecutionClass::Terminal, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1RandomGet, + module: "wasi_snapshot_preview1", + name: "random_get", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1RandomGet, + decode: DecodeId::WasiSnapshotPreview1RandomGet, + encode: EncodeId::WasiSnapshotPreview1RandomGetOutput, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: true, + prevalidate_outputs: true, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1SchedYield, + module: "wasi_snapshot_preview1", + name: "sched_yield", + signature: CoreSignatureId::NoParamsToI32, + status: ImportStatus::Canonical, + handler: HandlerId::WasiSnapshotPreview1SchedYield, + decode: DecodeId::WasiSnapshotPreview1SchedYield, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Local, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, + AbiBinding { + id: ImportId::WasiSnapshotPreview1SockShutdown, + module: "wasi_snapshot_preview1", + name: "sock_shutdown", + signature: CoreSignatureId::I32x2ToI32, + status: ImportStatus::Compatibility, + handler: HandlerId::WasiSnapshotPreview1SockShutdown, + decode: DecodeId::WasiSnapshotPreview1SockShutdown, + encode: EncodeId::WasiErrno, + return_kind: ReturnKind::WasiErrno, + execution_class: ExecutionClass::Host, + restartability: Restartability::Never, + transactional: false, + prevalidate_outputs: false, + permission_tiers: PermissionTiers::from_bits(15), + }, +]; + +pub const ALIAS_BINDINGS: &[AliasBinding] = &[ + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1ArgsGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1ArgsSizesGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1ClockResGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1ClockTimeGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1EnvironGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1EnvironSizesGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdAllocate, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdClose, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdDatasync, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdFdstatGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdFdstatSetFlags, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdFilestatGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdFilestatSetSize, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdFilestatSetTimes, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdPread, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdPrestatDirName, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdPrestatGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdPwrite, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdRead, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdReaddir, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdRenumber, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdSeek, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdSync, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdTell, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1FdWrite, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathCreateDirectory, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathFilestatGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathFilestatSetTimes, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathLink, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathOpen, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathReadlink, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathRemoveDirectory, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathRename, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathSymlink, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PathUnlinkFile, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1PollOneoff, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1ProcExit, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1RandomGet, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1SchedYield, + permission_tiers: PermissionTiers::from_bits(15), + }, + AliasBinding { + alias_module: "wasi_unstable", + canonical_module: "wasi_snapshot_preview1", + import: ImportId::WasiSnapshotPreview1SockShutdown, + permission_tiers: PermissionTiers::from_bits(15), + }, +]; + +pub fn binding(id: ImportId) -> &'static AbiBinding { + &ABI_BINDINGS[id as usize] +} + +pub fn core_signature(id: CoreSignatureId) -> &'static CoreSignature { + &CORE_SIGNATURES[id as usize] +} + +pub fn find_binding(module: &str, name: &str) -> Option<&'static AbiBinding> { + if let Some(binding) = ABI_BINDINGS + .iter() + .find(|binding| binding.module == module && binding.name == name) + { + return Some(binding); + } + let alias = ALIAS_BINDINGS + .iter() + .find(|alias| alias.alias_module == module && binding(alias.import).name == name)?; + Some(binding(alias.import)) +} diff --git a/crates/execution/src/abi/mod.rs b/crates/execution/src/abi/mod.rs new file mode 100644 index 0000000000..eac5bc8841 --- /dev/null +++ b/crates/execution/src/abi/mod.rs @@ -0,0 +1,57 @@ +//! Engine-neutral registry for the AgentOS-owned WebAssembly host ABI. +//! +//! The generated metadata describes import identity, core signatures, +//! permission availability, semantic handler/codec routing, and the execution +//! constraints shared by the V8 compatibility and native WASM adapters. It +//! contains no engine types and grants no authority by itself. + +mod generated; + +pub use generated::*; + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + #[test] + fn generated_registry_has_the_locked_inventory_shape() { + assert_eq!(ABI_BINDINGS.len(), 169); + assert_eq!(CORE_SIGNATURES.len(), 29); + assert_eq!(ALIAS_BINDINGS.len(), 40); + + let keys = ABI_BINDINGS + .iter() + .map(|binding| (binding.module, binding.name)) + .collect::>(); + assert_eq!(keys.len(), ABI_BINDINGS.len()); + assert!(ABI_BINDINGS.iter().all(|entry| { + binding(entry.id) == entry && core_signature(entry.signature).id == entry.signature + })); + } + + #[test] + fn generated_registry_has_the_locked_permission_counts() { + let count = |tier| { + ABI_BINDINGS + .iter() + .filter(|binding| binding.permission_tiers.contains(tier)) + .count() + }; + assert_eq!(count(PermissionTier::Isolated), 112); + assert_eq!(count(PermissionTier::ReadOnly), 121); + assert_eq!(count(PermissionTier::ReadWrite), 121); + assert_eq!(count(PermissionTier::Full), 169); + + let count_aliases = |tier| { + ALIAS_BINDINGS + .iter() + .filter(|binding| binding.permission_tiers.contains(tier)) + .count() + }; + assert_eq!(count_aliases(PermissionTier::Isolated), 40); + assert_eq!(count_aliases(PermissionTier::ReadOnly), 40); + assert_eq!(count_aliases(PermissionTier::ReadWrite), 40); + assert_eq!(count_aliases(PermissionTier::Full), 40); + } +} diff --git a/crates/execution/src/backend/error.rs b/crates/execution/src/backend/error.rs new file mode 100644 index 0000000000..516a66b91d --- /dev/null +++ b/crates/execution/src/backend/error.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::error::Error; +use std::fmt; + +/// Stable error crossing the kernel/host-service/adapter boundary. +/// +/// `code` is a Linux errno name or an AgentOS typed limit/runtime code. Engine +/// error strings are diagnostics only and must never be parsed to recover it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HostServiceError { + pub code: String, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl HostServiceError { + pub fn new(code: impl Into, message: impl Into) -> Self { + Self { + code: code.into(), + message: message.into(), + details: None, + } + } + + pub fn with_details(mut self, details: Value) -> Self { + self.details = Some(details); + self + } + + pub fn limit( + code: impl Into, + limit_name: &'static str, + limit: u64, + observed: u64, + ) -> Self { + let code = code.into(); + Self::new( + code, + format!( + "{limit_name} limit is {limit}, observed {observed}; raise {limit_name} if needed" + ), + ) + .with_details(serde_json::json!({ + "limitName": limit_name, + "configPath": limit_name, + "limit": limit, + "observed": observed, + })) + } +} + +impl fmt::Display for HostServiceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.code, self.message) + } +} + +impl Error for HostServiceError {} diff --git a/crates/execution/src/backend/event.rs b/crates/execution/src/backend/event.rs new file mode 100644 index 0000000000..24085a593d --- /dev/null +++ b/crates/execution/src/backend/event.rs @@ -0,0 +1,140 @@ +use super::{DirectHostReplyHandle, HostServiceError, PayloadLimit}; +use crate::host::{BoundedBytes, HostOperation}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoundedHostServiceError { + error: HostServiceError, + encoded_bytes: usize, +} + +impl BoundedHostServiceError { + pub fn try_new( + error: HostServiceError, + limit: &PayloadLimit, + ) -> Result { + let encoded_bytes = limit.admit_json(&error)?; + Ok(Self { + error, + encoded_bytes, + }) + } + + pub fn error(&self) -> &HostServiceError { + &self.error + } + + pub fn encoded_bytes(&self) -> usize { + self.encoded_bytes + } + + pub fn into_error(self) -> HostServiceError { + self.error + } +} + +impl std::fmt::Display for BoundedHostServiceError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(formatter) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputStream { + Stdout, + Stderr, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionExit { + Exited(i32), + Signaled { signal: i32, core_dumped: bool }, +} + +/// Common events emitted by all production execution backends. +/// +/// Adapter-specific Node stream/callback events remain behind an adapter +/// extension and are never consumed by shared host-service implementations. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ExecutionEvent { + Output { + stream: OutputStream, + bytes: BoundedBytes, + }, + HostCall { + operation: HostOperation, + reply: DirectHostReplyHandle, + }, + Warning(BoundedHostServiceError), + RuntimeFault(BoundedHostServiceError), + Exited(ExecutionExit), +} + +impl ExecutionEvent { + pub fn output( + stream: OutputStream, + bytes: Vec, + limit: &PayloadLimit, + ) -> Result { + Ok(Self::Output { + stream, + bytes: BoundedBytes::try_new(bytes, limit)?, + }) + } + + pub fn warning( + warning: HostServiceError, + limit: &PayloadLimit, + ) -> Result { + Ok(Self::Warning(BoundedHostServiceError::try_new( + warning, limit, + )?)) + } + + pub fn runtime_fault( + fault: HostServiceError, + limit: &PayloadLimit, + ) -> Result { + Ok(Self::RuntimeFault(BoundedHostServiceError::try_new( + fault, limit, + )?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn output_and_warning_events_require_named_admission_limits() { + let output_limit = PayloadLimit::new("maxOutputEventBytes", 4).expect("output limit"); + assert_eq!( + ExecutionEvent::output(OutputStream::Stdout, vec![0; 5], &output_limit) + .expect_err("oversized output") + .details + .expect("details")["limitName"], + "maxOutputEventBytes" + ); + + let warning_limit = PayloadLimit::new("maxWarningEventBytes", 64).expect("warning limit"); + let warning = HostServiceError::new("EIO", "x".repeat(128)) + .with_details(serde_json::json!({ "path": "/retained/details" })); + let error = ExecutionEvent::warning(warning, &warning_limit) + .expect_err("oversized warning must be rejected before event construction"); + assert_eq!(error.code, "E2BIG"); + assert_eq!( + error.details.expect("limit details")["limitName"], + "maxWarningEventBytes" + ); + + let fault_limit = PayloadLimit::new("maxRuntimeFaultBytes", 64).expect("fault limit"); + let fault = HostServiceError::new("ERR_AGENTOS_WASM_TRAP", "x".repeat(128)); + assert_eq!( + ExecutionEvent::runtime_fault(fault, &fault_limit) + .expect_err("oversized runtime fault must fail admission") + .details + .expect("details")["limitName"], + "maxRuntimeFaultBytes" + ); + } +} diff --git a/crates/execution/src/backend/lifecycle.rs b/crates/execution/src/backend/lifecycle.rs new file mode 100644 index 0000000000..22a581ac9c --- /dev/null +++ b/crates/execution/src/backend/lifecycle.rs @@ -0,0 +1,165 @@ +use super::wake::{ExecutionWakeHandle, ExecutionWakeIdentity}; +use super::{ExecutionExit, HostServiceError}; +use crate::host::ProcessHostCapabilitySet; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionBackendKind { + Javascript, + Python, + WebAssembly, + Binding, +} + +/// Determines who consumes the kernel's authoritative descendant wait state. +/// +/// Language runtimes with a guest-visible POSIX process model must leave +/// zombies available for the guest's `waitpid`; runtimes whose child-process +/// API is implemented entirely by the sidecar can be reaped after delivering +/// their terminal event. The sidecar always consumes executor lifecycle events +/// and commits the exit to the kernel; this policy controls only who consumes +/// the resulting kernel wait state. It is an execution-model capability, not +/// an engine identity: every implementation of the WebAssembly language +/// backend uses the same guest-owned policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescendantWaitOwnership { + Sidecar, + Guest, +} + +/// Determines who consumes inherited descendant stdout and stderr. +/// +/// Sidecar-native child-process APIs create explicit stream objects and route +/// output to those objects. Language runtimes with a guest-visible POSIX +/// process model instead consume the inherited kernel descriptors themselves; +/// claiming those bytes for a sidecar bridge would make ordinary shell +/// redirection and nested commands silently lose output. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescendantOutputOwnership { + SidecarBridge, + GuestDescriptors, +} + +/// How a synchronous compatibility transport submits a potentially blocking +/// descriptor write to the kernel. +/// +/// This is an adapter capability, not an engine identity. A transport that +/// cannot suspend its synchronous dispatcher must probe with nonblocking +/// writes and retry after readiness; an async import can use the ordinary +/// kernel operation because its admitted guest task can yield. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SynchronousFdWritePolicy { + Blocking, + NonblockingRetry, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShutdownReason { + Completed, + Signal(i32), + Deadline, + VmTeardown, + HostRequest, + RuntimeFault, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShutdownOutcome { + AwaitExit, + Exited(ExecutionExit), + ForwardSignal { process_id: u32, signal: i32 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalCheckpointOutcome { + Published, + ForwardToProcess { process_id: u32 }, + Unsupported, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PublishedSignalCheckpoint { + pub signal: i32, + pub delivery_token: u64, + pub flags: u32, +} + +/// Sidecar-facing lifecycle shared by thread-affine and Send backends. +/// +/// The owned backend is deliberately not required to be `Send`. Only its +/// generation-bound control, wake, and reply capabilities cross threads. +pub trait ExecutionBackend { + fn kind(&self) -> ExecutionBackendKind; + + fn synchronous_fd_write_policy(&self) -> SynchronousFdWritePolicy { + SynchronousFdWritePolicy::Blocking + } + + fn descendant_wait_ownership(&self) -> DescendantWaitOwnership { + DescendantWaitOwnership::Sidecar + } + + fn descendant_output_ownership(&self) -> DescendantOutputOwnership { + DescendantOutputOwnership::SidecarBridge + } + + /// Returns the host process that physically contains this execution, when + /// the adapter runs out of process. Embedded backends return `None`. + fn native_process_id(&self) -> Option { + None + } + + /// Returns the generation-bound, runtime-neutral wake capability for this + /// execution. Backends without an asynchronous event lane use the default + /// `None`; engine-specific session handles stay behind this boundary. + fn wake_handle(&self, _identity: ExecutionWakeIdentity) -> Option { + None + } + + /// Attach generation-bound common host services before a prepared backend + /// starts. A native adapter retains this handle in its execution/Store + /// state; compatibility adapters may continue decoding their legacy wire + /// calls in the sidecar while using the same submitted event path. + fn configure_host_services(&mut self, _host: ProcessHostCapabilitySet) {} + + fn is_prepared_for_start(&self) -> bool; + + fn start_prepared(&mut self) -> Result<(), HostServiceError>; + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result; + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError>; + + fn write_stdin(&mut self, bytes: &[u8]) -> Result<(), HostServiceError>; + + fn close_stdin(&mut self) -> Result<(), HostServiceError>; + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result; + + /// Takes one delivery already claimed by the kernel control plane and + /// published into this adapter's bounded, generation-scoped inbox. + fn take_signal_checkpoint( + &self, + _identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + Ok(None) + } + + /// Drops checkpoints claimed by the replaced image after a successful + /// kernel exec commit. The kernel has already cleared those delivery + /// scopes, so the replacement must never report their stale tokens. + fn discard_signal_checkpoints( + &self, + _identity: ExecutionWakeIdentity, + ) -> Result<(), HostServiceError> { + Ok(()) + } +} diff --git a/crates/execution/src/backend/mod.rs b/crates/execution/src/backend/mod.rs new file mode 100644 index 0000000000..a28c97f316 --- /dev/null +++ b/crates/execution/src/backend/mod.rs @@ -0,0 +1,27 @@ +mod error; +mod event; +mod lifecycle; +mod payload; +mod reply; +mod submission; +mod wake; + +pub use error::HostServiceError; +pub use event::{BoundedHostServiceError, ExecutionEvent, ExecutionExit, OutputStream}; +pub use lifecycle::{ + DescendantOutputOwnership, DescendantWaitOwnership, ExecutionBackend, ExecutionBackendKind, + PublishedSignalCheckpoint, ShutdownOutcome, ShutdownReason, SignalCheckpointOutcome, + SynchronousFdWritePolicy, +}; +pub use payload::{NearLimitWarning, NearLimitWarningHook, PayloadLimit}; +pub use reply::{ + direct_host_reply_channel, direct_host_reply_channel_with_limit, DirectHostReplyHandle, + DirectHostReplyReceiver, DirectHostReplyTarget, HostCallIdentity, HostCallReply, +}; +pub use submission::{ + bounded_execution_event_channel, ExecutionEventAdmission, ExecutionEventReceiver, + ExecutionEventSubmitHandle, ExecutionEventWakeTarget, +}; +pub use wake::{ + ExecutionWakeError, ExecutionWakeHandle, ExecutionWakeIdentity, ExecutionWakeTarget, +}; diff --git a/crates/execution/src/backend/payload.rs b/crates/execution/src/backend/payload.rs new file mode 100644 index 0000000000..2cd8e73169 --- /dev/null +++ b/crates/execution/src/backend/payload.rs @@ -0,0 +1,268 @@ +use super::HostServiceError; +use serde::Serialize; +use std::fmt; +use std::io; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +const NEAR_LIMIT_PERCENT: usize = 80; +const REARM_PERCENT: usize = 70; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NearLimitWarning { + pub limit_name: &'static str, + pub limit: usize, + pub observed: usize, +} + +pub trait NearLimitWarningHook: Send + Sync { + fn warn(&self, warning: NearLimitWarning); +} + +struct StderrNearLimitWarningHook; + +impl NearLimitWarningHook for StderrNearLimitWarningHook { + fn warn(&self, warning: NearLimitWarning) { + eprintln!( + "WARN_AGENTOS_PAYLOAD_NEAR_LIMIT: limit={} observed={} maximum={}", + warning.limit_name, warning.observed, warning.limit + ); + } +} + +struct PayloadLimitState { + limit_name: &'static str, + maximum: usize, + warning_hook: Option>, + warning_active: AtomicBool, +} + +/// A named admission bound supplied by the layer that owns configuration. +/// +/// The common execution layer deliberately provides no product default. A +/// sidecar or adapter must pass the configured name and value at construction. +#[derive(Clone)] +pub struct PayloadLimit { + inner: Arc, +} + +impl fmt::Debug for PayloadLimit { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PayloadLimit") + .field("limit_name", &self.inner.limit_name) + .field("maximum", &self.inner.maximum) + .finish_non_exhaustive() + } +} + +impl PayloadLimit { + pub fn new(limit_name: &'static str, maximum: usize) -> Result { + Self::with_stderr_warning(limit_name, maximum) + } + + pub fn with_stderr_warning( + limit_name: &'static str, + maximum: usize, + ) -> Result { + Self::with_warning_hook( + limit_name, + maximum, + Some(Arc::new(StderrNearLimitWarningHook)), + ) + } + + pub fn with_warning_hook( + limit_name: &'static str, + maximum: usize, + warning_hook: Option>, + ) -> Result { + if limit_name.is_empty() { + return Err(HostServiceError::new( + "EINVAL", + "payload limit name must not be empty", + )); + } + if maximum == 0 { + return Err(HostServiceError::new( + "EINVAL", + format!("{limit_name} must be greater than zero"), + )); + } + Ok(Self { + inner: Arc::new(PayloadLimitState { + limit_name, + maximum, + warning_hook, + warning_active: AtomicBool::new(false), + }), + }) + } + + pub fn name(&self) -> &'static str { + self.inner.limit_name + } + + pub fn maximum(&self) -> usize { + self.inner.maximum + } + + pub fn admit(&self, observed: usize) -> Result<(), HostServiceError> { + self.update_warning(observed); + if observed > self.inner.maximum { + return Err(HostServiceError::limit( + "E2BIG", + self.inner.limit_name, + self.inner.maximum as u64, + observed as u64, + )); + } + Ok(()) + } + + pub fn admit_json(&self, value: &T) -> Result { + let mut writer = LimitedCountingWriter::new(self.inner.maximum); + match serde_json::to_writer(&mut writer, value) { + Ok(()) => { + self.admit(writer.observed)?; + Ok(writer.observed) + } + Err(_) if writer.exceeded => { + let observed = self.inner.maximum.saturating_add(1); + self.update_warning(observed); + Err(HostServiceError::limit( + "E2BIG", + self.inner.limit_name, + self.inner.maximum as u64, + observed as u64, + )) + } + Err(error) => Err(HostServiceError::new("EIO", error.to_string())), + } + } + + fn update_warning(&self, observed: usize) { + let Some(hook) = &self.inner.warning_hook else { + return; + }; + let near = observed != 0 + && observed.saturating_mul(100) + >= self.inner.maximum.saturating_mul(NEAR_LIMIT_PERCENT); + if near { + if !self.inner.warning_active.swap(true, Ordering::AcqRel) { + hook.warn(NearLimitWarning { + limit_name: self.inner.limit_name, + limit: self.inner.maximum, + observed, + }); + } + } else if observed.saturating_mul(100) < self.inner.maximum.saturating_mul(REARM_PERCENT) { + self.inner.warning_active.store(false, Ordering::Release); + } + } +} + +struct LimitedCountingWriter { + maximum: usize, + observed: usize, + exceeded: bool, +} + +impl LimitedCountingWriter { + fn new(maximum: usize) -> Self { + Self { + maximum, + observed: 0, + exceeded: false, + } + } +} + +impl io::Write for LimitedCountingWriter { + fn write(&mut self, bytes: &[u8]) -> io::Result { + let next = self.observed.saturating_add(bytes.len()); + if next > self.maximum { + self.observed = self.maximum.saturating_add(1); + self.exceeded = true; + return Err(io::Error::new( + io::ErrorKind::FileTooLarge, + "encoded payload exceeds configured limit", + )); + } + self.observed = next; + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + #[derive(Default)] + struct RecordingWarnings(Mutex>); + + impl NearLimitWarningHook for RecordingWarnings { + fn warn(&self, warning: NearLimitWarning) { + self.0.lock().expect("warning lock").push(warning); + } + } + + #[test] + fn named_payload_limit_admits_at_limit_and_rejects_plus_one_with_typed_details() { + let limit = + PayloadLimit::with_warning_hook("limits.test.maxBytes", 8, None).expect("named limit"); + + limit.admit(8).expect("exact limit must be admitted"); + let error = limit.admit(9).expect_err("limit plus one must fail"); + assert_eq!(error.code, "E2BIG"); + let details = error.details.expect("typed limit details"); + assert_eq!(details["limitName"], "limits.test.maxBytes"); + assert_eq!(details["limit"], 8); + assert_eq!(details["observed"], 9); + } + + #[test] + fn json_measurement_stops_without_allocating_an_encoded_payload() { + let limit = PayloadLimit::new("maxReplyBytes", 4).expect("limit"); + let error = limit + .admit_json(&serde_json::json!({ "payload": "too large" })) + .expect_err("oversized JSON"); + assert_eq!(error.code, "E2BIG"); + assert_eq!( + error.details.expect("details")["limitName"], + "maxReplyBytes" + ); + } + + #[test] + fn near_limit_warning_is_coalesced_and_rearms_below_seventy_percent() { + let warnings = Arc::new(RecordingWarnings::default()); + let limit = PayloadLimit::with_warning_hook("maxEventBytes", 100, Some(warnings.clone())) + .expect("limit"); + + limit.admit(80).expect("near limit"); + limit.admit(90).expect("same warning window"); + limit.admit(69).expect("rearm"); + limit.admit(81).expect("second warning window"); + + let warnings = warnings.0.lock().expect("warning lock"); + assert_eq!(warnings.len(), 2); + assert_eq!(warnings[0].limit_name, "maxEventBytes"); + assert_eq!(warnings[1].observed, 81); + } + + #[test] + fn standard_constructor_enables_near_limit_warning_delivery() { + let standard = PayloadLimit::new("maxEventBytes", 100).expect("standard limit"); + assert!(standard.inner.warning_hook.is_some()); + + let deliberately_silent = + PayloadLimit::with_warning_hook("maxSilentBytes", 100, None).expect("silent limit"); + assert!(deliberately_silent.inner.warning_hook.is_none()); + } +} diff --git a/crates/execution/src/backend/reply.rs b/crates/execution/src/backend/reply.rs new file mode 100644 index 0000000000..72ae61f3a1 --- /dev/null +++ b/crates/execution/src/backend/reply.rs @@ -0,0 +1,834 @@ +use super::{HostServiceError, PayloadLimit}; +use serde_json::Value; +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; + +const REPLY_OPEN: u8 = 0; +const REPLY_CLAIMED: u8 = 1; +const REPLY_SETTLED: u8 = 2; +const REPLY_TRANSITIONING: u8 = 3; +const REPLY_DELIVERY_FAILED: u8 = 4; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HostCallIdentity { + pub generation: u64, + pub pid: u32, + pub call_id: u64, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum HostCallReply { + Empty, + Json(Value), + Raw(Vec), +} + +type DirectHostReplyResult = Result; + +struct DirectHostReplyWaiterTarget { + identity: HostCallIdentity, + sender: Mutex>>, +} + +impl DirectHostReplyTarget for DirectHostReplyWaiterTarget { + fn claim(&self, call_id: u64) -> Result { + self.validate_call_id(call_id)?; + let sender = self.sender.lock().map_err(|_| { + HostServiceError::new("EIO", "direct host-reply waiter lock is poisoned") + })?; + Ok(sender.as_ref().is_some_and(|sender| !sender.is_closed())) + } + + fn respond( + &self, + call_id: u64, + _claimed: bool, + result: DirectHostReplyResult, + ) -> Result<(), HostServiceError> { + self.validate_call_id(call_id)?; + let sender = self + .sender + .lock() + .map_err(|_| HostServiceError::new("EIO", "direct host-reply waiter lock is poisoned"))? + .take() + .ok_or_else(|| { + HostServiceError::new("EALREADY", "direct host-reply waiter is already settled") + })?; + sender + .send(result) + .map_err(|_| HostServiceError::new("EPIPE", "direct host-reply receiver was canceled")) + } +} + +impl DirectHostReplyWaiterTarget { + fn validate_call_id(&self, call_id: u64) -> Result<(), HostServiceError> { + if call_id == self.identity.call_id { + return Ok(()); + } + Err(HostServiceError::new( + "ESTALE", + "direct host-reply call identity does not match its waiter", + ) + .with_details(serde_json::json!({ + "generation": self.identity.generation, + "pid": self.identity.pid, + "expectedCallId": self.identity.call_id, + "actualCallId": call_id, + }))) + } +} + +/// Capacity-one, call-specific completion Future for a native execution +/// adapter. It receives only its registered reply and never scans an event +/// stream. +pub struct DirectHostReplyReceiver { + identity: HostCallIdentity, + receiver: tokio::sync::oneshot::Receiver, +} + +impl fmt::Debug for DirectHostReplyReceiver { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("DirectHostReplyReceiver") + .field("identity", &self.identity) + .finish_non_exhaustive() + } +} + +impl Future for DirectHostReplyReceiver { + type Output = DirectHostReplyResult; + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + Pin::new(&mut self.receiver).poll(context).map(|result| { + result.unwrap_or_else(|_| { + Err(HostServiceError::new( + "ECANCELED", + "direct host-reply sender was dropped without settlement", + ) + .with_details(serde_json::json!({ + "generation": self.identity.generation, + "pid": self.identity.pid, + "callId": self.identity.call_id, + }))) + }) + }) + } +} + +pub fn direct_host_reply_channel( + identity: HostCallIdentity, + max_payload_bytes: usize, +) -> Result<(DirectHostReplyHandle, DirectHostReplyReceiver), HostServiceError> { + direct_host_reply_channel_with_limit( + identity, + PayloadLimit::with_stderr_warning( + "limits.reactor.maxBridgeResponseBytes", + max_payload_bytes, + )?, + ) +} + +pub fn direct_host_reply_channel_with_limit( + identity: HostCallIdentity, + payload_limit: PayloadLimit, +) -> Result<(DirectHostReplyHandle, DirectHostReplyReceiver), HostServiceError> { + let (sender, receiver) = tokio::sync::oneshot::channel(); + let target = Arc::new(DirectHostReplyWaiterTarget { + identity, + sender: Mutex::new(Some(sender)), + }); + let reply = DirectHostReplyHandle::new_with_limit(identity, target, payload_limit)?; + Ok((reply, DirectHostReplyReceiver { identity, receiver })) +} + +/// Adapter-owned one-request response lane. +/// +/// Implementations retain only the adapter's response channel and pending-call +/// token. They must not retain an isolate, Store, process-table borrow, or the +/// sidecar's owned execution enum. +pub trait DirectHostReplyTarget: Send + Sync { + fn claim(&self, call_id: u64) -> Result; + + fn respond( + &self, + call_id: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError>; + + /// Complete a claimed request without resuming the old guest image. + /// This is only valid for a successful exec-style image replacement: the + /// adapter must already have removed the pending waiter during `claim`. + fn dismiss_claimed(&self, _call_id: u64) -> Result<(), HostServiceError> { + Err(HostServiceError::new( + "ENOTSUP", + "adapter does not support dismissing a claimed host reply", + )) + } +} + +struct DirectHostReplyState { + identity: HostCallIdentity, + target: Arc, + state: AtomicU8, + transition_lock: Mutex<()>, + payload_limit: PayloadLimit, + request_retention: Mutex>>, +} + +/// Cloneable, generation-bound direct reply capability for one host call. +/// Exactly one clone may claim or settle it. +#[derive(Clone)] +pub struct DirectHostReplyHandle { + inner: Arc, +} + +impl fmt::Debug for DirectHostReplyHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DirectHostReplyHandle") + .field("identity", &self.inner.identity) + .field("payload_limit", &self.inner.payload_limit) + .finish_non_exhaustive() + } +} + +impl DirectHostReplyHandle { + pub fn new( + identity: HostCallIdentity, + target: Arc, + max_payload_bytes: usize, + ) -> Result { + let payload_limit = PayloadLimit::with_stderr_warning( + "limits.reactor.maxBridgeResponseBytes", + max_payload_bytes, + )?; + Self::new_with_limit(identity, target, payload_limit) + } + + pub fn new_with_limit( + identity: HostCallIdentity, + target: Arc, + payload_limit: PayloadLimit, + ) -> Result { + Ok(Self { + inner: Arc::new(DirectHostReplyState { + identity, + target, + state: AtomicU8::new(REPLY_OPEN), + transition_lock: Mutex::new(()), + payload_limit, + request_retention: Mutex::new(None), + }), + }) + } + + pub fn identity(&self) -> HostCallIdentity { + self.inner.identity + } + + /// Retain opaque request accounting/ownership until this exact call is + /// terminal. The retention is released on success, typed failure, + /// dismissal, delivery failure, or final-handle drop. + pub fn retain_request(&self, retention: T) -> Result<(), HostServiceError> + where + T: Send + Sync + 'static, + { + let _transition = + self.inner.transition_lock.lock().map_err(|_| { + HostServiceError::new("EIO", "host reply transition lock is poisoned") + })?; + let state = self.inner.state.load(Ordering::Acquire); + if state != REPLY_OPEN { + return Err(already_settled(self.inner.identity)); + } + let mut slot = + self.inner.request_retention.lock().map_err(|_| { + HostServiceError::new("EIO", "host reply retention lock is poisoned") + })?; + if slot.is_some() { + return Err(HostServiceError::new( + "EALREADY", + format!( + "host call {} already owns request retention", + self.inner.identity.call_id + ), + )); + } + if self.inner.state.load(Ordering::Acquire) != REPLY_OPEN { + return Err(already_settled(self.inner.identity)); + } + *slot = Some(Box::new(retention)); + Ok(()) + } + + /// Claims the pending adapter request before a destructive host operation. + /// A false result means the guest timed out or replaced the request, so no + /// side effect may be performed. + pub fn claim(&self) -> Result { + let transition = + self.inner.transition_lock.lock().map_err(|_| { + HostServiceError::new("EIO", "host reply transition lock is poisoned") + })?; + self.transition(REPLY_OPEN)?; + drop(transition); + match self.inner.target.claim(self.inner.identity.call_id) { + Ok(true) => { + self.inner.state.store(REPLY_CLAIMED, Ordering::Release); + Ok(true) + } + Ok(false) => { + self.inner.state.store(REPLY_SETTLED, Ordering::Release); + self.release_request_retention(); + Ok(false) + } + Err(error) => { + self.inner.state.store(REPLY_OPEN, Ordering::Release); + Err(error) + } + } + } + + pub fn succeed(&self, reply: HostCallReply) -> Result<(), HostServiceError> { + self.settle(Ok(reply)) + } + + /// Settle a reply synchronously while retaining source-side accounting or + /// storage ownership until the adapter has encoded/transferred it. + /// + /// `T` remains deliberately opaque to the common execution layer: queue + /// reservations, reactor buffers, and engine-specific backing stores do + /// not become part of [`HostCallReply`] or its public ABI. + pub fn succeed_retained( + &self, + reply: HostCallReply, + retention: T, + ) -> Result<(), HostServiceError> { + let result = self.succeed(reply); + drop(retention); + result + } + + /// Admits bytes against this reply lane before constructing its reply + /// envelope. Common host-service implementations should prefer this over + /// constructing `HostCallReply::Raw` directly. + pub fn succeed_raw(&self, bytes: Vec) -> Result<(), HostServiceError> { + match self.inner.payload_limit.admit(bytes.len()) { + Ok(()) => self.settle_admitted(Ok(HostCallReply::Raw(bytes))), + Err(error) => self.settle_admitted(Err(error)), + } + } + + /// Measures JSON with a bounded counting writer before constructing its + /// reply envelope. No encoded temporary is allocated for admission. + pub fn succeed_json(&self, value: Value) -> Result<(), HostServiceError> { + match self.inner.payload_limit.admit_json(&value) { + Ok(_) => self.settle_admitted(Ok(HostCallReply::Json(value))), + Err(error) => self.settle_admitted(Err(error)), + } + } + + pub fn fail(&self, error: HostServiceError) -> Result<(), HostServiceError> { + self.settle(Err(error)) + } + + /// Mark a successfully claimed exec request complete without sending a + /// response into the replaced image. Ordinary operations must settle with + /// `succeed` or `fail`; using this on an open or settled lane is an error. + pub fn dismiss_claimed(&self) -> Result<(), HostServiceError> { + let transition = + self.inner.transition_lock.lock().map_err(|_| { + HostServiceError::new("EIO", "host reply transition lock is poisoned") + })?; + if self.inner.state.load(Ordering::Acquire) != REPLY_CLAIMED { + return Err(already_settled(self.inner.identity)); + } + self.transition(REPLY_CLAIMED)?; + drop(transition); + let result = self + .inner + .target + .dismiss_claimed(self.inner.identity.call_id); + self.inner.state.store( + if result.is_ok() { + REPLY_SETTLED + } else { + REPLY_DELIVERY_FAILED + }, + Ordering::Release, + ); + self.release_request_retention(); + result + } + + fn settle( + &self, + result: Result, + ) -> Result<(), HostServiceError> { + // A response that exceeds the configured lane bound is itself settled + // as a typed limit error. Returning the validation error without + // settling would leave the guest waiting until Drop converted it into + // an unrelated ECANCELED response. + let result = match self.validate_payload(&result) { + Ok(()) => result, + Err(error) => Err(error), + }; + self.settle_admitted(result) + } + + fn settle_admitted( + &self, + result: Result, + ) -> Result<(), HostServiceError> { + let transition = + self.inner.transition_lock.lock().map_err(|_| { + HostServiceError::new("EIO", "host reply transition lock is poisoned") + })?; + let current = self.inner.state.load(Ordering::Acquire); + if current != REPLY_OPEN && current != REPLY_CLAIMED { + return Err(already_settled(self.inner.identity)); + } + self.transition(current)?; + drop(transition); + let response = self.inner.target.respond( + self.inner.identity.call_id, + current == REPLY_CLAIMED, + result, + ); + self.inner.state.store( + if response.is_ok() { + REPLY_SETTLED + } else { + REPLY_DELIVERY_FAILED + }, + Ordering::Release, + ); + self.release_request_retention(); + response + } + + /// Whether the adapter's one response lane failed after settlement was + /// claimed. This is terminal: callers must fail or tear down the adapter + /// waiter instead of replaying a potentially destructive host operation. + pub fn delivery_failed(&self) -> bool { + self.inner.state.load(Ordering::Acquire) == REPLY_DELIVERY_FAILED + } + + fn release_request_retention(&self) { + let retention = self + .inner + .request_retention + .lock() + .unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_DIRECT_HOST_REPLY_RETENTION_POISONED: recovering request retention for call {}", + self.inner.identity.call_id + ); + poisoned.into_inner() + }) + .take(); + drop(retention); + } + + fn transition(&self, expected: u8) -> Result<(), HostServiceError> { + self.inner + .state + .compare_exchange( + expected, + REPLY_TRANSITIONING, + Ordering::AcqRel, + Ordering::Acquire, + ) + .map(|_| ()) + .map_err(|_| already_settled(self.inner.identity)) + } + + fn validate_payload( + &self, + result: &Result, + ) -> Result<(), HostServiceError> { + match result { + Ok(HostCallReply::Empty) => self.inner.payload_limit.admit(0), + Ok(HostCallReply::Raw(bytes)) => self.inner.payload_limit.admit(bytes.len()), + Ok(HostCallReply::Json(value)) => { + self.inner.payload_limit.admit_json(value).map(|_| ()) + } + Err(error) => self.inner.payload_limit.admit_json(error).map(|_| ()), + } + } +} + +impl Drop for DirectHostReplyState { + fn drop(&mut self) { + let state = self.state.swap(REPLY_SETTLED, Ordering::AcqRel); + if state != REPLY_OPEN && state != REPLY_CLAIMED { + return; + } + let error = HostServiceError::new( + "ECANCELED", + "host dropped a direct reply handle without settling it", + ) + .with_details(serde_json::json!({ + "generation": self.identity.generation, + "pid": self.identity.pid, + "callId": self.identity.call_id, + })); + if let Err(reply_error) = + self.target + .respond(self.identity.call_id, state == REPLY_CLAIMED, Err(error)) + { + eprintln!("ERR_AGENTOS_DIRECT_HOST_REPLY_DROP: {reply_error}"); + } + } +} + +fn already_settled(identity: HostCallIdentity) -> HostServiceError { + HostServiceError::new( + "EALREADY", + format!("host call {} already claimed or settled", identity.call_id), + ) + .with_details(serde_json::json!({ + "generation": identity.generation, + "pid": identity.pid, + "callId": identity.call_id, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; + use std::sync::Mutex; + + #[derive(Default)] + struct RecordingTarget { + replies: Mutex)>>, + dismissed: Mutex>, + fail_delivery: bool, + } + + impl DirectHostReplyTarget for RecordingTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + call_id: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + if self.fail_delivery { + return Err(HostServiceError::new( + "EPIPE", + "adapter reply lane is closed", + )); + } + self.replies + .lock() + .expect("reply lock") + .push((call_id, claimed, result)); + Ok(()) + } + + fn dismiss_claimed(&self, call_id: u64) -> Result<(), HostServiceError> { + self.dismissed.lock().expect("dismiss lock").push(call_id); + Ok(()) + } + } + + fn handle(target: Arc) -> DirectHostReplyHandle { + DirectHostReplyHandle::new( + HostCallIdentity { + generation: 7, + pid: 42, + call_id: 9, + }, + target, + 1024, + ) + .expect("reply handle") + } + + #[test] + fn only_one_clone_can_settle() { + let target = Arc::new(RecordingTarget::default()); + let first = handle(target.clone()); + let second = first.clone(); + first.succeed(HostCallReply::Empty).expect("first reply"); + assert_eq!( + second.succeed(HostCallReply::Empty).unwrap_err().code, + "EALREADY" + ); + assert_eq!(target.replies.lock().expect("reply lock").len(), 1); + } + + #[test] + fn claim_is_explicit_and_preserved_on_response() { + let target = Arc::new(RecordingTarget::default()); + let reply = handle(target.clone()); + assert!(reply.claim().expect("claim")); + reply.succeed(HostCallReply::Empty).expect("claimed reply"); + assert!(target.replies.lock().expect("reply lock")[0].1); + } + + #[test] + fn claimed_exec_lane_can_complete_without_resuming_the_old_image() { + let target = Arc::new(RecordingTarget::default()); + let reply = handle(target.clone()); + assert!(reply.claim().expect("claim exec request")); + reply.dismiss_claimed().expect("dismiss exec request"); + assert_eq!(*target.dismissed.lock().expect("dismiss lock"), vec![9]); + assert!(target.replies.lock().expect("reply lock").is_empty()); + } + + #[test] + fn request_retention_is_released_on_dismissal_and_final_drop() { + struct Retention(Arc); + impl Drop for Retention { + fn drop(&mut self) { + self.0.store(true, AtomicOrdering::Release); + } + } + + let dismissed = Arc::new(AtomicBool::new(false)); + let reply = handle(Arc::new(RecordingTarget::default())); + reply + .retain_request(Retention(Arc::clone(&dismissed))) + .expect("retain dismissed request"); + assert!(reply.claim().expect("claim dismissed request")); + reply.dismiss_claimed().expect("dismiss request"); + assert!(dismissed.load(AtomicOrdering::Acquire)); + + let dropped = Arc::new(AtomicBool::new(false)); + let reply = handle(Arc::new(RecordingTarget::default())); + reply + .retain_request(Retention(Arc::clone(&dropped))) + .expect("retain dropped request"); + drop(reply); + assert!(dropped.load(AtomicOrdering::Acquire)); + } + + #[test] + fn last_unsettled_clone_sends_typed_cancellation() { + let target = Arc::new(RecordingTarget::default()); + drop(handle(target.clone())); + let replies = target.replies.lock().expect("reply lock"); + assert_eq!(replies.len(), 1); + assert_eq!(replies[0].2.as_ref().unwrap_err().code, "ECANCELED"); + } + + #[test] + fn oversized_reply_is_settled_as_a_typed_limit_error() { + let target = Arc::new(RecordingTarget::default()); + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 7, + pid: 42, + call_id: 9, + }, + target.clone(), + 4, + ) + .expect("reply handle"); + + reply + .succeed(HostCallReply::Raw(vec![0; 5])) + .expect("limit reply"); + + let replies = target.replies.lock().expect("reply lock"); + assert_eq!(replies.len(), 1); + let error = replies[0].2.as_ref().unwrap_err(); + assert_eq!(error.code, "E2BIG"); + assert_eq!( + error.details.as_ref().expect("limit details")["limitName"], + "limits.reactor.maxBridgeResponseBytes" + ); + } + + #[test] + fn adapter_delivery_failure_is_an_explicit_terminal_state() { + let target = Arc::new(RecordingTarget { + fail_delivery: true, + ..RecordingTarget::default() + }); + let reply = handle(target); + + let error = reply + .succeed(HostCallReply::Empty) + .expect_err("closed adapter lane"); + assert_eq!(error.code, "EPIPE"); + assert!(reply.delivery_failed()); + assert_eq!( + reply.succeed(HostCallReply::Empty).unwrap_err().code, + "EALREADY" + ); + } + + #[test] + fn retained_source_lives_through_synchronous_adapter_response() { + struct Retention(Arc); + impl Drop for Retention { + fn drop(&mut self) { + self.0.store(true, AtomicOrdering::Release); + } + } + struct OrderingTarget(Arc); + impl DirectHostReplyTarget for OrderingTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + fn respond( + &self, + _: u64, + _: bool, + _: Result, + ) -> Result<(), HostServiceError> { + assert!( + !self.0.load(AtomicOrdering::Acquire), + "retention dropped before adapter transfer" + ); + Ok(()) + } + } + let dropped = Arc::new(AtomicBool::new(false)); + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 1, + pid: 2, + call_id: 3, + }, + Arc::new(OrderingTarget(dropped.clone())), + 1024, + ) + .expect("reply"); + reply + .succeed_retained(HostCallReply::Empty, Retention(dropped.clone())) + .expect("settle"); + assert!(dropped.load(AtomicOrdering::Acquire)); + } + + fn poll_direct_receiver( + receiver: DirectHostReplyReceiver, + ) -> Result { + let mut receiver = Box::pin(receiver); + let mut context = Context::from_waker(std::task::Waker::noop()); + match receiver.as_mut().poll(&mut context) { + Poll::Ready(result) => result, + Poll::Pending => panic!("direct reply should already be settled"), + } + } + + #[test] + fn native_direct_waiter_receives_only_its_typed_result() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 23, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + reply + .fail(HostServiceError::new("EACCES", "denied")) + .expect("settle error"); + let error = poll_direct_receiver(receiver).expect_err("typed error"); + assert_eq!(error.code, "EACCES"); + } + + #[test] + fn canceled_native_waiter_prevents_a_claimed_side_effect() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 24, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + drop(receiver); + assert!(!reply.claim().expect("canceled claim")); + assert_eq!( + reply.succeed(HostCallReply::Empty).unwrap_err().code, + "EALREADY" + ); + } + + #[test] + fn dropping_native_reply_settles_waiter_as_canceled() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 25, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + drop(reply); + let error = poll_direct_receiver(receiver).expect_err("drop cancellation"); + assert_eq!(error.code, "ECANCELED"); + } + + #[test] + fn retention_install_racing_settlement_never_leaks() { + use std::sync::Barrier; + + struct Retention(Arc); + impl Drop for Retention { + fn drop(&mut self) { + self.0.fetch_sub(1, AtomicOrdering::AcqRel); + } + } + + for call_id in 1..=128 { + let target = Arc::new(RecordingTarget::default()); + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 3, + pid: 41, + call_id, + }, + target, + 1024, + ) + .expect("reply"); + let retained = Arc::new(std::sync::atomic::AtomicUsize::new(1)); + let barrier = Arc::new(Barrier::new(3)); + let retain_reply = reply.clone(); + let retain_barrier = Arc::clone(&barrier); + let retain_count = Arc::clone(&retained); + let retain = std::thread::spawn(move || { + retain_barrier.wait(); + retain_reply.retain_request(Retention(retain_count)) + }); + let settle_reply = reply.clone(); + let settle_barrier = Arc::clone(&barrier); + let settle = std::thread::spawn(move || { + settle_barrier.wait(); + settle_reply.succeed(HostCallReply::Empty) + }); + barrier.wait(); + let retain_result = retain.join().expect("retention thread"); + let settle_result = settle.join().expect("settlement thread"); + assert!( + settle_result.is_ok(), + "call {call_id} did not reach its terminal settlement" + ); + assert!( + retain_result.is_ok() + || retain_result + .as_ref() + .is_err_and(|error| error.code == "EALREADY"), + "call {call_id} returned an unexpected retention result: {retain_result:?}" + ); + assert_eq!( + retained.load(AtomicOrdering::Acquire), + 0, + "call {call_id} retained request bytes after terminal settlement" + ); + drop(reply); + assert_eq!( + retained.load(AtomicOrdering::Acquire), + 0, + "call {call_id} leaked request retention" + ); + } + } +} diff --git a/crates/execution/src/backend/submission.rs b/crates/execution/src/backend/submission.rs new file mode 100644 index 0000000000..eadf6b2cef --- /dev/null +++ b/crates/execution/src/backend/submission.rs @@ -0,0 +1,605 @@ +use super::{ExecutionEvent, HostServiceError, PayloadLimit}; +use crate::host::HostProcessContext; +use serde::Serialize; +use std::collections::VecDeque; +use std::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +/// Runtime-neutral notification used after a common execution event becomes +/// durable. The callback may wake a sidecar broker or an executor event loop; +/// it must not run guest code itself. +pub trait ExecutionEventWakeTarget: Send + Sync { + fn wake(&self); +} + +impl ExecutionEventWakeTarget for F +where + F: Fn() + Send + Sync, +{ + fn wake(&self) { + self(); + } +} + +struct BoundedExecutionEventQueue { + state: Mutex, + capacity: usize, + retained_bytes: Arc, + closed: AtomicBool, + wake: Arc, +} + +struct ExecutionEventQueueState { + events: VecDeque, +} + +struct QueuedExecutionEvent { + event: ExecutionEvent, + _retention: Option, +} + +struct RetainedEventByteLedger { + used: Mutex, + limit: PayloadLimit, +} + +struct RetainedEventBytes { + ledger: Arc, + bytes: usize, +} + +impl Drop for RetainedEventBytes { + fn drop(&mut self) { + let mut used = self + .ledger + .used + .lock() + .unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_EXECUTION_EVENT_ACCOUNTING_POISONED: recovering retained-byte ledger during release" + ); + poisoned.into_inner() + }); + *used = used.saturating_sub(self.bytes); + } +} + +/// Pre-admitted retained-byte charge for one owned common event. Construction +/// is possible only through a named [`PayloadLimit`], or through the bound +/// submission handle's configured aggregate byte limit. +#[derive(Debug)] +pub struct ExecutionEventAdmission { + retained_bytes: usize, +} + +impl ExecutionEventAdmission { + pub fn try_new(retained_bytes: usize, limit: &PayloadLimit) -> Result { + limit.admit(retained_bytes)?; + Ok(Self { retained_bytes }) + } + + pub fn retained_bytes(&self) -> usize { + self.retained_bytes + } +} + +/// Cloneable, generation-bound producer for common backend events. +/// +/// The handle retains no executor object, guest engine, Store, isolate, or +/// sidecar process borrow. A host-call reply is validated against the bound +/// process before admission, and a rejected submission settles that exact +/// reply lane with the typed rejection. +#[derive(Clone)] +pub struct ExecutionEventSubmitHandle { + process: HostProcessContext, + queue: Arc, +} + +impl fmt::Debug for ExecutionEventSubmitHandle { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ExecutionEventSubmitHandle") + .field("process", &self.process) + .field("capacity", &self.queue.capacity) + .field("retained_bytes_limit", &self.queue.retained_bytes.limit) + .finish_non_exhaustive() + } +} + +impl ExecutionEventSubmitHandle { + pub fn process(&self) -> HostProcessContext { + self.process + } + + pub fn admit( + &self, + retained_bytes: usize, + ) -> Result { + ExecutionEventAdmission::try_new(retained_bytes, &self.queue.retained_bytes.limit) + } + + /// Measure an already-owned adapter request without allocating an encoded + /// copy, then admit its additional raw buffers against the queue's byte + /// bound. The resulting charge must be moved into `submit`. + pub fn admit_json( + &self, + value: &T, + additional_raw_bytes: usize, + ) -> Result { + let encoded = self.queue.retained_bytes.limit.admit_json(value)?; + let retained_bytes = encoded.checked_add(additional_raw_bytes).ok_or_else(|| { + HostServiceError::new("EOVERFLOW", "common event retained-byte charge overflowed") + })?; + self.admit(retained_bytes) + } + + pub fn submit( + &self, + event: ExecutionEvent, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + let reply = match &event { + ExecutionEvent::HostCall { reply, .. } => { + let identity = reply.identity(); + if identity.generation != self.process.generation + || identity.pid != self.process.pid + { + let error = HostServiceError::new( + "ESTALE", + "host-call reply identity does not match the bound execution generation", + ) + .with_details(serde_json::json!({ + "expectedGeneration": self.process.generation, + "expectedPid": self.process.pid, + "actualGeneration": identity.generation, + "actualPid": identity.pid, + "callId": identity.call_id, + })); + reply.fail(error.clone())?; + return Err(error); + } + Some(reply.clone()) + } + _ => None, + }; + + let result = self.submit_admitted_identity(event, admission); + if let Err(error) = &result { + if let Some(reply) = reply { + if let Err(delivery_error) = reply.fail(error.clone()) { + return Err(delivery_error); + } + } + } + result + } + + pub fn retained_bytes(&self) -> Result { + self.queue + .retained_bytes + .used + .lock() + .map(|used| *used) + .map_err(|_| { + HostServiceError::new( + "EIO", + "common execution-event retained-byte ledger lock is poisoned", + ) + }) + } + + fn submit_admitted_identity( + &self, + event: ExecutionEvent, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + if self.queue.closed.load(Ordering::Acquire) { + return Err(HostServiceError::new( + "EPIPE", + "common execution-event receiver is closed", + )); + } + let mut state = self.queue.state.lock().map_err(|_| { + HostServiceError::new("EIO", "common execution-event queue lock is poisoned") + })?; + if self.queue.closed.load(Ordering::Acquire) { + return Err(HostServiceError::new( + "EPIPE", + "common execution-event receiver is closed", + )); + } + if state.events.len() >= self.queue.capacity { + return Err(HostServiceError::limit( + "EAGAIN", + "limits.process.pendingEventCount/runtime.protocol.maxProcessEvents", + u64::try_from(self.queue.capacity).unwrap_or(u64::MAX), + u64::try_from(state.events.len().saturating_add(1)).unwrap_or(u64::MAX), + )); + } + let retention = { + let mut used = self.queue.retained_bytes.used.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "common execution-event retained-byte ledger lock is poisoned", + ) + })?; + let observed_bytes = used.checked_add(admission.retained_bytes).ok_or_else(|| { + HostServiceError::new( + "EOVERFLOW", + "common execution-event retained-byte total overflowed", + ) + })?; + self.queue.retained_bytes.limit.admit(observed_bytes)?; + *used = observed_bytes; + RetainedEventBytes { + ledger: Arc::clone(&self.queue.retained_bytes), + bytes: admission.retained_bytes, + } + }; + let queued_retention = match &event { + ExecutionEvent::HostCall { reply, .. } => { + if let Err(error) = reply.retain_request(retention) { + return Err(error); + } + None + } + _ => Some(retention), + }; + state.events.push_back(QueuedExecutionEvent { + event, + _retention: queued_retention, + }); + drop(state); + self.queue.wake.wake(); + Ok(()) + } +} + +/// Single-consumer side of a bounded common execution-event queue. +pub struct ExecutionEventReceiver { + queue: Arc, +} + +impl fmt::Debug for ExecutionEventReceiver { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ExecutionEventReceiver") + .field("capacity", &self.queue.capacity) + .finish_non_exhaustive() + } +} + +impl ExecutionEventReceiver { + pub fn try_recv(&self) -> Result, HostServiceError> { + self.queue + .state + .lock() + .map_err(|_| { + HostServiceError::new("EIO", "common execution-event queue lock is poisoned") + }) + .map(|mut state| { + let queued = state.events.pop_front()?; + Some(queued.event) + }) + } +} + +impl Drop for ExecutionEventReceiver { + fn drop(&mut self) { + self.queue.closed.store(true, Ordering::Release); + match self.queue.state.lock() { + Ok(mut state) => { + state.events.clear(); + } + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_EXECUTION_EVENT_QUEUE_POISONED: recovering queue during receiver teardown" + ); + let mut state = poisoned.into_inner(); + state.events.clear(); + } + } + } +} + +pub fn bounded_execution_event_channel( + process: HostProcessContext, + capacity: usize, + retained_bytes_limit: PayloadLimit, + wake: Arc, +) -> Result<(ExecutionEventSubmitHandle, ExecutionEventReceiver), HostServiceError> { + if capacity == 0 { + return Err(HostServiceError::new( + "EINVAL", + "common execution-event queue capacity must be greater than zero", + )); + } + let queue = Arc::new(BoundedExecutionEventQueue { + state: Mutex::new(ExecutionEventQueueState { + events: VecDeque::with_capacity(capacity), + }), + capacity, + retained_bytes: Arc::new(RetainedEventByteLedger { + used: Mutex::new(0), + limit: retained_bytes_limit, + }), + closed: AtomicBool::new(false), + wake, + }); + Ok(( + ExecutionEventSubmitHandle { + process, + queue: Arc::clone(&queue), + }, + ExecutionEventReceiver { queue }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{ + DirectHostReplyHandle, DirectHostReplyTarget, HostCallIdentity, HostCallReply, + }; + use crate::host::{HostOperation, ProcessOperation}; + use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; + + #[derive(Default)] + struct RecordingReplyTarget { + replies: Mutex>>, + } + + struct RejectingReplyTarget; + + impl DirectHostReplyTarget for RejectingReplyTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + _: bool, + _: Result, + ) -> Result<(), HostServiceError> { + Err(HostServiceError::new( + "EIO", + "reply target rejected settlement", + )) + } + } + + impl DirectHostReplyTarget for RecordingReplyTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + _: bool, + result: Result, + ) -> Result<(), HostServiceError> { + self.replies.lock().expect("reply lock").push(result); + Ok(()) + } + } + + fn reply( + process: HostProcessContext, + call_id: u64, + target: Arc, + ) -> DirectHostReplyHandle { + DirectHostReplyHandle::new( + HostCallIdentity { + generation: process.generation, + pid: process.pid, + call_id, + }, + target, + 1024, + ) + .expect("reply") + } + + #[test] + fn bounded_queue_wakes_and_preserves_the_direct_reply_lane() { + let process = HostProcessContext { + generation: 7, + pid: 42, + }; + let wakes = Arc::new(AtomicUsize::new(0)); + let wake_count = Arc::clone(&wakes); + let (submit, receiver) = bounded_execution_event_channel( + process, + 1, + PayloadLimit::new("limits.process.pendingEventBytes", 1024).expect("byte limit"), + Arc::new(move || { + wake_count.fetch_add(1, AtomicOrdering::Relaxed); + }), + ) + .expect("queue"); + let target = Arc::new(RecordingReplyTarget::default()); + submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 1, Arc::clone(&target)), + }, + submit.admit(128).expect("request admission"), + ) + .expect("submit"); + assert_eq!(wakes.load(AtomicOrdering::Relaxed), 1); + + let ExecutionEvent::HostCall { reply, .. } = + receiver.try_recv().expect("receive").expect("event") + else { + panic!("expected host call") + }; + reply + .succeed(HostCallReply::Empty) + .expect("settle direct lane"); + assert!(target.replies.lock().expect("replies")[0].is_ok()); + } + + #[test] + fn full_and_stale_submissions_settle_the_exact_waiter() { + let process = HostProcessContext { + generation: 7, + pid: 42, + }; + let (submit, _receiver) = bounded_execution_event_channel( + process, + 1, + PayloadLimit::new("limits.process.pendingEventBytes", 128).expect("byte limit"), + Arc::new(|| {}), + ) + .expect("queue"); + let target = Arc::new(RecordingReplyTarget::default()); + submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 1, Arc::clone(&target)), + }, + submit.admit(128).expect("request admission"), + ) + .expect("first submit"); + let error = submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 2, Arc::clone(&target)), + }, + submit.admit(1).expect("request admission"), + ) + .expect_err("full queue"); + assert_eq!(error.code, "EAGAIN"); + + let stale = HostProcessContext { + generation: 8, + pid: process.pid, + }; + let error = submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(stale, 3, Arc::clone(&target)), + }, + submit.admit(1).expect("request admission"), + ) + .expect_err("stale reply"); + assert_eq!(error.code, "ESTALE"); + + let replies = target.replies.lock().expect("replies"); + assert_eq!(replies.len(), 2); + assert_eq!(replies[0].as_ref().unwrap_err().code, "EAGAIN"); + assert_eq!(replies[1].as_ref().unwrap_err().code, "ESTALE"); + } + + #[test] + fn stale_submission_propagates_reply_settlement_failure() { + let process = HostProcessContext { + generation: 7, + pid: 42, + }; + let (submit, _receiver) = bounded_execution_event_channel( + process, + 1, + PayloadLimit::new("limits.process.pendingEventBytes", 128).expect("byte limit"), + Arc::new(|| {}), + ) + .expect("queue"); + let stale = HostProcessContext { + generation: 8, + pid: process.pid, + }; + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: stale.generation, + pid: stale.pid, + call_id: 1, + }, + Arc::new(RejectingReplyTarget), + 1024, + ) + .expect("reply"); + let error = submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply, + }, + submit.admit(1).expect("request admission"), + ) + .expect_err("reply settlement failure must propagate"); + assert_eq!(error.code, "EIO"); + } + + #[test] + fn aggregate_retained_bytes_survive_dequeue_until_settle() { + let process = HostProcessContext { + generation: 7, + pid: 42, + }; + let (submit, receiver) = bounded_execution_event_channel( + process, + 2, + PayloadLimit::new("limits.process.pendingEventBytes", 8).expect("byte limit"), + Arc::new(|| {}), + ) + .expect("queue"); + let target = Arc::new(RecordingReplyTarget::default()); + submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 1, Arc::clone(&target)), + }, + submit.admit(8).expect("exact admission"), + ) + .expect("first submit"); + let error = submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 2, Arc::clone(&target)), + }, + submit.admit(1).expect("individual admission"), + ) + .expect_err("aggregate byte bound"); + assert_eq!( + error.details.expect("limit details")["limitName"], + "limits.process.pendingEventBytes" + ); + let ExecutionEvent::HostCall { + reply: pending_reply, + .. + } = receiver + .try_recv() + .expect("receive") + .expect("queued request") + else { + panic!("expected host call") + }; + assert_eq!( + submit.retained_bytes().expect("retained bytes"), + 8, + "dequeue must not release a pending host request" + ); + pending_reply + .succeed(HostCallReply::Empty) + .expect("settle pending request"); + assert_eq!(submit.retained_bytes().expect("released bytes"), 0); + submit + .submit( + ExecutionEvent::HostCall { + operation: HostOperation::Process(ProcessOperation::GetPid), + reply: reply(process, 3, target), + }, + submit.admit(1).expect("re-admit released bytes"), + ) + .expect("submit after release"); + } +} diff --git a/crates/execution/src/backend/wake.rs b/crates/execution/src/backend/wake.rs new file mode 100644 index 0000000000..cfd8d9eac7 --- /dev/null +++ b/crates/execution/src/backend/wake.rs @@ -0,0 +1,258 @@ +use agentos_runtime::readiness::ReadyFlags; +use serde_json::Value; +use std::error::Error; +use std::fmt; +use std::sync::Arc; + +/// Identifies the one execution generation a wake target may notify. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExecutionWakeIdentity { + pub generation: u64, + pub pid: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutionWakeError { + code: &'static str, + message: String, + details: Option, +} + +impl ExecutionWakeError { + pub fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + details: None, + } + } + + pub fn with_details(mut self, details: Option) -> Self { + self.details = details; + self + } + + pub fn code(&self) -> &'static str { + self.code + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn details(&self) -> Option<&Value> { + self.details.as_ref() + } +} + +impl fmt::Display for ExecutionWakeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.code, self.message) + } +} + +impl Error for ExecutionWakeError {} + +/// Adapter-owned readiness sink. Implementations update durable per-capability +/// readiness before scheduling their one coalesced execution wake. +/// +/// This trait deliberately exposes no engine session, isolate, Store, or guest +/// memory. Resource owners retain level state; consuming a wake never clears +/// readiness by itself. +pub trait ExecutionWakeTarget: Send + Sync { + fn publish_readiness( + &self, + capability_id: u64, + capability_generation: u64, + flags: ReadyFlags, + ) -> Result<(), ExecutionWakeError>; + + fn remove_readiness( + &self, + capability_id: u64, + capability_generation: u64, + ) -> Result<(), ExecutionWakeError>; + + fn set_application_read_interest( + &self, + capability_id: u64, + capability_generation: u64, + enabled: bool, + ) -> Result<(), ExecutionWakeError>; + + fn publish_signal(&self, signal: i32, delivery_token: u64) -> Result<(), ExecutionWakeError>; + + /// Adapter extension for evented runtimes. Shared resource owners pass an + /// engine-neutral value; the adapter owns its wire encoding and enforces + /// the encoded-byte limit before queueing it. + fn send_adapter_event( + &self, + event_type: &str, + payload: &Value, + encoded_limit_name: &'static str, + max_encoded_bytes: usize, + ) -> Result<(), ExecutionWakeError>; +} + +#[derive(Clone)] +pub struct ExecutionWakeHandle { + identity: ExecutionWakeIdentity, + target: Arc, +} + +impl fmt::Debug for ExecutionWakeHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ExecutionWakeHandle") + .field("identity", &self.identity) + .finish_non_exhaustive() + } +} + +impl ExecutionWakeHandle { + pub fn new(identity: ExecutionWakeIdentity, target: Arc) -> Self { + Self { identity, target } + } + + pub fn identity(&self) -> ExecutionWakeIdentity { + self.identity + } + + pub fn publish_readiness( + &self, + capability_id: u64, + capability_generation: u64, + flags: ReadyFlags, + ) -> Result<(), ExecutionWakeError> { + self.target + .publish_readiness(capability_id, capability_generation, flags) + } + + pub fn remove_readiness( + &self, + capability_id: u64, + capability_generation: u64, + ) -> Result<(), ExecutionWakeError> { + self.target + .remove_readiness(capability_id, capability_generation) + } + + pub fn set_application_read_interest( + &self, + capability_id: u64, + capability_generation: u64, + enabled: bool, + ) -> Result<(), ExecutionWakeError> { + self.target + .set_application_read_interest(capability_id, capability_generation, enabled) + } + + pub fn publish_signal( + &self, + signal: i32, + delivery_token: u64, + ) -> Result<(), ExecutionWakeError> { + self.target.publish_signal(signal, delivery_token) + } + + pub fn send_adapter_event( + &self, + event_type: &str, + payload: &Value, + encoded_limit_name: &'static str, + max_encoded_bytes: usize, + ) -> Result<(), ExecutionWakeError> { + self.target + .send_adapter_event(event_type, payload, encoded_limit_name, max_encoded_bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + #[derive(Default)] + struct RecordingTarget { + readiness: Mutex>, + signals: Mutex>, + } + + impl ExecutionWakeTarget for RecordingTarget { + fn publish_readiness( + &self, + capability_id: u64, + capability_generation: u64, + flags: ReadyFlags, + ) -> Result<(), ExecutionWakeError> { + self.readiness.lock().expect("readiness lock").push(( + capability_id, + capability_generation, + flags, + )); + Ok(()) + } + + fn remove_readiness(&self, _: u64, _: u64) -> Result<(), ExecutionWakeError> { + Ok(()) + } + + fn set_application_read_interest( + &self, + _: u64, + _: u64, + _: bool, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + + fn publish_signal( + &self, + signal: i32, + delivery_token: u64, + ) -> Result<(), ExecutionWakeError> { + self.signals + .lock() + .expect("signals lock") + .push((signal, delivery_token)); + Ok(()) + } + + fn send_adapter_event( + &self, + _: &str, + _: &Value, + _: &'static str, + _: usize, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + } + + #[test] + fn handle_keeps_generation_identity_and_forwards_level_state() { + let target = Arc::new(RecordingTarget::default()); + let handle = ExecutionWakeHandle::new( + ExecutionWakeIdentity { + generation: 7, + pid: 41, + }, + target.clone(), + ); + handle + .publish_readiness(9, 3, ReadyFlags::READABLE) + .expect("publish readiness"); + handle + .publish_signal(15, 29) + .expect("publish signal delivery"); + + assert_eq!(handle.identity().generation, 7); + assert_eq!( + target.readiness.lock().expect("readiness lock").as_slice(), + &[(9, 3, ReadyFlags::READABLE)] + ); + assert_eq!( + target.signals.lock().expect("signals lock").as_slice(), + &[(15, 29)] + ); + } +} diff --git a/crates/execution/src/host/clock.rs b/crates/execution/src/host/clock.rs new file mode 100644 index 0000000000..268f77cf9a --- /dev/null +++ b/crates/execution/src/host/clock.rs @@ -0,0 +1,33 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestClockId { + Realtime, + Monotonic, + ProcessCpu, + ThreadCpu, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ClockOperation { + Time { + clock: GuestClockId, + precision_ns: u64, + /// Optional runtime-configured realtime value. This is owned adapter + /// input rather than a host clock lookup so deterministic VMs observe + /// the same value under every execution engine. + deterministic_realtime_ns: Option, + }, + Resolution { + clock: GuestClockId, + }, + /// Interruptible guest sleep. The sidecar owns its timer and settles the + /// adapter's direct reply lane when the deadline or a signal wins. + Sleep { + duration_ms: u64, + }, + RealIntervalGet, + RealIntervalSet { + initial_us: u64, + interval_us: u64, + }, +} diff --git a/crates/execution/src/host/entropy.rs b/crates/execution/src/host/entropy.rs new file mode 100644 index 0000000000..f33f3fc9e6 --- /dev/null +++ b/crates/execution/src/host/entropy.rs @@ -0,0 +1,6 @@ +use super::BoundedUsize; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EntropyOperation { + pub length: BoundedUsize, +} diff --git a/crates/execution/src/host/filesystem.rs b/crates/execution/src/host/filesystem.rs new file mode 100644 index 0000000000..3bdf14021a --- /dev/null +++ b/crates/execution/src/host/filesystem.rs @@ -0,0 +1,376 @@ +use super::{BoundedBytes, BoundedString, BoundedUsize, BoundedVec}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescriptorWhence { + Set, + Current, + End, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescriptorSyncKind { + Data, + All, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileRangeOperation { + Allocate, + PunchHole, + Zero, + Insert, + Collapse, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum XattrOperation { + Get, + List, + Set { flags: u32 }, + Remove, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MetadataTarget { + Descriptor(u32), + Path { dir_fd: u32, follow_symlinks: bool }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestOpenRights { + /// The guest supplied Preview1 rights. Explicit zero is a real capability + /// request and must not be confused with an omitted adapter value. + Explicit { base: u64, inheriting: u64 }, + /// A non-Preview1 adapter requested Linux-style open semantics. The shared + /// host layer derives the minimum descriptor rights from the open flags. + Synthesized, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GuestOpenSpec { + pub flags: u32, + pub mode: Option, + pub rights: GuestOpenRights, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FilesystemRecordLockKind { + Read, + Write, + Unlock, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecordLockCommand { + Query, + Set, + Wait, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FileTimeUpdate { + pub atime_ns: Option, + pub mtime_ns: Option, + pub atime_now: bool, + pub mtime_now: bool, +} + +/// Path metadata changes that must be applied as one admitted host operation. +/// +/// This is intentionally executor-neutral. Language adapters may expose a +/// combined `setattr` call, but the sidecar/kernel remain the sole semantic and +/// permission authority for every field. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PathAttributeUpdate { + pub mode: Option, + pub uid: Option, + pub gid: Option, + pub atime_ms: Option, + pub mtime_ms: Option, +} + +/// One Linux FIEMAP-style extent returned by the kernel. +/// +/// The indexed query keeps adapters from asking the sidecar to materialize an +/// unbounded extent list merely to answer the custom `fd_fiemap` ABI one row at +/// a time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FileExtent { + pub start: u64, + pub end: u64, + pub unwritten: bool, +} + +/// Complete semantic fd/path family used by Preview1 and `host_fs`. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum FilesystemOperation { + ReadFileAt { + dir_fd: u32, + path: BoundedString, + max_bytes: BoundedUsize, + }, + WriteFileAt { + dir_fd: u32, + path: BoundedString, + bytes: BoundedBytes, + mode: Option, + }, + OpenAt { + dir_fd: u32, + path: BoundedString, + options: GuestOpenSpec, + }, + OpenTmpfileAt { + dir_fd: u32, + path: BoundedString, + options: GuestOpenSpec, + linkable: bool, + }, + Pipe, + Preopen { + fd: u32, + }, + Close { + fd: u32, + }, + /// Close every guest-visible descriptor at or above `min_fd` as one + /// kernel-owned table mutation. Missing descriptors are ignored, matching + /// closefrom(2). Compatibility adapters whose display descriptors differ + /// from kernel descriptors supply the exact canonical target set instead + /// of translating the numeric cutoff. + CloseFrom { + min_fd: u32, + exact_fds: Option>, + }, + Renumber { + from: u32, + to: u32, + }, + Duplicate { + fd: u32, + }, + DuplicateTo { + fd: u32, + target_fd: u32, + }, + DuplicateMin { + fd: u32, + min_fd: u32, + }, + Move { + fd: u32, + replaced_fd: Option, + }, + Read { + fd: u32, + max_bytes: BoundedUsize, + offset: Option, + deadline_ms: Option, + }, + Write { + fd: u32, + bytes: BoundedBytes, + offset: Option, + deadline_ms: Option, + /// Attempt the unpositioned write once without waiting for capacity. + /// Adapter decoders choose this progress contract before dispatch. + nonblocking: bool, + }, + Seek { + fd: u32, + offset: i64, + whence: DescriptorWhence, + }, + Sync { + fd: u32, + kind: DescriptorSyncKind, + }, + DescriptorStatus { + fd: u32, + }, + DescriptorFileStat { + fd: u32, + }, + DescriptorPath { + fd: u32, + require_directory: bool, + }, + DescriptorFdFlags { + fd: u32, + }, + SetDescriptorFdFlags { + fd: u32, + flags: u32, + }, + SetDescriptorFlags { + fd: u32, + flags: u32, + }, + SetLength { + fd: u32, + length: u64, + }, + SetPathLength { + dir_fd: u32, + path: BoundedString, + length: u64, + }, + AdvisoryLock { + fd: u32, + operation: u32, + }, + RecordLock { + fd: u32, + command: RecordLockCommand, + kind: FilesystemRecordLockKind, + start: u64, + length: u64, + }, + CancelRecordLocks, + NamedPipePeerReady { + fd: u32, + }, + ReadDirectory { + fd: u32, + cookie: u64, + max_entries: BoundedUsize, + max_bytes: BoundedUsize, + }, + ReadDirectoryAt { + dir_fd: u32, + path: BoundedString, + max_entries: BoundedUsize, + max_reply_bytes: BoundedUsize, + }, + Stat { + target: MetadataTarget, + path: Option, + }, + NodeStatAt { + dir_fd: u32, + path: BoundedString, + }, + NodeLstatAt { + dir_fd: u32, + path: BoundedString, + }, + SetAttributesAt { + dir_fd: u32, + path: BoundedString, + update: PathAttributeUpdate, + follow_symlinks: bool, + }, + SetTimes { + target: MetadataTarget, + path: Option, + update: FileTimeUpdate, + }, + SetMode { + target: MetadataTarget, + path: Option, + mode: u32, + }, + SetOwner { + target: MetadataTarget, + path: Option, + uid: Option, + gid: Option, + }, + AccessAt { + dir_fd: u32, + path: BoundedString, + mode: u32, + effective_ids: bool, + }, + CreateDirectoryAt { + dir_fd: u32, + path: BoundedString, + mode: u32, + }, + CreateDirectoriesAt { + dir_fd: u32, + path: BoundedString, + mode: Option, + }, + MakeNodeAt { + dir_fd: u32, + path: BoundedString, + mode: u32, + device: u64, + }, + LinkAt { + old_dir_fd: u32, + old_path: BoundedString, + follow_old: bool, + new_dir_fd: u32, + new_path: BoundedString, + }, + LinkDescriptorAt { + fd: u32, + dir_fd: u32, + path: BoundedString, + }, + RenameAt { + old_dir_fd: u32, + old_path: BoundedString, + new_dir_fd: u32, + new_path: BoundedString, + flags: u32, + }, + SymlinkAt { + target: BoundedString, + dir_fd: u32, + path: BoundedString, + }, + ReadLinkAt { + dir_fd: u32, + path: BoundedString, + max_bytes: BoundedUsize, + }, + UnlinkAt { + dir_fd: u32, + path: BoundedString, + remove_directory: bool, + }, + Range { + fd: u32, + operation: FileRangeOperation, + offset: u64, + length: u64, + keep_size: bool, + }, + Extents { + fd: u32, + max_entries: BoundedUsize, + }, + ExtentAt { + fd: u32, + index: u32, + }, + Xattr { + target: MetadataTarget, + path: Option, + name: Option, + value: Option, + operation: XattrOperation, + max_result_bytes: BoundedUsize, + }, + FilesystemStatsAt { + dir_fd: u32, + path: BoundedString, + }, + Remount { + path: BoundedString, + options: BoundedString, + }, + StdinRead { + max_bytes: BoundedUsize, + timeout_ms: u64, + }, + StdioWrite { + fd: u32, + bytes: BoundedBytes, + }, + Preopens, +} diff --git a/crates/execution/src/host/identity.rs b/crates/execution/src/host/identity.rs new file mode 100644 index 0000000000..67580095a6 --- /dev/null +++ b/crates/execution/src/host/identity.rs @@ -0,0 +1,72 @@ +use super::{BoundedString, BoundedUsize, BoundedVec}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IdentityIdKind { + RealUser, + EffectiveUser, + SavedUser, + RealGroup, + EffectiveGroup, + SavedGroup, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum IdentityOperation { + GetId { + kind: IdentityIdKind, + }, + GetUserIds, + GetGroupIds, + Get, + SetId { + kind: IdentityIdKind, + value: Option, + }, + SetUserIds { + real: Option, + effective: Option, + saved: Option, + }, + SetRealEffectiveUserIds { + real: Option, + effective: Option, + }, + SetGroupIds { + real: Option, + effective: Option, + saved: Option, + }, + SetRealEffectiveGroupIds { + real: Option, + effective: Option, + }, + GetSupplementaryGroups, + SetSupplementaryGroups { + groups: BoundedVec, + }, + PasswdById { + uid: u32, + max_record_bytes: BoundedUsize, + }, + PasswdByName { + name: BoundedString, + max_record_bytes: BoundedUsize, + }, + NextPasswd { + index: usize, + max_record_bytes: BoundedUsize, + }, + GroupById { + gid: u32, + max_record_bytes: BoundedUsize, + }, + GroupByName { + name: BoundedString, + max_record_bytes: BoundedUsize, + }, + NextGroup { + index: usize, + max_record_bytes: BoundedUsize, + }, +} diff --git a/crates/execution/src/host/mod.rs b/crates/execution/src/host/mod.rs new file mode 100644 index 0000000000..c1a315257e --- /dev/null +++ b/crates/execution/src/host/mod.rs @@ -0,0 +1,605 @@ +//! Runtime-neutral host-service requests. +//! +//! These types contain owned values only. Guest pointers, V8 handles, +//! Wasmtime Stores, Python objects, and sidecar process borrows are adapter +//! concerns and must never enter this module. + +mod clock; +mod entropy; +mod filesystem; +mod identity; +mod network; +mod process; +mod signal; +mod terminal; + +pub use clock::*; +pub use entropy::*; +pub use filesystem::*; +pub use identity::*; +pub use network::*; +pub use process::*; +pub use signal::*; +pub use terminal::*; + +use crate::backend::{ + DirectHostReplyHandle, ExecutionEvent, ExecutionEventAdmission, ExecutionEventSubmitHandle, + HostServiceError, PayloadLimit, +}; +use std::fmt; +use std::sync::Arc; +/// Authority identifying the already-registered process issuing a host call. +/// Permission tier and resource rights are looked up from kernel state; they +/// are intentionally not caller-selectable fields. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HostProcessContext { + pub generation: u64, + pub pid: u32, +} + +/// Runtime-neutral operation accepted by the shared sidecar host dispatcher. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum HostOperation { + Filesystem(FilesystemOperation), + Network(NetworkOperation), + Process(ProcessOperation), + Terminal(TerminalOperation), + Signal(SignalOperation), + Identity(IdentityOperation), + Clock(ClockOperation), + Entropy(EntropyOperation), +} + +/// Capability-sized submission interface. Implementations enqueue or execute +/// bounded work and settle only the supplied direct reply handle. +pub trait HostCapability: Send + Sync { + fn submit( + &self, + process: HostProcessContext, + operation: Operation, + reply: DirectHostReplyHandle, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError>; +} + +/// Complete runtime-neutral host-service bundle supplied to an executor. +/// +/// The bundle is deliberately a router over capability-sized interfaces, not +/// a mega-trait. Executors can therefore share the exact filesystem, network, +/// process, terminal, signal, identity, clock, and entropy implementations +/// without depending on a sidecar execution enum or another engine. +#[derive(Clone)] +pub struct HostCapabilitySet { + filesystem: Arc>, + network: Arc>, + process: Arc>, + terminal: Arc>, + signal: Arc>, + identity: Arc>, + clock: Arc>, + entropy: Arc>, +} + +impl fmt::Debug for HostCapabilitySet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HostCapabilitySet").finish_non_exhaustive() + } +} + +impl HostCapabilitySet { + #[allow(clippy::too_many_arguments)] + pub fn new( + filesystem: Arc>, + network: Arc>, + process: Arc>, + terminal: Arc>, + signal: Arc>, + identity: Arc>, + clock: Arc>, + entropy: Arc>, + ) -> Self { + Self { + filesystem, + network, + process, + terminal, + signal, + identity, + clock, + entropy, + } + } + + pub fn submit( + &self, + process: HostProcessContext, + operation: HostOperation, + reply: DirectHostReplyHandle, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + match operation { + HostOperation::Filesystem(operation) => { + self.filesystem.submit(process, operation, reply, admission) + } + HostOperation::Network(operation) => { + self.network.submit(process, operation, reply, admission) + } + HostOperation::Process(operation) => { + self.process.submit(process, operation, reply, admission) + } + HostOperation::Terminal(operation) => { + self.terminal.submit(process, operation, reply, admission) + } + HostOperation::Signal(operation) => { + self.signal.submit(process, operation, reply, admission) + } + HostOperation::Identity(operation) => { + self.identity.submit(process, operation, reply, admission) + } + HostOperation::Clock(operation) => { + self.clock.submit(process, operation, reply, admission) + } + HostOperation::Entropy(operation) => { + self.entropy.submit(process, operation, reply, admission) + } + } + } + + /// Build capability-family adapters over one bounded common-event lane. + /// The adapters only wrap typed requests; all filesystem, network, + /// process, terminal, signal, identity, clock, and entropy semantics stay + /// in their sidecar capability-family implementations. + pub fn from_event_submission(events: ExecutionEventSubmitHandle) -> Self { + let adapter = Arc::new(EventSubmittingCapability { events }); + Self::new( + adapter.clone(), + adapter.clone(), + adapter.clone(), + adapter.clone(), + adapter.clone(), + adapter.clone(), + adapter.clone(), + adapter, + ) + } +} + +/// Cloneable executor-facing host services bound to one kernel process +/// generation. Callers cannot select another PID or generation per request. +#[derive(Clone)] +pub struct ProcessHostCapabilitySet { + process: HostProcessContext, + capabilities: HostCapabilitySet, + event_submission: Option, +} + +impl fmt::Debug for ProcessHostCapabilitySet { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ProcessHostCapabilitySet") + .field("process", &self.process) + .finish_non_exhaustive() + } +} + +impl ProcessHostCapabilitySet { + pub fn new(process: HostProcessContext, capabilities: HostCapabilitySet) -> Self { + Self { + process, + capabilities, + event_submission: None, + } + } + + pub fn from_event_submission(events: ExecutionEventSubmitHandle) -> Self { + let process = events.process(); + Self { + process, + capabilities: HostCapabilitySet::from_event_submission(events.clone()), + event_submission: Some(events), + } + } + + pub fn process(&self) -> HostProcessContext { + self.process + } + + pub fn admit_request( + &self, + retained_bytes: usize, + ) -> Result { + self.event_submission + .as_ref() + .ok_or_else(|| { + HostServiceError::new( + "ENOTSUP", + "host capability set does not use a common-event submission lane", + ) + })? + .admit(retained_bytes) + } + + pub fn admit_json_request( + &self, + value: &T, + additional_raw_bytes: usize, + ) -> Result { + self.event_submission + .as_ref() + .ok_or_else(|| { + HostServiceError::new( + "ENOTSUP", + "host capability set does not use a common-event submission lane", + ) + })? + .admit_json(value, additional_raw_bytes) + } + + pub fn submit( + &self, + operation: HostOperation, + reply: DirectHostReplyHandle, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + self.capabilities + .submit(self.process, operation, reply, admission) + } +} + +struct EventSubmittingCapability { + events: ExecutionEventSubmitHandle, +} + +macro_rules! impl_event_submitting_capability { + ($operation:ty, $variant:path) => { + impl HostCapability<$operation> for EventSubmittingCapability { + fn submit( + &self, + process: HostProcessContext, + operation: $operation, + reply: DirectHostReplyHandle, + admission: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + if process != self.events.process() { + let error = HostServiceError::new( + "ESTALE", + "host capability used with a different process generation", + ) + .with_details(serde_json::json!({ + "expectedGeneration": self.events.process().generation, + "expectedPid": self.events.process().pid, + "actualGeneration": process.generation, + "actualPid": process.pid, + })); + reply.fail(error.clone())?; + return Err(error); + } + self.events.submit( + ExecutionEvent::HostCall { + operation: $variant(operation), + reply, + }, + admission, + ) + } + } + }; +} + +impl_event_submitting_capability!(FilesystemOperation, HostOperation::Filesystem); +impl_event_submitting_capability!(NetworkOperation, HostOperation::Network); +impl_event_submitting_capability!(ProcessOperation, HostOperation::Process); +impl_event_submitting_capability!(TerminalOperation, HostOperation::Terminal); +impl_event_submitting_capability!(SignalOperation, HostOperation::Signal); +impl_event_submitting_capability!(IdentityOperation, HostOperation::Identity); +impl_event_submitting_capability!(ClockOperation, HostOperation::Clock); +impl_event_submitting_capability!(EntropyOperation, HostOperation::Entropy); + +/// Owned bytes admitted before a request is queued or copied again. +#[derive(Clone, PartialEq, Eq)] +pub struct BoundedBytes { + bytes: Vec, +} + +/// A guest-selected count admitted against a named limit before the operation +/// is constructed. Keeping the field private prevents adapters from attaching +/// an unchecked allocation or result size to a queued host request. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BoundedUsize(usize); + +impl BoundedUsize { + pub fn try_new(value: usize, limit: &PayloadLimit) -> Result { + limit.admit(value)?; + Ok(Self(value)) + } + + pub fn get(self) -> usize { + self.0 + } +} + +/// A collection admitted against an element-count limit before queueing. +#[derive(Clone, PartialEq, Eq)] +pub struct BoundedVec(Vec); + +impl std::fmt::Debug for BoundedVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BoundedVec") + .field("len", &self.0.len()) + .finish() + } +} + +impl BoundedVec { + pub fn try_new(values: Vec, limit: &PayloadLimit) -> Result { + limit.admit(values.len())?; + Ok(Self(values)) + } + + pub fn as_slice(&self) -> &[T] { + &self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl std::fmt::Debug for BoundedBytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BoundedBytes") + .field("len", &self.bytes.len()) + .finish() + } +} + +impl BoundedBytes { + pub fn try_new(bytes: Vec, limit: &PayloadLimit) -> Result { + limit.admit(bytes.len())?; + Ok(Self { bytes }) + } + + pub fn as_slice(&self) -> &[u8] { + &self.bytes + } + + pub fn len(&self) -> usize { + self.bytes.len() + } + + pub fn is_empty(&self) -> bool { + self.bytes.is_empty() + } + + pub fn into_vec(self) -> Vec { + self.bytes + } +} + +/// Owned UTF-8 string admitted against an explicit byte limit. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BoundedString(String); + +impl BoundedString { + pub fn try_new(value: String, limit: &PayloadLimit) -> Result { + if let Err(mut error) = limit.admit(value.len()) { + error.code = String::from("ENAMETOOLONG"); + return Err(error); + } + Ok(Self(value)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn into_string(self) -> String { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{ + bounded_execution_event_channel, DirectHostReplyTarget, HostCallIdentity, HostCallReply, + }; + use std::marker::PhantomData; + use std::sync::Mutex; + + struct RecordingReplyTarget; + + struct RejectingReplyTarget; + + impl DirectHostReplyTarget for RejectingReplyTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + _: bool, + _: Result, + ) -> Result<(), HostServiceError> { + Err(HostServiceError::new( + "EIO", + "reply target rejected settlement", + )) + } + } + + impl DirectHostReplyTarget for RecordingReplyTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + _: bool, + result: Result, + ) -> Result<(), HostServiceError> { + result.map(|_| ()) + } + } + + struct RecordingCapability { + family: &'static str, + seen: Arc>>, + operation: PhantomData, + } + + impl RecordingCapability { + fn new(family: &'static str, seen: Arc>>) -> Self { + Self { + family, + seen, + operation: PhantomData, + } + } + } + + impl HostCapability for RecordingCapability { + fn submit( + &self, + _: HostProcessContext, + _: Operation, + reply: DirectHostReplyHandle, + _: ExecutionEventAdmission, + ) -> Result<(), HostServiceError> { + self.seen.lock().expect("seen lock").push(self.family); + reply.succeed(HostCallReply::Empty) + } + } + + fn reply(call_id: u64) -> DirectHostReplyHandle { + DirectHostReplyHandle::new( + HostCallIdentity { + generation: 7, + pid: 42, + call_id, + }, + Arc::new(RecordingReplyTarget), + 1024, + ) + .expect("reply handle") + } + + #[test] + fn bounded_values_reject_before_admission() { + let limit = |name: &'static str| PayloadLimit::new(name, 4).expect("named limit"); + let error = BoundedBytes::try_new(vec![0; 5], &limit("maxWriteBytes")).unwrap_err(); + assert_eq!(error.code, "E2BIG"); + let error = + BoundedString::try_new(String::from("abcde"), &limit("maxPathBytes")).unwrap_err(); + assert_eq!(error.code, "ENAMETOOLONG"); + let error = BoundedUsize::try_new(5, &limit("maxPollFds")).unwrap_err(); + assert_eq!(error.details.unwrap()["limitName"], "maxPollFds"); + let error = BoundedVec::try_new(vec![1, 2, 3, 4, 5], &limit("maxGroups")).unwrap_err(); + assert_eq!(error.code, "E2BIG"); + } + + #[test] + fn event_capability_propagates_stale_reply_settlement_failure() { + let bound = HostProcessContext { + generation: 7, + pid: 42, + }; + let (events, _receiver) = bounded_execution_event_channel( + bound, + 1, + PayloadLimit::new("limits.process.pendingEventBytes", 128).expect("byte limit"), + Arc::new(|| {}), + ) + .expect("queue"); + let admission = events.admit(1).expect("request admission"); + let capability = EventSubmittingCapability { events }; + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 8, + pid: 42, + call_id: 1, + }, + Arc::new(RejectingReplyTarget), + 1024, + ) + .expect("reply"); + let error = >::submit( + &capability, + HostProcessContext { + generation: 8, + pid: 42, + }, + ProcessOperation::GetPid, + reply, + admission, + ) + .expect_err("reply settlement failure must propagate"); + assert_eq!(error.code, "EIO"); + } + + #[test] + fn capability_set_routes_every_family_without_an_executor_switchboard() { + let seen = Arc::new(Mutex::new(Vec::new())); + let capabilities = HostCapabilitySet::new( + Arc::new(RecordingCapability::new("filesystem", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("network", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("process", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("terminal", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("signal", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("identity", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("clock", Arc::clone(&seen))), + Arc::new(RecordingCapability::new("entropy", Arc::clone(&seen))), + ); + let process = HostProcessContext { + generation: 7, + pid: 42, + }; + let result_limit = PayloadLimit::new("maxResultBytes", 4).expect("result limit"); + let bounded_count = || BoundedUsize::try_new(1, &result_limit).unwrap(); + let operations = [ + HostOperation::Filesystem(FilesystemOperation::Preopens), + HostOperation::Network(NetworkOperation::LocalAddress { fd: 3 }), + HostOperation::Process(ProcessOperation::GetPid), + HostOperation::Terminal(TerminalOperation::IsTerminal { fd: 0 }), + HostOperation::Signal(SignalOperation::Pending), + HostOperation::Identity(IdentityOperation::Get), + HostOperation::Clock(ClockOperation::Resolution { + clock: GuestClockId::Monotonic, + }), + HostOperation::Entropy(EntropyOperation { + length: bounded_count(), + }), + ]; + for (call_id, operation) in operations.into_iter().enumerate() { + let admission = + ExecutionEventAdmission::try_new(1, &result_limit).expect("request admission"); + capabilities + .submit(process, operation, reply(call_id as u64 + 1), admission) + .expect("route operation"); + } + assert_eq!( + *seen.lock().expect("seen lock"), + [ + "filesystem", + "network", + "process", + "terminal", + "signal", + "identity", + "clock", + "entropy", + ] + ); + } +} diff --git a/crates/execution/src/host/network.rs b/crates/execution/src/host/network.rs new file mode 100644 index 0000000000..13cf6e6340 --- /dev/null +++ b/crates/execution/src/host/network.rs @@ -0,0 +1,357 @@ +use super::{BoundedBytes, BoundedString, BoundedUsize, BoundedVec, SignalSetValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketDomain { + Inet4, + Inet6, + Unix, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketKind { + Stream, + Datagram, + SeqPacket, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DnsAddressFamily { + Any, + Inet4, + Inet6, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ManagedUdpFamily { + Inet4, + Inet6, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ManagedUnixAddress { + Path(BoundedString), + AbstractHex(BoundedString), + Autobind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedTcpEndpoint { + pub host: Option, + pub port: Option, + pub unix: Option, + pub bound_server_id: Option, + pub local_address: Option, + pub local_port: Option, + pub local_reservation: Option, + pub backlog: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SocketAddress { + Inet { host: BoundedString, port: u16 }, + UnixPath(BoundedString), + UnixAbstract(BoundedBytes), + UnixAutobind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketShutdown { + Read, + Write, + Both, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketOptionName { + Error, + ReuseAddress, + ReusePort, + KeepAlive, + NoDelay, + Broadcast, + ReceiveBuffer, + SendBuffer, + Linger, + ReceiveTimeout, + SendTimeout, + Ipv6Only, + MulticastTtl, + MulticastLoop, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SocketOptionValue { + Bool(bool), + Integer(i64), + DurationMs(Option), + Linger { enabled: bool, seconds: u32 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PollInterest { + pub fd: u32, + pub readable: bool, + pub writable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HttpHeader { + pub name: BoundedString, + pub value: BoundedString, +} + +/// Raw Linux-compatible poll interest used by the AgentOS kernel ABI. +/// +/// The bitset is deliberately preserved instead of projecting it to a smaller +/// readable/writable pair: the guest ABI also relies on error, hangup, and +/// invalid-descriptor reporting remaining stable across executor engines. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KernelPollInterest { + pub fd: u32, + pub events: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketValidationRequirement { + Socket, + Listening, +} + +/// Canonical receive result shared by executor adapters. +/// +/// `message_len` records the original datagram length before truncation; +/// `bytes` is the bounded payload copied to the guest. Stream EOF is explicit +/// so adapters do not infer it from engine-specific JSON shapes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkReceive { + pub bytes: BoundedBytes, + pub source: Option, + pub message_len: u32, + pub truncated: bool, + pub eof: bool, +} + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum NetworkOperation { + HttpRequest { + url: BoundedString, + method: BoundedString, + headers: BoundedVec, + body: BoundedBytes, + max_response_bytes: BoundedUsize, + max_header_bytes: BoundedUsize, + max_body_bytes: BoundedUsize, + }, + ManagedBindUnix { + address: ManagedUnixAddress, + }, + ManagedBindConnectedUnix { + socket_id: BoundedString, + address: ManagedUnixAddress, + }, + ManagedReserveTcpPort { + host: Option, + port: Option, + }, + ManagedReleaseTcpPort { + reservation_id: BoundedString, + }, + ManagedConnect { + endpoint: ManagedTcpEndpoint, + }, + ManagedListen { + endpoint: ManagedTcpEndpoint, + }, + ManagedPoll { + socket_id: BoundedString, + wait_ms: u64, + }, + ManagedWaitConnect { + socket_id: BoundedString, + }, + ManagedRead { + socket_id: BoundedString, + max_bytes: u64, + peek: bool, + wait_ms: u64, + }, + ManagedWrite { + socket_id: BoundedString, + bytes: BoundedBytes, + }, + ManagedDestroy { + socket_id: BoundedString, + }, + ManagedAccept { + listener_id: BoundedString, + }, + ManagedCloseListener { + listener_id: BoundedString, + }, + ManagedTlsUpgrade { + socket_id: BoundedString, + options_json: BoundedString, + }, + ManagedUdpCreate { + family: ManagedUdpFamily, + }, + ManagedUdpBind { + socket_id: BoundedString, + host: Option, + port: u16, + }, + ManagedUdpSend { + socket_id: BoundedString, + bytes: BoundedBytes, + host: Option, + port: Option, + }, + ManagedUdpPoll { + socket_id: BoundedString, + wait_ms: u64, + /// Observe the next datagram without consuming it from the shared + /// sidecar-owned open description. + peek: bool, + /// Maximum datagram bytes exposed to this receive operation. `None` + /// preserves the full-datagram compatibility contract for adapters + /// whose own reply lane already admits the complete payload. + max_bytes: Option, + }, + ManagedUdpClose { + socket_id: BoundedString, + }, + ResolveDns { + host: BoundedString, + port: Option, + family: DnsAddressFamily, + max_results: BoundedUsize, + }, + ResolveDnsRecord { + host: BoundedString, + record_type: BoundedString, + raw: bool, + max_results: BoundedUsize, + }, + Socket { + domain: SocketDomain, + kind: SocketKind, + nonblocking: bool, + close_on_exec: bool, + }, + SocketPair { + kind: SocketKind, + nonblocking: bool, + close_on_exec: bool, + }, + SendDescriptorRights { + fd: u32, + bytes: BoundedBytes, + rights: BoundedVec, + flags: u32, + }, + ReceiveDescriptorRights { + fd: u32, + max_bytes: BoundedUsize, + max_rights: BoundedUsize, + close_on_exec: bool, + peek: bool, + dontwait: bool, + waitall: bool, + }, + Bind { + fd: u32, + address: SocketAddress, + }, + Connect { + fd: u32, + address: SocketAddress, + deadline_ms: Option, + }, + Listen { + fd: u32, + backlog: u32, + }, + Accept { + fd: u32, + nonblocking: bool, + close_on_exec: bool, + deadline_ms: Option, + }, + Receive { + fd: u32, + max_bytes: BoundedUsize, + flags: u32, + deadline_ms: Option, + }, + Validate { + fd: u32, + requirement: SocketValidationRequirement, + }, + Send { + fd: u32, + bytes: BoundedBytes, + flags: u32, + address: Option, + deadline_ms: Option, + }, + Shutdown { + fd: u32, + how: SocketShutdown, + }, + LocalAddress { + fd: u32, + }, + PeerAddress { + fd: u32, + }, + GetOption { + fd: u32, + name: SocketOptionName, + }, + SetOption { + fd: u32, + name: SocketOptionName, + value: SocketOptionValue, + }, + Poll { + interests: BoundedVec, + deadline_ms: Option, + }, + KernelPoll { + interests: BoundedVec, + /// `None` is poll(2)'s indefinite wait; `Some(0)` is a probe. + timeout_ms: Option, + }, + /// One sidecar-owned POSIX poll over both kernel and managed socket fds. + /// + /// The optional signal mask is installed and restored by the process + /// owner while the guest is parked. It is never represented by a guest + /// token, which keeps ppoll's mask swap atomic with admission and signal + /// interruption. + PosixPoll { + interests: BoundedVec, + /// `None` is poll(2)'s indefinite wait; `Some(0)` is a probe. + timeout_ms: Option, + signal_mask: Option, + }, + TlsConnect { + fd: u32, + server_name: BoundedString, + alpn: BoundedVec, + deadline_ms: Option, + }, + TlsRead { + session_id: u64, + max_bytes: BoundedUsize, + deadline_ms: Option, + }, + TlsWrite { + session_id: u64, + bytes: BoundedBytes, + deadline_ms: Option, + }, + TlsClose { + session_id: u64, + }, +} diff --git a/crates/execution/src/host/process.rs b/crates/execution/src/host/process.rs new file mode 100644 index 0000000000..df7fbcfe19 --- /dev/null +++ b/crates/execution/src/host/process.rs @@ -0,0 +1,326 @@ +use super::{ + BoundedBytes, BoundedString, BoundedUsize, BoundedVec, FilesystemOperation, SignalSetValue, +}; +use crate::backend::{HostServiceError, PayloadLimit}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceLimitKind { + AddressSpace, + Core, + Cpu, + Data, + FileSize, + LockedMemory, + OpenFiles, + Processes, + ResidentSet, + Stack, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceLimitValue { + pub soft: Option, + pub hard: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WaitTarget { + Any, + Pid(u32), + ProcessGroup(u32), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DescriptorAction { + Close(u32), + Dup2 { + from: u32, + to: u32, + }, + Open { + target_fd: u32, + operation: FilesystemOperation, + }, + SetCloseOnExec { + fd: u32, + enabled: bool, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProcessImage { + pub executable: BoundedString, + pub argv: BoundedVec, + pub env: BoundedVec<(BoundedString, BoundedString)>, + pub cwd: BoundedString, + pub descriptor_actions: BoundedVec, + pub process_group: Option, + pub session_leader: bool, +} + +/// Bounded view of the userspace image currently committed in the kernel. +/// Environment entries remain ordered key/value pairs so executor adapters +/// can encode the exact `key=value\0` Preview1 byte sequence without reading +/// or reconstructing process-local environment state. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CommittedProcessImage { + pub argv: BoundedVec, + pub env: BoundedVec<(BoundedString, BoundedString)>, +} + +/// One POSIX spawn file action decoded at an executor boundary. +/// +/// The numeric command is retained because the AgentOS libc extension is the +/// versioned ABI authority for the action set. The sidecar validates the +/// command and every descriptor again before mutating kernel state. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct ProcessSpawnFileAction { + pub command: u32, + #[serde(rename = "guestFd", default)] + pub guest_fd: Option, + pub fd: i32, + #[serde(rename = "sourceFd")] + pub source_fd: i32, + #[serde(rename = "guestSourceFd", default)] + pub guest_source_fd: Option, + pub oflag: i32, + pub mode: u32, + pub path: String, + #[serde(rename = "closeFromGuestFds", default)] + pub close_from_guest_fds: Vec, +} + +/// Sidecar-owned network description inherited by a spawned process. +/// Resource ownership is resolved from the parent process; none of these +/// guest-provided identifiers grant authority by themselves. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessSpawnHostNetworkDescriptor { + pub guest_fd: u32, + #[serde(default)] + pub description_id: Option, + #[serde(default)] + pub close_on_exec: bool, + #[serde(default)] + pub socket_id: Option, + #[serde(default)] + pub server_id: Option, + #[serde(default)] + pub udp_socket_id: Option, + #[serde(default)] + pub metadata: Value, +} + +/// Runtime-neutral process launch options shared by V8, Wasmtime, and Python +/// adapters. These fields describe Linux process semantics and sidecar +/// runtime selection; they do not contain engine handles or guest memory. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] +pub struct ProcessLaunchOptions { + #[serde(default)] + pub argv0: Option, + #[serde(rename = "cloexecFds", default)] + pub cloexec_fds: Vec, + #[serde(rename = "localReplacement", default)] + pub local_replacement: bool, + #[serde(rename = "executableFd", default)] + pub executable_fd: Option, + #[serde(default)] + pub cwd: Option, + #[serde(default)] + pub env: BTreeMap, + #[serde(rename = "internalBootstrapEnv", default)] + pub internal_bootstrap_env: BTreeMap, + #[serde(rename = "spawnAttrFlags", default)] + pub spawn_attr_flags: u32, + #[serde(rename = "spawnExactPath", default)] + pub spawn_exact_path: bool, + #[serde(rename = "spawnSearchPath", default)] + pub spawn_search_path: Option, + #[serde(rename = "spawnSchedPolicy", default)] + pub spawn_sched_policy: Option, + #[serde(rename = "spawnSchedPriority", default)] + pub spawn_sched_priority: Option, + #[serde(rename = "spawnPgroup", default)] + pub spawn_pgroup: Option, + #[serde(rename = "spawnSignalDefaults", default)] + pub spawn_signal_defaults: Vec, + #[serde(rename = "spawnSignalMask", default)] + pub spawn_signal_mask: Vec, + #[serde(rename = "spawnFileActions", default)] + pub spawn_file_actions: Vec, + #[serde(rename = "spawnFdMappings", default)] + pub spawn_fd_mappings: Vec<[u32; 2]>, + #[serde(rename = "spawnHostNetFds", default)] + pub spawn_host_net_fds: Vec, + #[serde(default)] + pub input: Option, + #[serde(default)] + pub shell: bool, + #[serde(default)] + pub detached: bool, + #[serde(default)] + pub stdio: Vec, + #[serde(default)] + pub timeout: Option, + #[serde(rename = "killSignal", default)] + pub kill_signal: Option, +} + +/// Fully owned runtime-neutral process image model. Executor adapters must +/// convert it to [`BoundedProcessLaunchRequest`] before queueing it as a host +/// operation; sidecar-internal launch preparation may use the plain form only +/// after that admission proof has been consumed. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ProcessLaunchRequest { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub options: ProcessLaunchOptions, +} + +/// A process launch admitted against the adapter's configured request-byte +/// limit before it can become a queued [`ProcessOperation`]. Keeping the inner +/// request private prevents a new executor from bypassing payload admission. +#[derive(Debug, Clone, PartialEq)] +pub struct BoundedProcessLaunchRequest(ProcessLaunchRequest); + +impl BoundedProcessLaunchRequest { + pub fn try_new( + request: ProcessLaunchRequest, + limit: &PayloadLimit, + ) -> Result { + limit.admit_json(&request)?; + Ok(Self(request)) + } + + pub fn as_request(&self) -> &ProcessLaunchRequest { + &self.0 + } + + pub fn into_request(self) -> ProcessLaunchRequest { + self.0 + } +} + +/// Exact source selected for an executable-image snapshot. +/// +/// Descriptor loading is intentionally a kernel operation: it reads the open +/// file description without advancing its cursor, so V8 and Wasmtime cannot +/// diverge through separate fd projections. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExecutableImageSource { + Path(BoundedString), + Descriptor(u32), +} + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum ProcessOperation { + Spawn(BoundedProcessLaunchRequest), + /// Spawn a child, capture its bounded stdout/stderr, and settle only when + /// the child exits. This is the common operation behind synchronous + /// language-runtime subprocess helpers. + RunCaptured { + request: BoundedProcessLaunchRequest, + max_buffer: BoundedUsize, + }, + /// Replace the current process image. `options.executable_fd` selects the + /// prepared in-place fexecve commit used after an executor has loaded the + /// exact open-file image; absence selects ordinary pathname execve. + Exec(BoundedProcessLaunchRequest), + /// Authorize and retain one immutable executable-image snapshot outside + /// the guest descriptor table. The sidecar bounds the snapshot with the + /// VM's WASM module-file limit and returns an opaque generation handle. + OpenExecutableImage { + source: ExecutableImageSource, + }, + ReadExecutableImage { + handle: u64, + offset: u64, + max_bytes: super::BoundedUsize, + }, + CloseExecutableImage { + handle: u64, + }, + PollChild { + child_id: BoundedString, + wait_ms: u64, + }, + WriteChildStdin { + child_id: BoundedString, + chunk: BoundedBytes, + }, + CloseChildStdin { + child_id: BoundedString, + }, + Wait { + target: WaitTarget, + options: u32, + deadline_ms: Option, + temporary_mask: Option, + }, + /// Consume only a stopped/continued child transition. This is separate + /// from `Wait` so an adapter cannot accidentally consume terminal status + /// while it is coordinating the child's final output event. + WaitTransition { + target: WaitTarget, + options: u32, + }, + Kill { + target: i32, + signal: i32, + }, + GetImage { + max_reply_bytes: BoundedUsize, + }, + GetPid, + GetParentPid, + GetProcessGroup { + pid: Option, + }, + SetProcessGroup { + pid: Option, + pgid: Option, + }, + GetResourceLimit { + kind: ResourceLimitKind, + }, + SetResourceLimit { + kind: ResourceLimitKind, + value: ResourceLimitValue, + }, + Umask { + new_mask: Option, + }, + SystemIdentity, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn queued_process_launch_requires_named_payload_admission() { + let request = ProcessLaunchRequest { + command: format!("/{}", "x".repeat(128)), + args: Vec::new(), + options: ProcessLaunchOptions::default(), + }; + let limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", 64).expect("launch limit"); + let error = BoundedProcessLaunchRequest::try_new(request, &limit) + .expect_err("oversized launch must not become a host operation"); + assert_eq!(error.code, "E2BIG"); + assert_eq!( + error + .details + .as_ref() + .and_then(|details| details["limitName"].as_str()), + Some("limits.reactor.maxBridgeRequestBytes") + ); + } +} diff --git a/crates/execution/src/host/signal.rs b/crates/execution/src/host/signal.rs new file mode 100644 index 0000000000..7afd3621cf --- /dev/null +++ b/crates/execution/src/host/signal.rs @@ -0,0 +1,51 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct SignalSetValue(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalDispositionValue { + Default, + Ignore, + User, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignalActionValue { + pub disposition: SignalDispositionValue, + pub flags: u32, + pub mask: SignalSetValue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalMaskHow { + Block, + Unblock, + Set, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum SignalOperation { + GetAction { + signal: i32, + }, + SetAction { + signal: i32, + action: SignalActionValue, + }, + UpdateMask { + how: SignalMaskHow, + set: SignalSetValue, + }, + Pending, + BeginDelivery, + TakePublishedDelivery, + EndDelivery { + token: u64, + }, + BeginTemporaryMask { + mask: SignalSetValue, + }, + EndTemporaryMask { + token: u64, + }, +} diff --git a/crates/execution/src/host/terminal.rs b/crates/execution/src/host/terminal.rs new file mode 100644 index 0000000000..b9cb9468a6 --- /dev/null +++ b/crates/execution/src/host/terminal.rs @@ -0,0 +1,56 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TerminalWindowSize { + pub rows: u16, + pub columns: u16, + pub x_pixels: u16, + pub y_pixels: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TerminalAttributes { + pub input_flags: u32, + pub output_flags: u32, + pub control_flags: u32, + pub local_flags: u32, + pub line_discipline: u8, + pub control_characters: [u8; 32], + pub input_speed: u32, + pub output_speed: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum TerminalOperation { + IsTerminal { + fd: u32, + }, + GetAttributes { + fd: u32, + }, + SetAttributes { + fd: u32, + attributes: TerminalAttributes, + }, + GetWindowSize { + fd: u32, + }, + SetWindowSize { + fd: u32, + size: TerminalWindowSize, + }, + GetForegroundProcessGroup { + fd: u32, + }, + SetForegroundProcessGroup { + fd: u32, + pgid: u32, + }, + GetSession { + fd: u32, + }, + SetRawMode { + fd: u32, + enabled: bool, + }, + OpenPty, +} diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index 0f052c5d15..9527c4c99e 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -1,3 +1,8 @@ +use crate::backend::{ + DirectHostReplyTarget, ExecutionBackend, ExecutionBackendKind, ExecutionExit, + ExecutionWakeHandle, ExecutionWakeIdentity, HostCallReply, HostServiceError, ShutdownOutcome, + ShutdownReason, SignalCheckpointOutcome, +}; use crate::common::stable_hash64; use crate::node_import_cache::{ NodeImportCache, NodeImportCacheCleanup, NODE_IMPORT_CACHE_ASSET_ROOT_ENV, @@ -6,11 +11,12 @@ use crate::runtime_support::{ NODE_COMPILE_CACHE_ENV, NODE_DISABLE_COMPILE_CACHE_ENV, NODE_FROZEN_TIME_ENV, NODE_SANDBOX_ROOT_ENV, }; -use crate::signal::NodeSignalHandlerRegistration; +use crate::signal::ExecutionSignalHandlerRegistration; use crate::v8_host::{V8RuntimeHost, V8SessionFrameReceiver, V8SessionHandle}; use crate::v8_ipc::BinaryFrame; use crate::v8_runtime; -use agentos_bridge::queue_tracker::{register_queue, TrackedLimit}; +use agentos_bridge::queue_tracker::{register_queue, QueueGauge, TrackedLimit}; +use agentos_runtime::accounting::ResourceClass; use agentos_runtime::RuntimeContext; use agentos_v8_runtime::runtime_protocol::{RuntimeCommand, WarmSessionHint}; use flume::{Receiver as EventReceiver, Sender as EventSender}; @@ -117,6 +123,7 @@ fn record_js_phase_stats( ) { let phases = phases.get_or_init(|| Mutex::new(BTreeMap::new())); let Ok(mut phases) = phases.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: JavaScript phase statistics lock is poisoned"); return; }; let stats = phases.entry(stage.to_string()).or_default(); @@ -142,7 +149,12 @@ fn record_js_phase_stats( stats.calls )); } - let _ = fs::write(path, output); + if let Err(error) = fs::write(&path, output) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_WRITE: failed to write JavaScript phase statistics to {}: {error}", + path.to_string_lossy() + ); + } } const DEFAULT_V8_CPU_TIME_LIMIT_MS: u32 = 30_000; @@ -189,6 +201,9 @@ fn record_sync_bridge_phase(method: &str, stage: &str, elapsed: Duration) { } let stats = SYNC_BRIDGE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new())); let Ok(mut stats) = stats.lock() else { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_STATE: JavaScript sync-bridge statistics lock is poisoned" + ); return; }; let elapsed_us = elapsed.as_micros() as u64; @@ -210,7 +225,11 @@ fn record_sync_bridge_phase(method: &str, stage: &str, elapsed: Duration) { value.calls, value.total_us, avg_us, value.max_us )); } - let _ = fs::write(path, lines); + if let Err(error) = fs::write(&path, lines) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_WRITE: failed to write JavaScript sync-bridge statistics to {path}: {error}" + ); + } } } @@ -220,6 +239,7 @@ pub fn record_sync_bridge_request_enqueued(call_id: u64, method: &str) { } let requests = SYNC_BRIDGE_REQUEST_ENQUEUED.get_or_init(|| Mutex::new(HashMap::new())); let Ok(mut requests) = requests.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: sync-bridge request timing lock is poisoned"); return; }; if requests.len() > 4096 { @@ -236,6 +256,7 @@ pub fn record_sync_bridge_request_observed(call_id: u64, fallback_method: &str) return; }; let Ok(mut requests) = requests.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: sync-bridge request timing lock is poisoned"); return; }; let Some((method, started)) = requests.remove(&call_id) else { @@ -295,7 +316,7 @@ enum NodeControlMessage { }, SignalState { signal: u32, - registration: NodeSignalHandlerRegistration, + registration: ExecutionSignalHandlerRegistration, }, } @@ -305,7 +326,7 @@ struct LinePrefixFilter { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct JavascriptSyncRpcRequest { +pub struct HostRpcRequest { pub id: u64, pub method: String, pub args: Vec, @@ -410,6 +431,40 @@ enum PendingSyncRpcResolution { Missing, } +#[derive(Debug)] +struct PendingSyncRpcRegistry { + states: BTreeMap, + maximum: usize, + gauge: Arc, +} + +impl PendingSyncRpcRegistry { + fn new(maximum: usize) -> Self { + let maximum = maximum.max(1); + Self { + states: BTreeMap::new(), + maximum, + gauge: register_queue(TrackedLimit::PendingSyncRpcCalls, maximum), + } + } + + fn observe_depth(&self) { + self.gauge.observe_depth(self.states.len()); + } +} + +/// Direct response lane for JavaScript, Python's embedded JavaScript bridge, +/// and compatibility-WASM host calls. +/// +/// This deliberately retains only the pending-call token and the thread-safe +/// V8 session lane. It does not retain or make `JavascriptExecution`/the V8 +/// isolate `Send`. +#[derive(Debug, Clone)] +pub struct JavascriptSyncRpcResponder { + pending_sync_rpc: Arc>, + v8_session: V8SessionHandle, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateJavascriptContextRequest { pub vm_id: String, @@ -539,10 +594,10 @@ pub struct StartJavascriptExecutionRequest { pub enum JavascriptExecutionEvent { Stdout(Vec), Stderr(Vec), - SyncRpcRequest(JavascriptSyncRpcRequest), + SyncRpcRequest(HostRpcRequest), SignalState { signal: u32, - registration: NodeSignalHandlerRegistration, + registration: ExecutionSignalHandlerRegistration, }, Exited(i32), } @@ -551,7 +606,7 @@ pub enum JavascriptExecutionEvent { enum JavascriptProcessEvent { Stdout(Vec), RawStderr(Vec), - SyncRpcRequest(JavascriptSyncRpcRequest), + SyncRpcRequest(HostRpcRequest), Control(NodeControlMessage), Exited(i32), } @@ -622,17 +677,20 @@ pub struct LocalModuleResolutionCache { pub trait ModuleFsReader { /// Realpath of `guest_path`, expressed as a guest path. `None` if the path /// does not resolve (does not exist / escapes the addressable tree). - fn canonical_guest_path(&mut self, guest_path: &str) -> Option; + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError>; /// Read the file at `guest_path` as a UTF-8 string, following symlinks. - fn read_to_string(&mut self, guest_path: &str) -> Option; + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError>; /// `Some(true)` if `guest_path` is a directory, `Some(false)` if it exists /// but is not a directory, `None` if it does not exist. Follows symlinks. - fn path_is_dir(&mut self, guest_path: &str) -> Option; + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError>; /// Whether `guest_path` exists, following symlinks. - fn path_exists(&mut self, guest_path: &str) -> bool; + fn path_exists(&mut self, guest_path: &str) -> Result; } /// Guest JavaScript module-resolution mode (the `moduleResolution` axis of @@ -815,6 +873,7 @@ fn polyfill_registry() -> &'static PolyfillRegistry { #[derive(Debug, Clone, PartialEq)] enum LocalBridgeCallResult { Immediate(Value), + Error(HostServiceError), Deferred, } @@ -840,16 +899,21 @@ fn timer_delay_ms(value: Option<&Value>) -> u64 { } } -fn timer_dispatch_error(message: String) -> Value { +fn timer_dispatch_error(error: HostServiceError) -> Value { json!({ "__bd_error": { "name": "Error", - "code": message.split(':').next().unwrap_or("ERR_AGENTOS_JAVASCRIPT_TIMER"), - "message": message, + "code": error.code, + "message": error.message, + "details": error.details, } }) } +fn javascript_timer_error(code: &'static str, message: impl Into) -> HostServiceError { + HostServiceError::new(code, message.into()) +} + /// Decide whether a woken timer action should fire, and reclaim its tracking /// entry. Returns `false` (suppressing the callback) when the timer is gone from /// the map (cleared, or wiped on session teardown) or its generation no longer @@ -862,22 +926,29 @@ fn timer_should_fire( timer_id: u64, generation: u64, ) -> bool { - timers - .lock() - .ok() - .and_then(|mut timers| { - let (current_generation, repeat) = timers + match timers.lock() { + Ok(mut timers) => { + let Some((current_generation, repeat)) = timers .get(&timer_id) - .map(|entry| (entry.generation, entry.repeat))?; + .map(|entry| (entry.generation, entry.repeat)) + else { + return false; + }; if current_generation != generation { - return Some(false); + return false; } if !repeat { timers.remove(&timer_id); } - Some(true) - }) - .unwrap_or(false) + true + } + Err(error) => { + eprintln!( + "ERR_AGENTOS_TIMER_STATE: failed to inspect timer {timer_id} generation {generation}: {error}" + ); + false + } + } } struct TimerWheel { @@ -978,14 +1049,15 @@ impl TimerAction { } impl TimerWheel { - fn get(runtime: &RuntimeContext) -> Result<&'static Arc, String> { + fn get(runtime: &RuntimeContext) -> Result<&'static Arc, HostServiceError> { if let Some(wheel) = JAVASCRIPT_TIMER_WHEEL.get() { return Ok(wheel); } let _initializing = JAVASCRIPT_TIMER_WHEEL_INIT.lock().map_err(|_| { - String::from( - "ERR_AGENTOS_JAVASCRIPT_TIMER_WHEEL_INIT: timer wheel initialization lock poisoned", + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_WHEEL_INIT", + "timer wheel initialization lock poisoned", ) })?; if let Some(wheel) = JAVASCRIPT_TIMER_WHEEL.get() { @@ -993,13 +1065,21 @@ impl TimerWheel { } let wheel = Self::start(runtime.clone())?; - let _ = JAVASCRIPT_TIMER_WHEEL.set(wheel); + JAVASCRIPT_TIMER_WHEEL.set(wheel).map_err(|_| { + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_WHEEL_INIT", + "timer wheel was concurrently installed despite the initialization lock", + ) + })?; JAVASCRIPT_TIMER_WHEEL.get().ok_or_else(|| { - String::from("ERR_AGENTOS_JAVASCRIPT_TIMER_WHEEL_INIT: timer wheel was not installed") + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_WHEEL_INIT", + "timer wheel was not installed", + ) }) } - fn start(runtime: RuntimeContext) -> Result, String> { + fn start(runtime: RuntimeContext) -> Result, HostServiceError> { let wheel = Arc::new(Self { state: Mutex::new(TimerWheelState::default()), ready: Notify::new(), @@ -1010,12 +1090,15 @@ impl TimerWheel { worker.run().await }) .map_err(|error| { - format!("ERR_AGENTOS_TASK_LIMIT: failed to start JavaScript timer wheel: {error}") + javascript_timer_error( + "ERR_AGENTOS_TASK_LIMIT", + format!("failed to start JavaScript timer wheel: {error}"), + ) })?; Ok(wheel) } - fn schedule(&self, delay_ms: u64, action: TimerAction) -> Result<(), String> { + fn schedule(&self, delay_ms: u64, action: TimerAction) -> Result<(), HostServiceError> { let now = Instant::now(); let deadline = now .checked_add(Duration::from_millis(delay_ms)) @@ -1028,8 +1111,9 @@ impl TimerWheel { let old_earliest = state.heap.peek().map(|Reverse((deadline, _))| *deadline); let seq = state.next_seq; state.next_seq = state.next_seq.checked_add(1).ok_or_else(|| { - String::from( - "ERR_AGENTOS_JAVASCRIPT_TIMER_SEQUENCE_EXHAUSTED: process timer sequence exhausted", + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_SEQUENCE_EXHAUSTED", + "process timer sequence exhausted", ) })?; state.heap.push(Reverse((deadline, seq))); @@ -1419,6 +1503,207 @@ impl GuestPathTranslator { let guest = self.host_to_guest_string(&canonical); (!guest.starts_with("/unknown/")).then_some(normalize_guest_path(&guest)) } + + /// Typed module-resolution variant of the legacy host translator. Missing + /// mappings remain a normal resolution miss; permission failures, symlink + /// loops, and other host filesystem errors cross the bridge as typed errors. + fn canonical_module_guest_path( + &self, + guest_path: &str, + ) -> Result, HostServiceError> { + let Some(host_path) = self.module_guest_to_host(guest_path)? else { + return Ok(None); + }; + let Some(canonical) = + module_fs_canonicalize_optional(&host_path, "canonicalize", guest_path)? + else { + return Ok(None); + }; + + for mapping in &self.mappings { + if strip_guest_prefix(guest_path, &mapping.guest_path).is_none() { + continue; + } + if let Ok(stripped) = canonical.strip_prefix(&mapping.host_path) { + return Ok(Some(join_guest_path( + &mapping.guest_path, + &stripped.to_string_lossy().replace('\\', "/"), + ))); + } + if let Some(real_mapping_path) = module_fs_canonicalize_optional( + &mapping.host_path, + "canonicalize mapping", + guest_path, + )? { + if let Ok(stripped) = canonical.strip_prefix(&real_mapping_path) { + return Ok(Some(join_guest_path( + &mapping.guest_path, + &stripped.to_string_lossy().replace('\\', "/"), + ))); + } + } + } + + if let Ok(stripped) = canonical.strip_prefix(&self.implicit_host_cwd) { + return Ok(Some(join_guest_path( + &self.implicit_guest_cwd, + &stripped.to_string_lossy().replace('\\', "/"), + ))); + } + if let Some(sandbox_root) = &self.sandbox_root { + if let Ok(stripped) = canonical.strip_prefix(sandbox_root) { + return Ok(Some(join_guest_path( + "/", + &stripped.to_string_lossy().replace('\\', "/"), + ))); + } + } + Ok(None) + } + + fn module_guest_to_host(&self, guest_path: &str) -> Result, HostServiceError> { + let normalized = normalize_guest_path(guest_path); + let mut fallback_candidate = None; + + for mapping in &self.mappings { + if let Some(suffix) = strip_guest_prefix(&normalized, &mapping.guest_path) { + let candidate = join_host_path(&mapping.host_path, suffix); + if module_fs_try_exists(&candidate, guest_path)? { + return self.confine_module_host_path(candidate, guest_path); + } + if let Some(real_mapping_path) = module_fs_canonicalize_optional( + &mapping.host_path, + "canonicalize mapping", + guest_path, + )? { + let real_candidate = join_host_path(&real_mapping_path, suffix); + if module_fs_try_exists(&real_candidate, guest_path)? { + return self.confine_module_host_path(real_candidate, guest_path); + } + if let Some(sibling_candidate) = resolve_pnpm_sibling_host_path_typed( + &real_mapping_path, + suffix, + guest_path, + )? { + return self.confine_module_host_path(sibling_candidate, guest_path); + } + } + fallback_candidate.get_or_insert(candidate); + } + } + + let candidate = + if let Some(suffix) = strip_guest_prefix(&normalized, &self.implicit_guest_cwd) { + Some(join_host_path(&self.implicit_host_cwd, suffix)) + } else if let Some(candidate) = fallback_candidate { + Some(candidate) + } else { + self.sandbox_root.as_ref().map(|sandbox_root| { + join_host_path(sandbox_root, normalized.trim_start_matches('/')) + }) + }; + match candidate { + Some(candidate) => self.confine_module_host_path(candidate, guest_path), + None => Ok(None), + } + } + + fn confine_module_host_path( + &self, + host_path: PathBuf, + guest_path: &str, + ) -> Result, HostServiceError> { + let mut allowed_roots = Vec::new(); + for root in self + .mappings + .iter() + .map(|mapping| mapping.host_path.as_path()) + .chain(std::iter::once(self.implicit_host_cwd.as_path())) + .chain(self.sandbox_root.as_deref()) + { + if let Some(canonical_root) = + module_fs_canonicalize_optional(root, "canonicalize root", guest_path)? + { + if !allowed_roots + .iter() + .any(|existing| existing == &canonical_root) + { + allowed_roots.push(canonical_root); + } + } + } + if allowed_roots.is_empty() { + return Ok(None); + } + + if let Some(canonical_path) = + module_fs_canonicalize_optional(&host_path, "canonicalize", guest_path)? + { + return Ok( + canonical_path_is_allowed(&canonical_path, &allowed_roots).then_some(host_path) + ); + } + + let mut ancestor = host_path.as_path(); + loop { + match fs::symlink_metadata(ancestor) { + Ok(_) => { + let canonical_ancestor = fs::canonicalize(ancestor).map_err(|error| { + module_fs_io_error("canonicalize ancestor", guest_path, error) + })?; + return Ok( + canonical_path_is_allowed(&canonical_ancestor, &allowed_roots) + .then_some(host_path), + ); + } + Err(error) if module_fs_io_is_missing(&error) => {} + Err(error) => { + return Err(module_fs_io_error("inspect ancestor", guest_path, error)); + } + } + let Some(parent) = ancestor.parent() else { + return Ok(None); + }; + ancestor = parent; + } + } +} + +fn module_fs_try_exists(path: &Path, guest_path: &str) -> Result { + path.try_exists() + .map_err(|error| module_fs_io_error("inspect", guest_path, error)) +} + +fn module_fs_canonicalize_optional( + path: &Path, + operation: &str, + guest_path: &str, +) -> Result, HostServiceError> { + match fs::canonicalize(path) { + Ok(path) => Ok(Some(path)), + Err(error) if module_fs_io_is_missing(&error) => Ok(None), + Err(error) => Err(module_fs_io_error(operation, guest_path, error)), + } +} + +fn resolve_pnpm_sibling_host_path_typed( + real_mapping_path: &Path, + suffix: &str, + guest_path: &str, +) -> Result, HostServiceError> { + let Some(trimmed) = suffix.strip_prefix("node_modules/") else { + return Ok(None); + }; + let mut current = Some(real_mapping_path); + while let Some(path) = current { + if path.file_name().and_then(|name| name.to_str()) == Some("node_modules") { + let candidate = join_host_path(path, trimmed); + return module_fs_try_exists(&candidate, guest_path) + .map(|exists| exists.then_some(candidate)); + } + current = path.parent(); + } + Ok(None) } fn sort_guest_path_mappings(mappings: &mut [GuestPathMapping]) { @@ -1488,11 +1773,29 @@ impl ModuleResolutionTestHarness { } pub fn resolve_import(&mut self, specifier: &str, from_path: &str) -> Option { + self.try_resolve_import(specifier, from_path) + .expect("host module filesystem resolution must succeed") + } + + pub fn try_resolve_import( + &mut self, + specifier: &str, + from_path: &str, + ) -> Result, HostServiceError> { self.local_bridge .resolve_module(specifier, from_path, ModuleResolveMode::Import) } pub fn resolve_require(&mut self, specifier: &str, from_path: &str) -> Option { + self.try_resolve_require(specifier, from_path) + .expect("host module filesystem resolution must succeed") + } + + pub fn try_resolve_require( + &mut self, + specifier: &str, + from_path: &str, + ) -> Result, HostServiceError> { self.local_bridge .resolve_module(specifier, from_path, ModuleResolveMode::Require) } @@ -1500,6 +1803,7 @@ impl ModuleResolutionTestHarness { pub fn module_format(&mut self, path: &str) -> Option<&'static str> { self.local_bridge .module_format(path) + .expect("host module filesystem format lookup must succeed") .map(LocalResolvedModuleFormat::as_str) } } @@ -1511,7 +1815,7 @@ pub fn handle_internal_bridge_call_from_host_context( env: &BTreeMap, method: &str, args: &[Value], -) -> Option { +) -> Result, HostServiceError> { // default + in-place assign: LocalBridgeState is Drop, so `..default()` (E0509) // is not allowed. let mut local_bridge = LocalBridgeState::default(); @@ -1519,8 +1823,9 @@ pub fn handle_internal_bridge_call_from_host_context( GuestPathTranslator::from_host_context(env, host_cwd.to_path_buf(), guest_cwd.to_owned()); match local_bridge.handle_internal_bridge_call(0, method, args) { - Some(LocalBridgeCallResult::Immediate(value)) => Some(value), - _ => None, + Some(LocalBridgeCallResult::Immediate(value)) => Ok(Some(value)), + Some(LocalBridgeCallResult::Error(error)) => Err(error), + _ => Ok(None), } } @@ -1721,8 +2026,10 @@ pub enum JavascriptExecutionError { PrepareImportCache(std::io::Error), Spawn(std::io::Error), PendingSyncRpcRequest(u64), + PendingSyncRpcLimit { limit: usize, observed: usize }, ExpiredSyncRpcRequest(u64), RpcResponse(String), + BridgeSettlement(agentos_v8_runtime::host_call::BridgeSettlementError), Terminate(std::io::Error), Control(std::io::Error), StdinClosed, @@ -1758,6 +2065,10 @@ impl fmt::Display for JavascriptExecutionError { "guest JavaScript execution requires servicing pending sync RPC request {id}" ) } + Self::PendingSyncRpcLimit { limit, observed } => write!( + f, + "ERR_AGENTOS_RESOURCE_LIMIT: pending JavaScript sync RPC calls observed {observed}, exceeding limits.reactor.maxBridgeCalls ({limit})" + ), Self::ExpiredSyncRpcRequest(id) => { write!(f, "sync RPC request {id} is no longer pending") } @@ -1767,6 +2078,9 @@ impl fmt::Display for JavascriptExecutionError { "failed to reply to guest JavaScript sync RPC request: {message}" ) } + Self::BridgeSettlement(error) => { + write!(f, "failed to settle guest JavaScript bridge response: {error}") + } Self::Terminate(err) => { write!(f, "failed to terminate guest JavaScript runtime: {err}") } @@ -1788,6 +2102,15 @@ impl fmt::Display for JavascriptExecutionError { impl std::error::Error for JavascriptExecutionError {} +fn javascript_bridge_response_error(error: std::io::Error) -> JavascriptExecutionError { + if let Some(settlement) = error.get_ref().and_then(|source| { + source.downcast_ref::() + }) { + return JavascriptExecutionError::BridgeSettlement(settlement.clone()); + } + JavascriptExecutionError::RpcResponse(error.to_string()) +} + #[derive(Debug)] pub struct JavascriptExecution { execution_id: String, @@ -1797,11 +2120,12 @@ pub struct JavascriptExecution { // forced blocking compatibility paths through Handle::block_on, which // panicked whenever those paths were reached from the unified runtime. events: EventReceiver, - pending_sync_rpc: Arc>>, + pending_sync_rpc: Arc>, exited: Arc, kernel_stdin: Arc, _import_cache_guard: Arc, v8_session: V8SessionHandle, + session_destroyed: bool, /// Fully prepared V8 execute request. Cross-runtime execve prepares the /// replacement isolate and its bridge before committing kernel process /// state, but must not enqueue guest code until that commit is complete. @@ -1832,16 +2156,19 @@ impl JavascriptExecution { &self.execution_id } - pub fn child_pid(&self) -> u32 { - self.child_pid + pub fn native_process_id(&self) -> Option { + (self.child_pid != 0).then_some(self.child_pid) } pub fn v8_session_handle(&self) -> V8SessionHandle { self.v8_session.clone() } - pub fn uses_shared_v8_runtime(&self) -> bool { - true + pub fn sync_rpc_responder(&self) -> JavascriptSyncRpcResponder { + JavascriptSyncRpcResponder { + pending_sync_rpc: Arc::clone(&self.pending_sync_rpc), + v8_session: self.v8_session.clone(), + } } pub fn has_exited(&self) -> bool { @@ -1908,7 +2235,7 @@ impl JavascriptExecution { pub fn read_kernel_stdin_sync_rpc( &self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { if request.method != "__kernel_stdin_read" { return Ok(Value::Null); @@ -1919,7 +2246,7 @@ impl JavascriptExecution { pub(crate) fn handle_kernel_stdin_sync_rpc( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { if request.method != "__kernel_stdin_read" { return Ok(false); @@ -1971,31 +2298,14 @@ impl JavascriptExecution { id: u64, result: Value, ) -> Result<(), JavascriptExecutionError> { - let phase_start = Instant::now(); - match self.clear_pending_sync_rpc(id)? { - PendingSyncRpcResolution::Pending => {} - PendingSyncRpcResolution::TimedOut => { - return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); - } - PendingSyncRpcResolution::Missing => {} - } - record_sync_bridge_phase( - "sync_rpc_response", - "response_clear_pending", - phase_start.elapsed(), - ); - - self.respond_claimed_sync_rpc_success(id, result) + self.sync_rpc_responder().respond_success(id, result) } /// Atomically claim the exact pending sync RPC before a caller performs a /// destructive operation on its behalf. A timed-out or replaced request /// must not consume bytes that belong to the guest's next retry. pub fn claim_sync_rpc_response(&mut self, id: u64) -> Result { - match self.clear_pending_sync_rpc(id)? { - PendingSyncRpcResolution::Pending => Ok(true), - PendingSyncRpcResolution::TimedOut | PendingSyncRpcResolution::Missing => Ok(false), - } + self.sync_rpc_responder().claim(id) } pub fn respond_claimed_sync_rpc_success( @@ -2003,28 +2313,8 @@ impl JavascriptExecution { id: u64, result: Value, ) -> Result<(), JavascriptExecutionError> { - let phase_start = Instant::now(); - let payload = translate_legacy_bridge_value_to_v8(&result); - record_sync_bridge_phase( - "sync_rpc_response", - "response_translate_value", - phase_start.elapsed(), - ); - let phase_start = Instant::now(); - let payload = v8_runtime::json_to_cbor_payload(&payload) - .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string()))?; - record_sync_bridge_phase( - "sync_rpc_response", - "response_encode_cbor", - phase_start.elapsed(), - ); - let phase_start = Instant::now(); - let result = self - .v8_session - .send_bridge_response(id, 0, payload) - .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string())); - record_sync_bridge_phase("sync_rpc_response", "response_send", phase_start.elapsed()); - result + self.sync_rpc_responder() + .respond_claimed_success(id, result) } pub fn respond_sync_rpc_raw_success( @@ -2032,31 +2322,7 @@ impl JavascriptExecution { id: u64, payload: Vec, ) -> Result<(), JavascriptExecutionError> { - let phase_start = Instant::now(); - match self.clear_pending_sync_rpc(id)? { - PendingSyncRpcResolution::Pending => {} - PendingSyncRpcResolution::TimedOut => { - return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); - } - PendingSyncRpcResolution::Missing => {} - } - record_sync_bridge_phase( - "sync_rpc_raw_response", - "response_clear_pending", - phase_start.elapsed(), - ); - - let phase_start = Instant::now(); - let result = self - .v8_session - .send_bridge_response(id, 2, payload) - .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string())); - record_sync_bridge_phase( - "sync_rpc_raw_response", - "response_send", - phase_start.elapsed(), - ); - result + self.sync_rpc_responder().respond_raw_success(id, payload) } pub fn respond_sync_rpc_error( @@ -2065,15 +2331,7 @@ impl JavascriptExecution { code: impl Into, message: impl Into, ) -> Result<(), JavascriptExecutionError> { - match self.clear_pending_sync_rpc(id)? { - PendingSyncRpcResolution::Pending => {} - PendingSyncRpcResolution::TimedOut => { - return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); - } - PendingSyncRpcResolution::Missing => {} - } - - self.respond_claimed_sync_rpc_error(id, code, message) + self.sync_rpc_responder().respond_error(id, code, message) } pub fn respond_claimed_sync_rpc_error( @@ -2082,10 +2340,8 @@ impl JavascriptExecution { code: impl Into, message: impl Into, ) -> Result<(), JavascriptExecutionError> { - let error_msg = format!("{}: {}", code.into(), message.into()); - self.v8_session - .send_bridge_response(id, 1, error_msg.into_bytes()) - .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string())) + self.sync_rpc_responder() + .respond_claimed_error(id, code, message) } pub async fn poll_event( @@ -2199,6 +2455,7 @@ impl JavascriptExecution { self.v8_session .destroy() .map_err(JavascriptExecutionError::Terminate)?; + self.session_destroyed = true; return Ok(JavascriptExecutionResult { execution_id, exit_code, @@ -2222,9 +2479,24 @@ impl JavascriptExecution { /// sidecar service loop and never calls this. pub fn try_service_standalone_module_sync_rpc( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { - let result = { + if !matches!( + request.method.as_str(), + "__resolve_module" + | "_resolveModule" + | "_resolveModuleSync" + | "__load_file" + | "_loadFile" + | "_loadFileSync" + | "__module_format" + | "_moduleFormat" + | "__batch_resolve_modules" + | "_batchResolveModules" + ) { + return Ok(false); + } + let result: Result = { let mut guard = self.module_resolution.lock().map_err(|_| { JavascriptExecutionError::RpcResponse(String::from( "standalone module resolution state poisoned", @@ -2232,7 +2504,7 @@ impl JavascriptExecution { })?; let (translator, cache) = &mut *guard; let mut resolver = ModuleResolver::new(translator, cache); - match request.method.as_str() { + (|| match request.method.as_str() { "__resolve_module" | "_resolveModule" | "_resolveModuleSync" => { let specifier = request.args.first().and_then(Value::as_str).unwrap_or(""); let parent = request.args.get(1).and_then(Value::as_str).unwrap_or("/"); @@ -2242,58 +2514,411 @@ impl JavascriptExecution { _ if request.method == "_resolveModuleSync" => ModuleResolveMode::Require, _ => ModuleResolveMode::Import, }; - resolver - .resolve_module(specifier, parent, mode) + Ok(resolver + .resolve_module(specifier, parent, mode)? .map(Value::String) - .unwrap_or(Value::Null) + .unwrap_or(Value::Null)) } - "__load_file" | "_loadFile" | "_loadFileSync" => resolver - .load_file(request.args.first().and_then(Value::as_str).unwrap_or("")) + "__load_file" | "_loadFile" | "_loadFileSync" => Ok(resolver + .load_file(request.args.first().and_then(Value::as_str).unwrap_or(""))? .map(Value::String) - .unwrap_or(Value::Null), - "__module_format" | "_moduleFormat" => resolver - .module_format(request.args.first().and_then(Value::as_str).unwrap_or("")) + .unwrap_or(Value::Null)), + "__module_format" | "_moduleFormat" => Ok(resolver + .module_format(request.args.first().and_then(Value::as_str).unwrap_or(""))? .map(|format| Value::String(String::from(format.as_str()))) - .unwrap_or(Value::Null), + .unwrap_or(Value::Null)), "__batch_resolve_modules" | "_batchResolveModules" => { resolver.batch_resolve_modules(&request.args) } - _ => return Ok(false), - } + _ => unreachable!("module method was checked before locking resolver state"), + })() }; - self.respond_sync_rpc_success(request.id, result)?; + match result { + Ok(result) => self.respond_sync_rpc_success(request.id, result)?, + Err(error) => self + .sync_rpc_responder() + .respond_host_error(request.id, error)?, + } Ok(true) } +} + +impl ExecutionBackend for JavascriptExecution { + fn kind(&self) -> ExecutionBackendKind { + ExecutionBackendKind::Javascript + } + + fn native_process_id(&self) -> Option { + JavascriptExecution::native_process_id(self) + } + + fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { + Some(ExecutionWakeHandle::new( + identity, + Arc::new(self.v8_session.clone()), + )) + } + + fn is_prepared_for_start(&self) -> bool { + JavascriptExecution::is_prepared_for_start(self) + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + JavascriptExecution::start_prepared(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_START", error.to_string()) + }) + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + if let ShutdownReason::Signal(signal) = reason { + if let Some(process_id) = self.native_process_id() { + return Ok(ShutdownOutcome::ForwardSignal { process_id, signal }); + } + // A shared isolate has no host process whose wait status can + // preserve the terminating signal. V8 termination only reports a + // generic runtime exit, so publish the kernel-owned signal status + // immediately after requesting isolate shutdown. + self.terminate().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + return Ok(ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal, + core_dumped: false, + })); + } + self.terminate().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + Ok(ShutdownOutcome::AwaitExit) + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + let result = if paused { + JavascriptExecution::pause(self) + } else { + JavascriptExecution::resume(self) + }; + result.map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_CONTROL", error.to_string()) + }) + } + + fn write_stdin(&mut self, bytes: &[u8]) -> Result<(), HostServiceError> { + JavascriptExecution::write_stdin(self, bytes).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_STDIN", error.to_string()) + }) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + JavascriptExecution::close_stdin(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_STDIN", error.to_string()) + }) + } + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + _flags: u32, + ) -> Result { + let Some(wake) = self.wake_handle(identity) else { + return Ok(if let Some(process_id) = self.native_process_id() { + SignalCheckpointOutcome::ForwardToProcess { process_id } + } else { + SignalCheckpointOutcome::Unsupported + }); + }; + wake.publish_signal(signal, delivery_token) + .map_err(|error| HostServiceError::new(error.code(), error.to_string()))?; + Ok(SignalCheckpointOutcome::Published) + } +} + +impl JavascriptSyncRpcResponder { + pub fn claim(&self, id: u64) -> Result { + match clear_pending_sync_rpc(&self.pending_sync_rpc, id)? { + PendingSyncRpcResolution::Pending => Ok(true), + PendingSyncRpcResolution::TimedOut | PendingSyncRpcResolution::Missing => Ok(false), + } + } + + pub fn respond_success(&self, id: u64, result: Value) -> Result<(), JavascriptExecutionError> { + let phase_start = Instant::now(); + match clear_pending_sync_rpc(&self.pending_sync_rpc, id)? { + PendingSyncRpcResolution::Pending => {} + PendingSyncRpcResolution::TimedOut => { + return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); + } + PendingSyncRpcResolution::Missing => {} + } + record_sync_bridge_phase( + "sync_rpc_response", + "response_clear_pending", + phase_start.elapsed(), + ); + self.respond_claimed_success(id, result) + } + + pub fn respond_claimed_success( + &self, + id: u64, + result: Value, + ) -> Result<(), JavascriptExecutionError> { + let phase_start = Instant::now(); + let payload = translate_legacy_bridge_value_to_v8(&result); + record_sync_bridge_phase( + "sync_rpc_response", + "response_translate_value", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + let payload = v8_runtime::json_to_cbor_payload(&payload) + .map_err(|error| JavascriptExecutionError::RpcResponse(error.to_string()))?; + record_sync_bridge_phase( + "sync_rpc_response", + "response_encode_cbor", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + let result = self + .v8_session + .send_bridge_response(id, 0, payload) + .map_err(javascript_bridge_response_error); + record_sync_bridge_phase("sync_rpc_response", "response_send", phase_start.elapsed()); + result + } + + pub fn respond_raw_success( + &self, + id: u64, + payload: Vec, + ) -> Result<(), JavascriptExecutionError> { + let phase_start = Instant::now(); + match clear_pending_sync_rpc(&self.pending_sync_rpc, id)? { + PendingSyncRpcResolution::Pending => {} + PendingSyncRpcResolution::TimedOut => { + return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); + } + PendingSyncRpcResolution::Missing => {} + } + record_sync_bridge_phase( + "sync_rpc_raw_response", + "response_clear_pending", + phase_start.elapsed(), + ); + self.respond_claimed_raw_success(id, payload) + } + + pub fn respond_claimed_raw_success( + &self, + id: u64, + payload: Vec, + ) -> Result<(), JavascriptExecutionError> { + let phase_start = Instant::now(); + let result = self + .v8_session + .send_bridge_response(id, 2, payload) + .map_err(javascript_bridge_response_error); + record_sync_bridge_phase( + "sync_rpc_raw_response", + "response_send", + phase_start.elapsed(), + ); + result + } + + pub fn respond_error( + &self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), JavascriptExecutionError> { + self.respond_host_error(id, HostServiceError::new(code.into(), message.into())) + } + + pub fn respond_host_error( + &self, + id: u64, + error: HostServiceError, + ) -> Result<(), JavascriptExecutionError> { + match clear_pending_sync_rpc(&self.pending_sync_rpc, id)? { + PendingSyncRpcResolution::Pending => {} + PendingSyncRpcResolution::TimedOut => { + return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id)); + } + PendingSyncRpcResolution::Missing => {} + } + self.respond_claimed_host_error(id, error) + } + + pub fn respond_claimed_error( + &self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), JavascriptExecutionError> { + self.respond_claimed_host_error(id, HostServiceError::new(code.into(), message.into())) + } + + pub(crate) fn respond_claimed_host_error( + &self, + id: u64, + error: HostServiceError, + ) -> Result<(), JavascriptExecutionError> { + let payload = encode_host_service_error_payload(&error)?; + self.v8_session + .send_bridge_response(id, 1, payload) + .map_err(javascript_bridge_response_error) + } +} + +impl DirectHostReplyTarget for JavascriptSyncRpcResponder { + fn claim(&self, call_id: u64) -> Result { + JavascriptSyncRpcResponder::claim(self, call_id).map_err(host_reply_adapter_error) + } + + fn respond( + &self, + call_id: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + let response = match result { + Ok(HostCallReply::Empty) if claimed => { + self.respond_claimed_success(call_id, Value::Null) + } + Ok(HostCallReply::Empty) => self.respond_success(call_id, Value::Null), + Ok(HostCallReply::Json(value)) if claimed => { + self.respond_claimed_success(call_id, value) + } + Ok(HostCallReply::Json(value)) => self.respond_success(call_id, value), + Ok(HostCallReply::Raw(bytes)) if claimed => { + self.respond_claimed_raw_success(call_id, bytes) + } + Ok(HostCallReply::Raw(bytes)) => self.respond_raw_success(call_id, bytes), + Err(error) => { + if claimed { + self.respond_claimed_host_error(call_id, error) + } else { + self.respond_host_error(call_id, error) + } + } + }; + map_host_reply_adapter_response(response) + } + + fn dismiss_claimed(&self, _call_id: u64) -> Result<(), HostServiceError> { + // `claim` already removed the exact pending request. A successful + // exec replaces or destroys the old image, so sending a bridge reply + // would incorrectly resume instructions following execve(2). + Ok(()) + } +} + +fn clear_pending_sync_rpc( + pending_sync_rpc: &Arc>, + id: u64, +) -> Result { + let mut pending = pending_sync_rpc.lock().map_err(|_| { + JavascriptExecutionError::RpcResponse(String::from( + "sync RPC pending-request state lock poisoned", + )) + })?; + let resolution = match pending.states.remove(&id) { + Some(PendingSyncRpcState::Pending(_)) => PendingSyncRpcResolution::Pending, + Some(PendingSyncRpcState::TimedOut(_)) => PendingSyncRpcResolution::TimedOut, + None => PendingSyncRpcResolution::Missing, + }; + pending.observe_depth(); + Ok(resolution) +} + +pub(crate) fn host_reply_adapter_error(error: JavascriptExecutionError) -> HostServiceError { + match error { + JavascriptExecutionError::PendingSyncRpcRequest(id) => HostServiceError::new( + "EBUSY", + JavascriptExecutionError::PendingSyncRpcRequest(id).to_string(), + ) + .with_details(json!({ "callId": id })), + JavascriptExecutionError::PendingSyncRpcLimit { limit, observed } => { + HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.reactor.maxBridgeCalls", + limit as u64, + observed as u64, + ) + } + JavascriptExecutionError::ExpiredSyncRpcRequest(id) => HostServiceError::new( + "ESTALE", + JavascriptExecutionError::ExpiredSyncRpcRequest(id).to_string(), + ) + .with_details(json!({ "callId": id })), + other => HostServiceError::new("ERR_AGENTOS_ADAPTER_REPLY", other.to_string()), + } +} - fn clear_pending_sync_rpc( - &self, - id: u64, - ) -> Result { - let mut pending = self.pending_sync_rpc.lock().map_err(|_| { - JavascriptExecutionError::RpcResponse(String::from( - "sync RPC pending-request state lock poisoned", - )) - })?; - match *pending { - Some(PendingSyncRpcState::Pending(current)) if current == id => { - *pending = None; - Ok(PendingSyncRpcResolution::Pending) - } - Some(PendingSyncRpcState::TimedOut(current)) if current == id => { - Ok(PendingSyncRpcResolution::TimedOut) - } - _ => Ok(PendingSyncRpcResolution::Missing), +pub(crate) fn map_host_reply_adapter_response( + response: Result<(), JavascriptExecutionError>, +) -> Result<(), HostServiceError> { + match response { + Err(JavascriptExecutionError::BridgeSettlement(error)) + if error.kind() + == agentos_v8_runtime::host_call::BridgeSettlementErrorKind::StaleCompletion => + { + // The V8 registry emits this marker only after proving that this + // exact session generation owned a host-visible route which was + // canceled before its claimed completion arrived. Unknown call IDs + // and mismatched generations remain fatal and take the branch below. + eprintln!("INFO_AGENTOS_STALE_BRIDGE_COMPLETION: {error}"); + Ok(()) } + Err(error) => Err(host_reply_adapter_error(error)), + Ok(()) => Ok(()), } } +fn encode_host_service_error_payload( + error: &HostServiceError, +) -> Result, JavascriptExecutionError> { + v8_runtime::json_to_cbor_payload(&serde_json::json!({ + "code": error.code, + "message": error.message, + "details": error.details, + })) + .map_err(|encode_error| JavascriptExecutionError::RpcResponse(encode_error.to_string())) +} + +fn decode_bridge_call_args( + method: &str, + call_id: u64, + payload: &[u8], +) -> Result, HostServiceError> { + v8_runtime::cbor_payload_to_json_args(payload).map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_BRIDGE_REQUEST_DECODE", + format!("failed to decode bridge request {method} for call_id={call_id}: {error}"), + ) + }) +} + impl Drop for JavascriptExecution { fn drop(&mut self) { // Closing the V8 producer lets the bridge task drain any terminal // warning/result and then finish when its per-session lane closes. // Aborting the task first would drop the lane while the session thread // was still completing teardown. - let _ = self.v8_session.destroy(); + if self.session_destroyed { + return; + } + if let Err(error) = self.v8_session.destroy() { + eprintln!( + "ERR_AGENTOS_JAVASCRIPT_SESSION_TEARDOWN: failed to destroy V8 session during execution drop: {error}" + ); + } } } @@ -2829,8 +3454,7 @@ impl JavascriptExecutionEngine { } else { build_v8_user_code(&guest_entrypoint, &request.env) }; - let user_code = prepend_v8_runtime_shim( - user_code, + let post_restore_script = build_v8_runtime_init_script( &guest_entrypoint, &process_argv, request.argv0.as_deref(), @@ -2848,7 +3472,13 @@ impl JavascriptExecutionEngine { // Start the event bridge before execution so early sync bridge calls // made during module instantiation/evaluation cannot deadlock waiting // for a response while no host thread is draining session frames yet. - let pending_sync_rpc = Arc::new(Mutex::new(None)); + let max_pending_sync_rpcs = runtime + .resources() + .configured_limit(ResourceClass::BridgeCalls) + .map_or(JAVASCRIPT_EVENT_CHANNEL_CAPACITY, |limit| limit.maximum); + let pending_sync_rpc = Arc::new(Mutex::new(PendingSyncRpcRegistry::new( + max_pending_sync_rpcs, + ))); let exited = Arc::new(AtomicBool::new(false)); let kernel_stdin = Arc::new(LocalKernelStdinBridge::default()); let standalone_translator = translator.clone(); @@ -2895,7 +3525,7 @@ impl JavascriptExecutionEngine { mode: if use_module_mode { 1 } else { 0 }, file_path: execution_file_path, bridge_code: V8RuntimeHost::bridge_code().to_owned(), - post_restore_script: String::new(), + post_restore_script, userland_code: snapshot_userland_code, high_resolution_time: request.guest_runtime.high_resolution_time, user_code, @@ -2930,6 +3560,7 @@ impl JavascriptExecutionEngine { kernel_stdin, _import_cache_guard: import_cache_guard, v8_session, + session_destroyed: false, prepared_execute, _event_bridge_task: event_bridge_task, module_resolution: Mutex::new(( @@ -2971,7 +3602,7 @@ impl JavascriptExecutionEngine { } fn set_pending_sync_rpc_state( - pending_sync_rpc: &Arc>>, + pending_sync_rpc: &Arc>, id: u64, ) -> Result<(), JavascriptExecutionError> { let mut pending = pending_sync_rpc.lock().map_err(|_| { @@ -2979,7 +3610,18 @@ fn set_pending_sync_rpc_state( "sync RPC pending-request state lock poisoned", )) })?; - *pending = Some(PendingSyncRpcState::Pending(id)); + if pending.states.contains_key(&id) { + return Err(JavascriptExecutionError::PendingSyncRpcRequest(id)); + } + let observed = pending.states.len().saturating_add(1); + if observed > pending.maximum { + return Err(JavascriptExecutionError::PendingSyncRpcLimit { + limit: pending.maximum, + observed, + }); + } + pending.states.insert(id, PendingSyncRpcState::Pending(id)); + pending.observe_depth(); Ok(()) } @@ -3118,7 +3760,7 @@ fn javascript_wall_clock_limit_ms(request: &StartJavascriptExecutionRequest) -> fn spawn_javascript_sync_rpc_timeout( id: u64, timeout: Duration, - pending_state: Arc>>, + pending_state: Arc>, responses: Option, ) { let Some(responses) = responses else { @@ -3137,20 +3779,22 @@ fn spawn_javascript_sync_rpc_timeout( if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Timer, async move { tokio::time::sleep(timeout).await; - let should_timeout = match pending_state.lock() { - Ok(mut guard) if *guard == Some(PendingSyncRpcState::Pending(id)) => { - *guard = Some(PendingSyncRpcState::TimedOut(id)); - true - } - Ok(_) => false, - Err(_) => false, - }; + let should_timeout = pending_state + .lock() + .map(|mut guard| match guard.states.get_mut(&id) { + Some(state @ PendingSyncRpcState::Pending(_)) => { + *state = PendingSyncRpcState::TimedOut(id); + true + } + _ => false, + }) + .unwrap_or(false); if !should_timeout { return; } - let _ = write_javascript_sync_rpc_response( + if let Err(error) = write_javascript_sync_rpc_response( &responses, json!({ "id": id, @@ -3163,17 +3807,21 @@ fn spawn_javascript_sync_rpc_timeout( ), }, }), - ); + ) { + eprintln!( + "ERR_AGENTOS_SYNC_RPC_TIMEOUT_REPLY: failed to publish timeout response for call_id={id}: {error}" + ); + } }) { eprintln!("ERR_AGENTOS_TASK_LIMIT: could not arm JavaScript sync RPC timeout: {error}"); } } #[cfg(test)] -fn parse_javascript_sync_rpc_request(line: &str) -> Result { +fn parse_javascript_sync_rpc_request(line: &str) -> Result { let wire: JavascriptSyncRpcRequestWire = serde_json::from_str(line).map_err(|error| error.to_string())?; - Ok(JavascriptSyncRpcRequest { + Ok(HostRpcRequest { id: wire.id, method: wire.method, args: wire.args, @@ -3494,8 +4142,7 @@ fn resolve_v8_entrypoint(cwd: &Path, entrypoint: &str) -> String { // Keep each injected process/runtime value explicit at this one serialization // boundary; grouping them would duplicate the already-typed guest config. #[allow(clippy::too_many_arguments)] -fn prepend_v8_runtime_shim( - user_code: String, +fn build_v8_runtime_init_script( entrypoint: &str, argv: &[String], argv0: Option<&str>, @@ -3563,6 +4210,15 @@ fn prepend_v8_runtime_shim( const nextCwd = {cwd_json}; const nextEnv = {env_json}; const nextHighResolutionTime = {high_resolution_time}; + const visibleEnv = Object.fromEntries( + Object.entries(nextEnv).filter(([key]) => !key.startsWith("AGENTOS_")) + ); + Object.defineProperty(globalThis, "__agentOSProcessConfigEnv", {{ + configurable: true, + enumerable: false, + value: nextEnv, + writable: true, + }}); try {{ const previousProcessConfig = typeof globalThis._processConfig === "object" && globalThis._processConfig !== null @@ -3574,7 +4230,7 @@ fn prepend_v8_runtime_shim( value: Object.freeze({{ ...previousProcessConfig, cwd: nextCwd, - env: nextEnv, + env: visibleEnv, argv: nextArgv, argv0: nextArgv0, high_resolution_time: nextHighResolutionTime, @@ -3582,19 +4238,6 @@ fn prepend_v8_runtime_shim( writable: false, }}); }} catch (_e) {{}} - if (typeof globalThis.__runtimeRefreshProcessConfig === "function") {{ - globalThis.__runtimeRefreshProcessConfig(); - }} - Object.defineProperty(globalThis, "__agentOSProcessConfigEnv", {{ - configurable: true, - enumerable: false, - value: nextEnv, - writable: true, - }}); - const visibleEnv = Object.fromEntries( - Object.entries(nextEnv).filter(([key]) => !key.startsWith("AGENTOS_")) - ); - // Refresh the process module's closure-backed state before user modules run. // Updating only globalThis.process leaves named ESM imports such as // `import {{ cwd }} from "process"` reading the warm snapshot's stale cwd. @@ -3605,10 +4248,7 @@ fn prepend_v8_runtime_shim( if (typeof process !== "undefined") {{ process.argv = nextArgv; process.argv0 = nextArgv0; - process.env = {{ - ...(process.env || {{}}), - ...visibleEnv, - }}; + process.env = {{ ...visibleEnv }}; const configuredHeapLimitMb = {heap_limit_mb}; if (Number.isFinite(configuredHeapLimitMb) && configuredHeapLimitMb > 0) {{ Object.defineProperty(globalThis, "__agentOSV8HeapLimitBytes", {{ @@ -3620,14 +4260,19 @@ fn prepend_v8_runtime_shim( }} if (nextEnv.AGENTOS_ALLOW_PROCESS_BINDINGS === "1" && typeof process.binding === "function") {{ const originalProcessBinding = process.binding.bind(process); + let constantsBinding = null; + try {{ + const bindingRequire = globalThis._moduleModule?.createRequire?.( + nextCwd === "/" + ? "/__agentos_runtime__.js" + : `${{nextCwd.replace(/\/+$/, "")}}/__agentos_runtime__.js`, + ); + constantsBinding = bindingRequire?.("node:constants") ?? null; + constantsBinding = constantsBinding?.default ?? constantsBinding; + }} catch (_e) {{}} process.binding = (name) => {{ const bindingName = String(name); - if ( - bindingName === "constants" && - typeof __agentOSConstantsBinding !== "undefined" - ) {{ - const constantsBinding = - __agentOSConstantsBinding.default ?? __agentOSConstantsBinding; + if (bindingName === "constants" && constantsBinding !== null) {{ return {{ fs: constantsBinding, crypto: constantsBinding, @@ -3678,6 +4323,9 @@ fn prepend_v8_runtime_shim( if (typeof __guestIdentity.execPath === "string" && __guestIdentity.execPath.length > 0) {{ process.execPath = __guestIdentity.execPath; }} + globalThis.__runtimeStreamStdin = nextEnv.AGENTOS_KEEP_STDIN_OPEN === "1"; + globalThis.__runtimeKernelStdin = + nextEnv.AGENTOS_FORWARD_KERNEL_STDIN_RPC === "1"; if (nextEnv.AGENTOS_NODE_IPC === "1" && typeof __runtimeInstallProcessIpcBridge === "function") {{ process.connected = true; __runtimeInstallProcessIpcBridge(); @@ -3822,8 +4470,7 @@ fn prepend_v8_runtime_shim( ].forEach(__dropGlobal); }} }} -}})(); -{user_code}"# +}})();"# ) } @@ -3836,7 +4483,7 @@ fn prepend_v8_runtime_shim( fn spawn_v8_event_bridge( runtime: &RuntimeContext, frame_receiver: V8SessionFrameReceiver, - pending_sync_rpc: Arc>>, + pending_sync_rpc: Arc>, exited: Arc, v8_session: V8SessionHandle, mut local_bridge: LocalBridgeState, @@ -3873,8 +4520,30 @@ fn spawn_v8_event_bridge( } => { // Convert CBOR payload to JSON args let phase_start = Instant::now(); - let args = - v8_runtime::cbor_payload_to_json_args(&payload).unwrap_or_default(); + let args = match decode_bridge_call_args(&method, call_id, &payload) { + Ok(args) => args, + Err(host_error) => { + let encoded = match encode_host_service_error_payload(&host_error) { + Ok(encoded) => encoded, + Err(error) => { + terminate_session_after_bridge_encoding_failure( + &v8_session, + call_id, + &error.to_string(), + ); + break; + } + }; + if let Err(reply_error) = + v8_session.send_bridge_response(call_id, 1, encoded) + { + eprintln!( + "INFO_AGENTOS_STALE_BRIDGE_COMPLETION: failed to send typed decode error for call_id={call_id}: {reply_error}" + ); + } + continue; + } + }; record_sync_bridge_phase( &method, "event_decode_args", @@ -3909,16 +4578,43 @@ fn spawn_v8_event_bridge( if let Some(response) = local_bridge.handle_internal_bridge_call(call_id, &method, &args) { - if let LocalBridgeCallResult::Immediate(response) = response { - let cbor_payload = v8_runtime::json_to_cbor_payload(&response) - .unwrap_or_default(); - if let Err(error) = - v8_session.send_bridge_response(call_id, 0, cbor_payload) - { - eprintln!( - "INFO_AGENTOS_STALE_BRIDGE_COMPLETION: call_id={call_id} error={error}" - ); + let encoded = match response { + LocalBridgeCallResult::Immediate(response) => { + match v8_runtime::json_to_cbor_payload(&response) { + Ok(payload) => (0, payload), + Err(error) => { + terminate_session_after_bridge_encoding_failure( + &v8_session, + call_id, + &error.to_string(), + ); + break; + } + } } + LocalBridgeCallResult::Error(error) => { + match encode_host_service_error_payload(&error) { + Ok(payload) => (1, payload), + Err(error) => { + terminate_session_after_bridge_encoding_failure( + &v8_session, + call_id, + &error.to_string(), + ); + break; + } + } + } + LocalBridgeCallResult::Deferred => continue, + }; + if let Err(error) = v8_session.send_bridge_response( + call_id, + encoded.0, + encoded.1, + ) { + eprintln!( + "INFO_AGENTOS_STALE_BRIDGE_COMPLETION: call_id={call_id} error={error}" + ); } continue; } @@ -3928,10 +4624,21 @@ fn spawn_v8_event_bridge( if method == "_log" || method == "_error" { let output = decode_bridge_output_args(&args); // Respond to the bridge call + let null_payload = match v8_runtime::json_to_cbor_payload(&Value::Null) { + Ok(payload) => payload, + Err(error) => { + terminate_session_after_bridge_encoding_failure( + &v8_session, + call_id, + &error.to_string(), + ); + break; + } + }; if let Err(error) = v8_session.send_bridge_response( call_id, 0, - v8_runtime::json_to_cbor_payload(&Value::Null).unwrap_or_default(), + null_payload, ) { eprintln!( "INFO_AGENTOS_STALE_BRIDGE_COMPLETION: call_id={call_id} error={error}" @@ -3973,10 +4680,34 @@ fn spawn_v8_event_bridge( phase_start.elapsed(), ); - // Track pending sync RPC + // Track exactly one pending sync RPC. A malformed or + // timed-out guest that submits another call before the + // old host operation settles receives a typed busy + // reply; the original waiter is never overwritten. let phase_start = Instant::now(); - if let Ok(mut pending) = pending_sync_rpc.lock() { - *pending = Some(PendingSyncRpcState::Pending(call_id)); + if let Err(error) = + set_pending_sync_rpc_state(&pending_sync_rpc, call_id) + { + let host_error = host_reply_adapter_error(error); + let payload = match encode_host_service_error_payload(&host_error) { + Ok(payload) => payload, + Err(error) => { + terminate_session_after_bridge_encoding_failure( + &v8_session, + call_id, + &error.to_string(), + ); + break; + } + }; + if let Err(reply_error) = + v8_session.send_bridge_response(call_id, 1, payload) + { + eprintln!( + "INFO_AGENTOS_STALE_BRIDGE_COMPLETION: call_id={call_id} error={reply_error}" + ); + } + continue; } record_sync_bridge_phase( &method, @@ -3987,16 +4718,16 @@ fn spawn_v8_event_bridge( let phase_start = Instant::now(); let request_args = translate_request_args_for_legacy(sidecar_method, &args); let mut raw_bytes_args = HashMap::new(); - if sidecar_method == "net.write" - || sidecar_method == "fs.writeSync" - || sidecar_method == "fs.writevSync" - || sidecar_method == "fs.writeFileSync" - || sidecar_method == "crypto.hashUpdate" - { + // Preserve every CBOR byte-string argument losslessly. + // Host-operation decoders, not the V8 bridge, own the + // method schema; a per-method allowlist here silently + // converted new binary syscall arguments into opaque + // JSON placeholders (for example sendmsg payloads). + for index in 0..args.len() { if let Ok(Some(bytes)) = - v8_runtime::cbor_payload_raw_byte_arg(&payload, 1) + v8_runtime::cbor_payload_raw_byte_arg(&payload, index) { - raw_bytes_args.insert(1, bytes); + raw_bytes_args.insert(index, bytes); } } if method == "_fsReadRaw" || method == "_fsReadFileRangeRaw" { @@ -4008,7 +4739,7 @@ fn spawn_v8_event_bridge( phase_start.elapsed(), ); Some(JavascriptExecutionEvent::SyncRpcRequest( - JavascriptSyncRpcRequest { + HostRpcRequest { id: call_id, method: sidecar_method.to_owned(), args: request_args, @@ -4136,6 +4867,21 @@ fn spawn_v8_event_bridge( Ok((receiver, task)) } +fn terminate_session_after_bridge_encoding_failure( + session: &V8SessionHandle, + call_id: u64, + error: &str, +) { + eprintln!( + "ERR_AGENTOS_BRIDGE_RESPONSE_ENCODE: failed to encode bridge response for call_id={call_id}: {error}; terminating the JavaScript session" + ); + if let Err(destroy_error) = session.destroy() { + eprintln!( + "ERR_AGENTOS_BRIDGE_RESPONSE_TEARDOWN: failed to terminate JavaScript session after bridge encoding failure for call_id={call_id}: {destroy_error}" + ); + } +} + async fn send_javascript_event_async( sender: &EventSender, gauge: &agentos_bridge::queue_tracker::QueueGauge, @@ -4295,6 +5041,10 @@ impl LocalBridgeState { let resolved = self.with_module_resolver(|resolver| { resolver.resolve_module(specifier, parent, mode) }); + let resolved = match resolved { + Ok(resolved) => resolved, + Err(error) => return Some(LocalBridgeCallResult::Error(error)), + }; if resolved.is_none() && self.has_module_reader() { return None; } @@ -4303,7 +5053,11 @@ impl LocalBridgeState { )) } "_moduleFormat" => { - let format = self.module_format(args.first().and_then(Value::as_str).unwrap_or("")); + let format = + match self.module_format(args.first().and_then(Value::as_str).unwrap_or("")) { + Ok(format) => format, + Err(error) => return Some(LocalBridgeCallResult::Error(error)), + }; if format.is_none() && self.has_module_reader() { return None; } @@ -4314,7 +5068,11 @@ impl LocalBridgeState { )) } "_loadFile" | "_loadFileSync" => { - let source = self.load_file(args.first().and_then(Value::as_str).unwrap_or("")); + let source = + match self.load_file(args.first().and_then(Value::as_str).unwrap_or("")) { + Ok(source) => source, + Err(error) => return Some(LocalBridgeCallResult::Error(error)), + }; if source.is_none() && self.has_module_reader() { return None; } @@ -4323,7 +5081,10 @@ impl LocalBridgeState { )) } "_batchResolveModules" => { - let resolved = self.batch_resolve_modules(args); + let resolved = match self.batch_resolve_modules(args) { + Ok(resolved) => resolved, + Err(error) => return Some(LocalBridgeCallResult::Error(error)), + }; if self.has_module_reader() && resolved .as_array() @@ -4339,8 +5100,11 @@ impl LocalBridgeState { "_cryptoRandomFill" => { let size = args.first().and_then(Value::as_u64).unwrap_or(16) as usize; let mut bytes = vec![0u8; size]; - if getrandom(&mut bytes).is_err() { - return Some(LocalBridgeCallResult::Immediate(Value::Null)); + if let Err(error) = getrandom(&mut bytes) { + return Some(LocalBridgeCallResult::Error(HostServiceError::new( + "ERR_AGENTOS_ENTROPY_UNAVAILABLE", + format!("failed to fill the guest random buffer: {error}"), + ))); } Some(LocalBridgeCallResult::Immediate(Value::String( v8_runtime::base64_encode_pub(&bytes), @@ -4348,8 +5112,11 @@ impl LocalBridgeState { } "_cryptoRandomUUID" => { let mut bytes = [0u8; 16]; - if getrandom(&mut bytes).is_err() { - return Some(LocalBridgeCallResult::Immediate(Value::Null)); + if let Err(error) = getrandom(&mut bytes) { + return Some(LocalBridgeCallResult::Error(HostServiceError::new( + "ERR_AGENTOS_ENTROPY_UNAVAILABLE", + format!("failed to generate a guest UUID: {error}"), + ))); } bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; @@ -4398,12 +5165,39 @@ impl LocalBridgeState { .map(Value::String) .unwrap_or(Value::Null); } - let (dispatch_method, payload_json) = dispatch + let Some((dispatch_method, payload_json)) = dispatch .strip_prefix("__bd:") .and_then(|value| value.split_once(':')) - .unwrap_or(("", "[]")); - let payload = serde_json::from_str::(payload_json).unwrap_or_else(|_| json!([])); - let args = payload.as_array().cloned().unwrap_or_default(); + else { + return Value::String( + timer_dispatch_error(javascript_timer_error( + "ERR_AGENTOS_BRIDGE_DISPATCH_DECODE", + "malformed bridge dispatch envelope", + )) + .to_string(), + ); + }; + let payload = match serde_json::from_str::(payload_json) { + Ok(payload) => payload, + Err(error) => { + return Value::String( + timer_dispatch_error(javascript_timer_error( + "ERR_AGENTOS_BRIDGE_DISPATCH_DECODE", + format!("invalid dispatch payload: {error}"), + )) + .to_string(), + ); + } + }; + let Some(args) = payload.as_array() else { + return Value::String( + timer_dispatch_error(javascript_timer_error( + "ERR_AGENTOS_BRIDGE_DISPATCH_DECODE", + "dispatch payload must be an array", + )) + .to_string(), + ); + }; let result = match dispatch_method { "kernelHandleRegister" => { if let (Some(id), Some(description)) = ( @@ -4490,41 +5284,58 @@ impl LocalBridgeState { } } - fn create_kernel_timer(&mut self, delay_ms: u64, repeat: bool) -> Result { + fn create_kernel_timer( + &mut self, + delay_ms: u64, + repeat: bool, + ) -> Result { self.register_timer(delay_ms, repeat) } /// Allocate a fresh timer id and register a one-shot (`repeat == false`) /// tracking entry at generation 0. Used by the bridge-timer path so the /// queued wheel action can be cancelled (its entry removed) on `clear`/teardown. - fn register_oneshot_timer(&mut self, delay_ms: u64) -> Result { + fn register_oneshot_timer(&mut self, delay_ms: u64) -> Result { self.register_timer(delay_ms, false) } - fn register_timer(&mut self, delay_ms: u64, repeat: bool) -> Result { + fn register_timer(&mut self, delay_ms: u64, repeat: bool) -> Result { let mut timers = self.timers.lock().map_err(|_| { - String::from( - "ERR_AGENTOS_JAVASCRIPT_TIMER_STATE: JavaScript timer registry lock poisoned", + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_STATE", + "JavaScript timer registry lock poisoned", ) })?; if timers.len() >= self.max_timers { - return Err(format!( - "ERR_AGENTOS_JAVASCRIPT_TIMER_LIMIT: execution exceeded {} active timers; raise limits.jsRuntime.maxTimers", - self.max_timers + return Err(HostServiceError::limit( + "ERR_AGENTOS_JAVASCRIPT_TIMER_LIMIT", + "limits.jsRuntime.maxTimers", + self.max_timers as u64, + timers.len().saturating_add(1) as u64, )); } let reservation = self .timer_resources .as_ref() .ok_or_else(|| { - String::from( - "ERR_AGENTOS_RUNTIME_NOT_INJECTED: JavaScript timers require a resource ledger", + javascript_timer_error( + "ERR_AGENTOS_RUNTIME_NOT_INJECTED", + "JavaScript timers require a resource ledger", ) })? .reserve(agentos_runtime::accounting::ResourceClass::Timers, 1) - .map_err(|error| error.to_string())?; + .map_err(|error| { + javascript_timer_error("ERR_AGENTOS_RESOURCE_LIMIT", error.to_string()) + .with_details(json!({ + "limitName": "limits.jsRuntime.maxTimers", + "requested": 1, + })) + })?; let timer_id = self.next_timer_id.checked_add(1).ok_or_else(|| { - String::from("ERR_AGENTOS_JAVASCRIPT_TIMER_ID_EXHAUSTED: execution exhausted timer IDs") + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_ID_EXHAUSTED", + "execution exhausted timer IDs", + ) })?; self.next_timer_id = timer_id; timers.insert( @@ -4539,33 +5350,40 @@ impl LocalBridgeState { Ok(timer_id) } - fn arm_kernel_timer(&self, timer_id: u64) -> Result<(), String> { + fn arm_kernel_timer(&self, timer_id: u64) -> Result<(), HostServiceError> { let Some(session) = self.v8_session.clone() else { - return Err(String::from( - "ERR_AGENTOS_JAVASCRIPT_TIMER_SESSION: timer has no live V8 session", + return Err(javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_SESSION", + "timer has no live V8 session", )); }; let (delay_ms, generation, timers) = { let mut timers = self.timers.lock().map_err(|_| { - String::from( - "ERR_AGENTOS_JAVASCRIPT_TIMER_STATE: JavaScript timer registry lock poisoned", + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_STATE", + "JavaScript timer registry lock poisoned", ) })?; let entry = timers.get_mut(&timer_id).ok_or_else(|| { - format!("ERR_AGENTOS_JAVASCRIPT_TIMER_UNKNOWN: unknown timer {timer_id}") + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_UNKNOWN", + format!("unknown timer {timer_id}"), + ) })?; entry.generation = entry.generation.checked_add(1).ok_or_else(|| { - format!( - "ERR_AGENTOS_JAVASCRIPT_TIMER_GENERATION_EXHAUSTED: timer {timer_id} exhausted generations" + javascript_timer_error( + "ERR_AGENTOS_JAVASCRIPT_TIMER_GENERATION_EXHAUSTED", + format!("timer {timer_id} exhausted generations"), ) })?; (entry.delay_ms, entry.generation, self.timers.clone()) }; let runtime = self.runtime.as_ref().ok_or_else(|| { - String::from( - "ERR_AGENTOS_RUNTIME_NOT_INJECTED: JavaScript timers require a process RuntimeContext", + javascript_timer_error( + "ERR_AGENTOS_RUNTIME_NOT_INJECTED", + "JavaScript timers require a process RuntimeContext", ) })?; TimerWheel::get(runtime)?.schedule( @@ -4602,7 +5420,7 @@ impl LocalBridgeState { let timer_id = match self.register_oneshot_timer(delay_ms) { Ok(timer_id) => timer_id, Err(error) => { - settle_timer_bridge_response(&session, call_id, 1, error.into_bytes()); + settle_timer_bridge_response(&session, call_id, 1, error.to_string().into_bytes()); return; } }; @@ -4619,7 +5437,7 @@ impl LocalBridgeState { Ok(wheel) => wheel, Err(error) => { self.clear_kernel_timer(timer_id); - settle_timer_bridge_response(&session, call_id, 1, error.into_bytes()); + settle_timer_bridge_response(&session, call_id, 1, error.to_string().into_bytes()); return; } }; @@ -4634,7 +5452,7 @@ impl LocalBridgeState { }, ) { self.clear_kernel_timer(timer_id); - settle_timer_bridge_response(&session, call_id, 1, error.into_bytes()); + settle_timer_bridge_response(&session, call_id, 1, error.to_string().into_bytes()); } } @@ -4642,7 +5460,7 @@ impl LocalBridgeState { self.module_reader.is_some() } - fn batch_resolve_modules(&mut self, args: &[Value]) -> Value { + fn batch_resolve_modules(&mut self, args: &[Value]) -> Result { self.with_module_resolver(|resolver| resolver.batch_resolve_modules(args)) } @@ -4651,16 +5469,18 @@ impl LocalBridgeState { specifier: &str, from_dir: &str, mode: ModuleResolveMode, - ) -> Option { + ) -> Result, HostServiceError> { if self.js_runtime_denies_specifier(specifier) { if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { eprintln!("resolve DENIED: {specifier} from {from_dir}"); } - return None; + return Ok(None); } let resolved = self .with_module_resolver(|resolver| resolver.resolve_module(specifier, from_dir, mode)); - if resolved.is_none() && std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { + if resolved.as_ref().is_ok_and(Option::is_none) + && std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() + { eprintln!("resolve MISS: {specifier} from {from_dir} mode={mode:?}"); } resolved @@ -4688,11 +5508,14 @@ impl LocalBridgeState { } } - fn module_format(&mut self, path: &str) -> Option { + fn module_format( + &mut self, + path: &str, + ) -> Result, HostServiceError> { self.with_module_resolver(|resolver| resolver.module_format(path)) } - fn load_file(&mut self, path: &str) -> Option { + fn load_file(&mut self, path: &str) -> Result, HostServiceError> { self.with_module_resolver(|resolver| resolver.load_file(path)) } @@ -4721,46 +5544,83 @@ impl LocalBridgeState { } impl ModuleFsReader for &mut dyn ModuleFsReader { - fn canonical_guest_path(&mut self, guest_path: &str) -> Option { + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { (**self).canonical_guest_path(guest_path) } - fn read_to_string(&mut self, guest_path: &str) -> Option { + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError> { (**self).read_to_string(guest_path) } - fn path_is_dir(&mut self, guest_path: &str) -> Option { + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError> { (**self).path_is_dir(guest_path) } - fn path_exists(&mut self, guest_path: &str) -> bool { + fn path_exists(&mut self, guest_path: &str) -> Result { (**self).path_exists(guest_path) } } impl ModuleFsReader for &mut GuestPathTranslator { - fn canonical_guest_path(&mut self, guest_path: &str) -> Option { - GuestPathTranslator::canonical_guest_path(self, guest_path) + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { + self.canonical_module_guest_path(guest_path) } - fn read_to_string(&mut self, guest_path: &str) -> Option { - let host_path = self.guest_to_host(guest_path)?; - fs::read_to_string(host_path).ok() + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError> { + let Some(host_path) = self.module_guest_to_host(guest_path)? else { + return Ok(None); + }; + match fs::read_to_string(&host_path) { + Ok(contents) => Ok(Some(contents)), + Err(error) if module_fs_io_is_missing(&error) => Ok(None), + Err(error) => Err(module_fs_io_error("read", guest_path, error)), + } } - fn path_is_dir(&mut self, guest_path: &str) -> Option { - self.guest_to_host(guest_path) - .and_then(|host_path| fs::metadata(host_path).ok()) - .map(|metadata| metadata.is_dir()) + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError> { + let Some(host_path) = self.module_guest_to_host(guest_path)? else { + return Ok(None); + }; + match fs::metadata(&host_path) { + Ok(metadata) => Ok(Some(metadata.is_dir())), + Err(error) if module_fs_io_is_missing(&error) => Ok(None), + Err(error) => Err(module_fs_io_error("stat", guest_path, error)), + } } - fn path_exists(&mut self, guest_path: &str) -> bool { - self.guest_to_host(guest_path) - .map(|host_path| host_path.exists()) - .unwrap_or(false) + fn path_exists(&mut self, guest_path: &str) -> Result { + Ok(self.path_is_dir(guest_path)?.is_some()) } } +fn module_fs_io_is_missing(error: &std::io::Error) -> bool { + matches!(error.raw_os_error(), Some(2 | 20)) || error.kind() == std::io::ErrorKind::NotFound +} + +fn module_fs_io_error( + operation: &str, + guest_path: &str, + error: std::io::Error, +) -> HostServiceError { + let code = match error.raw_os_error() { + Some(2) => "ENOENT", + Some(13) => "EACCES", + Some(20) => "ENOTDIR", + Some(40) => "ELOOP", + _ => "EIO", + }; + HostServiceError::new( + code, + format!("module filesystem {operation} failed for {guest_path}: {error}"), + ) +} + /// Standard Node module resolution executed as pure path algebra over a /// [`ModuleFsReader`]. The same algorithm backs both the legacy host-direct /// path (reader = host path translator) and the live VM path (reader = kernel @@ -4778,32 +5638,52 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { Self { reader, cache } } - pub fn batch_resolve_modules(&mut self, args: &[Value]) -> Value { + pub fn batch_resolve_modules(&mut self, args: &[Value]) -> Result { let requests = args .first() .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - Value::Array( - requests - .into_iter() - .map(|request| { - let pair = request.as_array().cloned().unwrap_or_default(); - let specifier = pair.first().and_then(Value::as_str).unwrap_or(""); - let referrer = pair.get(1).and_then(Value::as_str).unwrap_or("/"); - self.resolve_module(specifier, referrer, ModuleResolveMode::Import) - .and_then(|resolved| { - self.load_file(&resolved).map(|source| { - json!({ - "resolved": resolved, - "source": source, - }) - }) - }) - .unwrap_or(Value::Null) + .ok_or_else(|| { + HostServiceError::new( + "EINVAL", + "batch module resolution requires an array of [specifier, referrer] pairs", + ) + })? + .clone(); + let mut results = Vec::with_capacity(requests.len()); + for (index, request) in requests.into_iter().enumerate() { + let pair = request.as_array().ok_or_else(|| { + HostServiceError::new( + "EINVAL", + format!("batch module request {index} must be an array pair"), + ) + })?; + let specifier = pair.first().and_then(Value::as_str).ok_or_else(|| { + HostServiceError::new( + "EINVAL", + format!("batch module request {index} requires a string specifier"), + ) + })?; + let referrer = pair.get(1).and_then(Value::as_str).ok_or_else(|| { + HostServiceError::new( + "EINVAL", + format!("batch module request {index} requires a string referrer"), + ) + })?; + let value = if let Some(resolved) = + self.resolve_module(specifier, referrer, ModuleResolveMode::Import)? + { + self.load_file(&resolved)?.map_or(Value::Null, |source| { + json!({ + "resolved": resolved, + "source": source, + }) }) - .collect(), - ) + } else { + Value::Null + }; + results.push(value); + } + Ok(Value::Array(results)) } pub fn resolve_module( @@ -4811,39 +5691,44 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { specifier: &str, from_dir: &str, mode: ModuleResolveMode, - ) -> Option { + ) -> Result, HostServiceError> { let normalized_from_path = self .reader - .canonical_guest_path(from_dir) + .canonical_guest_path(from_dir)? .unwrap_or_else(|| normalize_guest_path(from_dir)); - let normalized_from = if self.cached_stat(&normalized_from_path) == Some(false) { + let normalized_from = if self.cached_stat(&normalized_from_path)? == Some(false) { dirname_guest_path(&normalized_from_path) } else { normalize_module_resolve_context(&normalized_from_path) }; let cache_key = (specifier.to_owned(), normalized_from.clone(), mode); if let Some(cached) = self.cache.resolve_results.get(&cache_key) { - return cached.clone(); + return Ok(cached.clone()); } let resolved = if let Some(builtin) = normalize_builtin_specifier(specifier) { Some(builtin) } else if specifier.starts_with("file:") { - guest_path_from_file_url(specifier) - .and_then(|file_path| self.resolve_path(&file_path, mode)) + match guest_path_from_file_url(specifier) { + Some(file_path) => self.resolve_path(&file_path, mode)?, + None => None, + } } else if specifier.starts_with('/') { - self.resolve_path(specifier, mode) + self.resolve_path(specifier, mode)? } else if specifier.starts_with("./") || specifier.starts_with("../") || specifier == "." || specifier == ".." { - self.resolve_path(&join_guest_path(&normalized_from, specifier), mode) + self.resolve_path(&join_guest_path(&normalized_from, specifier), mode)? } else if specifier.starts_with('#') { - self.resolve_package_imports(specifier, &normalized_from, mode) + self.resolve_package_imports(specifier, &normalized_from, mode)? } else { - self.resolve_package_self_reference(specifier, &normalized_from, mode) - .or_else(|| self.resolve_node_modules(specifier, &normalized_from, mode)) + let own = self.resolve_package_self_reference(specifier, &normalized_from, mode)?; + match own { + Some(resolved) => Some(resolved), + None => self.resolve_node_modules(specifier, &normalized_from, mode)?, + } }; if resolved.is_some() || module_resolution_miss_is_stable(&normalized_from) { @@ -4851,17 +5736,19 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { .resolve_results .insert(cache_key, resolved.clone()); } - resolved + Ok(resolved) } - pub fn load_file(&mut self, path: &str) -> Option { + pub fn load_file(&mut self, path: &str) -> Result, HostServiceError> { let bare = path.trim_start_matches("node:"); if is_builtin_specifier(path) { - return Some(build_builtin_module_wrapper(bare)); + return Ok(Some(build_builtin_module_wrapper(bare))); } - let source = self.reader.read_to_string(path)?; - Some( + let Some(source) = self.reader.read_to_string(path)? else { + return Ok(None); + }; + Ok(Some( if matches!( Path::new(path).extension().and_then(|ext| ext.to_str()), Some("js" | "mjs" | "cjs") @@ -4870,55 +5757,66 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } else { source }, - ) + )) } - pub fn module_format(&mut self, path: &str) -> Option { + pub fn module_format( + &mut self, + path: &str, + ) -> Result, HostServiceError> { if let Some(cached) = self.cache.module_format_results.get(path) { - return *cached; + return Ok(*cached); } - let format = self.detect_module_format(path); + let format = self.detect_module_format(path)?; self.cache .module_format_results .insert(path.to_owned(), format); - format + Ok(format) } - fn detect_module_format(&mut self, path: &str) -> Option { + fn detect_module_format( + &mut self, + path: &str, + ) -> Result, HostServiceError> { if is_builtin_specifier(path) { - return Some(LocalResolvedModuleFormat::Module); + return Ok(Some(LocalResolvedModuleFormat::Module)); } let normalized = normalize_guest_path(path); - match Path::new(&normalized) - .extension() - .and_then(|ext| ext.to_str()) - { - Some("mjs" | "mts") => Some(LocalResolvedModuleFormat::Module), - Some("cjs" | "cts") => Some(LocalResolvedModuleFormat::Commonjs), - Some("json") => Some(LocalResolvedModuleFormat::Json), - Some("js") => Some( - if self - .nearest_package_json_type_for_guest_path(&normalized) - .as_deref() - == Some("module") - { - LocalResolvedModuleFormat::Module - } else { - LocalResolvedModuleFormat::Commonjs - }, - ), - _ => None, - } + Ok( + match Path::new(&normalized) + .extension() + .and_then(|ext| ext.to_str()) + { + Some("mjs" | "mts") => Some(LocalResolvedModuleFormat::Module), + Some("cjs" | "cts") => Some(LocalResolvedModuleFormat::Commonjs), + Some("json") => Some(LocalResolvedModuleFormat::Json), + Some("js") => Some( + if self + .nearest_package_json_type_for_guest_path(&normalized)? + .as_deref() + == Some("module") + { + LocalResolvedModuleFormat::Module + } else { + LocalResolvedModuleFormat::Commonjs + }, + ), + _ => None, + }, + ) } - fn nearest_package_json_type_for_guest_path(&mut self, guest_path: &str) -> Option { + fn nearest_package_json_type_for_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { let mut dir = dirname_guest_path(guest_path); loop { let package_json_path = join_guest_path(&dir, "package.json"); - if let Some(package_json) = self.read_package_json(&package_json_path) { - return package_json.package_type; + if let Some(package_json) = self.read_package_json(&package_json_path)? { + return Ok(package_json.package_type); } // Node package scopes do not inherit `type` across a node_modules // boundary. This also matters for pnpm's nested symlink layout: if @@ -4930,7 +5828,7 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } dir = dirname_guest_path(&dir); } - None + Ok(None) } fn resolve_package_imports( @@ -4938,11 +5836,11 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { request: &str, from_dir: &str, mode: ModuleResolveMode, - ) -> Option { + ) -> Result, HostServiceError> { let mut dir = normalize_guest_path(from_dir); loop { let pkg_json_path = join_guest_path(&dir, "package.json"); - if let Some(pkg_json) = self.read_package_json(&pkg_json_path) { + if let Some(pkg_json) = self.read_package_json(&pkg_json_path)? { if let Some(imports) = &pkg_json.imports { if let Some(target) = resolve_imports_target(imports, request, mode) { let target_path = if target.starts_with('/') { @@ -4952,7 +5850,7 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { }; return self.resolve_path(&target_path, mode); } - return None; + return Ok(None); } } if dir == "/" { @@ -4960,7 +5858,7 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } dir = dirname_guest_path(&dir); } - None + Ok(None) } fn resolve_package_self_reference( @@ -4968,12 +5866,14 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { request: &str, from_dir: &str, mode: ModuleResolveMode, - ) -> Option { - let (package_name, subpath) = split_package_request(request)?; + ) -> Result, HostServiceError> { + let Some((package_name, subpath)) = split_package_request(request) else { + return Ok(None); + }; let mut dir = normalize_guest_path(from_dir); loop { let pkg_json_path = join_guest_path(&dir, "package.json"); - if let Some(pkg_json) = self.read_package_json(&pkg_json_path) { + if let Some(pkg_json) = self.read_package_json(&pkg_json_path)? { if pkg_json.name.as_deref() == Some(package_name) { return self.resolve_package_entry_from_dir(&dir, subpath, mode); } @@ -4983,7 +5883,7 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } dir = dirname_guest_path(&dir); } - None + Ok(None) } fn resolve_node_modules( @@ -4991,8 +5891,10 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { request: &str, from_dir: &str, mode: ModuleResolveMode, - ) -> Option { - let (package_name, subpath) = split_package_request(request)?; + ) -> Result, HostServiceError> { + let Some((package_name, subpath)) = split_package_request(request) else { + return Ok(None); + }; // Standard Node resolution over the faithful VFS: walk ancestor // `node_modules` directories (following symlinks via the importer's @@ -5003,9 +5905,9 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { loop { for package_dir in node_modules_direct_candidate_dirs(&dir, package_name) { if let Some(entry) = - self.resolve_package_entry_from_dir(&package_dir, subpath, mode) + self.resolve_package_entry_from_dir(&package_dir, subpath, mode)? { - return Some(entry); + return Ok(Some(entry)); } } if dir == "/" { @@ -5014,15 +5916,16 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { dir = dirname_guest_path(&dir); } - ["/root/node_modules", "/node_modules"] - .into_iter() - .find_map(|root| { - self.resolve_package_entry_from_dir( - &join_guest_path(root, package_name), - subpath, - mode, - ) - }) + for root in ["/root/node_modules", "/node_modules"] { + if let Some(entry) = self.resolve_package_entry_from_dir( + &join_guest_path(root, package_name), + subpath, + mode, + )? { + return Ok(Some(entry)); + } + } + Ok(None) } fn resolve_package_entry_from_dir( @@ -5030,11 +5933,11 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { package_dir: &str, subpath: &str, mode: ModuleResolveMode, - ) -> Option { + ) -> Result, HostServiceError> { let package_json_path = join_guest_path(package_dir, "package.json"); - let pkg_json = self.read_package_json(&package_json_path); - if pkg_json.is_none() && !self.cached_exists(package_dir) { - return None; + let pkg_json = self.read_package_json(&package_json_path)?; + if pkg_json.is_none() && !self.cached_exists(package_dir)? { + return Ok(None); } if let Some(pkg_json) = pkg_json.as_ref() { @@ -5044,9 +5947,13 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } else { format!("./{subpath}") }; - let exports_target = resolve_exports_target(exports, &exports_subpath, mode)?; + let Some(exports_target) = resolve_exports_target(exports, &exports_subpath, mode) + else { + return Ok(None); + }; let target_path = join_guest_path(package_dir, &exports_target); - return self.resolve_path(&target_path, mode).or(Some(target_path)); + let resolved = self.resolve_path(&target_path, mode)?; + return Ok(resolved.or(Some(target_path))); } } @@ -5059,93 +5966,109 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { .and_then(|pkg_json| pkg_json.main.as_deref()) .unwrap_or("index.js"); let entry_path = join_guest_path(package_dir, entry_field); - self.resolve_path(&entry_path, mode) - .or_else(|| self.resolve_path(&join_guest_path(package_dir, "index"), mode)) + if let Some(resolved) = self.resolve_path(&entry_path, mode)? { + return Ok(Some(resolved)); + } + self.resolve_path(&join_guest_path(package_dir, "index"), mode) } - fn resolve_path(&mut self, base_path: &str, mode: ModuleResolveMode) -> Option { - if self.cached_stat(base_path) == Some(false) { - return Some(normalize_guest_path(base_path)); + fn resolve_path( + &mut self, + base_path: &str, + mode: ModuleResolveMode, + ) -> Result, HostServiceError> { + if self.cached_stat(base_path)? == Some(false) { + return Ok(Some(normalize_guest_path(base_path))); } for extension in [".js", ".json", ".mjs", ".cjs"] { let candidate = format!("{}{}", normalize_guest_path(base_path), extension); - if self.cached_exists(&candidate) { - return Some(candidate); + if self.cached_exists(&candidate)? { + return Ok(Some(candidate)); } } - if self.cached_stat(base_path) == Some(true) { + if self.cached_stat(base_path)? == Some(true) { let pkg_json_path = join_guest_path(base_path, "package.json"); - if let Some(pkg_json) = self.read_package_json(&pkg_json_path) { + if let Some(pkg_json) = self.read_package_json(&pkg_json_path)? { if let Some(main) = pkg_json.main.as_deref() { let entry_path = join_guest_path(base_path, main); if entry_path != normalize_guest_path(base_path) { - if let Some(entry) = self.resolve_path(&entry_path, mode) { - return Some(entry); + if let Some(entry) = self.resolve_path(&entry_path, mode)? { + return Ok(Some(entry)); } } } if mode == ModuleResolveMode::Import && pkg_json.package_type.as_deref() == Some("module") - && self.cached_exists(&join_guest_path(base_path, "index.js")) + && self.cached_exists(&join_guest_path(base_path, "index.js"))? { - return Some(join_guest_path(base_path, "index.js")); + return Ok(Some(join_guest_path(base_path, "index.js"))); } } for extension in [".js", ".json", ".mjs", ".cjs"] { let index_path = join_guest_path(base_path, &format!("index{extension}")); - if self.cached_exists(&index_path) { - return Some(index_path); + if self.cached_exists(&index_path)? { + return Ok(Some(index_path)); } } } - None + Ok(None) } - fn read_package_json(&mut self, guest_path: &str) -> Option { + fn read_package_json( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { if let Some(cached) = self.cache.package_json_results.get(guest_path).cloned() { - return cached; + return Ok(cached); } - let parsed = self - .reader - .read_to_string(guest_path) - .and_then(|contents| serde_json::from_str::(&contents).ok()); + let parsed = match self.reader.read_to_string(guest_path)? { + Some(contents) => Some(serde_json::from_str::(&contents).map_err( + |error| { + HostServiceError::new( + "ERR_INVALID_PACKAGE_CONFIG", + format!("invalid package configuration {guest_path}: {error}"), + ) + }, + )?), + None => None, + }; if parsed.is_some() || module_path_miss_is_stable(guest_path) { self.cache .package_json_results .insert(guest_path.to_owned(), parsed.clone()); } - parsed + Ok(parsed) } - fn cached_exists(&mut self, guest_path: &str) -> bool { + fn cached_exists(&mut self, guest_path: &str) -> Result { if let Some(cached) = self.cache.exists_results.get(guest_path) { - return *cached; + return Ok(*cached); } - let exists = self.reader.path_exists(guest_path); + let exists = self.reader.path_exists(guest_path)?; if exists || module_path_miss_is_stable(guest_path) { self.cache .exists_results .insert(guest_path.to_owned(), exists); } - exists + Ok(exists) } - fn cached_stat(&mut self, guest_path: &str) -> Option { + fn cached_stat(&mut self, guest_path: &str) -> Result, HostServiceError> { if let Some(cached) = self.cache.stat_results.get(guest_path) { - return *cached; + return Ok(*cached); } - let result = self.reader.path_is_dir(guest_path); + let result = self.reader.path_is_dir(guest_path)?; if result.is_some() || module_path_miss_is_stable(guest_path) { self.cache .stat_results .insert(guest_path.to_owned(), result); } - result + Ok(result) } } @@ -7401,6 +8324,34 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::tempdir; + #[test] + fn malformed_bridge_payload_is_a_typed_decode_error() { + let error = decode_bridge_call_args("_resolveModule", 17, &[0xff]) + .expect_err("malformed CBOR must not become an empty argument list"); + assert_eq!(error.code, "ERR_AGENTOS_BRIDGE_REQUEST_DECODE"); + assert!(error.message.contains("call_id=17")); + } + + #[test] + fn malformed_batch_module_request_is_a_typed_validation_error() { + let mut bridge = LocalBridgeState::default(); + let error = bridge + .batch_resolve_modules(&[Value::Null]) + .expect_err("malformed batch request must not become an empty result"); + assert_eq!(error.code, "EINVAL"); + } + + #[test] + fn malformed_internal_dispatch_payload_returns_a_typed_error() { + let mut bridge = LocalBridgeState::default(); + let response = bridge + .handle_polyfill_dispatch(&[Value::String(String::from("__bd:kernelTimerCreate:{"))]); + assert!(response + .as_str() + .expect("dispatch error response") + .contains("ERR_AGENTOS_BRIDGE_DISPATCH_DECODE")); + } + #[test] fn dispose_context_reclaims_one_shot_metadata_without_reusing_ids() { let mut engine = JavascriptExecutionEngine::default(); @@ -7627,7 +8578,8 @@ mod tests { let writer = File::from(writer_fd); let response_writer = JavascriptSyncRpcResponseWriter::new(writer, Duration::from_millis(50)); - let pending = Arc::new(Mutex::new(Some(PendingSyncRpcState::Pending(7)))); + let pending = Arc::new(Mutex::new(PendingSyncRpcRegistry::new(2))); + set_pending_sync_rpc_state(&pending, 7).expect("register timeout test call"); spawn_javascript_sync_rpc_timeout( 7, @@ -7652,9 +8604,112 @@ mod tests { .expect("timeout message") .contains("timed out after 20ms")); assert_eq!( - *pending.lock().expect("pending state lock"), - Some(PendingSyncRpcState::TimedOut(7)) + pending.lock().expect("pending state lock").states.get(&7), + Some(&PendingSyncRpcState::TimedOut(7)) + ); + } + + #[test] + fn pending_sync_rpc_registry_is_bounded_keyed_and_nonlossy() { + let pending = Arc::new(Mutex::new(PendingSyncRpcRegistry::new(2))); + set_pending_sync_rpc_state(&pending, 7).expect("first concurrent waiter"); + set_pending_sync_rpc_state(&pending, 8).expect("second concurrent waiter"); + + let duplicate = set_pending_sync_rpc_state(&pending, 7) + .expect_err("duplicate call ID must not replace its waiter"); + assert!(matches!( + duplicate, + JavascriptExecutionError::PendingSyncRpcRequest(7) + )); + assert_eq!(host_reply_adapter_error(duplicate).code, "EBUSY"); + + let over_limit = set_pending_sync_rpc_state(&pending, 9) + .expect_err("third distinct waiter exceeds configured admission"); + assert!(matches!( + over_limit, + JavascriptExecutionError::PendingSyncRpcLimit { + limit: 2, + observed: 3, + } + )); + let host_error = host_reply_adapter_error(over_limit); + assert_eq!(host_error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + host_error.details.as_ref().unwrap()["limitName"], + "limits.reactor.maxBridgeCalls" + ); + + let guard = pending.lock().expect("pending state lock"); + assert_eq!(guard.states.len(), 2, "rejection preserves both waiters"); + assert_eq!(guard.states.get(&7), Some(&PendingSyncRpcState::Pending(7))); + assert_eq!(guard.states.get(&8), Some(&PendingSyncRpcState::Pending(8))); + drop(guard); + + assert_eq!( + clear_pending_sync_rpc(&pending, 8).expect("settle exact waiter"), + PendingSyncRpcResolution::Pending + ); + let guard = pending.lock().expect("pending state lock"); + assert_eq!(guard.states.len(), 1); + assert!(guard.states.contains_key(&7)); + } + + #[test] + fn direct_host_reply_ignores_only_proven_stale_bridge_completions() { + map_host_reply_adapter_response(Err(JavascriptExecutionError::BridgeSettlement( + agentos_v8_runtime::host_call::BridgeSettlementError::stale_completion(String::from( + "ERR_AGENTOS_BRIDGE_STALE_COMPLETION: response for canceled host-visible bridge call_id 7 in session v8-exec-1 generation Some(3)", + )), + ))) + .expect("exact retired-route completion is benign"); + + for fatal in [ + "ERR_AGENTOS_BRIDGE_UNKNOWN_CALL_ID: response for unknown bridge call_id 7", + "ERR_AGENTOS_BRIDGE_STALE_GENERATION: response call_id 7 named the wrong generation", + "prefix ERR_AGENTOS_BRIDGE_STALE_COMPLETION: guest-controlled text is not proof", + ] { + let error = map_host_reply_adapter_response(Err( + JavascriptExecutionError::RpcResponse(String::from(fatal)), + )) + .expect_err("unproven bridge response failure must stay fatal"); + assert_eq!(error.code, "ERR_AGENTOS_ADAPTER_REPLY"); + } + } + + #[test] + fn javascript_host_error_payload_preserves_fields_without_parsing_message() { + let payload = encode_host_service_error_payload( + &HostServiceError::new("EIO", "EACCES: guest-controlled diagnostic").with_details( + serde_json::json!({ + "path": "/guest/file", + "retryable": false, + }), + ), + ) + .expect("encode structured host error"); + let ciborium::Value::Map(entries) = + ciborium::from_reader::(payload.as_slice()) + .expect("decode structured host error") + else { + panic!("structured host error must be a CBOR map"); + }; + let field = |name: &str| { + entries.iter().find_map(|(key, value)| match key { + ciborium::Value::Text(key) if key == name => Some(value), + _ => None, + }) + }; + assert_eq!( + field("code"), + Some(&ciborium::Value::Text(String::from("EIO"))) + ); + assert_eq!( + field("message"), + Some(&ciborium::Value::Text(String::from( + "EACCES: guest-controlled diagnostic" + ))) ); + assert!(matches!(field("details"), Some(ciborium::Value::Map(_)))); } #[test] @@ -7855,9 +8910,9 @@ mod tests { assert_eq!( result, - Some(Value::String(String::from( + Ok(Some(Value::String(String::from( "/node_modules/next/dist/cli/next-build.js" - ))) + )))) ); fs::remove_dir_all(&root).expect("remove temp module tree"); @@ -8128,7 +9183,10 @@ mod tests { let error = state .register_timer(10, false) .expect_err("second timer must hit the ledger bound"); - assert!(error.contains("limits.jsRuntime.maxTimers"), "{error}"); + assert!( + error.message.contains("limits.jsRuntime.maxTimers"), + "{error:?}" + ); assert_eq!(state.timers.lock().unwrap().len(), 1); state.clear_kernel_timer(first); diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index ba69788fac..a49c6b86f7 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -11,7 +11,10 @@ pub mod v8_host; pub mod v8_ipc; pub mod v8_runtime; +pub mod abi; +pub mod backend; pub mod benchmark; +pub mod host; #[allow(dead_code, unused_imports)] pub mod javascript; pub mod python; @@ -22,11 +25,11 @@ pub use agentos_v8_runtime::bridge::EMULATED_OPENSSL_VERSION; pub use agentos_v8_runtime::execution::GuestModuleReader; pub use javascript::{ record_sync_bridge_request_enqueued, record_sync_bridge_request_observed, - CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptContext, JavascriptExecution, - JavascriptExecutionEngine, JavascriptExecutionError, JavascriptExecutionEvent, - JavascriptExecutionLimits, JavascriptExecutionResult, JavascriptSyncRpcRequest, - LocalModuleResolutionCache, LocalResolvedModuleFormat, ModuleFsReader, ModuleResolveMode, - ModuleResolver, StartJavascriptExecutionRequest, + CreateJavascriptContextRequest, GuestRuntimeConfig, HostRpcRequest, JavascriptContext, + JavascriptExecution, JavascriptExecutionEngine, JavascriptExecutionError, + JavascriptExecutionEvent, JavascriptExecutionLimits, JavascriptExecutionResult, + JavascriptSyncRpcResponder, LocalModuleResolutionCache, LocalResolvedModuleFormat, + ModuleFsReader, ModuleResolveMode, ModuleResolver, StartJavascriptExecutionRequest, }; pub use python::{ CreatePythonContextRequest, PythonContext, PythonExecution, PythonExecutionEngine, @@ -34,7 +37,7 @@ pub use python::{ PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponder, PythonVfsRpcResponsePayload, PythonVfsRpcStat, StartPythonExecutionRequest, }; -pub use signal::{NodeSignalDispositionAction, NodeSignalHandlerRegistration}; +pub use signal::{ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration}; pub use wasm::{ CreateWasmContextRequest, NativeBinaryFormat, StartWasmExecutionRequest, WasmContext, WasmExecution, WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, diff --git a/crates/execution/src/node_import_cache.rs b/crates/execution/src/node_import_cache.rs index 8cc1a04bd8..7879fa6762 100644 --- a/crates/execution/src/node_import_cache.rs +++ b/crates/execution/src/node_import_cache.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::env; use std::fs; use std::io; @@ -57,7 +56,9 @@ const BUNDLED_MICROPIP_WHL: &[u8] = const BUNDLED_CLICK_WHL: &[u8] = include_bytes!("../assets/pyodide/click-8.3.1-py3-none-any.whl"); const NODE_PYTHON_RUNNER_SOURCE: &str = include_str!("../assets/runners/python-runner.mjs"); -static CLEANED_NODE_IMPORT_CACHE_ROOTS: OnceLock>> = OnceLock::new(); +static NODE_IMPORT_CACHE_ROOT_CLEANUPS: OnceLock< + Mutex>>>, +> = OnceLock::new(); #[cfg(test)] static NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_MS: AtomicU64 = AtomicU64::new(0); #[cfg(test)] @@ -2397,15 +2398,32 @@ fn default_node_import_cache_base_dir() -> PathBuf { } fn cleanup_stale_node_import_caches_once(base_dir: &Path) { - let cleaned_roots = CLEANED_NODE_IMPORT_CACHE_ROOTS.get_or_init(|| Mutex::new(BTreeSet::new())); - let should_cleanup = cleaned_roots - .lock() - .map(|mut roots| roots.insert(base_dir.to_path_buf())) - .unwrap_or(true); - - if should_cleanup { - cleanup_stale_node_import_caches(base_dir); - } + run_node_import_cache_root_cleanup_once(base_dir, || { + cleanup_stale_node_import_caches(base_dir) + }); +} + +/// Synchronize the one-time stale-root cleanup itself, not just the decision +/// to start it. A concurrent cache constructor for the same base must wait +/// until deletion finishes; otherwise the first cleanup can remove the second +/// constructor's newly materialized directory. +fn run_node_import_cache_root_cleanup_once(base_dir: &Path, cleanup: impl FnOnce()) { + let cleanup_registry = NODE_IMPORT_CACHE_ROOT_CLEANUPS + .get_or_init(|| Mutex::new(std::collections::BTreeMap::new())); + let mut cleanup_registry = match cleanup_registry.lock() { + Ok(registry) => registry, + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_NODE_IMPORT_CACHE_CLEANUP_LOCK_POISONED: recovering the process-wide stale-cache cleanup registry after a panic" + ); + poisoned.into_inner() + } + }; + let cleanup_cell = cleanup_registry + .entry(base_dir.to_path_buf()) + .or_insert_with(|| Arc::new(OnceLock::new())) + .clone(); + cleanup_cell.get_or_init(|| cleanup()); } fn cleanup_stale_node_import_caches(base_dir: &Path) { @@ -2556,7 +2574,11 @@ impl NodeImportCache { let _materialization_guard = self .materialization_lock .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .map_err(|_| { + io::Error::other( + "ERR_AGENTOS_NODE_IMPORT_CACHE_LOCK_POISONED: cache materialization lock was poisoned by a prior panic", + ) + })?; if self.is_materialized() { return Ok(()); } @@ -3745,8 +3767,9 @@ fn write_file_if_changed(path: &Path, contents: &str) -> Result<(), io::Error> { #[cfg(test)] mod tests { use super::{ - NodeImportCache, NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_LOCK, - NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_MS, NODE_WASM_RUNNER_SOURCE, + run_node_import_cache_root_cleanup_once, NodeImportCache, + NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_LOCK, NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_MS, + NODE_WASM_RUNNER_SOURCE, }; use crate::host_node::node_binary; use serde_json::Value; @@ -3755,10 +3778,64 @@ mod tests { use std::io::Write; use std::path::Path; use std::process::{Command, Output, Stdio}; - use std::sync::atomic::Ordering; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::{mpsc, Arc}; + use std::thread; use std::time::Duration; use tempfile::tempdir; + #[test] + fn concurrent_cache_constructor_waits_for_same_root_cleanup() { + let temp = tempdir().expect("create cleanup test root"); + let base_dir = temp.path().join("shared-cache-base"); + let (cleanup_started_tx, cleanup_started_rx) = mpsc::channel(); + let (release_cleanup_tx, release_cleanup_rx) = mpsc::channel(); + let first_base = base_dir.clone(); + let first = thread::spawn(move || { + run_node_import_cache_root_cleanup_once(&first_base, || { + cleanup_started_tx.send(()).expect("publish cleanup start"); + release_cleanup_rx.recv().expect("release root cleanup"); + }); + }); + cleanup_started_rx.recv().expect("observe cleanup start"); + + let second_cleanup_ran = Arc::new(AtomicBool::new(false)); + let second_cleanup_ran_in_thread = Arc::clone(&second_cleanup_ran); + let (second_started_tx, second_started_rx) = mpsc::channel(); + let (second_finished_tx, second_finished_rx) = mpsc::channel(); + let second = thread::spawn(move || { + second_started_tx + .send(()) + .expect("publish second constructor start"); + run_node_import_cache_root_cleanup_once(&base_dir, || { + second_cleanup_ran_in_thread.store(true, Ordering::Release); + }); + second_finished_tx + .send(()) + .expect("publish second constructor finish"); + }); + second_started_rx + .recv() + .expect("observe second constructor start"); + assert!( + second_finished_rx + .recv_timeout(Duration::from_millis(50)) + .is_err(), + "same-root constructor must not pass while stale cleanup can still delete its cache" + ); + + release_cleanup_tx.send(()).expect("finish root cleanup"); + first.join().expect("first cleanup thread"); + second.join().expect("second constructor thread"); + second_finished_rx + .try_recv() + .expect("second constructor finishes after cleanup"); + assert!( + !second_cleanup_ran.load(Ordering::Acquire), + "same root must run stale cleanup exactly once" + ); + } + fn assert_node_available() { let output = Command::new(node_binary()) .arg("--version") @@ -4273,6 +4350,14 @@ print(json.dumps({ export async function loadPyodide(options) { return { setStdin(_stdin) {}, + FS: { + mkdirTree(_path) {}, + }, + globals: { + set(_name, _value) {}, + delete(_name) {}, + }, + runPython(_code) {}, async loadPackage(packages) { options.stdout(`packages:${packages.join(',')}`); options.stderr(`base:${options.packageBaseUrl}`); @@ -5010,13 +5095,17 @@ for (let index = 0; index < 520; index += 1) { assert!(NODE_WASM_RUNNER_SOURCE.contains("const cwdReadOnly = readOnlyForCwd(guestCwd);")); assert!(NODE_WASM_RUNNER_SOURCE .contains("preopens[cwdMount] = createPreopen(HOST_CWD, cwdReadOnly);")); + assert!(NODE_WASM_RUNNER_SOURCE.contains( + "guestPathIsReadOnly(guestPath) &&\n (hasMutationOpenFlags(oflags) || hasWriteRights(rightsBase))" + )); assert!(NODE_WASM_RUNNER_SOURCE - .contains("if (mapping.readOnly) {\n return WASI_ERRNO_ROFS;\n }")); + .contains("if (guestReadOnlyDenied) {\n return denyReadOnlyMutation();\n }")); assert!(NODE_WASM_RUNNER_SOURCE.contains("readOnly: preopenSpec?.readOnly === true,")); assert!(NODE_WASM_RUNNER_SOURCE .contains("resolveModuleGuestPathToHostMapping(guestPath)?.readOnly === true")); - assert!(NODE_WASM_RUNNER_SOURCE - .contains("if (handle?.readOnly === true) {\n return 1;\n }")); + assert!(NODE_WASM_RUNNER_SOURCE.contains( + "if (handle?.readOnly === true || isWorkspaceReadOnly()) {\n return WASI_ERRNO_ROFS;\n }" + )); } #[test] diff --git a/crates/execution/src/python.rs b/crates/execution/src/python.rs index b976b0feb5..b890db6971 100644 --- a/crates/execution/src/python.rs +++ b/crates/execution/src/python.rs @@ -1,8 +1,19 @@ +use crate::backend::{ + DirectHostReplyHandle, DirectHostReplyTarget, ExecutionBackend, ExecutionBackendKind, + ExecutionExit, ExecutionWakeHandle, ExecutionWakeIdentity, HostCallIdentity, HostCallReply, + HostServiceError, PayloadLimit, ShutdownOutcome, ShutdownReason, SignalCheckpointOutcome, +}; use crate::common::{encode_json_string, frozen_time_ms}; +use crate::host::{ + BoundedBytes, BoundedProcessLaunchRequest, BoundedString, BoundedUsize, BoundedVec, + DnsAddressFamily, FilesystemOperation, HostOperation, HttpHeader, ManagedTcpEndpoint, + ManagedUdpFamily, NetworkOperation, PathAttributeUpdate, ProcessLaunchOptions, + ProcessLaunchRequest, ProcessOperation, +}; use crate::javascript::{ - CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution, + CreateJavascriptContextRequest, GuestRuntimeConfig, HostRpcRequest, JavascriptExecution, JavascriptExecutionEngine, JavascriptExecutionError, JavascriptExecutionEvent, - JavascriptExecutionLimits, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest, + JavascriptExecutionLimits, JavascriptSyncRpcResponder, StartJavascriptExecutionRequest, }; use crate::node_import_cache::{NodeImportCache, NODE_IMPORT_CACHE_ASSET_ROOT_ENV}; use crate::runtime_support::{ @@ -10,6 +21,8 @@ use crate::runtime_support::{ NODE_DISABLE_COMPILE_CACHE_ENV, NODE_FROZEN_TIME_ENV, }; use crate::v8_runtime; +use agentos_bridge::queue_tracker::{register_queue, QueueGauge, TrackedLimit}; +use agentos_runtime::accounting::ResourceClass; use agentos_runtime::RuntimeContext; use base64::Engine as _; use serde::{Deserialize, Serialize}; @@ -41,11 +54,12 @@ const DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES: usize = 1024 * 1024; const DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS: u64 = 5 * 60 * 1000; const DEFAULT_PYTHON_MAX_OLD_SPACE_MB: usize = 0; const DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS: u64 = 30_000; +const DEFAULT_PYTHON_PENDING_VFS_RPCS: usize = 512; const PYTHON_SYNC_RPC_DATA_BYTES: usize = 20 * 1024 * 1024; const PYTHON_SYNC_RPC_WAIT_TIMEOUT_MS: u64 = 120_000; const PYTHON_PREWARM_TIMEOUT: Duration = Duration::from_secs(120); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum PythonVfsRpcMethod { Read, Write, @@ -101,7 +115,7 @@ impl PythonVfsRpcMethod { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct PythonVfsRpcRequest { pub id: u64, pub method: PythonVfsRpcMethod, @@ -323,7 +337,7 @@ pub struct StartPythonExecutionRequest { pub enum PythonExecutionEvent { Stdout(Vec), Stderr(Vec), - JavascriptSyncRpcRequest(JavascriptSyncRpcRequest), + HostRpcRequest(HostRpcRequest), VfsRpcRequest(Box), Exited(i32), } @@ -360,6 +374,10 @@ pub enum PythonExecutionError { Control(std::io::Error), TimedOut(Duration), PendingVfsRpcRequest(u64), + PendingVfsRpcLimit { + limit: usize, + observed: usize, + }, RpcResponse(String), OutputBufferExceeded { stream: &'static str, @@ -419,6 +437,10 @@ impl fmt::Display for PythonExecutionError { "guest Python execution requires servicing pending VFS RPC request {id}" ) } + Self::PendingVfsRpcLimit { limit, observed } => write!( + f, + "ERR_AGENTOS_RESOURCE_LIMIT: pending Python VFS RPC calls observed {observed}, exceeding limits.reactor.maxBridgeCalls ({limit})" + ), Self::RpcResponse(message) => { write!( f, @@ -462,7 +484,8 @@ pub struct PythonExecution { inner: JavascriptExecution, pyodide_dist_path: PathBuf, managed_host_files: PythonManagedHostFiles, - pending_vfs_rpc: Arc>>, + pending_vfs_rpc: Arc>, + managed_network: Arc>, v8_session: crate::v8_host::V8SessionHandle, output_buffer_max_bytes: usize, execution_timeout: Option, @@ -477,8 +500,228 @@ pub struct PythonExecution { /// parking the dispatcher or borrowing the process table across an await. #[derive(Debug, Clone)] pub struct PythonVfsRpcResponder { - pending_vfs_rpc: Arc>>, - v8_session: crate::v8_host::V8SessionHandle, + pending_vfs_rpc: Arc>, + managed_network: Arc>, + javascript_responder: JavascriptSyncRpcResponder, +} + +/// One Python adapter request normalized to the common host-service boundary. +#[derive(Debug)] +pub struct PythonHostCall { + pub operation: HostOperation, + pub reply: DirectHostReplyHandle, +} + +#[derive(Debug, Clone)] +enum PythonHostReplyKind { + Empty, + FileRead, + Stat, + ReadDirectory, + ReadLink, + RunCaptured, + Http, + Dns, + SocketCreated(PythonManagedSocketReservation), + SocketSent, + SocketReceived, + SocketClosed(u64), + UdpReceived, +} + +impl PythonHostReplyKind { + fn rollback_socket_reservation(&self) { + if let Self::SocketCreated(reservation) = self { + reservation.rollback(); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PythonManagedSocketKind { + Tcp, + Udp, +} + +#[derive(Debug, Clone)] +struct PythonManagedSocket { + kind: PythonManagedSocketKind, + host_socket_id: String, +} + +#[derive(Debug)] +enum PythonManagedSocketSlot { + Reserved(PythonManagedSocketKind), + Live(PythonManagedSocket), +} + +#[derive(Debug)] +struct PythonManagedSocketReservationInner { + socket_id: u64, + kind: PythonManagedSocketKind, + state: Arc>, +} + +impl Drop for PythonManagedSocketReservationInner { + fn drop(&mut self) { + let mut state = self.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED: recovering reserved socket {}", + self.socket_id + ); + poisoned.into_inner() + }); + if matches!( + state.sockets.get(&self.socket_id), + Some(PythonManagedSocketSlot::Reserved(kind)) if *kind == self.kind + ) { + state.sockets.remove(&self.socket_id); + } + } +} + +#[derive(Debug, Clone)] +struct PythonManagedSocketReservation(Arc); + +impl PythonManagedSocketReservation { + fn commit(&self, host_socket_id: String) -> Result { + let mut state = self.0.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED: recovering socket reservation {} during commit", + self.0.socket_id + ); + poisoned.into_inner() + }); + match state.sockets.get(&self.0.socket_id) { + Some(PythonManagedSocketSlot::Reserved(kind)) if *kind == self.0.kind => {} + _ => { + return Err(HostServiceError::new( + "ESTALE", + format!( + "Python socket reservation {} is no longer live", + self.0.socket_id + ), + )) + } + } + state.sockets.insert( + self.0.socket_id, + PythonManagedSocketSlot::Live(PythonManagedSocket { + kind: self.0.kind, + host_socket_id, + }), + ); + Ok(self.0.socket_id) + } + + fn rollback(&self) { + let mut state = self.0.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED: recovering socket reservation {}", + self.0.socket_id + ); + poisoned.into_inner() + }); + if matches!( + state.sockets.get(&self.0.socket_id), + Some(PythonManagedSocketSlot::Reserved(kind)) if *kind == self.0.kind + ) { + state.sockets.remove(&self.0.socket_id); + } + } +} + +#[derive(Debug)] +struct PythonManagedNetworkState { + next_socket_id: u64, + maximum: usize, + sockets: BTreeMap, +} + +impl PythonManagedNetworkState { + fn new(maximum: usize) -> Self { + Self { + next_socket_id: 1, + maximum, + sockets: BTreeMap::new(), + } + } + + fn reserve( + state: &Arc>, + kind: PythonManagedSocketKind, + ) -> Result { + let mut locked = state.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED", + "Python managed-network state lock poisoned", + ) + })?; + let observed = locked.sockets.len().saturating_add(1); + if observed > locked.maximum { + return Err(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.resources.maxOpenFds", + locked.maximum as u64, + observed as u64, + )); + } + let socket_id = locked.next_socket_id; + let next_socket_id = locked.next_socket_id.checked_add(1).ok_or_else(|| { + HostServiceError::new("EOVERFLOW", "Python managed socket id space exhausted") + })?; + locked.next_socket_id = next_socket_id; + locked + .sockets + .insert(socket_id, PythonManagedSocketSlot::Reserved(kind)); + drop(locked); + Ok(PythonManagedSocketReservation(Arc::new( + PythonManagedSocketReservationInner { + socket_id, + kind, + state: Arc::clone(state), + }, + ))) + } + + fn socket( + &self, + socket_id: u64, + expected: Option, + ) -> Result { + let socket = self.sockets.get(&socket_id).ok_or_else(|| { + HostServiceError::new("EBADF", format!("unknown Python socket {socket_id}")) + .with_details(json!({ "socketId": socket_id })) + })?; + let PythonManagedSocketSlot::Live(socket) = socket else { + return Err(HostServiceError::new( + "EBUSY", + format!("Python socket {socket_id} is still being created"), + ) + .with_details(json!({ "socketId": socket_id }))); + }; + let socket = socket.clone(); + if let Some(expected) = expected { + if socket.kind != expected { + return Err(HostServiceError::new( + "EINVAL", + format!("Python socket {socket_id} has the wrong socket kind"), + ) + .with_details(json!({ + "socketId": socket_id, + "expected": format!("{expected:?}"), + "actual": format!("{:?}", socket.kind), + }))); + } + } + Ok(socket) + } +} + +#[derive(Debug)] +struct PythonHostReplyTarget { + responder: PythonVfsRpcResponder, + kind: PythonHostReplyKind, } #[derive(Debug)] @@ -487,6 +730,28 @@ struct PendingVfsRpc { timeout_abort: Option, } +#[derive(Debug)] +struct PendingVfsRpcRegistry { + entries: BTreeMap, + maximum: usize, + gauge: Arc, +} + +impl PendingVfsRpcRegistry { + fn new(maximum: usize) -> Self { + let maximum = maximum.max(1); + Self { + entries: BTreeMap::new(), + maximum, + gauge: register_queue(TrackedLimit::PendingPythonVfsRpcCalls, maximum), + } + } + + fn observe_depth(&self) { + self.gauge.observe_depth(self.entries.len()); + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PendingVfsRpcState { Pending(u64), @@ -504,20 +769,21 @@ impl PythonExecution { pub fn vfs_rpc_responder(&self) -> PythonVfsRpcResponder { PythonVfsRpcResponder { pending_vfs_rpc: Arc::clone(&self.pending_vfs_rpc), - v8_session: self.v8_session.clone(), + managed_network: Arc::clone(&self.managed_network), + javascript_responder: self.inner.sync_rpc_responder(), } } - pub fn execution_id(&self) -> &str { - &self.execution_id + pub fn javascript_sync_rpc_responder(&self) -> crate::JavascriptSyncRpcResponder { + self.inner.sync_rpc_responder() } - pub fn child_pid(&self) -> u32 { - self.child_pid + pub fn execution_id(&self) -> &str { + &self.execution_id } - pub fn uses_shared_v8_runtime(&self) -> bool { - self.inner.uses_shared_v8_runtime() + pub fn native_process_id(&self) -> Option { + (self.child_pid != 0).then_some(self.child_pid) } pub fn start_prepared(&mut self) -> Result<(), PythonExecutionError> { @@ -677,7 +943,7 @@ impl PythonExecution { /// manually without a kernel/service loop. pub fn try_service_standalone_module_sync_rpc( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { self.inner .try_service_standalone_module_sync_rpc(request) @@ -689,7 +955,7 @@ impl PythonExecution { #[doc(hidden)] pub fn try_service_standalone_stdin_sync_rpc( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { self.inner .handle_kernel_stdin_sync_rpc(request) @@ -700,7 +966,7 @@ impl PythonExecution { &mut self, timeout: Duration, ) -> Result, PythonExecutionError> { - let deadline = Instant::now() + timeout; + let deadline = checked_python_poll_deadline(timeout)?; loop { let remaining = deadline.saturating_duration_since(Instant::now()); match self @@ -759,7 +1025,7 @@ impl PythonExecution { match event { Some(PythonExecutionEvent::Stdout(chunk)) => stdout.extend(&chunk), Some(PythonExecutionEvent::Stderr(chunk)) => stderr.extend(&chunk), - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { // Module-resolution sync RPCs are serviced host-directly via // the JS execution's own translator (the standalone Python // wait loop runs without a kernel/service loop). @@ -782,6 +1048,10 @@ impl PythonExecution { ))); } Some(PythonExecutionEvent::VfsRpcRequest(request)) => { + if let Some((code, message)) = python_vfs_rpc_standalone_error(request.method) { + self.respond_vfs_rpc_error(request.id, code, message)?; + continue; + } return Err(PythonExecutionError::PendingVfsRpcRequest(request.id)); } Some(PythonExecutionEvent::Exited(exit_code)) => { @@ -820,13 +1090,20 @@ impl PythonExecution { JavascriptExecutionEvent::SyncRpcRequest(request) => { if request.method == "_pythonRpc" { let request = parse_python_bridge_sync_rpc_request(&request)?; - set_pending_vfs_rpc_state(&self.pending_vfs_rpc, request.id)?; + if let Err(error) = set_pending_vfs_rpc_state(&self.pending_vfs_rpc, request.id) + { + self.inner + .sync_rpc_responder() + .respond_host_error(request.id, python_vfs_rpc_admission_error(error)) + .map_err(map_javascript_error)?; + return Ok(None); + } spawn_python_vfs_rpc_timeout( &self.runtime, request.id, self.vfs_rpc_timeout, self.pending_vfs_rpc.clone(), - self.v8_session.clone(), + self.inner.sync_rpc_responder(), )?; Ok(Some(PythonExecutionEvent::VfsRpcRequest(Box::new(request)))) } else { @@ -845,9 +1122,7 @@ impl PythonExecution { )?; Ok(None) } else { - Ok(Some(PythonExecutionEvent::JavascriptSyncRpcRequest( - request, - ))) + Ok(Some(PythonExecutionEvent::HostRpcRequest(request))) } } } @@ -855,11 +1130,579 @@ impl PythonExecution { } } +fn checked_python_poll_deadline(timeout: Duration) -> Result { + Instant::now().checked_add(timeout).ok_or_else(|| { + PythonExecutionError::InvalidLimit(format!( + "blocking poll timeout of {}ms exceeds the host clock range", + timeout.as_millis() + )) + }) +} + +fn validate_python_captured_reply_limit( + max_buffer: usize, + max_reply_bytes: usize, +) -> Result<(), HostServiceError> { + let worst_case_reply_bytes = max_buffer + .saturating_mul(2) + .saturating_mul(6) + .saturating_add(512); + if worst_case_reply_bytes > max_reply_bytes { + return Err(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.reactor.maxBridgeResponseBytes", + max_reply_bytes as u64, + worst_case_reply_bytes as u64, + )); + } + Ok(()) +} + +impl ExecutionBackend for PythonExecution { + fn kind(&self) -> ExecutionBackendKind { + ExecutionBackendKind::Python + } + + fn native_process_id(&self) -> Option { + PythonExecution::native_process_id(self) + } + + fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { + Some(ExecutionWakeHandle::new( + identity, + Arc::new(self.v8_session.clone()), + )) + } + + fn is_prepared_for_start(&self) -> bool { + PythonExecution::is_prepared_for_start(self) + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + PythonExecution::start_prepared(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_START", error.to_string()) + }) + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + if let ShutdownReason::Signal(signal) = reason { + if let Some(process_id) = self.native_process_id() { + return Ok(ShutdownOutcome::ForwardSignal { process_id, signal }); + } + // Shared Python runs inside V8 and therefore has no OS wait status + // from which to recover the terminating signal. + self.kill().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + return Ok(ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal, + core_dumped: false, + })); + } + self.kill().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + Ok(ShutdownOutcome::AwaitExit) + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + let result = if paused { + PythonExecution::pause(self) + } else { + PythonExecution::resume(self) + }; + result.map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_CONTROL", error.to_string()) + }) + } + + fn write_stdin(&mut self, _bytes: &[u8]) -> Result<(), HostServiceError> { + // Sidecar-managed Python reads fd 0 from the kernel pipe. + Ok(()) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + PythonExecution::close_stdin(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_STDIN", error.to_string()) + }) + } + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + _flags: u32, + ) -> Result { + let Some(wake) = self.wake_handle(identity) else { + return Ok(if let Some(process_id) = self.native_process_id() { + SignalCheckpointOutcome::ForwardToProcess { process_id } + } else { + SignalCheckpointOutcome::Unsupported + }); + }; + wake.publish_signal(signal, delivery_token) + .map_err(|error| HostServiceError::new(error.code(), error.to_string()))?; + Ok(SignalCheckpointOutcome::Published) + } +} + fn python_wait_remaining(timeout: Option, started: Instant) -> Option { timeout.map(|limit| limit.saturating_sub(started.elapsed())) } impl PythonVfsRpcResponder { + pub fn try_host_call( + &self, + request: PythonVfsRpcRequest, + identity: HostCallIdentity, + max_request_bytes: usize, + max_reply_bytes: usize, + ) -> Result, HostServiceError> { + const CWD_FD: u32 = u32::MAX; + const MAX_PATH_BYTES: usize = 4096; + const MAX_HOST_BYTES: usize = 253; + const MAX_HTTP_URL_BYTES: usize = 8 * 1024; + const MAX_HTTP_METHOD_BYTES: usize = 32; + const MAX_HTTP_HEADERS: usize = 256; + const MAX_HTTP_HEADER_BYTES: usize = 64 * 1024; + const MAX_DNS_RESULTS: usize = 64; + const PYTHON_SOCKET_DEFAULT_RECV: usize = 65_536; + const PYTHON_SOCKET_MAX_RECV: usize = 4 * 1024 * 1024; + + let request_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_request_bytes)?; + let reply_limit = + PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes)?; + let path_limit = PayloadLimit::new("runtime.filesystem.maxPathBytes", MAX_PATH_BYTES)?; + let host_limit = PayloadLimit::new("runtime.network.maxHostnameBytes", MAX_HOST_BYTES)?; + let socket_id_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_request_bytes)?; + let bounded_host = |host: String, label: &str| { + if host.is_empty() { + return Err(HostServiceError::new( + "EINVAL", + format!("{label} must not be empty"), + )); + } + BoundedString::try_new(host, &host_limit) + }; + let decode_body = |encoded: Option<&str>, label: &str| { + let Some(encoded) = encoded else { + return Ok(Vec::new()); + }; + base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|error| { + HostServiceError::new("EINVAL", format!("{label} is invalid: {error}")) + }) + }; + let managed_socket = |socket_id: Option, expected| { + let socket_id = socket_id.ok_or_else(|| { + HostServiceError::new("EINVAL", "Python socket operation requires socketId") + })?; + let state = self.managed_network.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED", + "Python managed-network state lock poisoned", + ) + })?; + Ok((socket_id, state.socket(socket_id, expected)?)) + }; + let bounded_path = |path: String, label: &str| { + if !path.starts_with('/') { + return Err(HostServiceError::new( + "EINVAL", + format!("{label} must be an absolute guest path"), + ) + .with_details(json!({ "path": path }))); + } + BoundedString::try_new(path, &path_limit) + }; + + let path = || bounded_path(request.path.clone(), "Python filesystem path"); + let (operation, kind) = match request.method { + PythonVfsRpcMethod::Read => { + // Account for base64 expansion and the small Python response + // envelope before any file body is allocated by the kernel. + let body_limit = max_reply_bytes.saturating_sub(128) / 4 * 3; + ( + HostOperation::Filesystem(FilesystemOperation::ReadFileAt { + dir_fd: CWD_FD, + path: path()?, + max_bytes: BoundedUsize::try_new(body_limit, &reply_limit)?, + }), + PythonHostReplyKind::FileRead, + ) + } + PythonVfsRpcMethod::Write => { + let encoded = request.content_base64.as_deref().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python fsWrite requires contentBase64") + })?; + let bytes = base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|error| { + HostServiceError::new( + "EINVAL", + format!("Python fsWrite contentBase64 is invalid: {error}"), + ) + })?; + ( + HostOperation::Filesystem(FilesystemOperation::WriteFileAt { + dir_fd: CWD_FD, + path: path()?, + bytes: BoundedBytes::try_new(bytes, &request_limit)?, + mode: request.mode, + }), + PythonHostReplyKind::Empty, + ) + } + PythonVfsRpcMethod::Stat => ( + HostOperation::Filesystem(FilesystemOperation::NodeStatAt { + dir_fd: CWD_FD, + path: path()?, + }), + PythonHostReplyKind::Stat, + ), + PythonVfsRpcMethod::Lstat => ( + HostOperation::Filesystem(FilesystemOperation::NodeLstatAt { + dir_fd: CWD_FD, + path: path()?, + }), + PythonHostReplyKind::Stat, + ), + PythonVfsRpcMethod::ReadDir => ( + HostOperation::Filesystem(FilesystemOperation::ReadDirectoryAt { + dir_fd: CWD_FD, + path: path()?, + max_entries: BoundedUsize::try_new( + 4096, + &PayloadLimit::new("runtime.filesystem.maxReaddirEntries", 4096)?, + )?, + max_reply_bytes: BoundedUsize::try_new(max_reply_bytes, &reply_limit)?, + }), + PythonHostReplyKind::ReadDirectory, + ), + PythonVfsRpcMethod::Mkdir => ( + HostOperation::Filesystem(if request.recursive { + FilesystemOperation::CreateDirectoriesAt { + dir_fd: CWD_FD, + path: path()?, + mode: request.mode, + } + } else { + FilesystemOperation::CreateDirectoryAt { + dir_fd: CWD_FD, + path: path()?, + mode: request.mode.unwrap_or(0o777), + } + }), + PythonHostReplyKind::Empty, + ), + PythonVfsRpcMethod::Unlink | PythonVfsRpcMethod::Rmdir => ( + HostOperation::Filesystem(FilesystemOperation::UnlinkAt { + dir_fd: CWD_FD, + path: path()?, + remove_directory: request.method == PythonVfsRpcMethod::Rmdir, + }), + PythonHostReplyKind::Empty, + ), + PythonVfsRpcMethod::Rename => { + let destination = request.destination.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python fsRename requires destination") + })?; + ( + HostOperation::Filesystem(FilesystemOperation::RenameAt { + old_dir_fd: CWD_FD, + old_path: path()?, + new_dir_fd: CWD_FD, + new_path: bounded_path(destination, "Python rename destination")?, + flags: 0, + }), + PythonHostReplyKind::Empty, + ) + } + PythonVfsRpcMethod::Symlink => { + let target = request.target.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python fsSymlink requires target") + })?; + ( + HostOperation::Filesystem(FilesystemOperation::SymlinkAt { + target: BoundedString::try_new(target, &path_limit)?, + dir_fd: CWD_FD, + path: path()?, + }), + PythonHostReplyKind::Empty, + ) + } + PythonVfsRpcMethod::ReadLink => ( + HostOperation::Filesystem(FilesystemOperation::ReadLinkAt { + dir_fd: CWD_FD, + path: path()?, + max_bytes: BoundedUsize::try_new( + MAX_PATH_BYTES.min(max_reply_bytes), + &reply_limit, + )?, + }), + PythonHostReplyKind::ReadLink, + ), + PythonVfsRpcMethod::Setattr => ( + HostOperation::Filesystem(FilesystemOperation::SetAttributesAt { + dir_fd: CWD_FD, + path: path()?, + update: PathAttributeUpdate { + mode: request.mode, + uid: request.uid, + gid: request.gid, + atime_ms: request.atime_ms, + mtime_ms: request.mtime_ms, + }, + follow_symlinks: true, + }), + PythonHostReplyKind::Empty, + ), + PythonVfsRpcMethod::SubprocessRun => { + let command = request.command.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python subprocessRun requires a command") + })?; + let launch = ProcessLaunchRequest { + command, + args: request.args.clone(), + options: ProcessLaunchOptions { + argv0: request.argv0.clone(), + cwd: request.cwd.clone(), + env: request.env.clone(), + shell: request.shell, + stdio: vec![ + String::from("pipe"), + String::from("pipe"), + String::from("pipe"), + ], + ..ProcessLaunchOptions::default() + }, + }; + let max_buffer = request.max_buffer.unwrap_or(1024 * 1024); + // The child service may retain maxBuffer independently for + // stdout and stderr. Each arbitrary input byte can require up + // to six JSON bytes (for example a control-character escape), + // plus the fixed result envelope. Reject before spawn when the + // final direct reply cannot be admitted. + validate_python_captured_reply_limit(max_buffer, max_reply_bytes)?; + ( + HostOperation::Process(ProcessOperation::RunCaptured { + request: BoundedProcessLaunchRequest::try_new(launch, &request_limit)?, + max_buffer: BoundedUsize::try_new(max_buffer, &reply_limit)?, + }), + PythonHostReplyKind::RunCaptured, + ) + } + PythonVfsRpcMethod::HttpRequest => { + let url = request.url.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python httpRequest requires a url") + })?; + let url_limit = + PayloadLimit::new("runtime.network.maxHttpUrlBytes", MAX_HTTP_URL_BYTES)?; + let method_limit = + PayloadLimit::new("runtime.network.maxHttpMethodBytes", MAX_HTTP_METHOD_BYTES)?; + let header_count_limit = + PayloadLimit::new("runtime.network.maxHttpHeaders", MAX_HTTP_HEADERS)?; + let mut headers = Vec::with_capacity(request.headers.len()); + for (name, value) in &request.headers { + headers.push(HttpHeader { + name: BoundedString::try_new(name.clone(), &request_limit)?, + value: BoundedString::try_new(value.clone(), &request_limit)?, + }); + } + let body = decode_body(request.body_base64.as_deref(), "Python HTTP bodyBase64")?; + let response_body_max = max_reply_bytes.saturating_sub(512) / 4 * 3; + let header_max = MAX_HTTP_HEADER_BYTES.min(max_reply_bytes.saturating_sub(256)); + ( + HostOperation::Network(NetworkOperation::HttpRequest { + url: BoundedString::try_new(url, &url_limit)?, + method: BoundedString::try_new( + request + .http_method + .clone() + .unwrap_or_else(|| String::from("GET")), + &method_limit, + )?, + headers: BoundedVec::try_new(headers, &header_count_limit)?, + body: BoundedBytes::try_new(body, &request_limit)?, + max_response_bytes: BoundedUsize::try_new(max_reply_bytes, &reply_limit)?, + max_header_bytes: BoundedUsize::try_new(header_max, &reply_limit)?, + max_body_bytes: BoundedUsize::try_new(response_body_max, &reply_limit)?, + }), + PythonHostReplyKind::Http, + ) + } + PythonVfsRpcMethod::DnsLookup => { + let host = request.hostname.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python dnsLookup requires a hostname") + })?; + let family = match request.family.unwrap_or(0) { + 0 => DnsAddressFamily::Any, + 4 => DnsAddressFamily::Inet4, + 6 => DnsAddressFamily::Inet6, + family => { + return Err(HostServiceError::new( + "EINVAL", + format!("unsupported Python DNS address family {family}"), + )) + } + }; + let maximum = MAX_DNS_RESULTS.min(max_reply_bytes / 64); + ( + HostOperation::Network(NetworkOperation::ResolveDns { + host: bounded_host(host, "Python DNS hostname")?, + port: None, + family, + max_results: BoundedUsize::try_new( + maximum, + &PayloadLimit::new("runtime.network.maxDnsResults", MAX_DNS_RESULTS)?, + )?, + }), + PythonHostReplyKind::Dns, + ) + } + PythonVfsRpcMethod::SocketConnect => { + let host = request.hostname.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python socketConnect requires a hostname") + })?; + let port = request.port.ok_or_else(|| { + HostServiceError::new("EINVAL", "Python socketConnect requires a port") + })?; + let host = bounded_host(host, "Python TCP hostname")?; + let reservation = PythonManagedNetworkState::reserve( + &self.managed_network, + PythonManagedSocketKind::Tcp, + )?; + ( + HostOperation::Network(NetworkOperation::ManagedConnect { + endpoint: ManagedTcpEndpoint { + host: Some(host), + port: Some(port), + unix: None, + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + }), + PythonHostReplyKind::SocketCreated(reservation), + ) + } + PythonVfsRpcMethod::SocketSend => { + let (_, socket) = + managed_socket(request.socket_id, Some(PythonManagedSocketKind::Tcp))?; + let body = decode_body(request.body_base64.as_deref(), "Python TCP bodyBase64")?; + ( + HostOperation::Network(NetworkOperation::ManagedWrite { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + bytes: BoundedBytes::try_new(body, &request_limit)?, + }), + PythonHostReplyKind::SocketSent, + ) + } + PythonVfsRpcMethod::SocketRecv => { + let (_, socket) = + managed_socket(request.socket_id, Some(PythonManagedSocketKind::Tcp))?; + let requested = request + .max_buffer + .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV) + .clamp(1, PYTHON_SOCKET_MAX_RECV); + let maximum = requested.min(max_reply_bytes.saturating_sub(128) / 4 * 3); + ( + HostOperation::Network(NetworkOperation::ManagedRead { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + max_bytes: maximum as u64, + peek: false, + wait_ms: request + .timeout_ms + .unwrap_or(DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS), + }), + PythonHostReplyKind::SocketReceived, + ) + } + PythonVfsRpcMethod::SocketClose => { + let (socket_id, socket) = managed_socket(request.socket_id, None)?; + let operation = match socket.kind { + PythonManagedSocketKind::Tcp => NetworkOperation::ManagedDestroy { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + }, + PythonManagedSocketKind::Udp => NetworkOperation::ManagedUdpClose { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + }, + }; + ( + HostOperation::Network(operation), + PythonHostReplyKind::SocketClosed(socket_id), + ) + } + PythonVfsRpcMethod::UdpCreate => { + let reservation = PythonManagedNetworkState::reserve( + &self.managed_network, + PythonManagedSocketKind::Udp, + )?; + ( + HostOperation::Network(NetworkOperation::ManagedUdpCreate { + family: ManagedUdpFamily::Inet4, + }), + PythonHostReplyKind::SocketCreated(reservation), + ) + } + PythonVfsRpcMethod::UdpSendto => { + let (_, socket) = + managed_socket(request.socket_id, Some(PythonManagedSocketKind::Udp))?; + let host = request.hostname.clone().ok_or_else(|| { + HostServiceError::new("EINVAL", "Python udpSendto requires a hostname") + })?; + let port = request.port.ok_or_else(|| { + HostServiceError::new("EINVAL", "Python udpSendto requires a port") + })?; + let body = decode_body(request.body_base64.as_deref(), "Python UDP bodyBase64")?; + ( + HostOperation::Network(NetworkOperation::ManagedUdpSend { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + bytes: BoundedBytes::try_new(body, &request_limit)?, + host: Some(bounded_host(host, "Python UDP hostname")?), + port: Some(port), + }), + PythonHostReplyKind::SocketSent, + ) + } + PythonVfsRpcMethod::UdpRecvfrom => { + let (_, socket) = + managed_socket(request.socket_id, Some(PythonManagedSocketKind::Udp))?; + let requested = request + .max_buffer + .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV) + .clamp(1, PYTHON_SOCKET_MAX_RECV); + let maximum = requested.min(max_reply_bytes.saturating_sub(256) / 4 * 3); + ( + HostOperation::Network(NetworkOperation::ManagedUdpPoll { + socket_id: BoundedString::try_new(socket.host_socket_id, &socket_id_limit)?, + wait_ms: request + .timeout_ms + .unwrap_or(DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS), + peek: false, + max_bytes: Some(BoundedUsize::try_new(maximum, &reply_limit)?), + }), + PythonHostReplyKind::UdpReceived, + ) + } + }; + let target = Arc::new(PythonHostReplyTarget { + responder: self.clone(), + kind, + }); + let reply = DirectHostReplyHandle::new(identity, target, max_reply_bytes)?; + Ok(Some(PythonHostCall { operation, reply })) + } + pub fn respond_success( &self, id: u64, @@ -948,11 +1791,9 @@ impl PythonVfsRpcResponder { }), }; - let payload = v8_runtime::json_to_cbor_payload(&result) - .map_err(|error| PythonExecutionError::RpcResponse(error.to_string()))?; - self.v8_session - .send_bridge_response(id, 0, payload) - .map_err(|error| PythonExecutionError::RpcResponse(error.to_string())) + self.javascript_responder + .respond_success(id, result) + .map_err(map_javascript_error) } pub fn respond_error( @@ -970,47 +1811,373 @@ impl PythonVfsRpcResponder { } } - let error = format!("{}: {}", code.into(), message.into()); - self.v8_session - .send_bridge_response(id, 1, error.into_bytes()) - .map_err(|error| PythonExecutionError::RpcResponse(error.to_string())) + self.javascript_responder + .respond_host_error(id, HostServiceError::new(code.into(), message.into())) + .map_err(map_javascript_error) + } + + pub fn respond_host_error( + &self, + id: u64, + error: HostServiceError, + ) -> Result<(), PythonExecutionError> { + match clear_pending_vfs_rpc(&self.pending_vfs_rpc, id)? { + PendingVfsRpcResolution::Pending => {} + PendingVfsRpcResolution::TimedOut | PendingVfsRpcResolution::Missing => { + return Err(PythonExecutionError::RpcResponse(format!( + "VFS RPC request {id} is no longer pending" + ))); + } + } + self.javascript_responder + .respond_host_error(id, error) + .map_err(map_javascript_error) + } +} + +impl DirectHostReplyTarget for PythonHostReplyTarget { + fn claim(&self, call_id: u64) -> Result { + let claimed = claim_pending_vfs_rpc(&self.responder.pending_vfs_rpc, call_id, || { + self.responder + .javascript_responder + .claim(call_id) + .map_err(crate::javascript::host_reply_adapter_error) + })?; + if !claimed { + self.kind.rollback_socket_reservation(); + } + Ok(claimed) + } + + fn respond( + &self, + call_id: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + if !claimed { + match clear_pending_vfs_rpc(&self.responder.pending_vfs_rpc, call_id) + .map_err(python_host_reply_adapter_error)? + { + PendingVfsRpcResolution::Pending => {} + PendingVfsRpcResolution::TimedOut | PendingVfsRpcResolution::Missing => { + return Err(HostServiceError::new( + "ESTALE", + format!("Python host call {call_id} is no longer pending"), + ) + .with_details(json!({ "callId": call_id }))); + } + } + } + + if result.is_err() { + self.kind.rollback_socket_reservation(); + } + let response = match result { + Ok(reply) => match map_python_host_reply( + &self.responder.managed_network, + self.kind.clone(), + reply, + ) { + Ok(value) if claimed => self + .responder + .javascript_responder + .respond_claimed_success(call_id, value), + Ok(value) => self + .responder + .javascript_responder + .respond_success(call_id, value), + Err(error) if claimed => self + .responder + .javascript_responder + .respond_claimed_host_error(call_id, error), + Err(error) => self + .responder + .javascript_responder + .respond_host_error(call_id, error), + }, + Err(error) if claimed => self + .responder + .javascript_responder + .respond_claimed_host_error(call_id, error), + Err(error) => self + .responder + .javascript_responder + .respond_host_error(call_id, error), + }; + crate::javascript::map_host_reply_adapter_response(response) } } +fn python_host_reply_adapter_error(error: PythonExecutionError) -> HostServiceError { + match error { + PythonExecutionError::PendingVfsRpcRequest(call_id) => HostServiceError::new( + "EBUSY", + format!("Python host call {call_id} is already pending"), + ) + .with_details(json!({ "callId": call_id })), + PythonExecutionError::PendingVfsRpcLimit { limit, observed } => HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.reactor.maxBridgeCalls", + limit as u64, + observed as u64, + ), + other => HostServiceError::new("ERR_AGENTOS_PYTHON_ADAPTER_REPLY", other.to_string()), + } +} + +fn map_python_host_reply( + managed_network: &Arc>, + kind: PythonHostReplyKind, + reply: HostCallReply, +) -> Result { + let reply_kind = format!("{kind:?}"); + let protocol_error = |expected: &'static str| { + HostServiceError::new("EPROTO", format!("Python host reply expected {expected}")) + .with_details(json!({ "replyKind": reply_kind })) + }; + match (kind, reply) { + (PythonHostReplyKind::Empty, HostCallReply::Empty) + | (PythonHostReplyKind::Empty, HostCallReply::Json(Value::Null)) => Ok(json!({})), + (PythonHostReplyKind::FileRead, HostCallReply::Raw(bytes)) => Ok(json!({ + "contentBase64": base64::engine::general_purpose::STANDARD.encode(bytes), + })), + (PythonHostReplyKind::Stat, HostCallReply::Json(Value::Object(stat))) => Ok(json!({ + "stat": { + "mode": stat.get("mode").cloned().unwrap_or(Value::Null), + "size": stat.get("size").cloned().unwrap_or(Value::Null), + "isDirectory": stat.get("isDirectory").cloned().unwrap_or(Value::Bool(false)), + "isSymbolicLink": stat.get("isSymbolicLink").cloned().unwrap_or(Value::Bool(false)), + } + })), + (PythonHostReplyKind::ReadDirectory, HostCallReply::Json(Value::Array(entries))) => { + Ok(json!({ "entries": entries })) + } + (PythonHostReplyKind::ReadLink, HostCallReply::Json(Value::String(target))) => { + Ok(json!({ "target": target })) + } + (PythonHostReplyKind::RunCaptured, HostCallReply::Json(Value::Object(result))) => { + Ok(json!({ + "exitCode": result.get("code").cloned().unwrap_or(Value::from(1)), + "stdout": result.get("stdout").cloned().unwrap_or(Value::String(String::new())), + "stderr": result.get("stderr").cloned().unwrap_or(Value::String(String::new())), + "maxBufferExceeded": result.get("maxBufferExceeded").cloned().unwrap_or(Value::Bool(false)), + })) + } + (PythonHostReplyKind::Http, HostCallReply::Json(Value::Object(result))) => { + Ok(Value::Object(result)) + } + (PythonHostReplyKind::Dns, HostCallReply::Json(Value::Array(addresses))) => { + let addresses = addresses + .into_iter() + .map(|entry| { + entry + .get("address") + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| protocol_error("DNS address objects")) + }) + .collect::, _>>()?; + Ok(json!({ "addresses": addresses })) + } + ( + PythonHostReplyKind::SocketCreated(reservation), + HostCallReply::Json(Value::Object(created)), + ) => { + let host_socket_id = created + .get("socketId") + .and_then(Value::as_str) + .ok_or_else(|| protocol_error("a managed socket object"))? + .to_owned(); + let socket_id = reservation.commit(host_socket_id)?; + Ok(json!({ "socketId": socket_id })) + } + (PythonHostReplyKind::SocketSent, HostCallReply::Json(Value::Number(written))) => { + Ok(json!({ "bytesSent": written })) + } + (PythonHostReplyKind::SocketSent, HostCallReply::Json(Value::Object(result))) => { + let written = result + .get("bytes") + .and_then(Value::as_u64) + .ok_or_else(|| protocol_error("a managed socket byte count"))?; + Ok(json!({ "bytesSent": written })) + } + (PythonHostReplyKind::SocketReceived, HostCallReply::Raw(bytes)) => Ok(json!({ + "dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes), + "closed": false, + "timedOut": false, + })), + (PythonHostReplyKind::SocketReceived, HostCallReply::Json(Value::Null)) => Ok(json!({ + "dataBase64": "", + "closed": true, + "timedOut": false, + })), + (PythonHostReplyKind::SocketReceived, HostCallReply::Json(Value::String(timeout))) + if timeout == "__agentos_net_timeout__" => + { + Ok(json!({ + "dataBase64": "", + "closed": false, + "timedOut": true, + })) + } + ( + PythonHostReplyKind::SocketClosed(socket_id), + HostCallReply::Empty | HostCallReply::Json(Value::Null), + ) => { + managed_network + .lock() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_PYTHON_NETWORK_STATE_POISONED", + "Python managed-network state lock poisoned", + ) + })? + .sockets + .remove(&socket_id); + Ok(json!({})) + } + (PythonHostReplyKind::UdpReceived, HostCallReply::Json(Value::Null)) => Ok(json!({ + "dataBase64": "", + "host": "", + "port": 0, + "timedOut": true, + })), + (PythonHostReplyKind::UdpReceived, HostCallReply::Json(Value::Object(message))) => { + if message.get("type").and_then(Value::as_str) == Some("error") { + return Err(HostServiceError::new( + message.get("code").and_then(Value::as_str).unwrap_or("EIO"), + message + .get("message") + .and_then(Value::as_str) + .unwrap_or("managed UDP receive failed"), + )); + } + let encoded = message + .get("data") + .and_then(Value::as_object) + .ok_or_else(|| protocol_error("a managed UDP datagram"))?; + if encoded.get("__agentOSType").and_then(Value::as_str) != Some("bytes") { + return Err(protocol_error("encoded managed UDP datagram bytes")); + } + let data_base64 = encoded + .get("base64") + .and_then(Value::as_str) + .ok_or_else(|| protocol_error("encoded managed UDP datagram bytes"))?; + base64::engine::general_purpose::STANDARD + .decode(data_base64) + .map_err(|_| protocol_error("valid managed UDP datagram base64"))?; + let host = message + .get("remoteAddress") + .and_then(Value::as_str) + .ok_or_else(|| protocol_error("a managed UDP remote address"))?; + let port = message + .get("remotePort") + .and_then(Value::as_u64) + .and_then(|port| u16::try_from(port).ok()) + .ok_or_else(|| protocol_error("a managed UDP remote port"))?; + Ok(json!({ + "dataBase64": data_base64, + "host": host, + "port": port, + "timedOut": false, + })) + } + (PythonHostReplyKind::Empty, _) => Err(protocol_error("an empty reply")), + (PythonHostReplyKind::FileRead, _) => Err(protocol_error("raw file bytes")), + (PythonHostReplyKind::Stat, _) => Err(protocol_error("a stat object")), + (PythonHostReplyKind::ReadDirectory, _) => Err(protocol_error("a directory-entry array")), + (PythonHostReplyKind::ReadLink, _) => Err(protocol_error("a symlink target")), + (PythonHostReplyKind::RunCaptured, _) => Err(protocol_error("a captured-process result")), + (PythonHostReplyKind::Http, _) => Err(protocol_error("an HTTP response object")), + (PythonHostReplyKind::Dns, _) => Err(protocol_error("a DNS address array")), + (PythonHostReplyKind::SocketCreated(_), _) => { + Err(protocol_error("a managed socket object")) + } + (PythonHostReplyKind::SocketSent, _) => Err(protocol_error("a socket byte count")), + (PythonHostReplyKind::SocketReceived, _) => Err(protocol_error("a TCP receive reply")), + (PythonHostReplyKind::SocketClosed(_), _) => Err(protocol_error("an empty close reply")), + (PythonHostReplyKind::UdpReceived, _) => Err(protocol_error("a UDP receive reply")), + } +} + +fn claim_pending_vfs_rpc( + pending_vfs_rpc: &Arc>, + id: u64, + downstream_claim: impl FnOnce() -> Result, +) -> Result { + let mut pending = pending_vfs_rpc.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_PYTHON_ADAPTER_REPLY", + "Python pending host-call registry lock poisoned", + ) + })?; + let Some(rpc) = pending.entries.get(&id) else { + return Ok(false); + }; + if rpc.state == PendingVfsRpcState::TimedOut(id) { + let rpc = pending + .entries + .remove(&id) + .expect("timed-out call remains registered"); + pending.observe_depth(); + if let Some(timeout_abort) = rpc.timeout_abort { + timeout_abort.abort(); + } + return Ok(false); + } + let claimed = downstream_claim()?; + let rpc = pending + .entries + .remove(&id) + .expect("pending call remains registered while its registry lock is held"); + pending.observe_depth(); + if let Some(timeout_abort) = rpc.timeout_abort { + timeout_abort.abort(); + } + Ok(claimed) +} + fn clear_pending_vfs_rpc( - pending_vfs_rpc: &Arc>>, + pending_vfs_rpc: &Arc>, id: u64, ) -> Result { let mut pending = pending_vfs_rpc .lock() .map_err(|_| PythonExecutionError::EventChannelClosed)?; - let resolution = match pending.as_ref().map(|rpc| rpc.state) { - Some(PendingVfsRpcState::Pending(current)) if current == id => { - PendingVfsRpcResolution::Pending - } - Some(PendingVfsRpcState::TimedOut(current)) if current == id => { - PendingVfsRpcResolution::TimedOut - } - _ => return Ok(PendingVfsRpcResolution::Missing), + let Some(rpc) = pending.entries.remove(&id) else { + return Ok(PendingVfsRpcResolution::Missing); }; - if let Some(rpc) = pending.take() { - if let Some(timeout_abort) = rpc.timeout_abort { - timeout_abort.abort(); - } + let resolution = match rpc.state { + PendingVfsRpcState::Pending(_) => PendingVfsRpcResolution::Pending, + PendingVfsRpcState::TimedOut(_) => PendingVfsRpcResolution::TimedOut, + }; + if let Some(timeout_abort) = rpc.timeout_abort { + timeout_abort.abort(); } + pending.observe_depth(); Ok(resolution) } -fn cancel_pending_vfs_rpc(pending_vfs_rpc: &Arc>>) { +fn cancel_pending_vfs_rpc(pending_vfs_rpc: &Arc>) { let pending = pending_vfs_rpc .lock() - .map(|mut pending| pending.take()) + .map(|mut pending| { + let entries = std::mem::take(&mut pending.entries); + pending.observe_depth(); + entries + }) .unwrap_or_else(|poisoned| { - eprintln!("ERR_AGENTOS_PYTHON_VFS_RPC_STATE_POISONED: cancelling pending timeout"); - poisoned.into_inner().take() + eprintln!("ERR_AGENTOS_PYTHON_VFS_RPC_STATE_POISONED: cancelling pending timeouts"); + let mut pending = poisoned.into_inner(); + let entries = std::mem::take(&mut pending.entries); + pending.observe_depth(); + entries }); - if let Some(timeout_abort) = pending.and_then(|rpc| rpc.timeout_abort) { - timeout_abort.abort(); + for rpc in pending.into_values() { + if let Some(timeout_abort) = rpc.timeout_abort { + timeout_abort.abort(); + } } } @@ -1344,13 +2511,21 @@ impl PythonExecutionEngine { defer_execute, }, )?; - let pending_vfs_rpc = Arc::new(Mutex::new(None)); + let max_pending_vfs_rpcs = runtime + .resources() + .configured_limit(ResourceClass::BridgeCalls) + .map_or(DEFAULT_PYTHON_PENDING_VFS_RPCS, |limit| limit.maximum); + let pending_vfs_rpc = + Arc::new(Mutex::new(PendingVfsRpcRegistry::new(max_pending_vfs_rpcs))); + let managed_network = Arc::new(Mutex::new(PythonManagedNetworkState::new( + python_managed_host_file_limit(&request), + ))); let vfs_rpc_timeout = python_vfs_rpc_timeout(&request); Ok(PythonExecution { runtime, execution_id, - child_pid: javascript_execution.child_pid(), + child_pid: javascript_execution.native_process_id().unwrap_or_default(), v8_session: javascript_execution.v8_session_handle(), inner: javascript_execution, pyodide_dist_path, @@ -1358,6 +2533,7 @@ impl PythonExecutionEngine { &request, )), pending_vfs_rpc, + managed_network, output_buffer_max_bytes: python_output_buffer_max_bytes(&request), execution_timeout: python_execution_timeout(&request), vfs_rpc_timeout, @@ -1374,31 +2550,50 @@ impl PythonExecutionEngine { } fn set_pending_vfs_rpc_state( - pending_vfs_rpc: &Arc>>, + pending_vfs_rpc: &Arc>, id: u64, ) -> Result<(), PythonExecutionError> { let mut pending = pending_vfs_rpc .lock() .map_err(|_| PythonExecutionError::EventChannelClosed)?; - if let Some(PendingVfsRpc { - state: PendingVfsRpcState::Pending(current), - .. - }) = pending.as_ref() - { - return Err(PythonExecutionError::PendingVfsRpcRequest(*current)); + if pending.entries.contains_key(&id) { + return Err(PythonExecutionError::PendingVfsRpcRequest(id)); } - if let Some(previous) = pending.take() { - if let Some(timeout_abort) = previous.timeout_abort { - timeout_abort.abort(); - } + let observed = pending.entries.len().saturating_add(1); + if observed > pending.maximum { + return Err(PythonExecutionError::PendingVfsRpcLimit { + limit: pending.maximum, + observed, + }); } - *pending = Some(PendingVfsRpc { - state: PendingVfsRpcState::Pending(id), - timeout_abort: None, - }); + pending.entries.insert( + id, + PendingVfsRpc { + state: PendingVfsRpcState::Pending(id), + timeout_abort: None, + }, + ); + pending.observe_depth(); Ok(()) } +fn python_vfs_rpc_admission_error(error: PythonExecutionError) -> HostServiceError { + match error { + PythonExecutionError::PendingVfsRpcRequest(id) => HostServiceError::new( + "EBUSY", + PythonExecutionError::PendingVfsRpcRequest(id).to_string(), + ) + .with_details(json!({ "callId": id })), + PythonExecutionError::PendingVfsRpcLimit { limit, observed } => HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.reactor.maxBridgeCalls", + limit as u64, + observed as u64, + ), + other => HostServiceError::new("ERR_AGENTOS_PYTHON_VFS_RPC", other.to_string()), + } +} + fn map_javascript_error(error: JavascriptExecutionError) -> PythonExecutionError { match error { JavascriptExecutionError::EmptyArgv => PythonExecutionError::Spawn(std::io::Error::new( @@ -1421,12 +2616,20 @@ fn map_javascript_error(error: JavascriptExecutionError) -> PythonExecutionError JavascriptExecutionError::PendingSyncRpcRequest(id) => { PythonExecutionError::PendingVfsRpcRequest(id) } + JavascriptExecutionError::PendingSyncRpcLimit { limit, observed } => { + PythonExecutionError::RpcResponse(format!( + "ERR_AGENTOS_RESOURCE_LIMIT: pending sync RPC calls observed {observed}, exceeding limits.reactor.maxBridgeCalls ({limit})" + )) + } JavascriptExecutionError::ExpiredSyncRpcRequest(id) => { PythonExecutionError::RpcResponse(format!("VFS RPC request {id} is no longer pending")) } JavascriptExecutionError::RpcResponse(message) => { PythonExecutionError::RpcResponse(message) } + JavascriptExecutionError::BridgeSettlement(error) => { + PythonExecutionError::RpcResponse(error.to_string()) + } JavascriptExecutionError::Terminate(error) => PythonExecutionError::Kill(error), JavascriptExecutionError::Control(error) => PythonExecutionError::Control(error), JavascriptExecutionError::StdinClosed => PythonExecutionError::StdinClosed, @@ -1629,8 +2832,6 @@ fn build_python_runner_module_source( ) -> Result { let runner_source = fs::read_to_string(import_cache.python_runner_path()) .map_err(PythonExecutionError::PrepareRuntime)?; - let runner_source = - format!("import * as __agentOSConstantsBinding from 'node:constants';\n{runner_source}"); let bootstrap = build_python_runner_bootstrap(internal_env, warmup_metrics); Ok(insert_python_runner_bootstrap(&runner_source, &bootstrap)) } @@ -1681,7 +2882,7 @@ fn insert_python_runner_bootstrap(source: &str, bootstrap: &str) -> String { } fn parse_python_bridge_sync_rpc_request( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { if request.method != "_pythonRpc" { return Err(PythonExecutionError::RpcResponse(format!( @@ -1809,8 +3010,8 @@ fn spawn_python_vfs_rpc_timeout( runtime: &RuntimeContext, id: u64, timeout: Duration, - pending: Arc>>, - v8_session: crate::v8_host::V8SessionHandle, + pending: Arc>, + javascript_responder: JavascriptSyncRpcResponder, ) -> Result<(), PythonExecutionError> { let cancellation = runtime.clone(); let pending_for_task = Arc::clone(&pending); @@ -1825,10 +3026,11 @@ fn spawn_python_vfs_rpc_timeout( ); poisoned.into_inner() }); - if guard.as_ref().map(|rpc| rpc.state) + if guard.entries.get(&id).map(|rpc| rpc.state) == Some(PendingVfsRpcState::Pending(id)) { - *guard = None; + guard.entries.remove(&id); + guard.observe_depth(); } return; } @@ -1840,30 +3042,30 @@ fn spawn_python_vfs_rpc_timeout( ); poisoned.into_inner() }); - let should_timeout = - if guard.as_ref().map(|rpc| rpc.state) == Some(PendingVfsRpcState::Pending(id)) { - *guard = Some(PendingVfsRpc { - state: PendingVfsRpcState::TimedOut(id), - timeout_abort: None, - }); + let should_timeout = if let Some(rpc) = guard.entries.get_mut(&id) { + if rpc.state == PendingVfsRpcState::Pending(id) { + rpc.state = PendingVfsRpcState::TimedOut(id); + rpc.timeout_abort = None; true } else { false - }; + } + } else { + false + }; drop(guard); if !should_timeout { return; } - if let Err(error) = v8_session.send_bridge_response( + if let Err(error) = javascript_responder.respond_error( id, - 1, + "ERR_AGENTOS_PYTHON_VFS_RPC_TIMEOUT", format!( - "ERR_AGENTOS_PYTHON_VFS_RPC_TIMEOUT: guest Python VFS RPC request {id} timed out after {}ms", + "guest Python VFS RPC request {id} timed out after {}ms", timeout.as_millis() - ) - .into_bytes(), + ), ) { eprintln!( "ERR_AGENTOS_PYTHON_VFS_RPC_TIMEOUT_DELIVERY: could not deliver timeout for request {id}: {error}" @@ -1880,7 +3082,7 @@ fn spawn_python_vfs_rpc_timeout( let mut guard = pending .lock() .map_err(|_| PythonExecutionError::EventChannelClosed)?; - if let Some(rpc) = guard.as_mut() { + if let Some(rpc) = guard.entries.get_mut(&id) { if rpc.state == PendingVfsRpcState::Pending(id) { rpc.timeout_abort = Some(timeout_abort); return Ok(()); @@ -2220,7 +3422,7 @@ fn python_managed_host_file_limit(request: &StartPythonExecutionRequest) -> usiz fn python_javascript_sync_rpc_action( pyodide_dist_path: &Path, managed_host_files: &mut PythonManagedHostFiles, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result, PythonExecutionError> { if matches!(request.method.as_str(), "fs.readSync" | "_fsReadRaw") { let Some(fd) = request.args.first().and_then(Value::as_u64) else { @@ -2736,9 +3938,7 @@ fn python_prewarm_sync_rpc_encoding(args: &[Value]) -> Option { }) } -fn python_javascript_sync_rpc_error( - request: &JavascriptSyncRpcRequest, -) -> Option<(&'static str, String)> { +fn python_javascript_sync_rpc_error(request: &HostRpcRequest) -> Option<(&'static str, String)> { if matches!( request.method.as_str(), "net.connect" @@ -2764,6 +3964,30 @@ fn python_javascript_sync_rpc_error( None } +fn python_vfs_rpc_standalone_error(method: PythonVfsRpcMethod) -> Option<(&'static str, String)> { + if matches!( + method, + PythonVfsRpcMethod::HttpRequest + | PythonVfsRpcMethod::DnsLookup + | PythonVfsRpcMethod::SocketConnect + | PythonVfsRpcMethod::SocketSend + | PythonVfsRpcMethod::SocketRecv + | PythonVfsRpcMethod::SocketClose + | PythonVfsRpcMethod::UdpCreate + | PythonVfsRpcMethod::UdpSendto + | PythonVfsRpcMethod::UdpRecvfrom + ) { + return Some(( + "ERR_ACCESS_DENIED", + String::from( + "network access is not available during standalone guest Python execution", + ), + )); + } + + None +} + fn warmup_marker_contents( import_cache: &NodeImportCache, context: &PythonContext, @@ -2825,13 +4049,15 @@ fn warmup_metrics_line( #[cfg(test)] mod tests { use super::{ - clear_pending_vfs_rpc, python_javascript_sync_rpc_action, python_managed_path_kind, - python_runner_javascript_limits, python_wait_remaining, CreatePythonContextRequest, - JavascriptSyncRpcRequest, PendingVfsRpc, PendingVfsRpcResolution, PendingVfsRpcState, - PythonExecutionEngine, PythonExecutionLimits, PythonJavascriptSyncRpcAction, - PythonManagedHostFiles, PythonManagedPathKind, PYODIDE_CACHE_GUEST_ROOT, - PYODIDE_GUEST_ROOT, + cancel_pending_vfs_rpc, clear_pending_vfs_rpc, python_javascript_sync_rpc_action, + python_managed_path_kind, python_runner_javascript_limits, python_vfs_rpc_admission_error, + python_wait_remaining, set_pending_vfs_rpc_state, CreatePythonContextRequest, + HostRpcRequest, PendingVfsRpc, PendingVfsRpcRegistry, PendingVfsRpcResolution, + PendingVfsRpcState, PythonExecutionEngine, PythonExecutionError, PythonExecutionLimits, + PythonHostReplyKind, PythonJavascriptSyncRpcAction, PythonManagedHostFiles, + PythonManagedPathKind, PYODIDE_CACHE_GUEST_ROOT, PYODIDE_GUEST_ROOT, }; + use crate::backend::HostCallReply; use std::collections::HashMap; use std::fs; #[cfg(unix)] @@ -2854,6 +4080,174 @@ mod tests { assert_eq!(javascript.bridge_call_timeout_ms, Some(54_321)); } + #[test] + fn common_host_replies_keep_the_python_wire_shape() { + let network = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(8))); + assert_eq!( + super::map_python_host_reply( + &network, + PythonHostReplyKind::RunCaptured, + HostCallReply::Json(serde_json::json!({ + "pid": 17, + "code": 3, + "stdout": "out", + "stderr": "err", + "maxBufferExceeded": true, + })), + ) + .expect("map captured process reply"), + serde_json::json!({ + "exitCode": 3, + "stdout": "out", + "stderr": "err", + "maxBufferExceeded": true, + }) + ); + assert_eq!( + super::map_python_host_reply( + &network, + PythonHostReplyKind::FileRead, + HostCallReply::Raw(b"shared-kernel".to_vec()), + ) + .expect("map file read reply"), + serde_json::json!({ "contentBase64": "c2hhcmVkLWtlcm5lbA==" }) + ); + } + + #[test] + fn canonical_udp_host_reply_maps_to_python_socket_shape() { + let network = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(8))); + let reply = super::map_python_host_reply( + &network, + PythonHostReplyKind::UdpReceived, + HostCallReply::Json(serde_json::json!({ + "type": "message", + "data": { + "__agentOSType": "bytes", + "base64": "cGluZyB1ZHA=", + }, + "remoteAddress": "127.0.0.1", + "remotePort": 43123, + "remoteFamily": "IPv4", + })), + ) + .expect("map canonical UDP reply"); + + assert_eq!( + reply, + serde_json::json!({ + "dataBase64": "cGluZyB1ZHA=", + "host": "127.0.0.1", + "port": 43123, + "timedOut": false, + }) + ); + } + + #[test] + fn mismatched_common_host_reply_is_a_typed_protocol_error() { + let network = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(8))); + let error = super::map_python_host_reply( + &network, + PythonHostReplyKind::ReadDirectory, + HostCallReply::Empty, + ) + .expect_err("reject mismatched host reply"); + assert_eq!(error.code, "EPROTO"); + assert_eq!( + error.details.expect("protocol error details")["replyKind"], + "ReadDirectory" + ); + } + + #[test] + fn managed_socket_reservations_admit_exactly_the_configured_cap() { + let state = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(1))); + let reservation = + super::PythonManagedNetworkState::reserve(&state, super::PythonManagedSocketKind::Tcp) + .expect("exact cap reservation"); + let error = + super::PythonManagedNetworkState::reserve(&state, super::PythonManagedSocketKind::Udp) + .expect_err("cap plus one is rejected before a host operation exists"); + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!(state.lock().expect("state").sockets.len(), 1); + drop(reservation); + assert!(state.lock().expect("state").sockets.is_empty()); + } + + #[test] + fn managed_socket_id_exhaustion_leaves_no_reserved_operation() { + let state = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(1))); + state.lock().expect("state").next_socket_id = u64::MAX; + let error = + super::PythonManagedNetworkState::reserve(&state, super::PythonManagedSocketKind::Tcp) + .expect_err("id exhaustion"); + assert_eq!(error.code, "EOVERFLOW"); + assert!(state.lock().expect("state").sockets.is_empty()); + } + + #[test] + fn managed_socket_reply_commits_the_pre_reserved_adapter_handle() { + let state = Arc::new(Mutex::new(super::PythonManagedNetworkState::new(1))); + let reservation = + super::PythonManagedNetworkState::reserve(&state, super::PythonManagedSocketKind::Tcp) + .expect("reservation"); + let socket_id = reservation.0.socket_id; + let reply = super::map_python_host_reply( + &state, + PythonHostReplyKind::SocketCreated(reservation), + HostCallReply::Json(serde_json::json!({ "socketId": "tcp-7" })), + ) + .expect("commit host socket"); + assert_eq!(reply, serde_json::json!({ "socketId": socket_id })); + let socket = state + .lock() + .expect("state") + .socket(socket_id, Some(super::PythonManagedSocketKind::Tcp)) + .expect("live socket"); + assert_eq!(socket.host_socket_id, "tcp-7"); + } + + #[test] + fn downstream_claim_error_preserves_python_pending_state() { + let pending = Arc::new(Mutex::new(PendingVfsRpcRegistry::new(4))); + pending.lock().expect("pending").entries.insert( + 42, + PendingVfsRpc { + state: PendingVfsRpcState::Pending(42), + timeout_abort: None, + }, + ); + let error = super::claim_pending_vfs_rpc(&pending, 42, || { + Err(crate::backend::HostServiceError::new( + "EIO", + "downstream claim failed", + )) + }) + .expect_err("claim error"); + assert_eq!(error.code, "EIO"); + assert!(pending.lock().expect("pending").entries.contains_key(&42)); + } + + #[test] + fn captured_process_reply_bound_is_enforced_before_spawn() { + super::validate_python_captured_reply_limit(10, 632).expect("exact bound"); + let error = super::validate_python_captured_reply_limit(11, 632) + .expect_err("reply expansion exceeds bridge bound"); + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + error.details.expect("details")["configPath"], + "limits.reactor.maxBridgeResponseBytes" + ); + } + + #[test] + fn blocking_poll_rejects_an_unrepresentable_deadline() { + let error = super::checked_python_poll_deadline(Duration::MAX) + .expect_err("Duration::MAX cannot fit in Instant"); + assert!(matches!(error, PythonExecutionError::InvalidLimit(_))); + } + #[test] fn dispose_context_reclaims_python_and_nested_javascript_metadata() { let mut engine = PythonExecutionEngine::default(); @@ -2892,7 +4286,7 @@ mod tests { #[test] fn stale_python_vfs_completion_has_no_pending_waiter() { - let pending = Arc::new(Mutex::new(None)); + let pending = Arc::new(Mutex::new(PendingVfsRpcRegistry::new(2))); assert_eq!( clear_pending_vfs_rpc(&pending, 41).expect("inspect pending request"), @@ -2902,16 +4296,80 @@ mod tests { #[test] fn timed_out_python_vfs_completion_is_consumed_as_stale() { - let pending = Arc::new(Mutex::new(Some(PendingVfsRpc { - state: PendingVfsRpcState::TimedOut(42), - timeout_abort: None, - }))); + let mut registry = PendingVfsRpcRegistry::new(2); + registry.entries.insert( + 42, + PendingVfsRpc { + state: PendingVfsRpcState::TimedOut(42), + timeout_abort: None, + }, + ); + registry.observe_depth(); + let pending = Arc::new(Mutex::new(registry)); assert_eq!( clear_pending_vfs_rpc(&pending, 42).expect("clear timed-out request"), PendingVfsRpcResolution::TimedOut ); - assert!(pending.lock().expect("pending request lock").is_none()); + assert!(pending + .lock() + .expect("pending request lock") + .entries + .is_empty()); + } + + #[test] + fn pending_python_vfs_rpc_registry_is_bounded_keyed_and_nonlossy() { + let pending = Arc::new(Mutex::new(PendingVfsRpcRegistry::new(2))); + set_pending_vfs_rpc_state(&pending, 41).expect("first concurrent VFS waiter"); + set_pending_vfs_rpc_state(&pending, 42).expect("second concurrent VFS waiter"); + + let duplicate = set_pending_vfs_rpc_state(&pending, 41) + .expect_err("duplicate call ID must not replace its waiter"); + assert!(matches!( + duplicate, + PythonExecutionError::PendingVfsRpcRequest(41) + )); + let duplicate_host_error = python_vfs_rpc_admission_error(duplicate); + assert_eq!(duplicate_host_error.code, "EBUSY"); + assert_eq!(duplicate_host_error.details.as_ref().unwrap()["callId"], 41); + + let over_limit = set_pending_vfs_rpc_state(&pending, 43) + .expect_err("third distinct VFS waiter exceeds admission"); + assert!(matches!( + over_limit, + PythonExecutionError::PendingVfsRpcLimit { + limit: 2, + observed: 3, + } + )); + let limit_host_error = python_vfs_rpc_admission_error(over_limit); + assert_eq!(limit_host_error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + limit_host_error.details.as_ref().unwrap()["limitName"], + "limits.reactor.maxBridgeCalls" + ); + + assert_eq!( + clear_pending_vfs_rpc(&pending, 42).expect("settle exact second waiter"), + PendingVfsRpcResolution::Pending + ); + assert_eq!( + pending + .lock() + .expect("pending request lock") + .entries + .get(&41) + .map(|rpc| rpc.state), + Some(PendingVfsRpcState::Pending(41)) + ); + + cancel_pending_vfs_rpc(&pending); + assert!(pending + .lock() + .expect("pending request lock") + .entries + .is_empty()); } #[test] @@ -2921,7 +4379,7 @@ mod tests { fs::create_dir_all(&pyodide).expect("create pyodide root"); fs::write(pyodide.join("python_stdlib.zip"), b"stdlib-bytes").expect("write managed asset"); let mut files = PythonManagedHostFiles::default(); - let open = JavascriptSyncRpcRequest { + let open = HostRpcRequest { id: 1, method: String::from("fs.openSync"), args: vec![ @@ -2941,7 +4399,7 @@ mod tests { other => panic!("unexpected managed open action: {other:?}"), }; - let read = JavascriptSyncRpcRequest { + let read = HostRpcRequest { id: 2, method: String::from("fs.readSync"), args: vec![ @@ -2961,7 +4419,7 @@ mod tests { other => panic!("unexpected managed read action: {other:?}"), } - let close = JavascriptSyncRpcRequest { + let close = HostRpcRequest { id: 3, method: String::from("fs.closeSync"), args: vec![serde_json::json!(fd)], @@ -2984,7 +4442,7 @@ mod tests { fs::create_dir_all(&pyodide).expect("create pyodide root"); fs::write(pyodide.join("python_stdlib.zip"), b"stdlib-bytes").expect("write managed asset"); let mut files = PythonManagedHostFiles::new(2); - let open = |id| JavascriptSyncRpcRequest { + let open = |id| HostRpcRequest { id, method: String::from("fs.openSync"), args: vec![ @@ -3021,7 +4479,7 @@ mod tests { )); assert_eq!(files.files.len(), 2); - let close = JavascriptSyncRpcRequest { + let close = HostRpcRequest { id: 4, method: String::from("fs.closeSync"), args: vec![serde_json::json!(first)], diff --git a/crates/execution/src/signal.rs b/crates/execution/src/signal.rs index 4a739f674a..e9d78a18fa 100644 --- a/crates/execution/src/signal.rs +++ b/crates/execution/src/signal.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum NodeSignalDispositionAction { +pub enum ExecutionSignalDispositionAction { Default, Ignore, User, @@ -10,8 +10,8 @@ pub enum NodeSignalDispositionAction { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct NodeSignalHandlerRegistration { - pub action: NodeSignalDispositionAction, +pub struct ExecutionSignalHandlerRegistration { + pub action: ExecutionSignalDispositionAction, pub mask: Vec, pub flags: u32, } diff --git a/crates/execution/src/v8_host.rs b/crates/execution/src/v8_host.rs index 32e3094cd7..eaf4946f01 100644 --- a/crates/execution/src/v8_host.rs +++ b/crates/execution/src/v8_host.rs @@ -290,8 +290,8 @@ impl V8SessionHandle { .set_application_read_interest(capability_id, capability_generation, enabled) } - pub fn publish_signal(&self, signal: i32) -> io::Result<()> { - self.inner.publish_signal(signal) + pub fn publish_signal(&self, signal: i32, delivery_token: u64) -> io::Result<()> { + self.inner.publish_signal(signal, delivery_token) } pub fn publish_timer(&self, timer_id: u64) -> io::Result<()> { @@ -366,6 +366,86 @@ impl Clone for V8SessionHandle { } } +impl crate::backend::ExecutionWakeTarget for V8SessionHandle { + fn publish_readiness( + &self, + capability_id: u64, + capability_generation: u64, + flags: agentos_runtime::readiness::ReadyFlags, + ) -> Result<(), crate::backend::ExecutionWakeError> { + V8SessionHandle::publish_readiness(self, capability_id, capability_generation, flags) + .map_err(|error| crate::backend::ExecutionWakeError::new("EIO", error.to_string())) + } + + fn remove_readiness( + &self, + capability_id: u64, + capability_generation: u64, + ) -> Result<(), crate::backend::ExecutionWakeError> { + V8SessionHandle::remove_readiness(self, capability_id, capability_generation) + .map_err(|error| crate::backend::ExecutionWakeError::new("EIO", error.to_string())) + } + + fn set_application_read_interest( + &self, + capability_id: u64, + capability_generation: u64, + enabled: bool, + ) -> Result<(), crate::backend::ExecutionWakeError> { + V8SessionHandle::set_application_read_interest( + self, + capability_id, + capability_generation, + enabled, + ) + .map_err(|error| crate::backend::ExecutionWakeError::new("EIO", error.to_string())) + } + + fn publish_signal( + &self, + signal: i32, + delivery_token: u64, + ) -> Result<(), crate::backend::ExecutionWakeError> { + V8SessionHandle::publish_signal(self, signal, delivery_token) + .map_err(|error| crate::backend::ExecutionWakeError::new("EIO", error.to_string())) + } + + fn send_adapter_event( + &self, + event_type: &str, + payload: &serde_json::Value, + encoded_limit_name: &'static str, + max_encoded_bytes: usize, + ) -> Result<(), crate::backend::ExecutionWakeError> { + let encoded_limit = + crate::backend::PayloadLimit::new(encoded_limit_name, max_encoded_bytes).map_err( + |error| { + crate::backend::ExecutionWakeError::new("EINVAL", error.message) + .with_details(error.details) + }, + )?; + // Reject an oversized structured value with a counting writer before + // the adapter constructs a CBOR value and encoded Vec. + encoded_limit.admit_json(payload).map_err(|error| { + crate::backend::ExecutionWakeError::new("E2BIG", error.message) + .with_details(error.details) + })?; + let encoded = crate::v8_runtime::json_to_cbor_payload(payload).map_err(|error| { + crate::backend::ExecutionWakeError::new( + "ERR_AGENTOS_ADAPTER_EVENT_ENCODE", + error.to_string(), + ) + })?; + let encoded = + crate::host::BoundedBytes::try_new(encoded, &encoded_limit).map_err(|error| { + crate::backend::ExecutionWakeError::new("E2BIG", error.message) + .with_details(error.details) + })?; + V8SessionHandle::send_stream_event(self, event_type, encoded.into_vec()) + .map_err(|error| crate::backend::ExecutionWakeError::new("EIO", error.to_string())) + } +} + /// Pre-build the per-sidecar snapshot for an agent-SDK `userland_code` bundle into /// the process-wide cache, so the FIRST session that uses it is already warm. Uses /// the shared embedded runtime directly (no per-call host lifecycle). Blocks until diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index af52e69681..39673258af 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -1,14 +1,20 @@ +use crate::backend::{ + DescendantOutputOwnership, DescendantWaitOwnership, ExecutionBackend, ExecutionBackendKind, + ExecutionExit, ExecutionWakeHandle, ExecutionWakeIdentity, HostServiceError, + PublishedSignalCheckpoint, ShutdownOutcome, ShutdownReason, SignalCheckpointOutcome, + SynchronousFdWritePolicy, +}; use crate::common::{ encode_json_string, encode_json_string_array, encode_json_string_map, frozen_time_ms, }; use crate::javascript::{ - CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution, + CreateJavascriptContextRequest, GuestRuntimeConfig, HostRpcRequest, JavascriptExecution, JavascriptExecutionEngine, JavascriptExecutionError, JavascriptExecutionEvent, - JavascriptExecutionLimits, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest, + JavascriptExecutionLimits, JavascriptSyncRpcResponder, StartJavascriptExecutionRequest, }; use crate::node_import_cache::NodeImportCache; use crate::runtime_support::{env_flag_enabled, file_fingerprint, warmup_marker_path}; -use crate::signal::{NodeSignalDispositionAction, NodeSignalHandlerRegistration}; +use crate::signal::{ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration}; use crate::v8_host::{V8RuntimeHost, V8SessionHandle}; use crate::v8_runtime; use agentos_bridge::queue_tracker::{ @@ -36,7 +42,6 @@ const WASM_PREWARM_ONLY_ENV: &str = "AGENTOS_WASM_PREWARM_ONLY"; const WASM_HOST_CWD_ENV: &str = "AGENTOS_WASM_HOST_CWD"; const WASM_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT"; const WASM_WARMUP_DEBUG_ENV: &str = "AGENTOS_WASM_WARMUP_DEBUG"; -pub const WASM_MAX_FUEL_ENV: &str = "AGENTOS_WASM_MAX_FUEL"; pub const WASM_MAX_MEMORY_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MEMORY_BYTES"; pub const WASM_MAX_STACK_BYTES_ENV: &str = "AGENTOS_WASM_MAX_STACK_BYTES"; pub const WASM_MAX_MODULE_FILE_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MODULE_FILE_BYTES"; @@ -46,6 +51,8 @@ const WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV: &str = "AGENTOS_WASM_MAX_SPAWN_FILE_ const WASM_MAX_SOCKETS_ENV: &str = "AGENTOS_WASM_MAX_SOCKETS"; const WASM_MAX_BLOCKING_READ_MS_ENV: &str = "AGENTOS_WASM_MAX_BLOCKING_READ_MS"; const WASM_INTERNAL_MAX_STACK_BYTES_ENV: &str = "AGENTOS_INTERNAL_WASM_MAX_STACK_BYTES"; +const WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV: &str = + "AGENTOS_INTERNAL_WASM_SYNC_RPC_RESPONSE_LINE_BYTES"; const WASM_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_WASM_WARMUP_METRICS__:"; const WASM_SIGNAL_STATE_PREFIX: &str = "__AGENTOS_WASM_SIGNAL_STATE__:"; const WASM_WARMUP_MARKER_VERSION: &str = "1"; @@ -88,7 +95,10 @@ const DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB: u32 = 2048; const _: () = assert!(DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB > 128); const MAX_SYNC_WASM_PREWARM_MODULE_BYTES: u64 = 16 * 1024 * 1024; const WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024; +const DEFAULT_WASM_PENDING_EVENT_COUNT: usize = 512; +const DEFAULT_WASM_PENDING_EVENT_BYTES: usize = 16 * 1024 * 1024; const WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024; +const DEFAULT_WASM_SYNC_RPC_RESPONSE_LINE_BYTES: u64 = 16 * 1024 * 1024; // `_processWasmSyncRpc` returns file-read bytes as one CBOR byte string. The // bridge contract bounds the encoded response payload, not the unencoded file // bytes, so the runner must leave room for CBOR's byte-string header. @@ -110,6 +120,7 @@ const WASM_SIDECAR_ROUTED_FS_SYNC_METHODS: &[&str] = &[ "fs.fallocateSync", "fs.fdatasyncSync", "fs.fiemapSync", + "fs.fiemapAtSync", "fs.fstatSync", "fs.fsyncSync", "fs.ftruncateSync", @@ -154,13 +165,6 @@ const WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS: &[&str] = &[ "__pty_set_raw_mode", ]; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WasmSignalDispositionAction { - Default, - Ignore, - User, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum WasmPermissionTier { @@ -181,13 +185,6 @@ impl WasmPermissionTier { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WasmSignalHandlerRegistration { - pub action: WasmSignalDispositionAction, - pub mask: Vec, - pub flags: u32, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateWasmContextRequest { pub vm_id: String, @@ -208,8 +205,14 @@ pub struct WasmContext { /// `crates/sidecar/CLAUDE.md`. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct WasmExecutionLimits { - /// Fuel budget, enforced as a wall-clock timeout (ms) by the WASI runtime. - pub max_fuel: Option, + /// Active CPU-time budget in milliseconds. The V8 compatibility backend + /// maps this to the trusted runner isolate's CPU watchdog. + pub active_cpu_time_limit_ms: Option, + /// Optional elapsed wall-clock backstop in milliseconds. + pub wall_clock_limit_ms: Option, + /// Optional deterministic instruction budget. V8 cannot meter this and + /// rejects an explicit value before starting guest execution. + pub deterministic_fuel: Option, /// Linear-memory cap in bytes, validated against the module's declared /// initial/maximum memory before execution. pub max_memory_bytes: Option, @@ -234,18 +237,28 @@ pub struct WasmExecutionLimits { pub prewarm_timeout_ms: Option, /// V8 heap cap for the trusted JS runner isolate that hosts WASI/WASM. pub runner_heap_limit_mb: Option, - /// Active-CPU cap for the trusted JS runner isolate that hosts WASI/WASM. - pub runner_cpu_time_limit_ms: Option, /// VM readiness work bound forwarded unchanged to the WASI V8 runner. pub reactor_work_quantum: Option, /// Per-call host bridge deadline forwarded unchanged to the WASI V8 runner. pub bridge_call_timeout_ms: Option, + /// Maximum encoded line retained by the fallback synchronous RPC response + /// pipe. Sidecar VMs supply `limits.reactor.maxBridgeResponseBytes` here. + pub max_sync_rpc_response_line_bytes: Option, + /// Maximum compatibility-adapter events retained before the sidecar + /// consumes them. Sidecar VMs supply limits.process.pendingEventCount. + pub pending_event_count: Option, + /// Maximum aggregate bytes retained by compatibility-adapter event queues. + /// Sidecar VMs supply limits.process.pendingEventBytes. + pub pending_event_bytes: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct StartWasmExecutionRequest { pub vm_id: String, pub context_id: String, + /// Whether this execution is attached to the sidecar's authoritative + /// kernel host. This is trusted adapter configuration, not guest env. + pub managed_kernel_host: bool, pub argv: Vec, pub env: BTreeMap, pub cwd: PathBuf, @@ -262,10 +275,10 @@ pub struct StartWasmExecutionRequest { pub enum WasmExecutionEvent { Stdout(Vec), Stderr(Vec), - SyncRpcRequest(JavascriptSyncRpcRequest), + SyncRpcRequest(HostRpcRequest), SignalState { signal: u32, - registration: WasmSignalHandlerRegistration, + registration: ExecutionSignalHandlerRegistration, }, Exited(i32), } @@ -310,6 +323,9 @@ pub enum WasmExecutionError { }, MissingModulePath, InvalidLimit(String), + DeterministicFuelUnsupported { + fuel: u64, + }, InvalidModule(String), NativeBinaryNotSupported { path: PathBuf, @@ -337,6 +353,15 @@ pub enum WasmExecutionError { stream: &'static str, limit: usize, }, + PendingEventLimit { + limit_name: &'static str, + limit: usize, + observed: usize, + }, + Internal { + code: &'static str, + message: &'static str, + }, EventChannelClosed, } @@ -356,6 +381,10 @@ impl fmt::Display for WasmExecutionError { f.write_str("guest WebAssembly execution requires a module path") } Self::InvalidLimit(message) => write!(f, "invalid WebAssembly limit: {message}"), + Self::DeterministicFuelUnsupported { fuel } => write!( + f, + "deterministic WebAssembly fuel budget {fuel} is not supported by the V8 compatibility backend" + ), Self::InvalidModule(message) => write!(f, "invalid WebAssembly module: {message}"), Self::NativeBinaryNotSupported { path, @@ -439,6 +468,15 @@ impl fmt::Display for WasmExecutionError { "guest WebAssembly {stream} exceeded the captured output limit of {limit} bytes" ) } + Self::PendingEventLimit { + limit_name, + limit, + observed, + } => write!( + f, + "ERR_AGENTOS_RESOURCE_LIMIT: {limit_name} limit is {limit}, observed {observed}; raise {limit_name} if needed" + ), + Self::Internal { code, message } => write!(f, "{code}: {message}"), Self::EventChannelClosed => { f.write_str("guest WebAssembly event channel closed unexpectedly") } @@ -456,9 +494,10 @@ pub struct WasmExecution { execution_timeout: Option, execution_started_at: Instant, timeout_reported: bool, - fuel_gauge: Option>, + wall_clock_gauge: Option>, internal_sync_rpc: WasmInternalSyncRpc, - pending_events: VecDeque, + pending_events: WasmEventQueue, + signal_checkpoints: WasmSignalCheckpointInbox, stdout_stream_buffer: Vec, stderr_stream_buffer: Vec, max_stack_bytes: Option, @@ -476,7 +515,281 @@ struct WasmInternalSyncRpc { route_fs_through_sidecar: bool, next_fd: u32, open_files: BTreeMap, - pending_events: VecDeque, + pending_events: WasmEventQueue, +} + +#[derive(Debug)] +struct QueuedSignalCheckpoint { + identity: ExecutionWakeIdentity, + delivery: PublishedSignalCheckpoint, + retained_bytes: usize, + budget: Arc, +} + +impl Drop for QueuedSignalCheckpoint { + fn drop(&mut self) { + self.budget.release(self.retained_bytes); + } +} + +#[derive(Debug)] +struct WasmSignalCheckpointInbox { + checkpoints: Mutex>, + budget: Arc, +} + +impl WasmSignalCheckpointInbox { + fn new(budget: Arc) -> Self { + Self { + checkpoints: Mutex::new(VecDeque::new()), + budget, + } + } + + fn publish( + &self, + identity: ExecutionWakeIdentity, + delivery: PublishedSignalCheckpoint, + ) -> Result<(), HostServiceError> { + let retained_bytes = std::mem::size_of::(); + self.budget.reserve(retained_bytes).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_RESOURCE_LIMIT", error.to_string()) + })?; + let mut checkpoints = match self.checkpoints.lock() { + Ok(checkpoints) => checkpoints, + Err(_) => { + self.budget.release(retained_bytes); + return Err(HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASM_SIGNAL_INBOX_POISONED: signal checkpoint state was poisoned by a prior panic", + )); + } + }; + checkpoints.push_back(QueuedSignalCheckpoint { + identity, + delivery, + retained_bytes, + budget: Arc::clone(&self.budget), + }); + Ok(()) + } + + fn take( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + let mut checkpoints = self + .checkpoints + .lock() + .map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASM_SIGNAL_INBOX_POISONED: signal checkpoint state was poisoned by a prior panic", + ) + })?; + let Some(pending) = checkpoints.front() else { + return Ok(None); + }; + if pending.identity != identity { + return Err(HostServiceError::new( + "ESTALE", + "published signal delivery identity does not match the active execution", + )); + } + Ok(checkpoints.pop_front().map(|pending| pending.delivery)) + } + + fn discard( + &self, + identity: ExecutionWakeIdentity, + delivery_token: u64, + ) -> Result<(), HostServiceError> { + let mut checkpoints = self.checkpoints.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASM_SIGNAL_INBOX_POISONED: signal checkpoint state was poisoned by a prior panic", + ) + })?; + let index = checkpoints + .iter() + .rposition(|pending| { + pending.identity == identity && pending.delivery.delivery_token == delivery_token + }) + .ok_or_else(|| { + HostServiceError::new( + "ESTALE", + "failed compatibility-WASM signal publication was no longer queued", + ) + })?; + checkpoints.remove(index); + Ok(()) + } + + fn discard_identity(&self, identity: ExecutionWakeIdentity) -> Result<(), HostServiceError> { + let mut checkpoints = self.checkpoints.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASM_SIGNAL_INBOX_POISONED: signal checkpoint state was poisoned by a prior panic", + ) + })?; + checkpoints.retain(|pending| pending.identity != identity); + Ok(()) + } +} + +#[derive(Debug)] +struct WasmPendingEventBudget { + state: Mutex<(usize, usize)>, + count_limit: usize, + byte_limit: usize, + count_gauge: Arc, + byte_gauge: Arc, +} + +impl WasmPendingEventBudget { + fn new(count_limit: usize, byte_limit: usize) -> Arc { + Arc::new(Self { + state: Mutex::new((0, 0)), + count_limit, + byte_limit, + count_gauge: agentos_bridge::queue_tracker::register_queue( + TrackedLimit::PendingExecutionEvents, + count_limit, + ), + byte_gauge: agentos_bridge::queue_tracker::register_queue( + TrackedLimit::PendingExecutionEventBytes, + byte_limit, + ), + }) + } + + fn reserve(&self, bytes: usize) -> Result<(), WasmExecutionError> { + let mut state = self + .state + .lock() + .map_err(|_| WasmExecutionError::Internal { + code: "ERR_AGENTOS_WASM_EVENT_ACCOUNTING_POISONED", + message: "pending-event accounting was poisoned by a prior panic", + })?; + let observed_count = state.0.saturating_add(1); + if observed_count > self.count_limit { + return Err(WasmExecutionError::PendingEventLimit { + limit_name: "limits.process.pendingEventCount", + limit: self.count_limit, + observed: observed_count, + }); + } + let observed_bytes = state.1.saturating_add(bytes); + if observed_bytes > self.byte_limit { + return Err(WasmExecutionError::PendingEventLimit { + limit_name: "limits.process.pendingEventBytes", + limit: self.byte_limit, + observed: observed_bytes, + }); + } + *state = (observed_count, observed_bytes); + self.count_gauge.observe_depth(observed_count); + self.byte_gauge.observe_depth(observed_bytes); + Ok(()) + } + + fn release(&self, bytes: usize) { + let mut state = match self.state.lock() { + Ok(state) => state, + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASM_EVENT_ACCOUNTING_POISONED: recovering pending-event accounting while releasing a reservation" + ); + poisoned.into_inner() + } + }; + state.0 = state.0.saturating_sub(1); + state.1 = state.1.saturating_sub(bytes); + self.count_gauge.observe_depth(state.0); + self.byte_gauge.observe_depth(state.1); + } + + #[cfg(test)] + fn usage(&self) -> (usize, usize) { + *self.state.lock().expect("pending-event accounting") + } +} + +#[derive(Debug)] +struct QueuedWasmEvent { + event: Option, + retained_bytes: usize, + budget: Arc, +} + +impl Drop for QueuedWasmEvent { + fn drop(&mut self) { + self.budget.release(self.retained_bytes); + } +} + +#[derive(Debug)] +struct WasmEventQueue { + events: VecDeque, + budget: Arc, +} + +impl WasmEventQueue { + fn new(budget: Arc) -> Self { + Self { + events: VecDeque::new(), + budget, + } + } + + fn push_back(&mut self, event: WasmExecutionEvent) -> Result<(), WasmExecutionError> { + let retained_bytes = wasm_event_retained_bytes(&event); + self.budget.reserve(retained_bytes)?; + self.events.push_back(QueuedWasmEvent { + event: Some(event), + retained_bytes, + budget: Arc::clone(&self.budget), + }); + Ok(()) + } + + fn pop_front(&mut self) -> Option { + self.events + .pop_front() + .and_then(|mut queued| queued.event.take()) + } +} + +impl Default for WasmEventQueue { + fn default() -> Self { + Self::new(WasmPendingEventBudget::new( + DEFAULT_WASM_PENDING_EVENT_COUNT, + DEFAULT_WASM_PENDING_EVENT_BYTES, + )) + } +} + +fn wasm_event_retained_bytes(event: &WasmExecutionEvent) -> usize { + let envelope = std::mem::size_of::(); + match event { + WasmExecutionEvent::Stdout(bytes) | WasmExecutionEvent::Stderr(bytes) => { + envelope.saturating_add(bytes.len()) + } + WasmExecutionEvent::SyncRpcRequest(request) => envelope + .saturating_add(request.method.len()) + .saturating_add(request.raw_bytes_args.values().map(Vec::len).sum::()) + // JSON args arrive through an independently frame-bounded bridge; + // retain a conservative envelope without reserializing attacker + // input merely to account it. + .saturating_add(4 * 1024), + WasmExecutionEvent::SignalState { registration, .. } => envelope.saturating_add( + registration + .mask + .len() + .saturating_mul(std::mem::size_of::()), + ), + WasmExecutionEvent::Exited(_) => envelope, + } } #[derive(Debug, Clone)] @@ -487,22 +800,22 @@ struct WasmGuestPathMapping { } impl WasmExecution { + pub fn sync_rpc_responder(&self) -> JavascriptSyncRpcResponder { + self.inner.sync_rpc_responder() + } + pub fn execution_id(&self) -> &str { &self.execution_id } - pub fn child_pid(&self) -> u32 { - self.child_pid + pub fn native_process_id(&self) -> Option { + (self.child_pid != 0).then_some(self.child_pid) } pub fn v8_session_handle(&self) -> V8SessionHandle { self.inner.v8_session_handle() } - pub fn uses_shared_v8_runtime(&self) -> bool { - self.inner.uses_shared_v8_runtime() - } - pub fn start_prepared(&mut self) -> Result<(), WasmExecutionError> { self.inner.start_prepared().map_err(map_javascript_error)?; self.execution_started_at = Instant::now(); @@ -641,9 +954,6 @@ impl WasmExecution { if self.handle_internal_sync_rpc(request)? { continue; } - if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? { - return Ok(Some(signal_state)); - } } self.enqueue_javascript_event(event)?; } @@ -672,9 +982,6 @@ impl WasmExecution { if self.handle_internal_sync_rpc(request)? { continue; } - if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? { - return Ok(Some(signal_state)); - } } self.enqueue_javascript_event(event)?; } @@ -706,9 +1013,6 @@ impl WasmExecution { if self.handle_internal_sync_rpc(request)? { continue; } - if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? { - return Ok(Some(signal_state)); - } } self.enqueue_javascript_event(event)?; } @@ -794,9 +1098,6 @@ impl WasmExecution { if self.handle_internal_sync_rpc(request)? { continue; } - if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? { - return Ok(signal_state); - } } self.enqueue_javascript_event(event)?; } @@ -828,7 +1129,7 @@ impl WasmExecution { // Observe elapsed usage on real event boundaries. The terminal path // below records the exact configured capacity when the one-shot // deadline wait expires. - if let Some(gauge) = &self.fuel_gauge { + if let Some(gauge) = &self.wall_clock_gauge { gauge.observe_depth(duration_millis_saturating_usize(elapsed)); } if elapsed < limit { @@ -838,9 +1139,9 @@ impl WasmExecution { self.inner.terminate().map_err(map_javascript_error)?; self.timeout_reported = true; let capacity = duration_millis_saturating_usize(limit); - warn_limit_exhausted(TrackedLimit::WasmFuelMs, capacity, capacity); + warn_limit_exhausted(TrackedLimit::WasmWallClockMs, capacity, capacity); self.enqueue_wasm_event(WasmExecutionEvent::Stderr( - b"WebAssembly fuel budget exhausted\n".to_vec(), + b"WebAssembly wall-clock limit exceeded\n".to_vec(), ))?; self.enqueue_wasm_event(WasmExecutionEvent::Exited(WASM_TIMEOUT_EXIT_CODE))?; Ok(self.pending_events.pop_front()) @@ -848,18 +1149,11 @@ impl WasmExecution { fn handle_internal_sync_rpc( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { handle_internal_wasm_sync_rpc_request(&mut self.inner, &mut self.internal_sync_rpc, request) } - fn handle_signal_state_sync_rpc( - &mut self, - request: &JavascriptSyncRpcRequest, - ) -> Result, WasmExecutionError> { - translate_wasm_signal_state_sync_rpc_request(&mut self.inner, request) - } - fn enqueue_javascript_event( &mut self, event: JavascriptExecutionEvent, @@ -883,7 +1177,7 @@ impl WasmExecution { } JavascriptExecutionEvent::SyncRpcRequest(request) => { self.pending_events - .push_back(WasmExecutionEvent::SyncRpcRequest(request)); + .push_back(WasmExecutionEvent::SyncRpcRequest(request))?; } JavascriptExecutionEvent::SignalState { signal, @@ -892,8 +1186,8 @@ impl WasmExecution { self.pending_events .push_back(WasmExecutionEvent::SignalState { signal, - registration: registration.into(), - }); + registration, + })?; } JavascriptExecutionEvent::Exited(code) => { if let Some(original) = self.pending_v8_stack_overflow.take() { @@ -908,9 +1202,9 @@ impl WasmExecution { }; self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)?; } - self.flush_stream_buffers(); + self.flush_stream_buffers()?; self.pending_events - .push_back(WasmExecutionEvent::Exited(code)); + .push_back(WasmExecutionEvent::Exited(code))?; } } Ok(()) @@ -925,11 +1219,11 @@ impl WasmExecution { self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)? } WasmExecutionEvent::Exited(code) => { - self.flush_stream_buffers(); + self.flush_stream_buffers()?; self.pending_events - .push_back(WasmExecutionEvent::Exited(code)); + .push_back(WasmExecutionEvent::Exited(code))?; } - other => self.pending_events.push_back(other), + other => self.pending_events.push_back(other)?, } Ok(()) } @@ -962,9 +1256,9 @@ impl WasmExecution { StreamChannel::Stderr => { WasmExecutionEvent::Stderr(std::mem::take(&mut pending_stream_chunk)) } - }); + })?; } - self.pending_events.push_back(signal_state); + self.pending_events.push_back(signal_state)?; continue; } pending_stream_chunk.extend_from_slice(&line); @@ -973,30 +1267,31 @@ impl WasmExecution { self.pending_events.push_back(match channel { StreamChannel::Stdout => WasmExecutionEvent::Stdout(pending_stream_chunk), StreamChannel::Stderr => WasmExecutionEvent::Stderr(pending_stream_chunk), - }); + })?; } Ok(()) } - fn flush_stream_buffers(&mut self) { + fn flush_stream_buffers(&mut self) -> Result<(), WasmExecutionError> { if !self.stdout_stream_buffer.is_empty() { self.pending_events .push_back(WasmExecutionEvent::Stdout(std::mem::take( &mut self.stdout_stream_buffer, - ))); + )))?; } if !self.stderr_stream_buffer.is_empty() { self.pending_events .push_back(WasmExecutionEvent::Stderr(std::mem::take( &mut self.stderr_stream_buffer, - ))); + )))?; } + Ok(()) } fn handle_wait_sync_rpc_request( &mut self, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, stdout: &mut Vec, stderr: &mut Vec, ) -> Result { @@ -1008,6 +1303,59 @@ impl WasmExecution { return Ok(true); } + // `wait()` is the standalone compatibility helper and has no kernel + // process table to own signal dispositions. Keep its historical + // best-effort behavior for direct engine consumers, but never take + // this path while the sidecar is driving events: pollers receive the + // request and reply only after committing it to the kernel. + if request.method == "process.signal_state" { + self.respond_sync_rpc_success(request.id, Value::Null)?; + return Ok(true); + } + if request.method == "process.signal_mask" { + self.respond_sync_rpc_success(request.id, json!({ "signals": [] }))?; + return Ok(true); + } + if request.method == "process.signal_mask_scope_begin" { + self.respond_sync_rpc_success(request.id, json!(1))?; + return Ok(true); + } + if matches!( + request.method.as_str(), + "process.signal_mask_scope_end" | "process.signal_end" + ) { + self.respond_sync_rpc_success(request.id, Value::Null)?; + return Ok(true); + } + if matches!( + request.method.as_str(), + "process.take_signal" | "process.signal_begin" + ) { + self.respond_sync_rpc_success(request.id, Value::Null)?; + return Ok(true); + } + if request.method == "process.wasm_sync_rpc" { + match request.args.first().and_then(Value::as_str) { + Some("process.signal_mask") => { + self.respond_sync_rpc_success(request.id, json!({ "signals": [] }))?; + return Ok(true); + } + Some("process.signal_mask_scope_begin") => { + self.respond_sync_rpc_success(request.id, json!(1))?; + return Ok(true); + } + Some("process.signal_mask_scope_end" | "process.signal_end") => { + self.respond_sync_rpc_success(request.id, Value::Null)?; + return Ok(true); + } + Some("process.take_signal" | "process.signal_begin") => { + self.respond_sync_rpc_success(request.id, Value::Null)?; + return Ok(true); + } + _ => {} + } + } + if request.method != "__kernel_stdio_write" { return Ok(false); } @@ -1038,6 +1386,133 @@ impl WasmExecution { } } +impl ExecutionBackend for WasmExecution { + fn kind(&self) -> ExecutionBackendKind { + ExecutionBackendKind::WebAssembly + } + + fn synchronous_fd_write_policy(&self) -> SynchronousFdWritePolicy { + SynchronousFdWritePolicy::NonblockingRetry + } + + fn descendant_wait_ownership(&self) -> DescendantWaitOwnership { + DescendantWaitOwnership::Guest + } + + fn descendant_output_ownership(&self) -> DescendantOutputOwnership { + DescendantOutputOwnership::GuestDescriptors + } + + fn native_process_id(&self) -> Option { + WasmExecution::native_process_id(self) + } + + fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { + self.inner.wake_handle(identity) + } + + fn is_prepared_for_start(&self) -> bool { + WasmExecution::is_prepared_for_start(self) + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + WasmExecution::start_prepared(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_START", error.to_string()) + }) + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + if let ShutdownReason::Signal(signal) = reason { + if let Some(process_id) = self.native_process_id() { + return Ok(ShutdownOutcome::ForwardSignal { process_id, signal }); + } + self.terminate().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + return Ok(ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal, + core_dumped: false, + })); + } + self.terminate().map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_SHUTDOWN", error.to_string()) + })?; + Ok(if reason == ShutdownReason::RuntimeFault { + ShutdownOutcome::Exited(ExecutionExit::Exited(1)) + } else { + ShutdownOutcome::AwaitExit + }) + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + let result = if paused { + WasmExecution::pause(self) + } else { + WasmExecution::resume(self) + }; + result.map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_CONTROL", error.to_string()) + }) + } + + fn write_stdin(&mut self, _bytes: &[u8]) -> Result<(), HostServiceError> { + // Sidecar-managed compatibility WASM reads fd 0 from the kernel pipe. + Ok(()) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + WasmExecution::close_stdin(self).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_EXECUTION_STDIN", error.to_string()) + }) + } + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result { + let Some(wake) = self.wake_handle(identity) else { + return Ok(if let Some(process_id) = self.native_process_id() { + SignalCheckpointOutcome::ForwardToProcess { process_id } + } else { + SignalCheckpointOutcome::Unsupported + }); + }; + self.signal_checkpoints.publish( + identity, + PublishedSignalCheckpoint { + signal, + delivery_token, + flags, + }, + )?; + if let Err(error) = wake.publish_signal(signal, delivery_token) { + self.signal_checkpoints.discard(identity, delivery_token)?; + return Err(HostServiceError::new(error.code(), error.to_string())); + } + Ok(SignalCheckpointOutcome::Published) + } + + fn take_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + self.signal_checkpoints.take(identity) + } + + fn discard_signal_checkpoints( + &self, + identity: ExecutionWakeIdentity, + ) -> Result<(), HostServiceError> { + self.signal_checkpoints.discard_identity(identity) + } +} + #[derive(Clone, Copy)] enum StreamChannel { Stdout, @@ -1205,6 +1680,8 @@ impl WasmExecutionEngine { }); } + reject_v8_deterministic_fuel(&request)?; + let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; @@ -1225,7 +1702,7 @@ impl WasmExecutionEngine { // not yet expose a per-module stack lever, so accepting the value would // claim to enforce a policy that the runtime actually ignores. wasm_stack_limit_bytes(&request)?; - let execution_timeout = resolve_wasm_execution_timeout(&request)?; + let execution_timeout = resolve_wasm_wall_clock_limit(&request)?; let import_cache = self .import_caches .get(&context.vm_id) @@ -1281,6 +1758,8 @@ impl WasmExecutionEngine { }); } + reject_v8_deterministic_fuel(&request)?; + let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; @@ -1299,7 +1778,7 @@ impl WasmExecutionEngine { let frozen_time_ms = frozen_time_ms(); validate_module_limits(&resolved_module, &request)?; wasm_stack_limit_bytes(&request)?; - let execution_timeout = resolve_wasm_execution_timeout(&request)?; + let execution_timeout = resolve_wasm_wall_clock_limit(&request)?; let import_cache = self .import_caches .get(&context.vm_id) @@ -1369,9 +1848,10 @@ impl WasmExecutionEngine { defer_execute, }, )?; - let child_pid = javascript_execution.child_pid(); + let child_pid = javascript_execution.native_process_id().unwrap_or_default(); let sandbox_root = wasm_sandbox_root(&request.env); let guest_path_mappings = wasm_guest_path_mappings(&request); + let pending_event_budget = wasm_pending_event_budget(&request.limits)?; Ok(WasmExecution { execution_id, @@ -1380,15 +1860,16 @@ impl WasmExecutionEngine { execution_timeout, execution_started_at: Instant::now(), timeout_reported: false, - // Approach-warn (~80%) before the WASM execution budget is exhausted; - // only registered when a timeout is actually set. - fuel_gauge: execution_timeout.map(|limit| { + // Approach-warn (~80%) before the optional WASM elapsed deadline; + // only registered when a wall-clock limit is configured. + wall_clock_gauge: execution_timeout.map(|limit| { register_limit( - TrackedLimit::WasmFuelMs, + TrackedLimit::WasmWallClockMs, duration_millis_saturating_usize(limit), ) }), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::new(Arc::clone(&pending_event_budget)), + signal_checkpoints: WasmSignalCheckpointInbox::new(Arc::clone(&pending_event_budget)), stdout_stream_buffer: Vec::new(), stderr_stream_buffer: Vec::new(), max_stack_bytes: request.limits.max_stack_bytes, @@ -1406,7 +1887,7 @@ impl WasmExecutionEngine { route_fs_through_sidecar: sandbox_root.is_some(), next_fd: 64, open_files: BTreeMap::new(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::new(pending_event_budget), }, }) } @@ -1420,6 +1901,28 @@ impl WasmExecutionEngine { } } +fn wasm_pending_event_budget( + limits: &WasmExecutionLimits, +) -> Result, WasmExecutionError> { + let count_limit = limits + .pending_event_count + .unwrap_or(DEFAULT_WASM_PENDING_EVENT_COUNT); + let byte_limit = limits + .pending_event_bytes + .unwrap_or(DEFAULT_WASM_PENDING_EVENT_BYTES); + if count_limit == 0 { + return Err(WasmExecutionError::InvalidLimit(String::from( + "limits.process.pendingEventCount must be greater than zero", + ))); + } + if byte_limit == 0 { + return Err(WasmExecutionError::InvalidLimit(String::from( + "limits.process.pendingEventBytes must be greater than zero", + ))); + } + Ok(WasmPendingEventBudget::new(count_limit, byte_limit)) +} + fn map_javascript_error(error: JavascriptExecutionError) -> WasmExecutionError { match error { JavascriptExecutionError::EmptyArgv => WasmExecutionError::Spawn(std::io::Error::new( @@ -1442,10 +1945,20 @@ fn map_javascript_error(error: JavascriptExecutionError) -> WasmExecutionError { JavascriptExecutionError::PendingSyncRpcRequest(id) => WasmExecutionError::RpcResponse( format!("guest WebAssembly sync RPC request {id} is still pending"), ), + JavascriptExecutionError::PendingSyncRpcLimit { limit, observed } => { + WasmExecutionError::PendingEventLimit { + limit_name: "limits.reactor.maxBridgeCalls", + limit, + observed, + } + } JavascriptExecutionError::ExpiredSyncRpcRequest(id) => WasmExecutionError::RpcResponse( format!("guest WebAssembly sync RPC request {id} is no longer pending"), ), JavascriptExecutionError::RpcResponse(message) => WasmExecutionError::RpcResponse(message), + JavascriptExecutionError::BridgeSettlement(error) => { + WasmExecutionError::RpcResponse(error.to_string()) + } JavascriptExecutionError::Terminate(error) => WasmExecutionError::Spawn(error), JavascriptExecutionError::Control(error) => WasmExecutionError::Control(error), JavascriptExecutionError::StdinClosed => WasmExecutionError::StdinClosed, @@ -1460,7 +1973,7 @@ fn map_javascript_error(error: JavascriptExecutionError) -> WasmExecutionError { fn handle_internal_wasm_sync_rpc_request( execution: &mut JavascriptExecution, internal_sync_rpc: &mut WasmInternalSyncRpc, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { // Module-resolution sync RPCs (the wasm runner imports node builtins + // its own ESM) are serviced host-directly via the execution's own @@ -1901,7 +2414,7 @@ fn handle_internal_wasm_sync_rpc_request( WasmExecutionEvent::Stdout(bytes) } else { WasmExecutionEvent::Stderr(bytes) - }); + })?; execution .respond_sync_rpc_success(request.id, json!(bytes_len)) .map_err(map_javascript_error)?; @@ -1958,7 +2471,7 @@ fn handle_internal_wasm_sync_rpc_request( } fn wasm_sync_rpc_method_routes_through_sidecar_kernel( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, internal_sync_rpc: &WasmInternalSyncRpc, ) -> bool { internal_sync_rpc.route_fs_through_sidecar @@ -2235,7 +2748,7 @@ fn wasm_read_only_filesystem_error(path: &str) -> std::io::Error { fn respond_wasm_sync_rpc_metadata( execution: &mut JavascriptExecution, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, label: &str, metadata: Result, ) -> Result<(), WasmExecutionError> { @@ -2249,7 +2762,7 @@ fn respond_wasm_sync_rpc_metadata( fn respond_wasm_sync_rpc_unit( execution: &mut JavascriptExecution, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, label: &str, result: Result<(), std::io::Error>, ) -> Result<(), WasmExecutionError> { @@ -2258,7 +2771,7 @@ fn respond_wasm_sync_rpc_unit( fn respond_wasm_sync_rpc_value( execution: &mut JavascriptExecution, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, label: &str, result: Result, ) -> Result<(), WasmExecutionError> { @@ -2503,61 +3016,6 @@ fn open_wasm_guest_file(path: &Path, flags: &Value) -> std::io::Result }) } -fn translate_wasm_signal_state_sync_rpc_request( - execution: &mut JavascriptExecution, - request: &JavascriptSyncRpcRequest, -) -> Result, WasmExecutionError> { - if request.method != "process.signal_state" { - return Ok(None); - } - - let signal = request - .args - .first() - .and_then(Value::as_u64) - .ok_or_else(|| WasmExecutionError::RpcResponse(String::from("missing signal number")))?; - let action = match request - .args - .get(1) - .and_then(Value::as_str) - .unwrap_or("default") - { - "ignore" => WasmSignalDispositionAction::Ignore, - "user" => WasmSignalDispositionAction::User, - _ => WasmSignalDispositionAction::Default, - }; - let mask = request - .args - .get(2) - .and_then(Value::as_str) - .map(serde_json::from_str::>) - .transpose() - .map_err(|error| WasmExecutionError::RpcResponse(error.to_string()))? - .unwrap_or_default(); - let flags = request - .args - .get(3) - .and_then(|value| { - value - .as_u64() - .or_else(|| value.as_i64().map(|signed| signed as u64)) - }) - .unwrap_or_default() as u32; - - execution - .respond_sync_rpc_success(request.id, Value::Null) - .map_err(map_javascript_error)?; - - Ok(Some(WasmExecutionEvent::SignalState { - signal: signal as u32, - registration: WasmSignalHandlerRegistration { - action, - mask, - flags, - }, - })) -} - fn parse_wasm_signal_state_line( line: &[u8], ) -> Result, WasmExecutionError> { @@ -2586,9 +3044,9 @@ fn parse_wasm_signal_state_line( .and_then(Value::as_str) .unwrap_or("default") { - "ignore" => WasmSignalDispositionAction::Ignore, - "user" => WasmSignalDispositionAction::User, - _ => WasmSignalDispositionAction::Default, + "ignore" => ExecutionSignalDispositionAction::Ignore, + "user" => ExecutionSignalDispositionAction::User, + _ => ExecutionSignalDispositionAction::Default, }; let mask = registration .get("mask") @@ -2608,7 +3066,7 @@ fn parse_wasm_signal_state_line( Ok(Some(WasmExecutionEvent::SignalState { signal: signal as u32, - registration: WasmSignalHandlerRegistration { + registration: ExecutionSignalHandlerRegistration { action, mask, flags, @@ -2673,7 +3131,12 @@ fn start_wasm_javascript_execution( .iter() .map(|(key, value)| (key.clone(), value.clone())), ); - build_wasm_runner_module_source(import_cache, &internal_env, options.warmup_metrics)? + build_wasm_runner_module_source( + import_cache, + &internal_env, + options.warmup_metrics, + request.managed_kernel_host, + )? } WasmSnapshotRunnerMode::Auto | WasmSnapshotRunnerMode::Block => { let userland_bundle = build_wasm_runner_userland_bundle(import_cache)?; @@ -2730,7 +3193,10 @@ fn start_wasm_javascript_execution( .map(|(key, value)| (key.clone(), value.clone())), ); guest_runtime.snapshot_userland_code = Some(userland_bundle); - build_wasm_snapshot_runner_inline_code(options.warmup_metrics) + build_wasm_snapshot_runner_inline_code( + options.warmup_metrics, + request.managed_kernel_host, + ) } else { env.extend( internal_env @@ -2741,6 +3207,7 @@ fn start_wasm_javascript_execution( import_cache, &internal_env, options.warmup_metrics, + request.managed_kernel_host, )? } } @@ -2780,7 +3247,7 @@ fn wasm_runner_javascript_limits( ) -> JavascriptExecutionLimits { JavascriptExecutionLimits { v8_heap_limit_mb: Some(runner_heap_limit_mb), - cpu_time_limit_ms: limits.runner_cpu_time_limit_ms, + cpu_time_limit_ms: limits.active_cpu_time_limit_ms, reactor_work_quantum: limits.reactor_work_quantum, bridge_call_timeout_ms: limits.bridge_call_timeout_ms, ..JavascriptExecutionLimits::default() @@ -2899,8 +3366,16 @@ fn build_wasm_internal_env( request.limits.max_stack_bytes, ); internal_env.insert( - WASM_MODULE_PATH_ENV.to_string(), - resolved_module.specifier.clone(), + WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV.to_string(), + request + .limits + .max_sync_rpc_response_line_bytes + .unwrap_or(DEFAULT_WASM_SYNC_RPC_RESPONSE_LINE_BYTES) + .to_string(), + ); + internal_env.insert( + WASM_MODULE_PATH_ENV.to_string(), + resolved_module.specifier.clone(), ); internal_env.insert( String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), @@ -2959,7 +3434,6 @@ fn wasm_snapshot_runner_base_env(request: &StartWasmExecutionRequest) -> BTreeMa fn scrub_migrated_wasm_limit_env(env: &mut BTreeMap) { for key in [ - WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, WASM_MAX_MODULE_FILE_BYTES_ENV, @@ -2968,6 +3442,7 @@ fn scrub_migrated_wasm_limit_env(env: &mut BTreeMap) { WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, WASM_MAX_SOCKETS_ENV, WASM_MAX_BLOCKING_READ_MS_ENV, + WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV, "AGENTOS_WASM_PREWARM_TIMEOUT_MS", "AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB", ] { @@ -3013,9 +3488,14 @@ fn build_wasm_runner_module_source( import_cache: &NodeImportCache, internal_env: &BTreeMap, warmup_metrics: Option<&[u8]>, + managed_kernel_host: bool, ) -> Result { let runner_source = transformed_wasm_runner_source(import_cache)?; let bootstrap = build_wasm_runner_bootstrap(internal_env, warmup_metrics); + let bootstrap = format!( + "{}\n{bootstrap}", + managed_kernel_host_bootstrap(managed_kernel_host) + ); Ok(insert_wasm_runner_bootstrap(&runner_source, &bootstrap)) } @@ -3100,10 +3580,26 @@ fn build_wasm_runner_snapshot_prelude() -> String { bootstrap.replace(wasm_internal_env_merge_source(), "") } -fn build_wasm_snapshot_runner_inline_code(warmup_metrics: Option<&[u8]>) -> String { +fn managed_kernel_host_bootstrap(managed_kernel_host: bool) -> String { + format!( + r#"Object.defineProperty(globalThis, "__agentOSManagedKernelHost", {{ + configurable: false, + enumerable: false, + value: {managed_kernel_host}, + writable: false, +}});"# + ) +} + +fn build_wasm_snapshot_runner_inline_code( + warmup_metrics: Option<&[u8]>, + managed_kernel_host: bool, +) -> String { let warmup_emit = wasm_warmup_metrics_emit_source(warmup_metrics); + let managed_kernel_host = managed_kernel_host_bootstrap(managed_kernel_host); format!( - r#"{warmup_emit}if (typeof process !== "undefined" && typeof globalThis.__agentOSProcessConfigEnv === "object") {{ + r#"{managed_kernel_host} +{warmup_emit}if (typeof process !== "undefined" && typeof globalThis.__agentOSProcessConfigEnv === "object") {{ process.env = {{ ...(process.env || {{}}), ...globalThis.__agentOSProcessConfigEnv }}; }} await globalThis.__agentOSWasmRunnerRun();"# @@ -3124,7 +3620,8 @@ fn build_wasm_runner_bootstrap( format!( r#"const __agentOSWasmInternalEnv = {internal_env_json}; -const __agentOSWasmSyncRpcReadPayloadBytes = {wasm_sync_rpc_read_payload_bytes}; + const __agentOSWasmSyncRpcReadPayloadBytes = {wasm_sync_rpc_read_payload_bytes}; + const __agentOSWasmEntropyLimitBytes = {WASM_SYNC_READ_LIMIT_BYTES}; const __agentOSRequireBuiltin = (specifier) => {{ if (typeof globalThis.require === "function") {{ return globalThis.require(specifier); @@ -3424,7 +3921,14 @@ if (typeof globalThis !== "undefined") {{ throw new Error("secure-exec WASM signal-drain bridge is unavailable"); }} return _processTakeSignal.applySync(void 0, args); + case "process.exec_image_open": + case "process.exec_image_open_fd": + case "process.exec_image_read": + case "process.exec_image_close": + case "process.image": case "process.getpgid": + case "process.getrlimit": + case "process.setrlimit": case "process.getuid": case "process.getgid": case "process.geteuid": @@ -3448,6 +3952,21 @@ if (typeof globalThis !== "undefined") {{ case "process.setresgid": case "process.setgroups": case "process.umask": + case "process.clock_time": + case "process.clock_resolution": + case "process.sleep": + case "process.system_identity": + case "process.signal_begin": + case "process.signal_end": + case "process.signal_mask": + case "process.signal_mask_scope_begin": + case "process.signal_mask_scope_end": + case "__kernel_tcgetattr": + case "__kernel_tcsetattr": + case "__kernel_tcgetpgrp": + case "__kernel_tcsetpgrp": + case "__kernel_tcgetsid": + case "__kernel_tty_set_size": case "fs.accessSync": case "fs.blockingIoTimeoutMsSync": case "fs.chmodForProcessSync": @@ -3455,6 +3974,11 @@ if (typeof globalThis !== "undefined") {{ case "fs.collapseRangeSync": case "fs.fallocateSync": case "fs.fiemapSync": + case "fs.fiemapAtSync": + case "fs.fgetxattrSync": + case "fs.flistxattrSync": + case "fs.fsetxattrSync": + case "fs.fremovexattrSync": case "fs.getxattrSync": case "fs.insertRangeSync": case "fs.lchownSync": @@ -3473,12 +3997,14 @@ if (typeof globalThis !== "undefined") {{ case "fs.zeroRangeSync": case "process.setpgid": case "process.waitpid_transition": + case "process.waitpid": case "process.itimer_real": case "process.fd_pipe": case "process.fd_open": case "process.path_open_at": case "process.path_mkdir_at": case "process.path_stat_at": + case "process.path_chmod_at": case "process.path_utimes_at": case "process.path_chown_at": case "process.path_link_at": @@ -3487,7 +4013,27 @@ if (typeof globalThis !== "undefined") {{ case "process.path_rename_at": case "process.path_symlink_at": case "process.path_unlink_at": + case "process.random_get": case "process.fd_snapshot": + case "process.hostnet_fd_open": + case "process.hostnet_bind": + case "process.hostnet_connect": + case "process.hostnet_listen": + case "process.hostnet_accept": + case "process.hostnet_validate": + case "process.hostnet_recv": + case "process.hostnet_send": + case "process.hostnet_local_address": + case "process.hostnet_peer_address": + case "process.hostnet_get_option": + case "process.hostnet_set_option": + case "process.hostnet_poll": + case "process.hostnet_tls_connect": + case "process.posix_poll": + case "process.fd_description_identity": + case "process.fd_description_alias_count": + case "process.fd_preopens": + case "process.fd_preopen": case "process.fd_read": case "process.fd_pread": case "process.fd_write": @@ -3496,6 +4042,7 @@ if (typeof globalThis !== "undefined") {{ case "process.fd_datasync": case "process.fd_readdir": case "process.fd_close": + case "process.fd_closefrom": case "process.fd_stat": case "process.fd_filestat": case "process.fd_chmod": @@ -3508,14 +4055,18 @@ if (typeof globalThis !== "undefined") {{ case "process.fd_record_lock": case "process.fd_record_lock_cancel": case "process.fd_dup": + case "process.fd_move": case "process.fd_dup2": case "process.fd_dup_min": case "process.fd_seek": + case "process.fd_path": case "process.fd_chdir_path": case "process.fd_socketpair": + case "process.pty_open": case "process.fd_sendmsg_rights": case "process.fd_recvmsg_rights": case "process.fd_socket_shutdown": + case "dns.resolveRawRr": if (typeof _processWasmSyncRpc === "undefined") {{ throw new Error("secure-exec WASM process-syscall bridge is unavailable"); }} @@ -3690,7 +4241,7 @@ fn prewarm_wasm_path( route_fs_through_sidecar: false, next_fd: 64, open_files: BTreeMap::new(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; let mut stdout = Vec::new(); let mut stderr = Vec::new(); @@ -3832,7 +4383,7 @@ async fn prewarm_wasm_path_async( route_fs_through_sidecar: false, next_fd: 64, open_files: BTreeMap::new(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; let mut stdout = Vec::new(); let mut stderr = Vec::new(); @@ -4186,22 +4737,30 @@ fn warmup_metrics_line( ) } -fn resolve_wasm_execution_timeout( +fn resolve_wasm_wall_clock_limit( request: &StartWasmExecutionRequest, ) -> Result, WasmExecutionError> { - // Node's WASI runtime does not expose per-instruction fuel metering, so an - // EXPLICITLY configured "fuel" budget is enforced as a tight wall-clock - // timeout. The value rides the typed `limits.max_fuel` (from the BARE-wire - // resource limits), not an `AGENTOS_WASM_MAX_FUEL` env var. - // - // With no explicit fuel budget there is NO default wall-clock timeout — - // matching the JS execution philosophy (wall-clock backstop is opt-in). + // Wall-clock time is an independent, opt-in elapsed deadline. With no + // explicit value there is no outer timeout, matching interactive command + // semantics. // The guest stays bounded by default anyway: the wasm module executes on // the runner isolate's thread, whose TRUE-CPU budget (the V8 CPU-time // watchdog, default 30s ACTIVE CPU) terminates an infinite-loop module // while letting an idle interactive guest (vim blocked in a kernel input // wait) live indefinitely, exactly like native Linux. - Ok(request.limits.max_fuel.map(Duration::from_millis)) + Ok(request + .limits + .wall_clock_limit_ms + .map(Duration::from_millis)) +} + +fn reject_v8_deterministic_fuel( + request: &StartWasmExecutionRequest, +) -> Result<(), WasmExecutionError> { + match request.limits.deterministic_fuel { + Some(fuel) => Err(WasmExecutionError::DeterministicFuelUnsupported { fuel }), + None => Ok(()), + } } /// Resolve the per-execution WASM stack cap from the typed wire limit. The V8 @@ -4649,26 +5208,6 @@ fn read_varuint_usize( }) } -impl From for WasmSignalDispositionAction { - fn from(value: NodeSignalDispositionAction) -> Self { - match value { - NodeSignalDispositionAction::Default => Self::Default, - NodeSignalDispositionAction::Ignore => Self::Ignore, - NodeSignalDispositionAction::User => Self::User, - } - } -} - -impl From for WasmSignalHandlerRegistration { - fn from(value: NodeSignalHandlerRegistration) -> Self { - Self { - action: value.action.into(), - mask: value.mask, - flags: value.flags, - } - } -} - fn resolve_path_like_specifier(cwd: &Path, specifier: &str) -> Option { if specifier.starts_with("file://") { return Some(PathBuf::from(specifier.trim_start_matches("file://"))); @@ -4689,33 +5228,39 @@ fn resolve_path_like_specifier(cwd: &Path, specifier: &str) -> Option { #[cfg(test)] mod tests { use super::{ - build_wasm_internal_env, build_wasm_runner_bootstrap, max_cbor_byte_string_payload_bytes, - open_wasm_guest_file, resolve_wasm_execution_timeout, resolve_wasm_prewarm_timeout, - resolve_wasm_stack_limit_bytes, resolved_module_path, translate_wasm_guest_path, + build_wasm_internal_env, build_wasm_runner_bootstrap, + build_wasm_snapshot_runner_inline_code, managed_kernel_host_bootstrap, + max_cbor_byte_string_payload_bytes, open_wasm_guest_file, reject_v8_deterministic_fuel, + resolve_wasm_prewarm_timeout, resolve_wasm_stack_limit_bytes, + resolve_wasm_wall_clock_limit, resolved_module_path, translate_wasm_guest_path, translate_wasm_host_symlink_target, wasm_guest_module_paths, wasm_host_path_is_read_only, wasm_memory_limit_bytes, wasm_memory_limit_pages, wasm_mutation_touches_read_only_mapping, wasm_read_only_filesystem_error, wasm_runner_base_env, wasm_runner_heap_limit_mb, wasm_runner_javascript_limits, wasm_sandbox_root, wasm_snapshot_runner_base_env, wasm_sync_read_length, wasm_sync_rpc_error_code, wasm_sync_rpc_method_routes_through_sidecar_kernel, CreateWasmContextRequest, - GuestRuntimeConfig, JavascriptSyncRpcRequest, ResolvedWasmModule, - StartWasmExecutionRequest, Value, WasmExecutionEngine, WasmExecutionError, - WasmExecutionLimits, WasmInternalSyncRpc, WasmPermissionTier, + ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration, GuestRuntimeConfig, + HostRpcRequest, ResolvedWasmModule, StartWasmExecutionRequest, Value, WasmEventQueue, + WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, WasmExecutionLimits, + WasmInternalSyncRpc, WasmPendingEventBudget, WasmPermissionTier, WasmSignalCheckpointInbox, DEFAULT_WASM_PREWARM_TIMEOUT_MS, DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB, NODE_WASI_MODULE_SOURCE, WASM_CAPTURED_OUTPUT_LIMIT_BYTES, - WASM_INTERNAL_MAX_STACK_BYTES_ENV, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, - WASM_MAX_MODULE_FILE_BYTES_ENV, WASM_MAX_SPAWN_FILE_ACTIONS_ENV, + WASM_INTERNAL_MAX_STACK_BYTES_ENV, WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV, + WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_MODULE_FILE_BYTES_ENV, WASM_MAX_SPAWN_FILE_ACTIONS_ENV, WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, WASM_PAGE_BYTES, WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES, WASM_SANDBOX_ROOT_ENV, WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SYNC_READ_LIMIT_BYTES, }; - use std::collections::{BTreeMap, BTreeSet, VecDeque}; + use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; + use std::sync::Arc; use std::time::Duration; use tempfile::tempdir; + use crate::backend::{ExecutionWakeIdentity, PublishedSignalCheckpoint}; + #[test] fn wasm_runner_forwards_vm_reactor_limits_to_javascript() { let limits = WasmExecutionLimits { @@ -4730,6 +5275,367 @@ mod tests { assert_eq!(javascript.bridge_call_timeout_ms, Some(12_345)); } + #[test] + fn managed_kernel_host_mode_is_trusted_frozen_bootstrap_not_guest_env() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + assert!(runner.contains( + "const SIDECAR_MANAGED_PROCESS = globalThis.__agentOSManagedKernelHost === true;" + )); + assert!(!runner.contains("typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string'")); + + let managed = managed_kernel_host_bootstrap(true); + assert!(managed.contains("configurable: false")); + assert!(managed.contains("writable: false")); + assert!(managed.contains("value: true")); + + let standalone = managed_kernel_host_bootstrap(false); + assert!(standalone.contains("value: false")); + + let snapshot = build_wasm_snapshot_runner_inline_code(None, true); + let mode = snapshot + .find("__agentOSManagedKernelHost") + .expect("trusted mode bootstrap"); + let run = snapshot + .find("__agentOSWasmRunnerRun") + .expect("snapshotted runner invocation"); + assert!( + mode < run, + "mode must be frozen before runner code executes" + ); + } + + #[test] + fn managed_stdio_duplication_uses_kernel_descriptions() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let helper = runner + .find("function managedKernelFdForDuplicate(fd) {") + .expect("managed duplicate helper"); + let dup = runner + .find(" fd_dup(fd, retNewFdPtr) {") + .expect("fd_dup import"); + let dup_min = runner + .find(" fd_dup_min(fd, minFd, retNewFdPtr) {") + .expect("fd_dup_min import"); + assert!(helper < dup); + + let dup_min_end = runner[dup_min..] + .find(" fd_getfd(") + .map(|offset| dup_min + offset) + .expect("fd_dup_min terminator"); + for (section, rpc) in [ + ( + &runner[dup..dup_min], + "callSyncRpc('process.fd_dup', [kernelSourceFd])", + ), + ( + &runner[dup_min..dup_min_end], + "callSyncRpc('process.fd_dup_min', [kernelSourceFd, minimumFdNumber])", + ), + ] { + let kernel_source = section + .find("managedKernelFdForDuplicate(sourceFd)") + .expect("managed stdio duplicate source"); + let kernel_dup = section.find(rpc).expect("kernel duplicate call"); + let local_clone = section + .find("cloneFdHandle(sourceFd)") + .expect("standalone duplicate fallback"); + assert!(kernel_source < kernel_dup); + assert!(kernel_dup < local_clone); + } + assert!( + runner[dup_min..dup_min_end].contains("authoritative\n // RLIMIT_NOFILE") + ); + assert!(runner[dup_min..dup_min_end].contains( + "registerKernelDelegateFd(\n callSyncRpc('process.fd_dup_min', [kernelSourceFd, minimumFdNumber]),\n null,\n minimumFdNumber," + )); + + let fdstat_get = runner + .find("wasiImport.fd_fdstat_get = (fd, statPtr) => {") + .expect("fdstat get override"); + let fdstat_set = runner + .find("wasiImport.fd_fdstat_set_flags = (fd, flags) => {") + .expect("fdstat set override"); + let get_section = &runner[fdstat_get..fdstat_set]; + assert!( + get_section + .find("callSyncRpc('process.fd_stat'") + .expect("kernel fdstat projection") + < get_section + .find("if (!SIDECAR_MANAGED_PROCESS && hostNetSocket") + .expect("standalone host-net fallback") + ); + assert!(get_section.contains("BigInt(stat?.rightsBase ?? 0)")); + + let set_end = runner[fdstat_set..] + .find("wasiImport.fd_filestat_get") + .map(|offset| fdstat_set + offset) + .expect("fdstat set terminator"); + let set_section = &runner[fdstat_set..set_end]; + assert!( + set_section + .find("callSyncRpc('process.fd_set_flags'") + .expect("kernel status flag update") + < set_section + .find("hostNetSocket.nonblock =") + .expect("managed transport metadata update") + ); + assert!(set_section.contains("if (!SIDECAR_MANAGED_PROCESS && hostNetSocket")); + + let open_alias = runner + .find("function kernelOpenPathForGuestPath(guestPath) {") + .expect("managed proc-fd open translation"); + let open_alias_end = runner[open_alias..] + .find("function fsOpenNumericFlagsForManagedPath") + .map(|offset| open_alias + offset) + .expect("managed proc-fd helper terminator"); + let open_alias = &runner[open_alias..open_alias_end]; + assert!(open_alias.contains("(?:proc\\/self\\/fd|dev\\/fd)")); + assert!(open_alias.contains("lookupFdHandle(guestFd)")); + assert!(open_alias.contains("handle.targetFd")); + assert!(open_alias.contains("`dev/fd/${Number(handle.targetFd) >>> 0}`")); + let path_open = &runner[runner + .find(" wasiImport.path_open = (") + .expect("path_open override")..]; + assert!(path_open.contains( + "const managedOpenPath = SIDECAR_MANAGED_PROCESS\n ? kernelOpenPathForGuestPath" + )); + assert!(path_open.matches("managedOpenPath,").count() >= 2); + + let write_start = runner + .find("wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => {") + .expect("fd_write override"); + let write_end = runner[write_start..] + .find("wasiImport.fd_close = (fd) => {") + .map(|offset| write_start + offset) + .expect("fd_write terminator"); + let write = &runner[write_start..write_end]; + assert!(runner.contains("function kernelFdStdioStream(targetFd, descriptorPath) {")); + assert!(runner.contains("callSyncRpc('__kernel_isatty', [targetFd])")); + assert!(runner + .contains("callSyncRpc('process.fd_description_identity', [targetFd])?.descriptionId")); + let path_lookup = write + .find("callSyncRpc('process.fd_path', [kernelFd])") + .expect("kernel output aliases must resolve their authoritative path"); + let stdio_write = write + .find("callSyncRpc('__kernel_stdio_write', [kernelFd, bytes])") + .expect("stdout/stderr aliases must use the ordered output path"); + let ordinary_write = write + .find("writeKernelFdCooperatively(kernelFd, bytes)") + .expect("non-output descriptions retain the generic kernel write path"); + assert!(path_lookup < stdio_write); + assert!(path_lookup < ordinary_write); + } + + #[test] + fn optional_posix_identity_ids_use_explicit_null_on_the_typed_bridge() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + assert!(runner.contains( + "function hostUserOptionalId(value) {\n const id = Number(value) >>> 0;\n return id === 0xffffffff ? null : id;\n}" + )); + for method in ["setreuid", "setresuid", "setregid", "setresgid"] { + assert!( + runner.contains(&format!("callSyncRpc('process.{method}', [")), + "{method} must use the typed identity bridge" + ); + } + } + + #[test] + fn path_owner_accepts_libc_at_fdcwd_sentinel() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find(" path_owner(fd, pathPtr, pathLen, followSymlinks, retUidPtr, retGidPtr) {") + .expect("path-owner import"); + let section = &runner[start..]; + let end = section.find(" fd_owner(").expect("path-owner import end"); + let section = §ion[..end]; + + assert!(section.contains("const numericFd = Number(fd) >>> 0;")); + assert!(runner.contains("const NODE_CWD_FD = 0xffffffff;")); + assert!(section.contains("numericFd === NODE_CWD_FD")); + assert!(section.contains("dirFd: NODE_CWD_FD")); + assert!(section.contains("path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget)")); + assert!(section.contains("kernelPathOperand(numericFd, pathPtr, pathLen)")); + + let chown_start = runner + .find(" path_chown(fd, pathPtr, pathLen, uid, gid, followSymlinks) {") + .expect("path-chown import"); + let chown = &runner[chown_start..]; + let chown_end = chown.find(" fd_chown(").expect("path-chown import end"); + let chown = &chown[..chown_end]; + assert!(chown.contains("numericFd === NODE_CWD_FD")); + assert!(chown.contains("dirFd: NODE_CWD_FD")); + } + + #[test] + fn proc_fd_readlink_rewrites_absolute_and_preopen_relative_guest_paths() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function kernelProcFdPathForGuestPath(guestPath) {") + .expect("proc-fd path rewrite"); + let section = &runner[start..]; + let end = section + .find("function fsOpenNumericFlagsForManagedPath") + .expect("proc-fd path rewrite end"); + let section = §ion[..end]; + + assert!(section.contains(r"/^(\/?)proc\/self\/fd\/(\d+)$/u")); + assert!(section.contains("const guestFd = Number(match[2]);")); + assert!(section.contains("`${match[1]}proc/self/fd/${Number(handle.targetFd) >>> 0}`")); + } + + #[test] + fn wasm_pending_event_queue_rejects_count_limit_without_leaking_reservation() { + let budget = WasmPendingEventBudget::new(1, usize::MAX); + let mut queue = WasmEventQueue::new(Arc::clone(&budget)); + + queue + .push_back(WasmExecutionEvent::Exited(0)) + .expect("event at count limit"); + let usage_at_limit = budget.usage(); + let error = queue + .push_back(WasmExecutionEvent::Exited(1)) + .expect_err("event over count limit"); + + assert!(matches!( + error, + WasmExecutionError::PendingEventLimit { + limit_name: "limits.process.pendingEventCount", + limit: 1, + observed: 2, + } + )); + assert_eq!(budget.usage(), usage_at_limit, "rejection must roll back"); + assert_eq!(queue.pop_front(), Some(WasmExecutionEvent::Exited(0))); + assert_eq!(budget.usage(), (0, 0), "dequeue must release reservation"); + } + + #[test] + fn wasm_pending_event_queue_rejects_byte_limit_without_leaking_reservation() { + let event = WasmExecutionEvent::Stdout(vec![7; 8]); + let retained_bytes = super::wasm_event_retained_bytes(&event); + let budget = WasmPendingEventBudget::new(2, retained_bytes); + let mut queue = WasmEventQueue::new(Arc::clone(&budget)); + + queue.push_back(event.clone()).expect("event at byte limit"); + let error = queue + .push_back(event.clone()) + .expect_err("event over byte limit"); + + assert!(matches!( + error, + WasmExecutionError::PendingEventLimit { + limit_name: "limits.process.pendingEventBytes", + limit, + observed, + } if limit == retained_bytes && observed == retained_bytes * 2 + )); + assert_eq!(budget.usage(), (1, retained_bytes)); + assert_eq!(queue.pop_front(), Some(event)); + assert_eq!(budget.usage(), (0, 0)); + } + + #[test] + fn wasm_outer_and_internal_signal_events_share_one_budget() { + let signal_event = WasmExecutionEvent::SignalState { + signal: 15, + registration: ExecutionSignalHandlerRegistration { + action: ExecutionSignalDispositionAction::User, + mask: vec![1, 2, 3], + flags: 0, + }, + }; + let retained_bytes = super::wasm_event_retained_bytes(&signal_event); + let budget = WasmPendingEventBudget::new(1, retained_bytes * 2); + let mut outer = WasmEventQueue::new(Arc::clone(&budget)); + let mut internal = WasmEventQueue::new(Arc::clone(&budget)); + + outer + .push_back(signal_event.clone()) + .expect("first expanded signal event"); + let error = internal + .push_back(signal_event.clone()) + .expect_err("signal expansion cannot bypass the shared count bound"); + assert!(matches!( + error, + WasmExecutionError::PendingEventLimit { + limit_name: "limits.process.pendingEventCount", + limit: 1, + observed: 2, + } + )); + assert_eq!(budget.usage(), (1, retained_bytes)); + + assert_eq!(outer.pop_front(), Some(signal_event.clone())); + internal + .push_back(signal_event.clone()) + .expect("released capacity is reusable by internal queue"); + assert_eq!(internal.pop_front(), Some(signal_event)); + assert_eq!(budget.usage(), (0, 0)); + } + + #[test] + fn wasm_signal_checkpoint_inbox_is_bounded_and_generation_scoped() { + let retained_bytes = std::mem::size_of::(); + let budget = WasmPendingEventBudget::new(1, retained_bytes); + let inbox = WasmSignalCheckpointInbox::new(Arc::clone(&budget)); + let identity = ExecutionWakeIdentity { + generation: 7, + pid: 42, + }; + let delivery = PublishedSignalCheckpoint { + signal: 15, + delivery_token: 99, + flags: 0x8000_0000, + }; + + inbox + .publish(identity, delivery) + .expect("first checkpoint fits the shared pending-event budget"); + let limit_error = inbox + .publish(identity, delivery) + .expect_err("second checkpoint must hit the count limit"); + assert_eq!(limit_error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert!(limit_error + .message + .contains("limits.process.pendingEventCount")); + assert_eq!(budget.usage(), (1, retained_bytes)); + + let stale_error = inbox + .take(ExecutionWakeIdentity { + generation: identity.generation + 1, + ..identity + }) + .expect_err("another execution generation cannot consume the checkpoint"); + assert_eq!(stale_error.code, "ESTALE"); + assert_eq!(budget.usage(), (1, retained_bytes)); + + assert_eq!( + inbox.take(identity).expect("matching execution identity"), + Some(delivery) + ); + assert_eq!(budget.usage(), (0, 0), "take must release the budget"); + assert_eq!(inbox.take(identity).expect("empty inbox"), None); + + inbox + .publish(identity, delivery) + .expect("checkpoint can be queued again after take"); + inbox + .discard(identity, delivery.delivery_token) + .expect("failed wake publication can roll back its checkpoint"); + assert_eq!(budget.usage(), (0, 0), "discard must release the budget"); + assert_eq!(inbox.take(identity).expect("discarded inbox"), None); + + inbox + .publish(identity, delivery) + .expect("old exec image checkpoint"); + inbox + .discard_identity(identity) + .expect("successful exec discards old-image checkpoints"); + assert_eq!(budget.usage(), (0, 0), "exec discard must release budget"); + assert_eq!(inbox.take(identity).expect("exec-discarded inbox"), None); + } + #[test] fn wasm_process_reads_fit_the_encoded_bridge_response_budget() { let raw_limit = max_cbor_byte_string_payload_bytes(WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES); @@ -4746,10 +5652,592 @@ mod tests { assert!(bootstrap.contains(&format!( "const __agentOSWasmSyncRpcReadPayloadBytes = {raw_limit};" ))); + for method in [ + "process.exec_image_open", + "process.exec_image_open_fd", + "process.exec_image_read", + "process.exec_image_close", + ] { + assert!( + bootstrap.contains(&format!("case \"{method}\":")), + "{method} must route through the V8 host-process bridge" + ); + } let runner = include_str!("../assets/runners/wasm-runner.mjs"); assert!(runner.contains("boundedWasmSyncRpcReadLength(")); assert!(runner.contains("callSyncRpc('process.fd_read'")); assert!(runner.contains("callSyncRpc('process.fd_pread'")); + assert!(runner.contains("callSyncRpc('process.exec_image_open'")); + assert!(runner.contains("callSyncRpc('process.exec_image_open_fd'")); + assert!(runner.contains("callSyncRpc('process.exec_image_read'")); + assert!(runner.contains("callSyncRpc('process.exec_image_close'")); + assert!(runner.contains("finally {")); + } + + #[test] + fn wasm_sync_rpc_response_line_limit_is_internal_config_and_source_guarded() { + let mut request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits { + max_sync_rpc_response_line_bytes: Some(8_192), + ..WasmExecutionLimits::default() + }); + request.env.insert( + WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV.to_string(), + String::from("999999"), + ); + let resolved_module = ResolvedWasmModule { + specifier: String::from("./guest.wasm"), + resolved_path: PathBuf::from("/tmp/guest.wasm"), + }; + + let internal_env = + build_wasm_internal_env(&resolved_module, &request, 1_234, false).expect("env"); + assert_eq!( + internal_env.get(WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV), + Some(&String::from("8192")), + "the typed limit must override an ambient internal env value" + ); + assert!( + !wasm_runner_base_env(&request) + .contains_key(WASM_INTERNAL_SYNC_RPC_RESPONSE_LINE_BYTES_ENV), + "the internal line cap must not remain guest-configurable" + ); + + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + assert!(runner.contains("Math.ceil(maxSyncRpcResponseLineBytes * 0.8)")); + assert!(runner.contains("ERR_AGENTOS_RESOURCE_LIMIT")); + assert!(runner.contains("raise limits.reactor.maxBridgeResponseBytes")); + assert!(runner.contains("remaining > 4095 ? 4096 : remaining + 1")); + } + + #[test] + fn wasm_sync_rpc_response_line_runtime_rejects_before_retaining_overflow() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let marker_begin = "// AGENTOS_SYNC_RPC_RESPONSE_LIMIT_HELPERS_BEGIN"; + let marker_end = "// AGENTOS_SYNC_RPC_RESPONSE_LIMIT_HELPERS_END"; + let helper_start = runner.find(marker_begin).expect("helper begin marker"); + let helper_end = runner.find(marker_end).expect("helper end marker"); + let helpers = &runner[helper_start + marker_begin.len()..helper_end]; + let script = format!( + r#"{helpers} +let retained = Buffer.alloc(0); +retained = appendSyncRpcResponseChunk(retained, Buffer.from('12345678'), 8).buffer; +let overflow; +try {{ + appendSyncRpcResponseChunk(retained, Buffer.from('9'), 8); +}} catch (error) {{ + overflow = {{ + code: error.code, + details: error.details, + message: error.message, + retainedBytes: retained.byteLength, + }}; +}} +const exact = appendSyncRpcResponseChunk(Buffer.alloc(0), Buffer.from('12345678\n'), 8); +process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRetained: exact.buffer.byteLength }})); +"# + ); + let output = std::process::Command::new("node") + .args(["--input-type=module", "--eval", script.as_str()]) + .output() + .expect("run node response-line helper"); + assert!( + output.status.success(), + "node helper failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let result: Value = serde_json::from_slice(&output.stdout).expect("helper JSON"); + assert_eq!(result["overflow"]["code"], "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + result["overflow"]["details"]["limitName"], + "limits.reactor.maxBridgeResponseBytes" + ); + assert_eq!(result["overflow"]["details"]["limit"], 8); + assert_eq!(result["overflow"]["details"]["observed"], 9); + assert_eq!(result["overflow"]["retainedBytes"], 8); + assert_eq!(result["exactLine"], "12345678"); + assert_eq!(result["exactRetained"], 0); + } + + #[test] + fn wasm_host_tty_read_prevalidates_guest_destination_before_kernel_read() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let tty_start = runner + .find("const hostTtyImport = {") + .expect("host_tty import must exist"); + let tty_end = runner[tty_start..] + .find(" isatty(fd) {") + .map(|offset| tty_start + offset) + .expect("host_tty read must precede isatty"); + let tty_read = &runner[tty_start..tty_end]; + let validation = tty_read + .find("guestRangeIsValid(ptr, cap)") + .expect("host_tty read must validate its full output range"); + let consuming_read = tty_read + .find("callSyncRpc('__kernel_stdin_read'") + .expect("host_tty read must call the kernel stdin RPC"); + + assert!( + validation < consuming_read, + "guest output memory must be validated before PTY bytes are consumed" + ); + } + + #[test] + fn wasm_host_tty_size_returns_typed_native_errno_instead_of_throwing_rpc_errors() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let size_start = runner + .find(" get_size(fd, colsPtr, rowsPtr) {") + .expect("host_tty get_size must exist"); + let size_end = runner[size_start..] + .find(" set_size(fd, cols, rows) {") + .map(|offset| size_start + offset) + .expect("host_tty get_size must precede set_size"); + let get_size = &runner[size_start..size_end]; + + assert!(get_size.contains("try {")); + assert!(get_size.contains("if (error?.code === 'EBADF') return 9;")); + assert!(get_size.contains("if (error?.code === 'ENOTTY') return 25;")); + assert!( + get_size.find("guestRangeIsValid(colsPtr, 2)").unwrap() + < get_size.find("callSyncRpc('__kernel_tty_size'").unwrap(), + "guest output pointers must be validated before the tty-size RPC" + ); + } + + #[test] + fn wasm_preview1_memory_is_prevalidated_before_host_side_effects() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + + let assert_precedes = |start: &str, validation: &str, effect: &str| { + let section_start = runner + .rfind(start) + .unwrap_or_else(|| panic!("missing {start}")); + let section = &runner[section_start..]; + let validation_offset = section + .find(validation) + .unwrap_or_else(|| panic!("missing validation {validation} in {start}")); + let effect_offset = section + .find(effect) + .unwrap_or_else(|| panic!("missing effect {effect} in {start}")); + assert!( + validation_offset < effect_offset, + "{start} must validate guest memory before {effect}" + ); + }; + + assert!(runner.contains("const LINUX_IOV_MAX = 1024;")); + assert_precedes( + "wasiImport.fd_read =", + "validateGuestIovRequest(iovs, iovsLen)", + "getHostNetSocket(numericFd)", + ); + assert_precedes( + "wasiImport.fd_write =", + "validateGuestIovRequest(iovs, iovsLen)", + "getHostNetSocket(numericFd)", + ); + assert_precedes( + "wasiImport.fd_pread =", + "guestRangeIsValid(nreadPtr, 4)", + "callSyncRpc('process.fd_pread'", + ); + assert_precedes( + "wasiImport.fd_pwrite =", + "guestRangeIsValid(nwrittenPtr, 4)", + "callSyncRpc('process.fd_pwrite'", + ); + assert_precedes( + "wasiImport.fd_readdir =", + "guestRangesAreValid([bufPtr, bufferLength], [bufUsedPtr, 4])", + "lookupFdHandle(numericFd)", + ); + assert_precedes( + "wasiImport.fd_seek =", + "guestRangeIsValid(newOffsetPtr, 8)", + "callSyncRpc('process.fd_seek'", + ); + assert_precedes( + "wasiImport.fd_tell =", + "guestRangeIsValid(offsetPtr, 8)", + "callSyncRpc('process.fd_seek'", + ); + assert_precedes( + "wasiImport.fd_filestat_get =", + "guestRangeIsValid(statPtr, 64)", + "callSyncRpc('process.fd_filestat'", + ); + assert_precedes( + "wasiImport.poll_oneoff =", + "guestRangesAreValid(", + "new DataView(instanceMemory.buffer)", + ); + + let path_filestat = runner + .find(" path_filestat_get(args) {") + .expect("kernel path_filestat_get handler"); + let path_filestat = &runner[path_filestat..]; + assert!( + path_filestat + .find("guestRangesAreValid([args[2]") + .expect("path_filestat_get validation") + < path_filestat + .find("callSyncRpc('process.path_stat_at'") + .expect("path_filestat_get RPC") + ); + + for (start, end, ambient_io) in [ + ( + "wasiImport.fd_pread =", + "wasiImport.fd_pwrite =", + "fsModule.readSync(", + ), + ( + "wasiImport.fd_pwrite =", + "wasiImport.fd_sync =", + "fsModule.writeSync(", + ), + ] { + let start = runner.rfind(start).expect("positioned I/O wrapper"); + let section = &runner[start..]; + let end = section.find(end).expect("positioned I/O wrapper end"); + let section = §ion[..end]; + assert!( + section + .find("if (SIDECAR_MANAGED_PROCESS) return WASI_ERRNO_BADF;") + .expect("managed ambient-I/O guard") + < section.find(ambient_io).expect("standalone ambient I/O"), + "managed positioned I/O must stop before {ambient_io}" + ); + } + + let path_open = runner + .find(" wasiImport.path_open = (") + .expect("path_open wrapper"); + let section = &runner[path_open..]; + assert!( + section + .find("guestRangesAreValid([pathPtr") + .expect("path_open validation") + < section + .find("pendingOpenCreateMode") + .expect("path_open state consumption") + ); + } + + #[test] + fn wasm_clock_and_system_outputs_are_prevalidated_before_process_rpc() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + for (start, end, validation, rpc) in [ + ( + "\nwasiImport.clock_time_get = (", + "\nwasiImport.clock_res_get = (", + "guestRangeIsValid(resultPtr, 8)", + "callSyncRpc('process.clock_time'", + ), + ( + "\nwasiImport.clock_res_get = (", + "// Managed VMs use the shared sidecar entropy source.", + "guestRangeIsValid(resultPtr, 8)", + "callSyncRpc('process.clock_resolution'", + ), + ( + "const hostSystemImport = {", + "// Terminal event source", + "guestRangeIsValid(bufferPtr, capacity)", + "callSyncRpc('process.system_identity'", + ), + ] { + let start_offset = runner.find(start).expect("provider start marker"); + let provider = &runner[start_offset..]; + let end_offset = provider.find(end).expect("provider end marker"); + let provider = &provider[..end_offset]; + let validation_offset = provider.find(validation).expect("output validation"); + let rpc_offset = provider.find(rpc).expect("shared process RPC"); + assert!( + validation_offset < rpc_offset, + "{start} must validate guest output before host work" + ); + } + + let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None); + for method in [ + "process.clock_time", + "process.clock_resolution", + "process.system_identity", + ] { + assert!(bootstrap.contains(&format!("case \"{method}\":"))); + } + } + + #[test] + fn wasm_process_outputs_are_prevalidated_before_side_effects() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let cases = [ + ( + "function returnWaitedChild(\n", + "function returnLegacyWaitedChild(", + "guestRangesAreValid(\n", + "dispatchPendingWasmSignals()", + ), + ( + "function returnLegacyWaitedChild(", + "function returnRawWaitedChild(", + "guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])", + "dispatchPendingWasmSignals()", + ), + ( + "function returnRawWaitedChild(", + "function processChildEvent(", + "guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])", + "dispatchPendingWasmSignals()", + ), + ( + " proc_spawn(\n", + " proc_spawn_v3(\n", + "guestRangeIsValid(retPidPtr, 4)", + "readGuestBytes(argvPtr, argvLen)", + ), + ( + " proc_spawn_v3(\n", + " proc_spawn_v4(\n", + "guestRangeIsValid(retPidPtr, 4)", + "activeSpawnCallContext = {", + ), + ( + " proc_spawn_v4(\n", + " proc_spawn_v2(\n", + "guestRangeIsValid(retPidPtr, 4)", + "activeSpawnCallContext = {", + ), + ( + " proc_spawn_v2(\n", + " proc_exec(\n", + "guestRangeIsValid(retPidPtr, 4)", + "callSyncRpc('child_process.spawn'", + ), + ( + " proc_waitpid(pid, options, retStatusPtr, retPidPtr) {", + " proc_waitpid_v2(\n", + "guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])", + "pollChildEvent(record, 0)", + ), + ( + " proc_waitpid_v2(\n", + " proc_waitpid_v3(", + "guestRangesAreValid(\n", + "pollChildEvent(record, 0)", + ), + ( + " proc_waitpid_v3(", + " proc_kill(", + "guestRangesAreValid([retStatusPtr, 4], [retPidPtr, 4])", + "callSyncRpc('process.waitpid_transition'", + ), + ( + " proc_getrlimit(", + " proc_setrlimit(", + "guestRangesAreValid([retSoftPtr, 8], [retHardPtr, 8])", + "callSyncRpc('process.getrlimit'", + ), + ( + " proc_umask(", + " umask(", + "guestRangeIsValid(retPreviousPtr, 4)", + "callSyncRpc('process.umask'", + ), + ( + " umask(", + " proc_itimer_real(", + "guestRangeIsValid(retPreviousPtr, 4)", + "callSyncRpc('process.umask'", + ), + ( + " proc_itimer_real(", + " proc_getpgid(", + "guestRangesAreValid([retRemainingUsPtr, 8], [retIntervalUsPtr, 8])", + "callSyncRpc('process.itimer_real'", + ), + ( + " proc_getpgid(", + " proc_setpgid(", + "guestRangeIsValid(retPgidPtr, 4)", + "callSyncRpc('process.getpgid'", + ), + ( + " fd_pipe(", + " fd_dup(", + "guestRangesAreValid([retReadFdPtr, 4], [retWriteFdPtr, 4])", + "hasRunnerOpenFdCapacity(2)", + ), + ( + " fd_dup(fd, retNewFdPtr) {", + " fd_dup2(", + "guestRangeIsValid(retNewFdPtr, 4)", + "allocateHostNetDuplicateFd(0)", + ), + ( + " fd_dup_min(", + " fd_getfd(", + "guestRangeIsValid(retNewFdPtr, 4)", + "allocateHostNetDuplicateFd(minimumFdNumber)", + ), + ( + " fd_getfd(", + " fd_setfd(", + "guestRangeIsValid(retFlagsPtr, 4)", + "callSyncRpc('process.fd_getfd'", + ), + ( + " fd_socketpair(", + " fd_sendmsg_rights(", + "guestRangeIsValid(retFirstPtr, 4)", + "callSyncRpc('process.fd_socketpair'", + ), + ( + " fd_sendmsg_rights(", + " fd_recvmsg_rights(", + "guestRangeIsValid(retSentPtr, 4)", + "callSyncRpc('process.fd_sendmsg_rights'", + ), + ( + " proc_signal_mask_v2(", + " proc_ppoll_v1(", + "guestRangesAreValid([retOldLoPtr, 4], [retOldHiPtr, 4])", + "callSyncRpc('process.signal_mask'", + ), + ( + " proc_ppoll_v1(", + "\n};\n\nconst limitedHostProcessImport", + "guestRangeIsValid(retReadyPtr, 4)", + "return hostNetImport.net_poll(", + ), + ]; + + for (start, end, validation, side_effect) in cases { + let start_offset = runner + .find(start) + .unwrap_or_else(|| panic!("missing process import marker {start:?}")); + let tail = &runner[start_offset..]; + let end_offset = tail + .find(end) + .unwrap_or_else(|| panic!("missing process import terminator {end:?}")); + let import = &tail[..end_offset]; + let validation_offset = import + .find(validation) + .unwrap_or_else(|| panic!("{start:?} must validate {validation:?}")); + let side_effect_offset = import + .find(side_effect) + .unwrap_or_else(|| panic!("{start:?} must retain {side_effect:?}")); + assert!( + validation_offset < side_effect_offset, + "{start:?} must validate guest output before {side_effect:?}" + ); + } + } + + #[test] + fn guest_byte_reads_use_typed_bounds_checks() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function readGuestBytes(ptr, len) {") + .expect("guest byte reader"); + let section = &runner[start..]; + let end = section + .find("function readGuestString") + .expect("guest byte reader end"); + let section = §ion[..end]; + assert!(section.contains("const end = start + length;")); + assert!(section.contains("end > instanceMemory.buffer.byteLength")); + assert!(section.contains("throw execError('EFAULT'")); + assert!( + section.find("throw execError('EFAULT'").unwrap() + < section.find("new Uint8Array(").unwrap() + ); + assert_eq!( + runner + .matches("case 'EFAULT':\n return WASI_ERRNO_FAULT;") + .count(), + 2 + ); + } + + #[test] + fn record_lock_query_prevalidates_its_atomic_copyout() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find(" fd_record_lock(\n") + .expect("record-lock import"); + let section = &runner[start..]; + let end = section + .find(" proc_closefrom(") + .expect("record-lock import end"); + let section = §ion[..end]; + let validation = section + .find("!guestRangesAreValid(\n [retTypePtr, 4],") + .expect("record-lock output validation"); + let host_query = section + .find("callSyncRpc('process.fd_record_lock'") + .expect("record-lock host query"); + assert!(validation < host_query); + } + + #[test] + fn managed_fexecve_snapshots_the_exact_kernel_description() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function loadExecImageFromFd(fd, argv, closeFds) {") + .expect("fd exec loader"); + let section = &runner[start..]; + let end = section + .find("function traceHostProcess") + .expect("fd exec loader end"); + let section = §ion[..end]; + let managed = section.find("if (SIDECAR_MANAGED_PROCESS) {").unwrap(); + let projection = section + .find("const handle = lookupFdHandle(descriptor);") + .unwrap(); + assert!(managed < projection); + assert!(section.contains("canonicalKernelFdForSpawnAction(descriptor)")); + assert!(section.contains("callSyncRpc('process.exec_image_open_fd'")); + assert!(section.contains("callSyncRpc('process.exec_image_read'")); + assert!(section.contains("callSyncRpc('process.exec_image_close'")); + assert!(!section[managed..projection].contains("fsModule.")); + } + + #[test] + fn managed_closefrom_uses_one_bulk_kernel_mutation() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find(" proc_closefrom(") + .expect("closefrom import"); + let section = &runner[start..]; + let end = section + .find(" fd_socketpair(") + .expect("closefrom end"); + let section = §ion[..end]; + assert_eq!( + section + .matches("callSyncRpc('process.fd_closefrom'") + .count(), + 1 + ); + assert!(section.contains("exactKernelFds.add(Number(handle.targetFd) >>> 0)")); + assert!(section.contains("[...exactKernelFds]")); + assert!(section.contains("response?.closedFds")); + assert!(section.contains("forgetSidecarClosedKernelTargetFd(fd)")); + assert!(section.contains("handle?.internalPreopen === true")); + } + + #[test] + fn managed_fiemap_reads_one_indexed_extent() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner.find(" fd_fiemap(").expect("fiemap import"); + let section = &runner[start..]; + let end = section.find(" fd_punch_hole(").expect("fiemap end"); + let section = §ion[..end]; + assert!(section.contains("callSyncRpc('fs.fiemapAtSync'")); + assert!(!section.contains("callSyncRpc('fs.fiemapSync'")); + assert!(section.contains("return WASI_ERRNO_NODATA;")); } #[test] @@ -4777,12 +6265,13 @@ mod tests { } fn request_with_env(cwd: &Path, env: BTreeMap) -> StartWasmExecutionRequest { - // Translate the legacy `AGENTOS_WASM_*` limit env keys these tests still - // express into the typed limits the engine now reads (mirrors the - // sidecar's config→limits flow). + // Translate the remaining runner-bootstrap `AGENTOS_WASM_*` limit env + // keys these tests express into typed limits. let parse = |key: &str| env.get(key).and_then(|value| value.parse::().ok()); let limits = WasmExecutionLimits { - max_fuel: parse(WASM_MAX_FUEL_ENV), + active_cpu_time_limit_ms: None, + wall_clock_limit_ms: None, + deterministic_fuel: None, max_memory_bytes: parse(WASM_MAX_MEMORY_BYTES_ENV), max_stack_bytes: parse(WASM_MAX_STACK_BYTES_ENV), max_module_file_bytes: None, @@ -4793,15 +6282,18 @@ mod tests { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: None, - runner_cpu_time_limit_ms: None, reactor_work_quantum: None, bridge_call_timeout_ms: None, + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, }; StartWasmExecutionRequest { limits, guest_runtime: GuestRuntimeConfig::default(), vm_id: String::from("vm-wasm"), context_id: String::from("ctx-wasm"), + managed_kernel_host: false, argv: Vec::new(), env, cwd: cwd.to_path_buf(), @@ -4829,8 +6321,8 @@ mod tests { .collect() } - fn wasm_sync_rpc_request(method: &str) -> JavascriptSyncRpcRequest { - JavascriptSyncRpcRequest { + fn wasm_sync_rpc_request(method: &str) -> HostRpcRequest { + HostRpcRequest { id: 1, method: method.to_string(), args: Vec::new(), @@ -4848,11 +6340,11 @@ mod tests { guest_runtime: GuestRuntimeConfig::default(), vm_id: String::from("vm-wasm"), context_id: String::from("ctx-wasm"), + managed_kernel_host: false, argv: Vec::new(), - // Deliberately huge env values: if any limit were still sourced from - // env, the assertions below would observe these instead. + // Deliberately huge remaining bootstrap env values: if any migrated + // limit were still sourced from env, assertions would observe these. env: BTreeMap::from([ - (String::from(WASM_MAX_FUEL_ENV), String::from("999999")), ( String::from(WASM_MAX_MEMORY_BYTES_ENV), String::from("999999"), @@ -4886,7 +6378,9 @@ mod tests { #[test] fn wasm_limits_are_read_from_typed_fields_and_env_is_inert() { let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits { - max_fuel: Some(25), + active_cpu_time_limit_ms: Some(1_234), + wall_clock_limit_ms: Some(25), + deterministic_fuel: None, max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), max_module_file_bytes: Some(262_144), @@ -4897,15 +6391,17 @@ mod tests { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), - runner_cpu_time_limit_ms: Some(1_234), reactor_work_quantum: Some(64), bridge_call_timeout_ms: Some(30_000), + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, }); assert_eq!( - resolve_wasm_execution_timeout(&request).expect("fuel timeout"), + resolve_wasm_wall_clock_limit(&request).expect("wall-clock limit"), Some(Duration::from_millis(25)), - "fuel must come from the typed wire limit, not AGENTOS_WASM_MAX_FUEL" + "wall-clock time must come from the typed wire limit" ); assert_eq!( wasm_memory_limit_bytes(&request).expect("memory limit"), @@ -4936,14 +6432,14 @@ mod tests { } #[test] - fn wasm_limits_default_to_bounded_timeout_when_unset_even_with_env_present() { - // Same misleading env, but no typed limits: no wall-clock fuel timeout + fn wasm_limits_leave_wall_clock_disabled_when_unset_even_with_env_present() { + // Same misleading env, but no typed limits: no wall-clock timeout // (the runner's V8 TRUE-CPU budget bounds runaways), and memory and // stack limits remain absent. let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits::default()); assert_eq!( - resolve_wasm_execution_timeout(&request).expect("fuel"), + resolve_wasm_wall_clock_limit(&request).expect("wall clock"), None ); assert_eq!(wasm_memory_limit_bytes(&request).expect("memory"), None); @@ -4964,7 +6460,9 @@ mod tests { #[test] fn wasm_internal_env_scrubs_migrated_limit_env_keys() { let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits { - max_fuel: Some(25), + active_cpu_time_limit_ms: Some(1_234), + wall_clock_limit_ms: Some(25), + deterministic_fuel: None, max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), max_module_file_bytes: Some(262_144), @@ -4975,9 +6473,11 @@ mod tests { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), - runner_cpu_time_limit_ms: Some(1_234), reactor_work_quantum: Some(64), bridge_call_timeout_ms: Some(30_000), + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, }); let resolved_module = ResolvedWasmModule { specifier: String::from("./guest.wasm"), @@ -5008,7 +6508,6 @@ mod tests { Some(&String::from("131072")) ); assert!(!internal_env.contains_key(WASM_MAX_STACK_BYTES_ENV)); - assert!(!internal_env.contains_key(WASM_MAX_FUEL_ENV)); assert!(!internal_env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS")); assert!(!internal_env.contains_key("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB")); } @@ -5016,7 +6515,9 @@ mod tests { #[test] fn wasm_runner_base_env_scrubs_migrated_limit_env_keys() { let mut request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits { - max_fuel: Some(25), + active_cpu_time_limit_ms: Some(1_234), + wall_clock_limit_ms: Some(25), + deterministic_fuel: None, max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), max_module_file_bytes: Some(262_144), @@ -5027,9 +6528,11 @@ mod tests { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), - runner_cpu_time_limit_ms: Some(1_234), reactor_work_quantum: Some(64), bridge_call_timeout_ms: Some(30_000), + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, }); request .env @@ -5042,7 +6545,6 @@ mod tests { assert_eq!(env.get("USER_VISIBLE"), Some(&String::from("kept"))); assert_eq!(env.get("AGENTOS_TRACE_ID"), Some(&String::from("kept"))); - assert!(!env.contains_key(WASM_MAX_FUEL_ENV)); assert!(!env.contains_key(WASM_MAX_MEMORY_BYTES_ENV)); assert!(!env.contains_key(WASM_MAX_MODULE_FILE_BYTES_ENV)); assert!(!env.contains_key(WASM_MAX_STACK_BYTES_ENV)); @@ -5053,7 +6555,9 @@ mod tests { #[test] fn wasm_snapshot_runner_base_env_scrubs_internal_and_migrated_limit_env_keys() { let mut request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits { - max_fuel: Some(25), + active_cpu_time_limit_ms: Some(1_234), + wall_clock_limit_ms: Some(25), + deterministic_fuel: None, max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), max_module_file_bytes: Some(262_144), @@ -5064,9 +6568,11 @@ mod tests { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), - runner_cpu_time_limit_ms: Some(1_234), reactor_work_quantum: Some(64), bridge_call_timeout_ms: Some(30_000), + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, }); request .env @@ -5080,7 +6586,6 @@ mod tests { assert_eq!(env.get("USER_VISIBLE"), Some(&String::from("kept"))); assert!(!env.contains_key("NODE_SYNC_RPC_WAIT_TIMEOUT_MS")); - assert!(!env.contains_key(WASM_MAX_FUEL_ENV)); assert!(!env.contains_key(WASM_MAX_MEMORY_BYTES_ENV)); assert!(!env.contains_key(WASM_MAX_STACK_BYTES_ENV)); assert!(!env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS")); @@ -5117,16 +6622,14 @@ mod tests { } #[test] - fn wasm_prewarm_timeout_is_separate_from_execution_timeout() { + fn wasm_prewarm_timeout_is_separate_from_wall_clock_limit() { let temp = tempdir().expect("create temp dir"); - let mut request = request_with_env( - temp.path(), - BTreeMap::from([(String::from(WASM_MAX_FUEL_ENV), String::from("25"))]), - ); + let mut request = request_with_env(temp.path(), BTreeMap::new()); + request.limits.wall_clock_limit_ms = Some(25); request.limits.prewarm_timeout_ms = Some(750); assert_eq!( - resolve_wasm_execution_timeout(&request).expect("execution timeout"), + resolve_wasm_wall_clock_limit(&request).expect("wall-clock limit"), Some(Duration::from_millis(25)) ); assert_eq!( @@ -5135,26 +6638,40 @@ mod tests { ); } - // No explicit fuel budget means no wasm-specific wall-clock timeout. Runaway + // No explicit wall-clock budget means no wasm-specific elapsed timeout. Runaway // wasm stays bounded by the runner isolate's active-CPU watchdog, so idle // interactive guests are not killed on wall time. #[test] - fn wasm_execution_timeout_is_unset_without_fuel_budget() { + fn wasm_wall_clock_limit_is_unset_by_default() { let temp = tempdir().expect("create temp dir"); let request = request_with_env(temp.path(), BTreeMap::new()); - let timeout = resolve_wasm_execution_timeout(&request) - .expect("execution timeout resolves without fuel env"); + let timeout = + resolve_wasm_wall_clock_limit(&request).expect("wall-clock limit resolves when unset"); assert_eq!( timeout, None, - "no explicit fuel budget means no wall-clock timeout; the runner \ + "no explicit wall-clock limit means no elapsed timeout; the runner \ isolate's TRUE-CPU budget (default 30s active CPU) is the bound \ that terminates an infinite-loop module (F-004), so an idle \ interactive guest is not killed on wall time" ); } + #[test] + fn v8_rejects_explicit_deterministic_fuel_with_typed_error() { + let temp = tempdir().expect("create temp dir"); + let mut request = request_with_env(temp.path(), BTreeMap::new()); + request.limits.deterministic_fuel = Some(123_456); + + let error = + reject_v8_deterministic_fuel(&request).expect_err("V8 cannot meter deterministic fuel"); + assert!(matches!( + error, + WasmExecutionError::DeterministicFuelUnsupported { fuel: 123_456 } + )); + } + #[test] fn wasm_captured_output_rejects_output_over_limit() { let mut stdout = vec![b'x'; WASM_CAPTURED_OUTPUT_LIMIT_BYTES - 1]; @@ -5227,12 +6744,17 @@ mod tests { #[test] fn wasi_preview1_import_manifest_matches_native_runner() { - let expected: BTreeSet = serde_json::from_str::>(include_str!( - "../assets/wasi-preview1-imports.json" - )) - .expect("parse WASI preview1 import manifest") - .into_iter() - .collect(); + let manifest: Value = serde_json::from_str(include_str!("../assets/agentos-wasm-abi.json")) + .expect("parse AgentOS WASM ABI manifest"); + let expected = manifest["imports"] + .as_array() + .expect("ABI imports array") + .iter() + .filter(|entry| { + entry["module"] == "wasi_snapshot_preview1" && entry["status"] != "compatibility" + }) + .map(|entry| entry["name"].as_str().expect("ABI import name").to_string()) + .collect::>(); assert_eq!(expected, wasi_imports_from_source(NODE_WASI_MODULE_SOURCE)); } @@ -5277,7 +6799,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5311,7 +6833,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5350,7 +6872,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5390,7 +6912,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5447,7 +6969,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5483,7 +7005,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; let host_path = translate_wasm_guest_path("/node_modules/package.json", &internal_sync_rpc) @@ -5544,7 +7066,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert!(wasm_mutation_touches_read_only_mapping( @@ -5596,7 +7118,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; assert_eq!( @@ -5632,7 +7154,7 @@ mod tests { route_fs_through_sidecar: false, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; let sidecar_managed = WasmInternalSyncRpc { module_guest_paths: Vec::new(), @@ -5644,7 +7166,7 @@ mod tests { route_fs_through_sidecar: true, next_fd: 64, open_files: Default::default(), - pending_events: VecDeque::new(), + pending_events: WasmEventQueue::default(), }; for method in WASM_SIDECAR_ROUTED_FS_SYNC_METHODS { @@ -5802,6 +7324,186 @@ mod tests { )); } + #[test] + fn managed_host_network_fds_use_kernel_description_authority() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + + assert!(runner.contains("const HOST_NET_SOCK_NONBLOCK = 0x4000;")); + assert!(runner.contains("const HOST_NET_SOCK_CLOEXEC = 0x2000;")); + assert!(runner.contains("const standaloneHostNetSockets = new Map();")); + assert!(!runner.contains("const managedHostNetDescriptions = new Map();")); + assert!(!runner.contains("const hostNetSockets = new Map();")); + assert!(runner.contains("callSyncRpc('process.hostnet_fd_open'")); + assert!(runner.contains("callSyncRpc('process.hostnet_validate'")); + assert!(runner.contains("validateHostNetSocketDescriptor(fd, true)")); + assert!(!runner.contains("callSyncRpc('process.fd_description_alias_count'")); + assert!(runner.contains("callSyncRpc('process.fd_dup'")); + assert!(runner.contains("attachManagedHostNetDescription(guestFd, descriptionId);")); + assert!(runner.contains("descriptionId: handle?.hostNetDescriptionId ?? null")); + assert!(runner.contains("fd = registerKernelDelegateFd(received.fd);")); + assert!(runner.contains("const managedHostNetKernelGuestFds = new Set(")); + assert!(runner.contains( + "initialKernelGuestFds.has(guestFd) && !managedHostNetKernelGuestFds.has(guestFd)" + )); + } + + #[test] + fn managed_runner_keeps_only_bounded_process_and_fd_projections() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + + for obsolete in [ + "const spawnedChildren =", + "const spawnedChildrenById =", + "const syntheticFdEntries =", + "runnerCloexecFds", + "delegateManagedFdRefCounts", + ] { + assert!( + !runner.contains(obsolete), + "obsolete authority map: {obsolete}" + ); + } + for required in [ + "const childCorrelationsByPid = new Map();", + "const childCorrelationsById = new Map();", + "const managedKernelFdProjections = new Map();", + "const standaloneSyntheticFdEntries = new Map();", + "const standaloneCloexecFds = new Set();", + "const standaloneDelegateFdRefCounts = new Map();", + "managed descriptor projections must reference a kernel fd", + "managed execution cannot retain a Node-WASI delegate fd", + "callSyncRpc('process.fd_getfd'", + "callSyncRpc('process.fd_path'", + "callSyncRpc('process.fd_move'", + "callSyncRpc('process.waitpid'", + "record.terminalEventObserved = true;", + "function collectStaleManagedChildCorrelations()", + "...(SIDECAR_MANAGED_PROCESS ? {} : { processGroup })", + ] { + assert!( + runner.contains(required), + "missing managed-mode guard: {required}" + ); + } + assert!(runner + .contains("const handle = {\n kind: 'kernel-fd',\n targetFd: kernelFd,\n };")); + assert!(!runner.contains("openedHandle.guestPath =")); + assert!(!runner.contains("callSyncRpc('child_process.kill'")); + assert!(runner.contains("collectStaleManagedChildCorrelations();\n return false;")); + } + + #[test] + fn managed_guest_fd_reuse_does_not_close_or_replace_hidden_preopen_backing_descriptors() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + assert!(runner.contains( + "if (handle.kind === 'kernel-fd') {\n // Managed preopens are hidden capability roots" + )); + assert!(runner.contains( + "if (handle.internalPreopen === true) {\n return;\n }\n callSyncRpc('process.fd_close'" + )); + assert!(runner.contains("function kernelFdMappingsForSpawn()")); + assert!(runner.contains("handle.open !== false &&\n handle.internalPreopen !== true")); + let start = runner + .find("wasiImport.fd_renumber = (from, to) => {") + .expect("fd_renumber implementation"); + let end = runner[start..] + .find("wasiImport.poll_oneoff =") + .map(|offset| start + offset) + .expect("fd_renumber implementation end"); + let implementation = &runner[start..end]; + assert!(implementation.contains( + "managedTarget?.internalPreopen === true && hiddenPreopenHandles.has(targetFd)" + )); + assert!(implementation + .contains("managedTarget?.kind === 'kernel-fd' && !shadowsInternalPreopen")); + assert!(implementation.contains("shadowsInternalPreopen,")); + } + + #[test] + fn host_process_errno_mapping_uses_only_typed_error_codes() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function mapHostProcessError(error) {") + .expect("host process errno mapper"); + let end = runner[start..] + .find("function seekGuestFileHandle") + .map(|offset| start + offset) + .expect("host process errno mapper end"); + let mapper = &runner[start..end]; + assert!(mapper.contains("case 'ENOENT':\n return WASI_ERRNO_NOENT;")); + assert!(mapper.contains("case 'EINTR':\n return WASI_ERRNO_INTR;")); + assert!(mapper.contains("default:\n return WASI_ERRNO_FAULT;")); + assert!(!mapper.contains("command not found")); + assert!(!mapper.contains("error?.message")); + } + + #[test] + fn managed_signal_dispatch_drains_the_published_delivery_without_double_claiming() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function dispatchPendingWasmSignals(") + .expect("signal dispatch helper"); + let helper = &runner[start..]; + let end = helper + .find("Object.defineProperty(globalThis, '__secureExecWasmSignalDispatch'") + .expect("signal dispatch helper end"); + let helper = &helper[..end]; + + assert_eq!( + helper.matches("callSyncRpc('process.take_signal'").count(), + 1 + ); + assert!(!helper.contains("callSyncRpc('process.signal_begin'")); + assert!(runner.contains("callSyncRpc('process.signal_end', [token])")); + assert!(runner.contains("function pumpSpawnedChildrenOrWaitRestartable(waitMs)")); + assert!(runner.contains("return dispatchPendingWasmSignals(true);")); + assert!(runner.contains("numericSignal === 0 || numericSignal > 64")); + assert!(runner.contains( + "registration.action === 'user' &&\n typeof instance?.exports?.__wasi_signal_trampoline !== 'function'" + )); + } + + #[test] + fn managed_wait_uses_one_kernel_authoritative_direct_reply() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function takeManagedWaitTransition(") + .expect("managed wait helper"); + let end = runner[start..] + .find("function reapManagedChildCorrelation(") + .map(|offset| start + offset) + .expect("managed wait helper end"); + let helper = &runner[start..end]; + assert_eq!(helper.matches("callSyncRpc('process.waitpid'").count(), 1); + assert!(!helper.contains("process.waitpid_transition")); + assert!(!helper.contains("pumpSpawnedChildren")); + assert!(!helper.contains("Atomics.wait")); + assert!(helper.contains("dispatchPendingWasmSignals(true)")); + assert!(helper.contains("error?.code === 'EINTR' && !mustInterrupt")); + assert!(helper.contains("continue;")); + assert!(runner.contains("case 'ECHILD':\n return WASI_ERRNO_CHILD;")); + + let pump = runner + .find("function pumpSpawnedChildren(waitMs) {") + .expect("child pump"); + let pump = &runner[pump..]; + let pump_end = pump + .find("function pumpSpawnedChildrenOrWait") + .expect("child pump end"); + assert!(pump[..pump_end] + .contains("if (SIDECAR_MANAGED_PROCESS) {\n // Managed child lifecycle")); + assert!(pump[pump_end..].contains("callSyncRpc('process.sleep', [boundedWaitMs])")); + let sleep = runner + .find(" sleep_ms(milliseconds) {") + .expect("sleep import"); + let sleep = &runner[sleep..]; + let sleep_end = sleep.find(" pty_open(").expect("sleep import end"); + let sleep = &sleep[..sleep_end]; + assert!(sleep.contains("if (!SIDECAR_MANAGED_PROCESS)")); + assert!(sleep.contains("Atomics.wait(syntheticWaitArray, 0, 0, durationMs)")); + assert!(sleep.contains("callSyncRpc('process.sleep', [durationMs])")); + } + #[test] fn wasm_memory_limit_pages_floor_to_whole_wasm_pages() { assert_eq!( diff --git a/crates/execution/tests/backend_lifecycle.rs b/crates/execution/tests/backend_lifecycle.rs new file mode 100644 index 0000000000..8f980845c2 --- /dev/null +++ b/crates/execution/tests/backend_lifecycle.rs @@ -0,0 +1,11 @@ +use agentos_execution::backend::ExecutionBackend; +use agentos_execution::{JavascriptExecution, PythonExecution, WasmExecution}; + +fn assert_execution_backend() {} + +#[test] +fn every_production_execution_adapter_implements_the_lifecycle_contract() { + assert_execution_backend::(); + assert_execution_backend::(); + assert_execution_backend::(); +} diff --git a/crates/execution/tests/backend_payload_bounds.rs b/crates/execution/tests/backend_payload_bounds.rs new file mode 100644 index 0000000000..7b816ec0d5 --- /dev/null +++ b/crates/execution/tests/backend_payload_bounds.rs @@ -0,0 +1,138 @@ +use agentos_execution::backend::{ + DirectHostReplyHandle, DirectHostReplyTarget, HostCallIdentity, HostCallReply, + HostServiceError, NearLimitWarning, NearLimitWarningHook, PayloadLimit, +}; +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +#[derive(Default)] +struct RecordingTarget { + replies: Mutex>>, +} + +impl DirectHostReplyTarget for RecordingTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + _: bool, + result: Result, + ) -> Result<(), HostServiceError> { + self.replies.lock().expect("reply lock").push(result); + Ok(()) + } +} + +#[derive(Default)] +struct RecordingWarnings(Mutex>); + +impl NearLimitWarningHook for RecordingWarnings { + fn warn(&self, warning: NearLimitWarning) { + self.0.lock().expect("warning lock").push(warning); + } +} + +#[test] +fn direct_reply_admission_warns_and_preserves_typed_error_details() { + let target = Arc::new(RecordingTarget::default()); + let warnings = Arc::new(RecordingWarnings::default()); + let limit = PayloadLimit::with_warning_hook( + "runtime.resources.maxBridgeResponseBytes", + 150, + Some(warnings.clone()), + ) + .expect("reply limit"); + let reply = DirectHostReplyHandle::new_with_limit( + HostCallIdentity { + generation: 3, + pid: 41, + call_id: 7, + }, + target.clone(), + limit, + ) + .expect("reply handle"); + + let typed = HostServiceError::new("EACCES", "d".repeat(60)) + .with_details(serde_json::json!({ "path": "/private" })); + reply.fail(typed.clone()).expect("typed error reply"); + + let replies = target.replies.lock().expect("reply lock"); + assert_eq!(replies[0].as_ref().unwrap_err(), &typed); + assert_eq!(warnings.0.lock().expect("warning lock").len(), 1); +} + +#[test] +fn oversized_json_is_settled_as_a_named_limit_error() { + let target = Arc::new(RecordingTarget::default()); + let limit = PayloadLimit::new("limits.bridge.maxReplyBytes", 32).expect("reply limit"); + let reply = DirectHostReplyHandle::new_with_limit( + HostCallIdentity { + generation: 3, + pid: 41, + call_id: 8, + }, + target.clone(), + limit, + ) + .expect("reply handle"); + + reply + .succeed_json(serde_json::json!({ "payload": "x".repeat(256) })) + .expect("settle typed limit reply"); + + let replies = target.replies.lock().expect("reply lock"); + let error = replies[0].as_ref().unwrap_err(); + assert_eq!(error.code, "E2BIG"); + assert_eq!( + error.details.as_ref().expect("limit details")["limitName"], + "limits.bridge.maxReplyBytes" + ); +} + +#[test] +fn common_payload_constructors_require_named_limits() { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let backend = manifest.join("src/backend"); + let host = fs::read_to_string(manifest.join("src/host/mod.rs")).expect("host source"); + let reply = fs::read_to_string(backend.join("reply.rs")).expect("reply source"); + let event = fs::read_to_string(backend.join("event.rs")).expect("event source"); + let v8_host = fs::read_to_string(manifest.join("src/v8_host.rs")).expect("V8 adapter source"); + + assert!( + host.matches("limit: &PayloadLimit").count() >= 4, + "every retained byte/string/vector/count constructor must require a named PayloadLimit" + ); + assert!( + reply.contains("payload_limit: PayloadLimit") + && reply.contains("PayloadLimit::with_stderr_warning(") + && reply.contains("\"limits.reactor.maxBridgeResponseBytes\"") + && reply.contains("pub fn succeed_raw(") + && reply.contains("pub fn succeed_json("), + "direct replies must retain their configured named bound and expose pre-envelope admission" + ); + assert!( + !reply.contains("serde_json::to_vec"), + "direct reply admission must never allocate an unbounded encoded JSON temporary" + ); + assert!( + event.contains("Warning(BoundedHostServiceError)") + && event.contains("pub fn output(") + && event.contains("pub fn warning("), + "common output and warning events must be admitted through bounded constructors" + ); + let pre_admit = v8_host + .find("encoded_limit.admit_json(payload)") + .expect("structured adapter-event pre-admission"); + let encode = v8_host + .find("json_to_cbor_payload(payload)") + .expect("adapter event encoding"); + assert!( + pre_admit < encode, + "structured adapter events must be admitted before CBOR construction" + ); +} diff --git a/crates/execution/tests/javascript_v8.rs b/crates/execution/tests/javascript_v8.rs index 22e0014121..66d6f64396 100644 --- a/crates/execution/tests/javascript_v8.rs +++ b/crates/execution/tests/javascript_v8.rs @@ -2,8 +2,8 @@ mod support; use agentos_execution::{ v8_runtime::map_bridge_method, CreateJavascriptContextRequest, GuestRuntimeConfig, - JavascriptExecution, JavascriptExecutionEvent, JavascriptExecutionLimits, - JavascriptExecutionResult, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest, + HostRpcRequest, JavascriptExecution, JavascriptExecutionEvent, JavascriptExecutionLimits, + JavascriptExecutionResult, StartJavascriptExecutionRequest, }; use base64::Engine; use serde::Deserialize; @@ -166,7 +166,7 @@ impl HostChildProcessHarness { fn handle_request( &mut self, host_cwd: &Path, - request: JavascriptSyncRpcRequest, + request: HostRpcRequest, ) -> Result { match request.method.as_str() { "child_process.spawn" => self.spawn(host_cwd, &request.args), @@ -739,10 +739,7 @@ fn decode_guest_or_string_bytes(value: &Value) -> Result, String> { /// sync RPCs that surface during module loading. Module resolution now flows as /// sync RPCs (`__resolve_module` / `__batch_resolve_modules` / `__load_file` / /// `__module_format`); these tests assert on the *next* non-module sync RPC. -fn expect_next_sync_rpc( - execution: &mut JavascriptExecution, - what: &str, -) -> JavascriptSyncRpcRequest { +fn expect_next_sync_rpc(execution: &mut JavascriptExecution, what: &str) -> HostRpcRequest { loop { match execution .poll_event_blocking(Duration::from_secs(5)) @@ -762,6 +759,70 @@ fn expect_next_sync_rpc( } } +#[test] +fn javascript_execution_v8_preserves_binary_args_for_every_sync_rpc_method() { + let temp = tempdir().expect("create temp dir"); + let mut engine = support::javascript_engine(); + let context = engine.create_context(CreateJavascriptContextRequest { + vm_id: String::from("vm-js"), + bootstrap_module: None, + compile_cache_root: None, + }); + let mut execution = engine + .start_execution(StartJavascriptExecutionRequest { + limits: Default::default(), + guest_runtime: Default::default(), + vm_id: String::from("vm-js"), + context_id: context.context_id, + argv: vec![String::from("./entry.mjs")], + argv0: None, + env: BTreeMap::new(), + cwd: temp.path().to_path_buf(), + wasm_module_bytes: None, + inline_code: Some(String::from( + r#" +const payload = Buffer.from([0, 255, 1, 128]); +_processWasmSyncRpc.applySync(void 0, [ + "process.fd_sendmsg_rights", + 7, + payload, + [8], +]); +console.log("BINARY_SYNC_RPC_OK"); +"#, + )), + }) + .expect("start JavaScript execution"); + + let request = expect_next_sync_rpc(&mut execution, "binary WASM sync RPC"); + assert_eq!(request.method, "process.wasm_sync_rpc"); + assert_eq!( + request.args.first(), + Some(&json!("process.fd_sendmsg_rights")) + ); + assert_eq!( + request.raw_bytes_args.get(&2), + Some(&vec![0, 255, 1, 128]), + "the V8 bridge must preserve bytes at every argument index without a method allowlist" + ); + execution + .respond_sync_rpc_success(request.id, json!(0)) + .expect("respond to binary WASM sync RPC"); + + let result = execution.wait().expect("wait for JavaScript execution"); + assert_eq!( + result.exit_code, + 0, + "stderr: {}", + String::from_utf8_lossy(&result.stderr) + ); + assert!( + String::from_utf8_lossy(&result.stdout).contains("BINARY_SYNC_RPC_OK"), + "stdout: {}", + String::from_utf8_lossy(&result.stdout) + ); +} + fn wait_with_host_child_process_bridge( mut execution: JavascriptExecution, host_cwd: &Path, @@ -921,13 +982,9 @@ fn javascript_execution_uses_v8_runtime_without_spawning_guest_node_binary() { }) .expect("start JavaScript execution"); - assert!( - execution.uses_shared_v8_runtime(), - "guest JS should run inside the shared V8 runtime" - ); assert_eq!( - execution.child_pid(), - 0, + execution.native_process_id(), + None, "shared V8 runtime executions should keep the embedded host pid internal" ); @@ -6139,10 +6196,7 @@ fn run_js_runtime_guest( inline_code: Some(inline_code.to_owned()), }) .expect("start JavaScript execution"); - assert!( - execution.uses_shared_v8_runtime(), - "guest JS must run inside the shared V8 runtime" - ); + assert_eq!(execution.native_process_id(), None); execution.wait().expect("wait for JavaScript execution") } diff --git a/crates/execution/tests/module_resolution.rs b/crates/execution/tests/module_resolution.rs index e91eb4b2d1..0b489f1a6a 100644 --- a/crates/execution/tests/module_resolution.rs +++ b/crates/execution/tests/module_resolution.rs @@ -764,6 +764,33 @@ fn pnpm_symlinked_referrer_can_resolve_sibling_dependency() { ); } +#[test] +fn host_module_resolution_preserves_symlink_loop_errors() { + let fixture = Fixture::new(); + fixture.mkdir("node_modules"); + symlink("loop-b", fixture.host_path("node_modules/loop-a")).expect("create first loop symlink"); + symlink("loop-a", fixture.host_path("node_modules/loop-b")) + .expect("create second loop symlink"); + + let mut resolver = fixture.resolver(); + let error = resolver + .try_resolve_require("loop-a", "/root/project/index.js") + .expect_err("symlink loop must be a typed filesystem error, not a module miss"); + assert_eq!(error.code, "ELOOP"); +} + +#[test] +fn malformed_present_package_json_is_a_typed_error() { + let fixture = Fixture::new(); + fixture.write("node_modules/bad/package.json", "{"); + + let mut resolver = fixture.resolver(); + let error = resolver + .try_resolve_require("bad", "/root/project/index.js") + .expect_err("malformed present package.json must not become MODULE_NOT_FOUND"); + assert_eq!(error.code, "ERR_INVALID_PACKAGE_CONFIG"); +} + #[test] fn pnpm_symlinked_referrer_prefers_package_store_dependency_over_generic_hoist() { let fixture = Fixture::new(); diff --git a/crates/execution/tests/permission_flags.rs b/crates/execution/tests/permission_flags.rs index 11c0dcc96c..450f49be83 100644 --- a/crates/execution/tests/permission_flags.rs +++ b/crates/execution/tests/permission_flags.rs @@ -279,7 +279,7 @@ export async function loadPyodide() { { Some(PythonExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(PythonExecutionEvent::Stderr(chunk)) => stderr.extend(chunk), - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { // Module-resolution sync RPCs surface during startup; service // them host-directly, then expect the pyodide cache mkdir. if execution @@ -340,12 +340,13 @@ fn wasm_execution_applies_runtime_memory_and_fuel_limits_inside_v8_runtime() { .start_execution(StartWasmExecutionRequest { guest_runtime: Default::default(), limits: WasmExecutionLimits { - max_fuel: Some(250_000), + wall_clock_limit_ms: Some(250_000), max_memory_bytes: Some(131_072), ..Default::default() }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: vec![String::from("./guest.wasm")], env: BTreeMap::new(), cwd: wasm_cwd, @@ -398,6 +399,7 @@ fn wasm_permission_tiers_do_not_fall_back_to_host_node_binary() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: vec![String::from("./guest.wasm")], env: BTreeMap::new(), cwd: wasm_cwd, diff --git a/crates/execution/tests/process.rs b/crates/execution/tests/process.rs index 4cfc5f05d3..8b6ce410d7 100644 --- a/crates/execution/tests/process.rs +++ b/crates/execution/tests/process.rs @@ -61,8 +61,7 @@ fn embedded_runtime_process_keeps_host_pid_internal_for_javascript() { }) .expect("start JavaScript execution"); - assert!(execution.uses_shared_v8_runtime()); - assert_eq!(execution.child_pid(), 0); + assert_eq!(execution.native_process_id(), None); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); @@ -93,6 +92,7 @@ fn embedded_runtime_process_keeps_host_pid_internal_for_wasm() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: vec![module_path.to_string_lossy().into_owned()], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -100,8 +100,7 @@ fn embedded_runtime_process_keeps_host_pid_internal_for_wasm() { }) .expect("start wasm execution"); - assert!(execution.uses_shared_v8_runtime()); - assert_eq!(execution.child_pid(), 0); + assert_eq!(execution.native_process_id(), None); let result = execution.wait().expect("wait for wasm execution"); assert_eq!( @@ -153,8 +152,7 @@ export async function loadPyodide(options) { }) .expect("start Python execution"); - assert!(execution.uses_shared_v8_runtime()); - assert_eq!(execution.child_pid(), 0); + assert_eq!(execution.native_process_id(), None); let ready_deadline = Instant::now() + Duration::from_secs(5); let mut saw_ready = false; @@ -184,7 +182,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request during kill test: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -217,7 +215,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request after kill: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) diff --git a/crates/execution/tests/python.rs b/crates/execution/tests/python.rs index 5c19eba79d..cc2f3d89f6 100644 --- a/crates/execution/tests/python.rs +++ b/crates/execution/tests/python.rs @@ -372,7 +372,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request during stdout test: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { // Module-resolution sync RPCs now surface here (the runner // module imports node builtins); service them host-directly. let serviced = execution @@ -545,7 +545,7 @@ export async function loadPyodide(options) { break; } } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -582,7 +582,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request during stdin test: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -744,7 +744,7 @@ export async function loadPyodide(options) { } } } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -832,8 +832,7 @@ export async function loadPyodide() { cwd: temp.path().to_path_buf(), }) .expect("start Python execution"); - let child_pid = execution.child_pid(); - let uses_shared_v8_runtime = execution.uses_shared_v8_runtime(); + let native_process_id = execution.native_process_id(); let error = execution .wait(Some(Duration::from_millis(100))) @@ -845,8 +844,8 @@ export async function loadPyodide() { other => panic!("expected timeout error, got {other:?}"), } - if !uses_shared_v8_runtime { - assert_process_exits(child_pid); + if let Some(process_id) = native_process_id { + assert_process_exits(process_id); } } @@ -898,8 +897,7 @@ export async function loadPyodide() { cwd: temp.path().to_path_buf(), }) .expect("start Python execution"); - let child_pid = execution.child_pid(); - let uses_shared_v8_runtime = execution.uses_shared_v8_runtime(); + let native_process_id = execution.native_process_id(); let error = execution .wait(None) @@ -911,8 +909,8 @@ export async function loadPyodide() { other => panic!("expected timeout error, got {other:?}"), } - if !uses_shared_v8_runtime { - assert_process_exits(child_pid); + if let Some(process_id) = native_process_id { + assert_process_exits(process_id); } } @@ -963,8 +961,7 @@ export async function loadPyodide() { cwd: temp.path().to_path_buf(), }) .expect("start Python execution"); - let child_pid = execution.child_pid(); - let uses_shared_v8_runtime = execution.uses_shared_v8_runtime(); + let native_process_id = execution.native_process_id(); let mut saw_request = false; let mut stderr = Vec::new(); @@ -980,7 +977,7 @@ export async function loadPyodide() { assert_eq!(request.method, PythonVfsRpcMethod::Read); assert_eq!(request.path, "/workspace/never.txt"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -1015,8 +1012,8 @@ export async function loadPyodide() { || stderr.contains("timed out after 50ms"), "unexpected stderr: {stderr}" ); - if !uses_shared_v8_runtime { - assert_process_exits(child_pid); + if let Some(process_id) = native_process_id { + assert_process_exits(process_id); } } @@ -1116,8 +1113,7 @@ export async function loadPyodide(options) { cwd: temp.path().to_path_buf(), }) .expect("start Python execution"); - let child_pid = execution.child_pid(); - let uses_shared_v8_runtime = execution.uses_shared_v8_runtime(); + let native_process_id = execution.native_process_id(); let ready_deadline = Instant::now() + Duration::from_secs(5); let mut saw_ready = false; @@ -1144,7 +1140,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request during kill test: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -1180,7 +1176,7 @@ export async function loadPyodide(options) { Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request after kill: {request:?}"); } - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) @@ -1193,8 +1189,8 @@ export async function loadPyodide(options) { } assert_eq!(exit_code, Some(1)); - if !uses_shared_v8_runtime { - assert_process_exits(child_pid); + if let Some(process_id) = native_process_id { + assert_process_exits(process_id); } } diff --git a/crates/execution/tests/python_prewarm.rs b/crates/execution/tests/python_prewarm.rs index 67f7943658..08ba4b24fd 100644 --- a/crates/execution/tests/python_prewarm.rs +++ b/crates/execution/tests/python_prewarm.rs @@ -61,7 +61,7 @@ fn run_python_execution( { Some(PythonExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(PythonExecutionEvent::Stderr(chunk)) => stderr.extend(chunk), - Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { + Some(PythonExecutionEvent::HostRpcRequest(request)) => { let serviced = execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC"); diff --git a/crates/execution/tests/runtime_topology.rs b/crates/execution/tests/runtime_topology.rs index 75b47c47d9..6369796b1d 100644 --- a/crates/execution/tests/runtime_topology.rs +++ b/crates/execution/tests/runtime_topology.rs @@ -118,6 +118,7 @@ fn default_engine_does_not_silently_select_process_topology() { .start_execution(StartWasmExecutionRequest { vm_id: String::from("vm-unbound"), context_id: String::from("missing-context"), + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: PathBuf::from("/"), diff --git a/crates/execution/tests/wasm.rs b/crates/execution/tests/wasm.rs index 486993c551..0952b1d2a5 100644 --- a/crates/execution/tests/wasm.rs +++ b/crates/execution/tests/wasm.rs @@ -1,25 +1,25 @@ mod support; use agentos_execution::wasm::{ - NativeBinaryFormat, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, + NativeBinaryFormat, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, }; use agentos_execution::{ - CreateWasmContextRequest, StartWasmExecutionRequest, WasmExecutionEngine, WasmExecutionError, - WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier, + CreateWasmContextRequest, StartWasmExecutionRequest, WasmExecution, WasmExecutionEngine, + WasmExecutionError, WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier, }; use base64::Engine; -use serde_json::json; +use serde_json::{json, Value}; use std::collections::BTreeMap; use std::fs; use std::os::unix::fs::symlink; use std::path::Path; use std::process::Command; -use std::sync::mpsc; use std::sync::{Mutex, MutexGuard, OnceLock}; -use std::thread; use std::time::Duration; use tempfile::tempdir; +const TEST_WASM_WALL_CLOCK_LIMIT_MS: &str = "TEST_WASM_WALL_CLOCK_LIMIT_MS"; + const WASM_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_WASM_WARMUP_METRICS__:"; fn node_binary_env_lock() -> &'static Mutex<()> { @@ -113,6 +113,33 @@ fn decode_sync_rpc_bytes(value: &serde_json::Value) -> Vec { .expect("decode sync rpc bytes") } +fn poll_wasm_event_after_initial_signal_mask( + execution: &mut WasmExecution, + timeout: Duration, +) -> Option { + let deadline = std::time::Instant::now() + timeout; + loop { + let remaining = deadline + .checked_duration_since(std::time::Instant::now()) + .expect("timed out after acknowledging the initial signal mask"); + match execution + .poll_event_blocking(remaining) + .expect("poll wasm event") + { + Some(WasmExecutionEvent::SyncRpcRequest(request)) + if request.method == "process.wasm_sync_rpc" + && request.args.first().and_then(Value::as_str) + == Some("process.signal_mask") => + { + execution + .respond_sync_rpc_success(request.id, json!({ "signals": [] })) + .expect("commit the initial standalone signal mask"); + } + event => return event, + } + } +} + fn write_fake_node_binary(path: &Path, log_path: &Path) { let script = format!( "#!/bin/sh\nset -eu\nprintf 'host-node-invoked\\n' >> \"{}\"\nexit 1\n", @@ -204,14 +231,15 @@ fn parse_unicode_escape_unit(chars: &mut std::str::Chars<'_>) -> u16 { u16::from_str_radix(&hex, 16).expect("unicode escape value") } -/// Mirror the sidecar's config→limits flow for tests that still express WASM -/// limits via the historical `AGENTOS_WASM_*` env keys: translate them into the -/// typed `WasmExecutionLimits` the engine now reads. Production sources these -/// from the BARE-wire resource limits, never env. +/// Mirror the sidecar's config→limits flow for tests that express selected WASM +/// limits through fixture env values. Production sources these from typed VM +/// config, never env. fn wasm_limits_from_env(env: &BTreeMap) -> WasmExecutionLimits { let parse = |key: &str| env.get(key).and_then(|value| value.parse::().ok()); WasmExecutionLimits { - max_fuel: parse(WASM_MAX_FUEL_ENV), + active_cpu_time_limit_ms: None, + wall_clock_limit_ms: parse(TEST_WASM_WALL_CLOCK_LIMIT_MS), + deterministic_fuel: None, max_memory_bytes: parse(WASM_MAX_MEMORY_BYTES_ENV), max_stack_bytes: parse(WASM_MAX_STACK_BYTES_ENV), max_module_file_bytes: None, @@ -222,9 +250,11 @@ fn wasm_limits_from_env(env: &BTreeMap) -> WasmExecutionLimits { max_sockets: None, max_blocking_read_ms: None, runner_heap_limit_mb: None, - runner_cpu_time_limit_ms: None, reactor_work_quantum: None, bridge_call_timeout_ms: None, + max_sync_rpc_response_line_bytes: None, + pending_event_count: None, + pending_event_bytes: None, } } @@ -255,6 +285,7 @@ fn run_wasm_execution_with_limits( guest_runtime: Default::default(), vm_id: String::from("vm-wasm"), context_id, + managed_kernel_host: false, argv, env, cwd: cwd.to_path_buf(), @@ -356,6 +387,46 @@ fn wasm_getrlimit_nofile_module(expected_limit: u64) -> Vec { .expect("compile getrlimit nofile fixture") } +fn wasm_process_output_prevalidation_module() -> Vec { + wat::parse_str( + r#" +(module + (type $getrlimit_t (func (param i32 i32 i32) (result i32))) + (type $waitpid_t (func (param i32 i32 i32 i32) (result i32))) + (type $exit_t (func (param i32))) + (import "host_process" "proc_getrlimit" (func $getrlimit (type $getrlimit_t))) + (import "host_process" "proc_waitpid" (func $waitpid (type $waitpid_t))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (type $exit_t))) + (memory (export "memory") 1) + (func $_start (export "_start") + (i64.store (i32.const 0) (i64.const 1234605616436508552)) + ;; The soft output is valid but the hard output crosses memory. EFAULT must + ;; be returned before the valid first output is partially overwritten. + (if + (i32.ne + (call $getrlimit (i32.const 7) (i32.const 0) (i32.const 65532)) + (i32.const 21)) + (then (call $proc_exit (i32.const 41)))) + (if + (i64.ne + (i64.load (i32.const 0)) + (i64.const 1234605616436508552)) + (then (call $proc_exit (i32.const 42)))) + ;; With no children, output validation must still win over ECHILD and must + ;; happen before the wait implementation inspects or pumps child state. + (if + (i32.ne + (call $waitpid + (i32.const -1) (i32.const 1) + (i32.const 65534) (i32.const 65534)) + (i32.const 21)) + (then (call $proc_exit (i32.const 43))))) +) +"#, + ) + .expect("compile process output prevalidation fixture") +} + fn wasm_stdin_echo_module() -> Vec { wat::parse_str( r#" @@ -513,6 +584,7 @@ fn wasm_signal_state_module() -> Vec { (import "host_process" "proc_sigaction" (func $proc_sigaction (type $proc_sigaction_t))) (memory (export "memory") 1) (data (i32.const 32) "signal:ready\n") + (func (export "__wasi_signal_trampoline") (param i32)) (func $_start (export "_start") (drop (call $proc_sigaction @@ -995,6 +1067,7 @@ fn wasm_execution_runs_guest_module_through_v8() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: vec![String::from("guest.wasm")], env: BTreeMap::from([(String::from("IGNORED_FOR_NOW"), String::from("ok"))]), cwd: temp.path().to_path_buf(), @@ -1114,6 +1187,31 @@ fn wasm_getrlimit_nofile_reports_typed_fd_limit() { assert!(stderr.is_empty(), "unexpected stderr: {stderr:?}"); } +fn wasm_process_outputs_fault_before_partial_write_or_wait_state() { + assert_node_available(); + + let temp = tempdir().expect("create temp dir"); + write_fixture( + &temp.path().join("guest.wasm"), + &wasm_process_output_prevalidation_module(), + ); + let mut engine = support::wasm_engine(); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm"), + module_path: Some(String::from("./guest.wasm")), + }); + let (stdout, stderr, exit_code) = run_wasm_execution_with_limits( + &mut engine, + context.context_id, + temp.path(), + Vec::new(), + BTreeMap::new(), + WasmPermissionTier::Full, + WasmExecutionLimits::default(), + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + fn wasm_snapshot_runner_block_round_trips_twice() { assert_node_available(); let _mode = EnvVarGuard::set_value("AGENTOS_WASM_SNAPSHOT_RUNNER", "block"); @@ -1470,6 +1568,7 @@ fn wasm_execution_rejects_vm_mismatch() { limits: Default::default(), vm_id: String::from("vm-other"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: Path::new("/tmp").to_path_buf(), @@ -1482,7 +1581,7 @@ fn wasm_execution_rejects_vm_mismatch() { .contains("guest WebAssembly context belongs to vm vm-wasm, not vm-other")); } -fn wasm_execution_streams_exit_event() { +fn wasm_execution_streams_exit_event_after_signal_mask_commit() { assert_node_available(); let temp = tempdir().expect("create temp dir"); @@ -1500,6 +1599,7 @@ fn wasm_execution_streams_exit_event() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -1508,6 +1608,7 @@ fn wasm_execution_streams_exit_event() { .expect("start wasm execution"); let mut saw_stdout = false; + let mut saw_initial_signal_mask = false; let mut saw_exit = false; while !saw_exit { @@ -1527,12 +1628,27 @@ fn wasm_execution_streams_exit_event() { Some(WasmExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } - Some(WasmExecutionEvent::SyncRpcRequest(_)) => {} + Some(WasmExecutionEvent::SyncRpcRequest(request)) => { + assert_eq!(request.method, "process.wasm_sync_rpc"); + assert_eq!( + request.args.first().and_then(Value::as_str), + Some("process.signal_mask"), + "the smoke guest should only await its initial kernel signal mask" + ); + execution + .respond_sync_rpc_success(request.id, json!({ "signals": [] })) + .expect("commit the initial standalone signal mask"); + saw_initial_signal_mask = true; + } Some(WasmExecutionEvent::SignalState { .. }) => {} None => panic!("timed out waiting for wasm execution event"), } } + assert!( + saw_initial_signal_mask, + "the exit lifecycle must cross the reply-bearing signal-mask boundary" + ); assert!(saw_stdout, "expected stdout event before exit"); } @@ -1554,6 +1670,7 @@ fn wasm_execution_can_route_stdio_through_kernel_sync_rpc() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::from([( String::from("AGENTOS_WASI_STDIO_SYNC_RPC"), @@ -1564,13 +1681,11 @@ fn wasm_execution_can_route_stdio_through_kernel_sync_rpc() { }) .expect("start wasm execution"); - let request = match execution - .poll_event_blocking(Duration::from_secs(5)) - .expect("poll wasm event") - { - Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, - other => panic!("expected kernel stdio sync RPC request, got {other:?}"), - }; + let request = + match poll_wasm_event_after_initial_signal_mask(&mut execution, Duration::from_secs(5)) { + Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, + other => panic!("expected kernel stdio sync RPC request, got {other:?}"), + }; assert_eq!(request.method, "__kernel_stdio_write"); assert_eq!(request.args.first(), Some(&json!(1))); @@ -1611,6 +1726,7 @@ fn wasm_execution_reads_streaming_stdin_via_kernel_bridge() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::from([( String::from("AGENTOS_WASI_STDIO_SYNC_RPC"), @@ -1653,6 +1769,7 @@ fn wasm_execution_poll_oneoff_uses_kernel_poll_for_multiple_fds() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -1660,13 +1777,11 @@ fn wasm_execution_poll_oneoff_uses_kernel_poll_for_multiple_fds() { }) .expect("start wasm execution"); - let request = match execution - .poll_event_blocking(Duration::from_secs(5)) - .expect("poll wasm event") - { - Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, - other => panic!("expected sync RPC request, got {other:?}"), - }; + let request = + match poll_wasm_event_after_initial_signal_mask(&mut execution, Duration::from_secs(5)) { + Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, + other => panic!("expected sync RPC request, got {other:?}"), + }; assert_eq!(request.method, "__kernel_poll"); assert_eq!( @@ -1701,7 +1816,7 @@ fn wasm_execution_poll_oneoff_uses_kernel_poll_for_multiple_fds() { assert_eq!(stdout, "poll-ready\n"); } -fn wasm_execution_emits_signal_state_from_control_channel() { +fn wasm_execution_waits_for_kernel_signal_state_commit_before_continuing() { assert_node_available(); let temp = tempdir().expect("create temp dir"); @@ -1719,6 +1834,7 @@ fn wasm_execution_emits_signal_state_from_control_channel() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -1727,7 +1843,7 @@ fn wasm_execution_emits_signal_state_from_control_channel() { .expect("start wasm execution"); let mut saw_stdout = false; - let mut saw_signal = false; + let mut saw_signal_request = false; let mut saw_exit = false; while !saw_exit { @@ -1740,18 +1856,28 @@ fn wasm_execution_emits_signal_state_from_control_channel() { .expect("stdout utf8") .contains("signal:ready"); } - Some(WasmExecutionEvent::SignalState { - signal, - registration, - }) => { - assert_eq!(signal, 2); + Some(WasmExecutionEvent::SyncRpcRequest(request)) => { + if request.method == "process.wasm_sync_rpc" + && request.args.first().and_then(Value::as_str) == Some("process.signal_mask") + { + execution + .respond_sync_rpc_success(request.id, json!({ "signals": [] })) + .expect("return the initial kernel signal mask"); + continue; + } + assert_eq!(request.method, "process.signal_state"); assert_eq!( - registration.action, - agentos_execution::wasm::WasmSignalDispositionAction::User + request.args, + vec![json!(2), json!("user"), json!("[15]"), json!(4660)] ); - assert_eq!(registration.mask, vec![15]); - assert_eq!(registration.flags, 0x1234); - saw_signal = true; + assert!( + !saw_stdout, + "the guest must remain blocked until the host commits signal state" + ); + execution + .respond_sync_rpc_success(request.id, Value::Null) + .expect("acknowledge committed signal state"); + saw_signal_request = true; } Some(WasmExecutionEvent::Exited(code)) => { assert_eq!(code, 0); @@ -1760,13 +1886,18 @@ fn wasm_execution_emits_signal_state_from_control_channel() { Some(WasmExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } - Some(WasmExecutionEvent::SyncRpcRequest(_)) => {} + Some(WasmExecutionEvent::SignalState { .. }) => { + panic!("managed signal state must use the reply-bearing host-call path") + } None => panic!("timed out waiting for wasm execution event"), } } assert!(saw_stdout, "expected stdout event before exit"); - assert!(saw_signal, "expected signal-state event before exit"); + assert!( + saw_signal_request, + "expected a reply-bearing signal-state request before exit" + ); } fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk() { @@ -1790,6 +1921,7 @@ fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk( limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -1802,10 +1934,7 @@ fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk( let mut saw_exit = false; while !saw_exit { - match execution - .poll_event_blocking(Duration::from_secs(5)) - .expect("poll wasm event") - { + match poll_wasm_event_after_initial_signal_mask(&mut execution, Duration::from_secs(5)) { Some(WasmExecutionEvent::Stdout(chunk)) => stdout.push(chunk), Some(WasmExecutionEvent::SignalState { signal, @@ -1814,7 +1943,7 @@ fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk( assert_eq!(signal, 2); assert_eq!( registration.action, - agentos_execution::wasm::WasmSignalDispositionAction::User + agentos_execution::ExecutionSignalDispositionAction::User ); assert_eq!(registration.mask, vec![15]); assert_eq!(registration.flags, 0x1234); @@ -1827,7 +1956,9 @@ fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk( Some(WasmExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } - Some(WasmExecutionEvent::SyncRpcRequest(_)) => {} + Some(WasmExecutionEvent::SyncRpcRequest(request)) => { + panic!("unexpected sync RPC request: {request:?}") + } None => panic!("timed out waiting for wasm execution event"), } } @@ -1857,6 +1988,7 @@ fn wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -1869,10 +2001,7 @@ fn wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks() { let mut stdout = Vec::new(); while !saw_exit { - match execution - .poll_event_blocking(Duration::from_secs(5)) - .expect("poll wasm event") - { + match poll_wasm_event_after_initial_signal_mask(&mut execution, Duration::from_secs(5)) { Some(WasmExecutionEvent::Stdout(chunk)) => stdout.push(chunk), Some(WasmExecutionEvent::SignalState { signal, @@ -1881,7 +2010,7 @@ fn wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks() { assert_eq!(signal, 2); assert_eq!( registration.action, - agentos_execution::wasm::WasmSignalDispositionAction::User + agentos_execution::ExecutionSignalDispositionAction::User ); assert_eq!(registration.mask, vec![15]); assert_eq!(registration.flags, 0x1234); @@ -1894,7 +2023,9 @@ fn wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks() { Some(WasmExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } - Some(WasmExecutionEvent::SyncRpcRequest(_)) => {} + Some(WasmExecutionEvent::SyncRpcRequest(request)) => { + panic!("unexpected sync RPC request: {request:?}") + } None => panic!("timed out waiting for wasm execution event"), } } @@ -2244,7 +2375,7 @@ fn wasm_warmup_metrics_encode_emoji_module_paths_as_json() { assert!(stderr.contains("\\ud83d\\ude00"), "stderr: {stderr}"); } -fn wasm_execution_times_out_when_fuel_budget_is_exhausted() { +fn wasm_execution_times_out_when_wall_clock_limit_is_exceeded() { assert_node_available(); let temp = tempdir().expect("create temp dir"); @@ -2264,19 +2395,22 @@ fn wasm_execution_times_out_when_fuel_budget_is_exhausted() { context.context_id, temp.path(), Vec::new(), - BTreeMap::from([(String::from(WASM_MAX_FUEL_ENV), String::from("25"))]), + BTreeMap::from([( + String::from(TEST_WASM_WALL_CLOCK_LIMIT_MS), + String::from("25"), + )]), WasmPermissionTier::Full, ); assert_eq!(exit_code, 124, "stdout={stdout} stderr={stderr}"); assert!(stdout.is_empty(), "stdout={stdout}"); assert!( - stderr.contains("fuel budget exhausted"), - "stderr should mention the exhausted fuel budget: {stderr}" + stderr.contains("wall-clock limit exceeded"), + "stderr should mention the elapsed wall-clock limit: {stderr}" ); } -fn wasm_execution_poll_path_times_out_when_fuel_budget_is_exhausted() { +fn wasm_execution_poll_path_times_out_at_wall_clock_limit() { assert_node_available(); let temp = tempdir().expect("create temp dir"); @@ -2295,11 +2429,12 @@ fn wasm_execution_poll_path_times_out_when_fuel_budget_is_exhausted() { .start_execution(StartWasmExecutionRequest { guest_runtime: Default::default(), limits: WasmExecutionLimits { - max_fuel: Some(25), + wall_clock_limit_ms: Some(25), ..Default::default() }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2314,18 +2449,20 @@ fn wasm_execution_poll_path_times_out_when_fuel_budget_is_exhausted() { let remaining = deadline .checked_duration_since(std::time::Instant::now()) .expect("poll path did not time out within the bounded test window"); - match execution - .poll_event_blocking(remaining.min(Duration::from_millis(250))) - .expect("poll wasm event") - { + match poll_wasm_event_after_initial_signal_mask( + &mut execution, + remaining.min(Duration::from_millis(250)), + ) { Some(WasmExecutionEvent::Stderr(chunk)) => { stderr.push_str(&String::from_utf8_lossy(&chunk)); } Some(WasmExecutionEvent::Exited(code)) => { exit_code = Some(code); } + Some(WasmExecutionEvent::SyncRpcRequest(request)) => { + panic!("unexpected sync RPC request: {request:?}") + } Some(WasmExecutionEvent::Stdout(_)) - | Some(WasmExecutionEvent::SyncRpcRequest(_)) | Some(WasmExecutionEvent::SignalState { .. }) | None => {} } @@ -2333,12 +2470,12 @@ fn wasm_execution_poll_path_times_out_when_fuel_budget_is_exhausted() { assert_eq!(exit_code, Some(124), "stderr={stderr}"); assert!( - stderr.contains("fuel budget exhausted"), - "stderr should mention the exhausted fuel budget: {stderr}" + stderr.contains("wall-clock limit exceeded"), + "stderr should mention the elapsed wall-clock limit: {stderr}" ); } -fn wasm_execution_allows_prewarm_timeout_to_differ_from_execution_timeout() { +fn wasm_execution_allows_prewarm_timeout_to_differ_from_wall_clock_limit() { assert_node_available(); let temp = tempdir().expect("create temp dir"); @@ -2358,15 +2495,18 @@ fn wasm_execution_allows_prewarm_timeout_to_differ_from_execution_timeout() { context.context_id, temp.path(), Vec::new(), - BTreeMap::from([(String::from(WASM_MAX_FUEL_ENV), String::from("25"))]), + BTreeMap::from([( + String::from(TEST_WASM_WALL_CLOCK_LIMIT_MS), + String::from("25"), + )]), WasmPermissionTier::Full, ); assert_eq!(exit_code, 124, "stdout={stdout} stderr={stderr}"); assert!(stdout.is_empty(), "stdout={stdout}"); assert!( - stderr.contains("fuel budget exhausted"), - "stderr should mention the exhausted fuel budget: {stderr}" + stderr.contains("wall-clock limit exceeded"), + "stderr should mention the elapsed wall-clock limit: {stderr}" ); } @@ -2395,6 +2535,7 @@ fn wasm_execution_rejects_modules_whose_memory_cap_exceeds_limit() { }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2468,6 +2609,7 @@ fn wasm_execution_rejects_modules_that_exceed_parser_file_size_cap() { }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2510,6 +2652,7 @@ fn wasm_execution_rejects_modules_with_too_many_import_entries() { }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2550,6 +2693,7 @@ fn wasm_execution_rejects_modules_with_too_many_memory_entries() { }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2592,6 +2736,7 @@ fn wasm_execution_rejects_varuints_that_exceed_parser_iteration_cap() { }, vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2652,6 +2797,7 @@ fi\n"; limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: vec![shim_path.to_string_lossy().into_owned()], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2717,6 +2863,7 @@ fn wasm_execution_rejects_random_non_wasm_bytes_with_typed_error() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2782,6 +2929,7 @@ fn wasm_execution_rejects_native_binary_headers_with_explicit_error() { limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, + managed_kernel_host: false, argv: Vec::new(), env: BTreeMap::new(), cwd: temp.path().to_path_buf(), @@ -2849,74 +2997,49 @@ fn wasm_unbounded_recursion_module() -> Vec { .expect("compile unbounded-recursion wasm fixture") } -// Watchdog runner for WASM cases that may run unbounded. The whole execution -// (engine + context + wait) happens on a spawned thread, so a guest that the -// engine never terminates cannot hang the test binary: the test thread reclaims -// control after `wall_clock_budget` and reports `None`. -fn run_wasm_execution_with_watchdog( - module_bytes: Vec, - env: BTreeMap, - wall_clock_budget: Duration, -) -> Option<(String, String, i32)> { - let (tx, rx) = mpsc::channel::<(String, String, i32)>(); - thread::spawn(move || { - let temp = match tempdir() { - Ok(temp) => temp, - Err(_) => return, - }; - write_fixture(&temp.path().join("guest.wasm"), &module_bytes); +// SE-EXEC-05 (B.1) SAFEGUARD [x-ref FAILURES.md#F-002]: V8 has no enforceable +// per-module stack-byte lever. A configured limit must therefore fail closed at +// admission and name the unenforceable requested bound instead of starting an +// unbounded guest or relying on V8's generic RangeError guard. +fn wasm_configured_stack_byte_limit_fails_closed_for_v8() { + assert_node_available(); - let mut engine = support::wasm_engine(); - let context = engine.create_context(CreateWasmContextRequest { - vm_id: String::from("vm-wasm"), - module_path: Some(String::from("./guest.wasm")), - }); + let temp = tempdir().expect("create temp dir"); + write_fixture( + &temp.path().join("guest.wasm"), + &wasm_unbounded_recursion_module(), + ); - let result = run_wasm_execution( - &mut engine, - context.context_id, - temp.path(), - Vec::new(), - env, - WasmPermissionTier::Full, - ); - let _ = tx.send(result); + let mut engine = support::wasm_engine(); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm"), + module_path: Some(String::from("./guest.wasm")), }); - - rx.recv_timeout(wall_clock_budget).ok() -} - -// SE-EXEC-05 (B.1) SAFEGUARD [x-ref FAILURES.md#F-002]: with -// `AGENTOS_WASM_MAX_STACK_BYTES` configured, never-returning recursion must be -// terminated nonzero AND the failure must cite the operator-configured stack -// byte budget instead of the engine's generic default-guard message. Before the -// fix the env was never read by the engine (dead cap), so the guest trapped on -// V8's default `RangeError` with a generic message. The run is watchdog-bound so -// it cannot hang CI, and the configured cap makes it terminate fast. -fn wasm_deep_recursion_respects_configured_stack_byte_limit() { - assert_node_available(); - let env = BTreeMap::from([( String::from(WASM_MAX_STACK_BYTES_ENV), String::from("65536"), )]); - let outcome = run_wasm_execution_with_watchdog( - wasm_unbounded_recursion_module(), - env, - Duration::from_secs(45), - ); - - let (stdout, stderr, exit_code) = - outcome.expect("deep recursion run did not finish within the watchdog budget"); + let error = engine + .start_execution(StartWasmExecutionRequest { + guest_runtime: Default::default(), + limits: wasm_limits_from_env(&env), + vm_id: String::from("vm-wasm"), + context_id: context.context_id, + managed_kernel_host: false, + argv: Vec::new(), + env, + cwd: temp.path().to_path_buf(), + permission_tier: WasmPermissionTier::Full, + }) + .expect_err("V8 must reject an unenforceable stack-byte limit"); - assert_ne!( - exit_code, 0, - "deep recursion should be terminated, not run unbounded: stdout={stdout} stderr={stderr}" - ); - assert!( - stderr.contains("65536") || stderr.to_lowercase().contains("configured"), - "termination should cite the configured stack byte limit, not a generic default guard: stderr={stderr}" - ); + match error { + WasmExecutionError::InvalidLimit(message) => assert_eq!( + message, + "configured wasm max stack byte limit 65536 cannot be enforced by the V8 runner" + ), + other => panic!("expected InvalidLimit, got {other:?}"), + } } // Separate libtest cases in this binary still trip a V8 teardown/init crash, so @@ -2924,7 +3047,7 @@ fn wasm_deep_recursion_respects_configured_stack_byte_limit() { // // NOT split for cargo-nextest (unlike `python_suite`/`kill_cleanup_suite`): three // cases here run an infinite-loop guest module (`wasm_execution_times_out_when_ -// fuel_budget_is_exhausted`, `..._poll_path_times_out_...`, `..._allows_prewarm_ +// wall_clock_limit_is_exceeded`, `..._poll_path_times_out_...`, `..._allows_prewarm_ // timeout_to_differ_...`). In this collapsed run they are cheap because earlier // cases warmed the process-global V8 state, but in a COLD nextest process the // infinite loop is bounded only by the ~30s V8 CPU-time watchdog, so each costs @@ -2939,6 +3062,7 @@ fn wasm_suite() { wasm_execution_runs_guest_module_through_v8(); wasm_spawn_action_decoder_enforces_typed_limits_with_e2big(); wasm_getrlimit_nofile_reports_typed_fd_limit(); + wasm_process_outputs_fault_before_partial_write_or_wait_state(); wasm_snapshot_runner_block_round_trips_twice(); wasm_snapshot_runner_warm_worker_pool_hits(); wasm_snapshot_runner_warm_worker_pool_disabled_falls_back(); @@ -2949,11 +3073,11 @@ fn wasm_suite() { wasm_execution_ignores_guest_overrides_for_internal_node_env(); wasm_execution_freezes_wasi_clock_time(); wasm_execution_rejects_vm_mismatch(); - wasm_execution_streams_exit_event(); + wasm_execution_streams_exit_event_after_signal_mask_commit(); wasm_execution_can_route_stdio_through_kernel_sync_rpc(); wasm_execution_reads_streaming_stdin_via_kernel_bridge(); wasm_execution_poll_oneoff_uses_kernel_poll_for_multiple_fds(); - wasm_execution_emits_signal_state_from_control_channel(); + wasm_execution_waits_for_kernel_signal_state_commit_before_continuing(); wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk(); wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks(); wasm_read_only_tier_blocks_workspace_writes_but_read_write_allows_them(); @@ -2964,9 +3088,9 @@ fn wasm_suite() { wasm_execution_reuses_shared_warmup_path_across_contexts(); wasm_execution_rewarms_when_symlink_target_changes_with_same_size_module(); wasm_warmup_metrics_encode_emoji_module_paths_as_json(); - wasm_execution_times_out_when_fuel_budget_is_exhausted(); - wasm_execution_poll_path_times_out_when_fuel_budget_is_exhausted(); - wasm_execution_allows_prewarm_timeout_to_differ_from_execution_timeout(); + wasm_execution_times_out_when_wall_clock_limit_is_exceeded(); + wasm_execution_poll_path_times_out_at_wall_clock_limit(); + wasm_execution_allows_prewarm_timeout_to_differ_from_wall_clock_limit(); wasm_execution_rejects_modules_whose_memory_cap_exceeds_limit(); wasm_execution_enforces_runtime_memory_growth_limit_for_modules_without_declared_maximum(); wasm_execution_rejects_modules_that_exceed_parser_file_size_cap(); @@ -2979,7 +3103,7 @@ fn wasm_suite() { // SE-EXEC-05 (B.1) SAFEGUARD [x-ref FAILURES.md#F-002]: the configured WASM // stack byte cap must now bound runaway recursion and attribute the failure. - wasm_deep_recursion_respects_configured_stack_byte_limit(); + wasm_configured_stack_byte_limit_fails_closed_for_v8(); // Convergence item C: the official WASI preview1 conformance subset runs on // the native backend of the SINGLE shared runner (same manifest the browser diff --git a/crates/execution/tests/wasm_abi_link_contract.rs b/crates/execution/tests/wasm_abi_link_contract.rs new file mode 100644 index 0000000000..fe69f5ef0c --- /dev/null +++ b/crates/execution/tests/wasm_abi_link_contract.rs @@ -0,0 +1,160 @@ +mod support; + +use agentos_execution::{CreateWasmContextRequest, StartWasmExecutionRequest, WasmPermissionTier}; +use agentos_wasm_abi_generator::{ + imports_module, single_import_module, AbiImport, AbiManifest, CallArguments, +}; +use std::{collections::BTreeMap, fs}; +use tempfile::tempdir; + +const ABI_MANIFEST: &str = include_str!("../assets/agentos-wasm-abi.json"); + +fn run_fixture( + engine: &mut agentos_execution::WasmExecutionEngine, + root: &std::path::Path, + file_name: &str, + bytes: &[u8], + tier: WasmPermissionTier, +) -> agentos_execution::WasmExecutionResult { + fs::write(root.join(file_name), bytes).expect("write generated ABI fixture"); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm-abi-link"), + module_path: Some(format!("./{file_name}")), + }); + engine + .start_execution(StartWasmExecutionRequest { + guest_runtime: Default::default(), + limits: Default::default(), + vm_id: String::from("vm-wasm-abi-link"), + context_id: context.context_id, + managed_kernel_host: false, + argv: Vec::new(), + env: BTreeMap::new(), + cwd: root.to_path_buf(), + permission_tier: tier, + }) + .expect("start generated ABI fixture") + .wait() + .expect("wait for generated ABI fixture") +} + +#[test] +fn every_permitted_import_and_preview1_alias_links_at_every_tier() { + let manifest = AbiManifest::parse(ABI_MANIFEST); + let temp = tempdir().expect("create temp dir"); + let mut engine = support::wasm_engine(); + + for (tier_name, tier) in [ + ("isolated", WasmPermissionTier::Isolated), + ("read-only", WasmPermissionTier::ReadOnly), + ("read-write", WasmPermissionTier::ReadWrite), + ("full", WasmPermissionTier::Full), + ] { + let permitted = manifest.permitted_imports(tier_name); + assert!(!permitted.is_empty(), "{tier_name} ABI must not be empty"); + let result = run_fixture( + &mut engine, + temp.path(), + &format!("linkable-{tier_name}.wasm"), + &imports_module(&permitted, false, CallArguments::Zero), + tier, + ); + assert_eq!( + result.exit_code, + 0, + "{tier_name} ABI failed to link: stdout={} stderr={}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + } +} + +#[test] +fn preview1_proc_exit_and_compatibility_alias_are_terminal_calls() { + let manifest = AbiManifest::parse(ABI_MANIFEST); + let proc_exit = manifest + .imports + .iter() + .find(|import| import.module == "wasi_snapshot_preview1" && import.name == "proc_exit") + .expect("Preview1 proc_exit manifest entry"); + let temp = tempdir().expect("create temp dir"); + let mut engine = support::wasm_engine(); + + for module in ["wasi_snapshot_preview1", "wasi_unstable"] { + let mut import = proc_exit.clone(); + import.module = module.to_string(); + let result = run_fixture( + &mut engine, + temp.path(), + &format!("proc-exit-{module}.wasm"), + &single_import_module(&import, true, CallArguments::Zero), + WasmPermissionTier::Full, + ); + assert_eq!( + result.exit_code, + 0, + "{module}.proc_exit did not execute: {}", + String::from_utf8_lossy(&result.stderr) + ); + } +} + +#[test] +fn manifest_permission_tiers_omit_denied_and_undeclared_imports() { + let manifest = AbiManifest::parse(ABI_MANIFEST); + let temp = tempdir().expect("create temp dir"); + let mut engine = support::wasm_engine(); + + let cases = [ + ("host_net", "net_socket", WasmPermissionTier::ReadWrite), + ( + "host_process", + "proc_spawn_v4", + WasmPermissionTier::ReadWrite, + ), + ("host_process", "fd_getfd", WasmPermissionTier::Isolated), + ]; + for (module, name, tier) in cases { + let import = manifest + .imports + .iter() + .find(|import| import.module == module && import.name == name) + .unwrap_or_else(|| panic!("missing {module}.{name} manifest entry")); + let result = run_fixture( + &mut engine, + temp.path(), + &format!("denied-{module}-{name}.wasm"), + &single_import_module(import, false, CallArguments::Zero), + tier, + ); + let stderr = String::from_utf8_lossy(&result.stderr); + assert_ne!( + result.exit_code, 0, + "{module}.{name} must not link at {tier:?}" + ); + assert!( + stderr.contains(module) || stderr.contains(name), + "unexpected denied-import error for {module}.{name}: {stderr}" + ); + } + + let undeclared = AbiImport { + module: String::from("host_unknown"), + name: String::from("ambient_escape"), + params: Vec::new(), + results: Vec::new(), + }; + let rejected = run_fixture( + &mut engine, + temp.path(), + "undeclared-import.wasm", + &single_import_module(&undeclared, false, CallArguments::Zero), + WasmPermissionTier::Full, + ); + let stderr = String::from_utf8_lossy(&rejected.stderr); + assert_ne!(rejected.exit_code, 0, "undeclared import must not link"); + assert!( + stderr.contains("host_unknown") || stderr.contains("ambient_escape"), + "unexpected undeclared-import error: {stderr}" + ); +} diff --git a/crates/execution/tests/wasm_host_fs_errno_contract.rs b/crates/execution/tests/wasm_host_fs_errno_contract.rs index 4785f6f617..9b1f6ad51e 100644 --- a/crates/execution/tests/wasm_host_fs_errno_contract.rs +++ b/crates/execution/tests/wasm_host_fs_errno_contract.rs @@ -42,13 +42,18 @@ fn path_open_preserves_directory_and_nofollow_before_kernel_mutation() { "path_open must pass O_DIRECTORY and a missing SYMLINK_FOLLOW lookup flag to the kernel" ); assert!( - source.contains("kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags)"), + source.contains( + "kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags, requestedDirect)" + ), "path_open must include its WASI lookup flags in the kernel conversion" ); assert!( source.contains( "function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedFdPtr)" - ) && source.contains("return WASI_ERRNO_LOOP;"), - "runner-local /proc/self/fd and /dev/fd aliases must not bypass O_NOFOLLOW" + ) && source.contains("return WASI_ERRNO_LOOP;") + && source.contains( + "const procFdResult = SIDECAR_MANAGED_PROCESS\n ? null\n : openProcSelfFdAlias(" + ), + "standalone aliases must honor O_NOFOLLOW and managed aliases must use kernel path_open" ); } diff --git a/crates/execution/tests/wasm_host_net_errno_contract.rs b/crates/execution/tests/wasm_host_net_errno_contract.rs index dbd13e5d12..da90e4a443 100644 --- a/crates/execution/tests/wasm_host_net_errno_contract.rs +++ b/crates/execution/tests/wasm_host_net_errno_contract.rs @@ -72,13 +72,32 @@ fn host_net_fd_read_keeps_guest_faults_separate_from_socket_errors() { #[test] fn host_net_fd_write_keeps_guest_faults_separate_from_socket_errors() { let source = runner_source(); + let fd_write = between( + &source, + "wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => {", + "wasiImport.fd_close = (fd) => {", + ); + let prevalidation = fd_write + .find("const iovRequest = validateGuestIovRequest(iovs, iovsLen);") + .expect("fd_write must prevalidate the guest iovec table"); + let socket_lookup = fd_write + .find("const hostNetSocket = getHostNetSocket(numericFd);") + .expect("fd_write must look up a host-net socket"); + let admitted_request = fd_write + .find("iovsLen,\n nwrittenPtr,\n iovRequest,") + .expect("fd_write must pass the admitted iovec request to host-net"); + assert!( + prevalidation < socket_lookup && socket_lookup < admitted_request, + "host-net fd_write must validate guest ranges before socket work" + ); + let socket_write = between( &source, "function writeHostNetSocketFromGuestIovs(", "function dequeuePipeBytes(", ); let guest_fault = socket_write - .find("bytes = collectGuestIovBytes(iovs, iovsLen);") + .find("bytes = collectGuestIovBytes(iovs, iovsLen, iovRequest);") .expect("host-net fd_write must collect guest iovecs"); let rpc = socket_write .find("callSyncRpc('net.write'") @@ -166,8 +185,8 @@ fn host_net_empty_read_invalidates_cached_poll_readiness() { fn blocking_kernel_pipe_writes_pump_wasm_children_on_backpressure() { let source = runner_source(); let start = source - .rfind("wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => {") - .expect("missing final WASM fd_write override"); + .rfind("function writeKernelFdCooperatively(targetFd, bytes) {") + .expect("missing cooperative kernel fd writer"); let end = source[start..] .find("wasiImport.poll_oneoff =") .expect("missing poll_oneoff after final fd_write"); @@ -175,7 +194,9 @@ fn blocking_kernel_pipe_writes_pump_wasm_children_on_backpressure() { assert!( fd_write.contains("error?.code !== 'EAGAIN'") && fd_write.contains("process.fd_stat") - && fd_write.contains("pumpSpawnedChildrenOrWait(SPAWNED_CHILD_WAIT_SLICE_MS)"), - "blocking kernel-pipe writes must schedule the child that can free pipe capacity" + && fd_write.contains("pumpSpawnedChildren(0)") + && fd_write.contains("callSyncRpc('__kernel_poll'") + && fd_write.contains("events: KERNEL_POLLOUT"), + "blocking kernel-pipe writes must schedule children and wait for kernel write readiness" ); } diff --git a/crates/execution/tests/wasm_nonblocking_stdin.rs b/crates/execution/tests/wasm_nonblocking_stdin.rs index 8e59310952..f3425c2950 100644 --- a/crates/execution/tests/wasm_nonblocking_stdin.rs +++ b/crates/execution/tests/wasm_nonblocking_stdin.rs @@ -1,8 +1,10 @@ +mod support; + use std::{collections::BTreeMap, fs, process::Command, time::Duration}; use agentos_execution::{ - CreateWasmContextRequest, JavascriptSyncRpcRequest, StartWasmExecutionRequest, WasmExecution, - WasmExecutionEngine, WasmExecutionEvent, WasmPermissionTier, + CreateWasmContextRequest, HostRpcRequest, StartWasmExecutionRequest, WasmExecution, + WasmExecutionEvent, WasmPermissionTier, }; use base64::Engine; use serde_json::{json, Value}; @@ -52,17 +54,43 @@ fn module() -> Vec { .expect("compile WASI stdin fixture") } -fn request(execution: &mut WasmExecution) -> JavascriptSyncRpcRequest { - match execution - .poll_event_blocking(Duration::from_secs(5)) - .expect("poll WASM event") - { - Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, - other => panic!("expected sync RPC request, got {other:?}"), +fn request(execution: &mut WasmExecution) -> HostRpcRequest { + loop { + let mut request = match execution + .poll_event_blocking(Duration::from_secs(5)) + .expect("poll WASM event") + { + Some(WasmExecutionEvent::SyncRpcRequest(request)) => request, + other => panic!("expected sync RPC request, got {other:?}"), + }; + if request.method == "process.wasm_sync_rpc" { + request.method = request.args.remove(0).as_str().unwrap().to_owned(); + request.raw_bytes_args = request + .raw_bytes_args + .into_iter() + .filter_map(|(index, bytes)| index.checked_sub(1).map(|index| (index, bytes))) + .collect(); + } + let fallback = match request.method.as_str() { + "process.signal_mask" => Some(json!({ "signals": [] })), + "process.signal_mask_scope_begin" => Some(json!(1)), + "process.signal_mask_scope_end" + | "process.signal_end" + | "process.take_signal" + | "process.signal_begin" => Some(Value::Null), + _ => None, + }; + if let Some(value) = fallback { + execution + .respond_sync_rpc_success(request.id, value) + .expect("respond to standalone signal-state RPC"); + continue; + } + return request; } } -fn request_bytes(request: &JavascriptSyncRpcRequest) -> Vec { +fn request_bytes(request: &HostRpcRequest) -> Vec { let encoded = request.args[1] .get("base64") .and_then(Value::as_str) @@ -83,7 +111,7 @@ fn fd0_nonblocking_returns_eagain_without_starving_progress_and_blocking_still_w let temp = tempdir().expect("create temp dir"); fs::write(temp.path().join("guest.wasm"), module()).expect("write WASM fixture"); - let mut engine = WasmExecutionEngine::default(); + let mut engine = support::wasm_engine(); let context = engine.create_context(CreateWasmContextRequest { vm_id: "vm-wasm-nonblock".into(), module_path: Some("./guest.wasm".into()), @@ -98,6 +126,7 @@ fn fd0_nonblocking_returns_eagain_without_starving_progress_and_blocking_still_w env: BTreeMap::from([("AGENTOS_WASI_STDIO_SYNC_RPC".into(), "1".into())]), cwd: temp.path().to_path_buf(), permission_tier: WasmPermissionTier::Full, + managed_kernel_host: false, }) .expect("start WASM fixture"); diff --git a/crates/execution/tests/wasm_preview1_memory_bounds.rs b/crates/execution/tests/wasm_preview1_memory_bounds.rs new file mode 100644 index 0000000000..e2c2cc80fe --- /dev/null +++ b/crates/execution/tests/wasm_preview1_memory_bounds.rs @@ -0,0 +1,109 @@ +mod support; + +use agentos_execution::{CreateWasmContextRequest, StartWasmExecutionRequest, WasmPermissionTier}; +use std::{collections::BTreeMap, fs}; +use tempfile::tempdir; + +fn preview1_memory_bounds_module() -> Vec { + wat::parse_str( + r#" +(module + (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) + (type $poll_oneoff_t (func (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t))) + (import "wasi_snapshot_preview1" "poll_oneoff" (func $poll_oneoff (type $poll_oneoff_t))) + (memory (export "memory") 1) + (data (i32.const 256) "must-not-write") + (data (i32.const 280) "bounds-ok\0a") + (func $_start (export "_start") + ;; The first iovec is valid but the second is not. fd_write must return + ;; EFAULT without consuming or emitting the valid prefix. + (i32.store (i32.const 0) (i32.const 256)) + (i32.store (i32.const 4) (i32.const 14)) + (i32.store (i32.const 8) (i32.const 65530)) + (i32.store (i32.const 12) (i32.const 16)) + (if + (i32.ne + (call $fd_write (i32.const 1) (i32.const 0) (i32.const 2) (i32.const 200)) + (i32.const 21) + ) + (then unreachable) + ) + + ;; Linux-compatible IOV_MAX is enforced before reading the table. + (if + (i32.ne + (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1025) (i32.const 200)) + (i32.const 28) + ) + (then unreachable) + ) + + ;; A zero-time clock subscription must still validate the complete output + ;; event table before waiting or attempting a partial event copyout. + (i64.store (i32.const 64) (i64.const 7)) + (i32.store8 (i32.const 72) (i32.const 0)) + (i64.store (i32.const 88) (i64.const 0)) + (if + (i32.ne + (call $poll_oneoff + (i32.const 64) + (i32.const 65530) + (i32.const 1) + (i32.const 200) + ) + (i32.const 21) + ) + (then unreachable) + ) + + (i32.store (i32.const 32) (i32.const 280)) + (i32.store (i32.const 36) (i32.const 10)) + (if + (i32.ne + (call $fd_write (i32.const 1) (i32.const 32) (i32.const 1) (i32.const 200)) + (i32.const 0) + ) + (then unreachable) + ) + ) +) +"#, + ) + .expect("compile Preview1 memory-bounds wasm fixture") +} + +#[test] +fn invalid_preview1_memory_faults_before_host_work_or_copyout() { + let temp = tempdir().expect("create temp dir"); + fs::write( + temp.path().join("guest.wasm"), + preview1_memory_bounds_module(), + ) + .expect("write wasm fixture"); + + let mut engine = support::wasm_engine(); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm-bounds"), + module_path: Some(String::from("./guest.wasm")), + }); + let execution = engine + .start_execution(StartWasmExecutionRequest { + guest_runtime: Default::default(), + limits: Default::default(), + vm_id: String::from("vm-wasm-bounds"), + context_id: context.context_id, + managed_kernel_host: false, + argv: Vec::new(), + env: BTreeMap::new(), + cwd: temp.path().to_path_buf(), + permission_tier: WasmPermissionTier::Full, + }) + .expect("start wasm execution"); + + let result = execution.wait().expect("wait for wasm execution"); + let stdout = String::from_utf8(result.stdout).expect("stdout utf8"); + let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); + assert_eq!(result.exit_code, 0, "stderr={stderr}"); + assert_eq!(stdout, "bounds-ok\n"); +} diff --git a/crates/execution/tests/wasm_structured_error_contract.rs b/crates/execution/tests/wasm_structured_error_contract.rs new file mode 100644 index 0000000000..082034f8c0 --- /dev/null +++ b/crates/execution/tests/wasm_structured_error_contract.rs @@ -0,0 +1,21 @@ +use std::{fs, path::PathBuf}; + +#[test] +fn pipe_sync_rpc_decoder_preserves_structured_error_details() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/runners/wasm-runner.mjs"); + let source = fs::read_to_string(path).expect("read wasm runner"); + let start = source.find("function callSyncRpc(").expect("callSyncRpc"); + let section = &source[start..]; + let code = section + .find("error.code = response.error.code") + .expect("structured error code"); + let details = section + .find("error.details = decodeSyncRpcValue(response.error.details)") + .expect("structured error details"); + let throw = details + + section[details..] + .find("throw error;") + .expect("structured error throw"); + + assert!(code < details && details < throw); +} diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index d03fd751a5..adb85f4665 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -8,6 +8,7 @@ description = "Shared kernel plane for secure-exec native and browser sidecars" [dependencies] agentos-bridge = { workspace = true } +agentos-resource = { workspace = true } vfs = { workspace = true } base64 = "0.22" event-listener = "5.4" @@ -19,10 +20,7 @@ serde_json = "1.0" web-time = "1.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -agentos-runtime = { workspace = true } getrandom = "0.2" -hickory-resolver = "=0.26.0-beta.3" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/crates/kernel/src/command_registry.rs b/crates/kernel/src/command_registry.rs index a2b4d1dbe5..4e2b11d6cf 100644 --- a/crates/kernel/src/command_registry.rs +++ b/crates/kernel/src/command_registry.rs @@ -1,7 +1,7 @@ use crate::vfs::{VfsError, VfsResult, VirtualFileSystem}; use std::collections::BTreeMap; -const COMMAND_STUB: &[u8] = b"#!/bin/sh\n# kernel command stub\n"; +pub(crate) const COMMAND_STUB: &[u8] = b"#!/bin/sh\n# kernel command stub\n"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommandDriver { @@ -68,6 +68,45 @@ impl CommandRegistry { Ok(()) } + /// Replace the complete command set currently owned by one driver. + /// + /// Ordinary `register` remains additive for bootstrap compatibility. + /// Runtime reconfiguration uses this exact replacement operation so a + /// removed package command cannot remain authoritative in the kernel. + pub fn replace(&mut self, driver: CommandDriver) -> VfsResult> { + driver.validate_commands()?; + let driver_name = driver.name().to_owned(); + let replacement = driver + .commands() + .iter() + .cloned() + .collect::>(); + let obsolete = self + .commands + .iter() + .filter_map(|(command, owner)| { + (owner.name() == driver_name && !replacement.contains(command)) + .then_some(command.clone()) + }) + .collect::>(); + for command in &obsolete { + self.commands.remove(command); + } + for command in driver.commands() { + if let Some(existing) = self.commands.get(command) { + if existing.name() != driver_name { + self.warnings.push(format!( + "command \"{command}\" overridden: {} -> {}", + existing.name(), + driver.name() + )); + } + } + self.commands.insert(command.clone(), driver.clone()); + } + Ok(obsolete) + } + pub fn warnings(&self) -> &[String] { &self.warnings } diff --git a/crates/kernel/src/device_layer.rs b/crates/kernel/src/device_layer.rs index 2b967698a7..b41be58169 100644 --- a/crates/kernel/src/device_layer.rs +++ b/crates/kernel/src/device_layer.rs @@ -1,5 +1,6 @@ use crate::vfs::{ - VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualUtimeSpec, + FileExtent, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, + VirtualUtimeSpec, }; use getrandom::getrandom; use web_time::{SystemTime, UNIX_EPOCH}; @@ -441,6 +442,16 @@ impl VirtualFileSystem for DeviceLayer { self.inner.unwritten_ranges(path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + if is_device_path(path) || is_device_dir(path) { + return Err(VfsError::new( + "EINVAL", + format!("device does not support extent mapping: {path}"), + )); + } + self.inner.extent_at(path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { if let Some(bytes) = read_stream_device(path, length) { return bytes; diff --git a/crates/kernel/src/dns.rs b/crates/kernel/src/dns.rs index d36b62d815..da01fed9de 100644 --- a/crates/kernel/src/dns.rs +++ b/crates/kernel/src/dns.rs @@ -1,20 +1,12 @@ -#[cfg(not(target_arch = "wasm32"))] -use agentos_runtime::BlockingJobError; use hickory_proto::rr::domain::Name; use hickory_proto::rr::rdata::{A, AAAA}; use hickory_proto::rr::{RData, Record, RecordType}; -#[cfg(not(target_arch = "wasm32"))] -use hickory_resolver::config::{NameServerConfig, ResolverConfig}; -#[cfg(not(target_arch = "wasm32"))] -use hickory_resolver::net::runtime::TokioRuntimeProvider; -#[cfg(not(target_arch = "wasm32"))] -use hickory_resolver::TokioResolver; use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; use std::fmt; +use std::future::Future; use std::net::{IpAddr, SocketAddr}; -#[cfg(not(target_arch = "wasm32"))] -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::pin::Pin; use std::sync::Arc; #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -224,227 +216,40 @@ impl fmt::Display for DnsResolverError { impl Error for DnsResolverError {} -pub trait DnsResolver { +pub trait DnsResolver: Send + Sync { fn lookup_ip(&self, request: &DnsLookupRequest) -> Result, DnsResolverError>; fn lookup_records( &self, request: &DnsRecordLookupRequest, ) -> Result, DnsResolverError>; -} - -pub type SharedDnsResolver = Arc; - -#[cfg(not(target_arch = "wasm32"))] -#[derive(Default)] -pub struct HickoryDnsResolver { - runtime: Option, -} - -/// On wasm the kernel has no tokio runtime or host DNS stack, so the resolver is -/// a unit type whose `DnsResolver` impl reports that name resolution is -/// unavailable; guests must supply DNS overrides or literal addresses. -#[cfg(target_arch = "wasm32")] -pub struct HickoryDnsResolver; - -#[cfg(target_arch = "wasm32")] -impl Default for HickoryDnsResolver { - fn default() -> Self { - Self - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl HickoryDnsResolver { - pub fn with_runtime(runtime: agentos_runtime::RuntimeContext) -> Self { - Self { - runtime: Some(runtime), - } - } - - fn send_lookup_ip( - &self, - hostname: String, - name_servers: Vec, - ) -> Result, DnsResolverError> { - let runtime = self.runtime.as_ref().cloned().ok_or_else(|| { - DnsResolverError::lookup_failed( - "DNS resolver has no injected sidecar runtime; configure HickoryDnsResolver::with_runtime", - ) - })?; - let resolver = { - let _entered = runtime.handle().enter(); - resolver_for(&name_servers)? - }; - let reserved_bytes = dns_lookup_input_bytes(&hostname, &name_servers); - let handle = runtime.handle().clone(); - let timeout = runtime.blocking_job_timeout(); - runtime - .blocking() - .run_sync(reserved_bytes, timeout, move || { - handle.block_on(async move { - tokio::time::timeout(timeout, lookup_ip_with_resolver(resolver, hostname)) - .await - .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) - }) - }) - .map_err(map_blocking_lookup_error)? - } - - fn send_lookup_records( - &self, - hostname: String, - name_servers: Vec, - record_type: RecordType, - ) -> Result, DnsResolverError> { - let runtime = self.runtime.as_ref().cloned().ok_or_else(|| { - DnsResolverError::lookup_failed( - "DNS resolver has no injected sidecar runtime; configure HickoryDnsResolver::with_runtime", - ) - })?; - let resolver = { - let _entered = runtime.handle().enter(); - resolver_for(&name_servers)? - }; - let reserved_bytes = dns_lookup_input_bytes(&hostname, &name_servers); - let handle = runtime.handle().clone(); - let timeout = runtime.blocking_job_timeout(); - runtime - .blocking() - .run_sync(reserved_bytes, timeout, move || { - handle.block_on(async move { - tokio::time::timeout( - timeout, - lookup_records_with_resolver(resolver, hostname, record_type), - ) - .await - .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) - }) - }) - .map_err(map_blocking_lookup_error)? - } -} -#[cfg(not(target_arch = "wasm32"))] -impl DnsResolver for HickoryDnsResolver { - fn lookup_ip(&self, request: &DnsLookupRequest) -> Result, DnsResolverError> { - self.send_lookup_ip( - request.hostname().to_owned(), - request.name_servers().to_vec(), - ) + fn lookup_ip_async<'a>( + &'a self, + request: DnsLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { self.lookup_ip(&request) }) } - fn lookup_records( - &self, - request: &DnsRecordLookupRequest, - ) -> Result, DnsResolverError> { - self.send_lookup_records( - request.hostname().to_owned(), - request.name_servers().to_vec(), - request.record_type(), - ) + fn lookup_records_async<'a>( + &'a self, + request: DnsRecordLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { self.lookup_records(&request) }) } } -#[cfg(not(target_arch = "wasm32"))] -fn resolver_for(name_servers: &[SocketAddr]) -> Result { - let resolver_config = resolver_config_from_name_servers(name_servers); - let builder = if let Some(config) = resolver_config { - TokioResolver::builder_with_config(config, TokioRuntimeProvider::default()) - } else { - TokioResolver::builder_tokio().map_err(|error| { - DnsResolverError::lookup_failed(format!( - "failed to initialize DNS resolver from system configuration: {error}" - )) - })? - }; - builder.build().map_err(|error| { - DnsResolverError::lookup_failed(format!("failed to build DNS resolver: {error}")) - }) -} - -#[cfg(not(target_arch = "wasm32"))] -fn dns_lookup_input_bytes(hostname: &str, name_servers: &[SocketAddr]) -> usize { - hostname.len().saturating_add( - name_servers - .len() - .saturating_mul(std::mem::size_of::()), - ) -} - -#[cfg(not(target_arch = "wasm32"))] -fn map_blocking_lookup_error(error: BlockingJobError) -> DnsResolverError { - DnsResolverError::lookup_failed(format!("ERR_AGENTOS_DNS_LOOKUP_EXECUTOR: {error}")) -} - -#[cfg(not(target_arch = "wasm32"))] -fn dns_lookup_timeout_error(timeout: std::time::Duration) -> DnsResolverError { - DnsResolverError::lookup_failed(format!( - "ERR_AGENTOS_DNS_LOOKUP_TIMEOUT: DNS lookup exceeded {}ms; raise runtime.blocking.jobTimeoutMs", - timeout.as_millis() - )) -} - -#[cfg(not(target_arch = "wasm32"))] -async fn lookup_ip_with_resolver( - resolver: TokioResolver, - hostname: String, -) -> Result, DnsResolverError> { - let lookup = resolver.lookup_ip(&hostname).await.map_err(|error| { - DnsResolverError::lookup_failed(format!( - "failed to resolve DNS address {hostname}: {error}" - )) - })?; - - let mut addresses = Vec::new(); - let mut seen = BTreeSet::new(); - for ip in lookup.iter() { - if seen.insert(ip) { - addresses.push(ip); - } - } - - if addresses.is_empty() { - return Err(DnsResolverError::lookup_failed(format!( - "failed to resolve DNS address {hostname}" - ))); - } +pub type SharedDnsResolver = Arc; - Ok(addresses) -} +/// Neutral default used when a host integration has not injected a resolver. +/// Literal addresses and configured overrides are still resolved entirely by +/// the kernel before this implementation is reached. +#[derive(Debug, Default)] +pub struct UnavailableDnsResolver; -#[cfg(not(target_arch = "wasm32"))] -async fn lookup_records_with_resolver( - resolver: TokioResolver, - hostname: String, - record_type: RecordType, -) -> Result, DnsResolverError> { - let lookup = resolver - .lookup(&hostname, record_type) - .await - .map_err(|error| { - let message = format!("failed to resolve DNS {record_type} record {hostname}: {error}"); - if error.is_nx_domain() { - DnsResolverError::nx_domain(message) - } else if error.is_no_records_found() { - DnsResolverError::no_data(message) - } else { - DnsResolverError::lookup_failed(message) - } - })?; - let records = lookup.answers().to_vec(); - if records.is_empty() { - return Err(DnsResolverError::no_data(format!( - "failed to resolve DNS {record_type} record {hostname}" - ))); - } - Ok(records) -} - -#[cfg(target_arch = "wasm32")] -impl DnsResolver for HickoryDnsResolver { +impl DnsResolver for UnavailableDnsResolver { fn lookup_ip(&self, request: &DnsLookupRequest) -> Result, DnsResolverError> { Err(DnsResolverError::lookup_failed(format!( - "browser sidecar DNS resolver is unavailable for {}; configure DNS overrides or pass a literal address", + "host DNS resolver is unavailable for {}; inject a resolver, configure a DNS override, or pass a literal address", request.hostname() ))) } @@ -454,10 +259,34 @@ impl DnsResolver for HickoryDnsResolver { request: &DnsRecordLookupRequest, ) -> Result, DnsResolverError> { Err(DnsResolverError::lookup_failed(format!( - "browser sidecar DNS record resolver is unavailable for {}; configure DNS overrides or pass a literal address", + "host DNS record resolver is unavailable for {}; inject a resolver, configure a DNS override, or pass a literal address", request.hostname() ))) } + + fn lookup_ip_async<'a>( + &'a self, + request: DnsLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { + Err(DnsResolverError::lookup_failed(format!( + "host DNS resolver is unavailable for {}; inject a resolver, configure a DNS override, or pass a literal address", + request.hostname() + ))) + }) + } + + fn lookup_records_async<'a>( + &'a self, + request: DnsRecordLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { + Err(DnsResolverError::lookup_failed(format!( + "host DNS record resolver is unavailable for {}; inject a resolver, configure a DNS override, or pass a literal address", + request.hostname() + ))) + }) + } } pub fn normalize_dns_hostname(hostname: &str) -> Result { @@ -512,6 +341,44 @@ pub fn resolve_dns( )) } +pub async fn resolve_dns_async( + config: &DnsConfig, + resolver: &dyn DnsResolver, + hostname: &str, +) -> Result { + let trimmed = hostname.trim(); + if let Ok(ip_addr) = trimmed.parse::() { + return Ok(DnsResolution::new( + ip_addr.to_string(), + DnsResolutionSource::Literal, + vec![ip_addr], + )); + } + + let normalized_hostname = normalize_dns_hostname(trimmed)?; + if let Some(addresses) = config.overrides.get(&normalized_hostname) { + return Ok(DnsResolution::new( + normalized_hostname, + DnsResolutionSource::Override, + addresses.clone(), + )); + } + + let request = DnsLookupRequest::new(normalized_hostname.clone(), config.name_servers.clone()); + let addresses = resolver.lookup_ip_async(request).await?; + if addresses.is_empty() { + return Err(DnsResolverError::lookup_failed(format!( + "failed to resolve DNS address {normalized_hostname}" + ))); + } + + Ok(DnsResolution::new( + normalized_hostname, + DnsResolutionSource::Resolver, + dedupe_addresses(addresses), + )) +} + pub fn resolve_dns_records( config: &DnsConfig, resolver: &dyn DnsResolver, @@ -562,41 +429,63 @@ pub fn resolve_dns_records( )) } -fn canonical_dns_subject(hostname: &str) -> Result { +pub async fn resolve_dns_records_async( + config: &DnsConfig, + resolver: &dyn DnsResolver, + hostname: &str, + record_type: RecordType, +) -> Result { let trimmed = hostname.trim(); - if let Ok(ip_addr) = trimmed.parse::() { - return Ok(ip_addr.to_string()); + let normalized_hostname = normalize_dns_hostname(trimmed)?; + let owner_name = normalized_hostname.parse::().map_err(|error| { + DnsResolverError::invalid_input(format!("invalid DNS hostname: {error}")) + })?; + + if let Some(records) = records_from_literal(trimmed, owner_name.clone(), record_type) { + return Ok(DnsRecordResolution::new( + normalized_hostname, + DnsResolutionSource::Literal, + records, + )); } - normalize_dns_hostname(trimmed) -} + if let Some(addresses) = config.overrides.get(&normalized_hostname) { + let records = records_from_addresses(owner_name, addresses, record_type); + if !records.is_empty() { + return Ok(DnsRecordResolution::new( + normalized_hostname, + DnsResolutionSource::Override, + records, + )); + } + } -#[cfg(not(target_arch = "wasm32"))] -fn resolver_config_from_name_servers(name_servers: &[SocketAddr]) -> Option { - if name_servers.is_empty() { - return None; + let request = DnsRecordLookupRequest::new( + normalized_hostname.clone(), + config.name_servers.clone(), + record_type, + ); + let records = resolver.lookup_records_async(request).await?; + if records.is_empty() { + return Err(DnsResolverError::no_data(format!( + "failed to resolve DNS {record_type} record {normalized_hostname}" + ))); } - let name_servers = name_servers - .iter() - .map(|server| { - let mut config = NameServerConfig::udp_and_tcp(server.ip()); - for connection in &mut config.connections { - connection.port = server.port(); - connection.bind_addr = Some(SocketAddr::new( - if server.is_ipv6() { - IpAddr::V6(Ipv6Addr::UNSPECIFIED) - } else { - IpAddr::V4(Ipv4Addr::UNSPECIFIED) - }, - 0, - )); - } - config - }) - .collect(); + Ok(DnsRecordResolution::new( + normalized_hostname, + DnsResolutionSource::Resolver, + records, + )) +} - Some(ResolverConfig::from_parts(None, vec![], name_servers)) +fn canonical_dns_subject(hostname: &str) -> Result { + let trimmed = hostname.trim(); + if let Ok(ip_addr) = trimmed.parse::() { + return Ok(ip_addr.to_string()); + } + + normalize_dns_hostname(trimmed) } fn dedupe_addresses(addresses: Vec) -> Vec { diff --git a/crates/kernel/src/fd_table.rs b/crates/kernel/src/fd_table.rs index 38af512e3f..1bf0139315 100644 --- a/crates/kernel/src/fd_table.rs +++ b/crates/kernel/src/fd_table.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard}; use crate::vfs::VirtualStat; -pub const MAX_FDS_PER_PROCESS: usize = 256; +pub const MAX_FDS_PER_PROCESS: usize = 1024; pub const O_RDONLY: u32 = 0; pub const O_WRONLY: u32 = 1; @@ -38,9 +38,136 @@ pub const FILETYPE_DIRECTORY: u8 = 3; pub const FILETYPE_REGULAR_FILE: u8 = 4; pub const FILETYPE_SOCKET_DGRAM: u8 = 5; pub const FILETYPE_SOCKET_STREAM: u8 = 6; -pub const FILETYPE_PIPE: u8 = FILETYPE_SOCKET_STREAM; +// Preview1 has no pipe filetype. Expose UNKNOWN to guests so libc does not +// mistake pipes for sockets; PipeManager's description registry remains the +// authoritative internal pipe classification. +pub const FILETYPE_PIPE: u8 = FILETYPE_UNKNOWN; pub const FILETYPE_SYMBOLIC_LINK: u8 = 7; +pub const WASI_RIGHT_FD_DATASYNC: u64 = 1 << 0; +pub const WASI_RIGHT_FD_READ: u64 = 1 << 1; +pub const WASI_RIGHT_FD_SEEK: u64 = 1 << 2; +pub const WASI_RIGHT_FD_FDSTAT_SET_FLAGS: u64 = 1 << 3; +pub const WASI_RIGHT_FD_SYNC: u64 = 1 << 4; +pub const WASI_RIGHT_FD_TELL: u64 = 1 << 5; +pub const WASI_RIGHT_FD_WRITE: u64 = 1 << 6; +pub const WASI_RIGHT_FD_ADVISE: u64 = 1 << 7; +pub const WASI_RIGHT_FD_ALLOCATE: u64 = 1 << 8; +pub const WASI_RIGHT_PATH_CREATE_DIRECTORY: u64 = 1 << 9; +pub const WASI_RIGHT_PATH_LINK_SOURCE: u64 = 1 << 10; +pub const WASI_RIGHT_PATH_LINK_TARGET: u64 = 1 << 11; +pub const WASI_RIGHT_PATH_OPEN: u64 = 1 << 13; +pub const WASI_RIGHT_FD_READDIR: u64 = 1 << 14; +pub const WASI_RIGHT_PATH_READLINK: u64 = 1 << 15; +pub const WASI_RIGHT_PATH_RENAME_SOURCE: u64 = 1 << 16; +pub const WASI_RIGHT_PATH_RENAME_TARGET: u64 = 1 << 17; +pub const WASI_RIGHT_PATH_FILESTAT_GET: u64 = 1 << 18; +pub const WASI_RIGHT_PATH_FILESTAT_SET_SIZE: u64 = 1 << 19; +pub const WASI_RIGHT_PATH_FILESTAT_SET_TIMES: u64 = 1 << 20; +pub const WASI_RIGHT_FD_FILESTAT_GET: u64 = 1 << 21; +pub const WASI_RIGHT_FD_FILESTAT_SET_SIZE: u64 = 1 << 22; +pub const WASI_RIGHT_FD_FILESTAT_SET_TIMES: u64 = 1 << 23; +pub const WASI_RIGHT_PATH_SYMLINK: u64 = 1 << 24; +pub const WASI_RIGHT_PATH_REMOVE_DIRECTORY: u64 = 1 << 25; +pub const WASI_RIGHT_PATH_UNLINK_FILE: u64 = 1 << 26; +pub const WASI_RIGHT_POLL_FD_READWRITE: u64 = 1 << 27; + +pub const WASI_PREOPEN_READ_RIGHTS_BASE: u64 = WASI_RIGHT_FD_READ + | WASI_RIGHT_FD_SEEK + | WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | WASI_RIGHT_FD_TELL + | WASI_RIGHT_PATH_OPEN + | WASI_RIGHT_FD_READDIR + | WASI_RIGHT_PATH_READLINK + | WASI_RIGHT_PATH_FILESTAT_GET + | WASI_RIGHT_FD_FILESTAT_GET + | WASI_RIGHT_POLL_FD_READWRITE; +// Preview1 libc derives a newly opened descriptor's base rights from the +// parent directory's inheriting mask. Path rights must therefore propagate +// through an opened subdirectory or ordinary POSIX `openat(dirfd, ...)` loses +// PATH_OPEN/metadata authority after one level. The operation still requires +// a directory filetype, and `path_open` intersects every explicit request +// with this mask before installing the child descriptor. +pub const WASI_PREOPEN_READ_RIGHTS_INHERITING: u64 = WASI_PREOPEN_READ_RIGHTS_BASE; +/// Read-write tier rights intentionally exclude namespace destruction. These +/// match the owned runner's historical read-write capability set. +pub const WASI_PREOPEN_READ_WRITE_RIGHTS_BASE: u64 = WASI_PREOPEN_READ_RIGHTS_BASE + | WASI_RIGHT_FD_DATASYNC + | WASI_RIGHT_FD_SYNC + | WASI_RIGHT_FD_WRITE + | WASI_RIGHT_FD_ADVISE + | WASI_RIGHT_FD_ALLOCATE + | WASI_RIGHT_PATH_CREATE_DIRECTORY + | WASI_RIGHT_PATH_FILESTAT_SET_SIZE + | WASI_RIGHT_PATH_FILESTAT_SET_TIMES + | WASI_RIGHT_FD_FILESTAT_SET_SIZE + | WASI_RIGHT_FD_FILESTAT_SET_TIMES; +pub const WASI_PREOPEN_READ_WRITE_RIGHTS_INHERITING: u64 = WASI_PREOPEN_READ_WRITE_RIGHTS_BASE; +pub const WASI_PREOPEN_WRITE_RIGHTS_BASE: u64 = WASI_PREOPEN_READ_WRITE_RIGHTS_BASE + | WASI_RIGHT_PATH_LINK_SOURCE + | WASI_RIGHT_PATH_LINK_TARGET + | WASI_RIGHT_PATH_RENAME_SOURCE + | WASI_RIGHT_PATH_RENAME_TARGET + | WASI_RIGHT_PATH_SYMLINK + | WASI_RIGHT_PATH_REMOVE_DIRECTORY + | WASI_RIGHT_PATH_UNLINK_FILE; +pub const WASI_PREOPEN_WRITE_RIGHTS_INHERITING: u64 = WASI_PREOPEN_WRITE_RIGHTS_BASE; + +pub const WASI_STDIO_READ_RIGHTS: u64 = WASI_RIGHT_FD_READ + | WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | WASI_RIGHT_FD_FILESTAT_GET + | WASI_RIGHT_POLL_FD_READWRITE; +pub const WASI_STDIO_WRITE_RIGHTS: u64 = WASI_RIGHT_FD_WRITE + | WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | WASI_RIGHT_FD_FILESTAT_GET + | WASI_RIGHT_POLL_FD_READWRITE; + +/// Default rights for descriptors created by Linux-style kernel operations. +/// Preview1 `path_open` replaces these with the guest's explicit request after +/// validating it against the parent capability and permission tier. +pub fn wasi_rights_for_open(flags: u32, filetype: u8) -> (u64, u64) { + let access = flags & (O_WRONLY | O_RDWR); + let readable = access != O_WRONLY; + let writable = access != O_RDONLY; + let seekable = matches!(filetype, FILETYPE_REGULAR_FILE | FILETYPE_DIRECTORY); + + let mut base = + WASI_RIGHT_FD_FDSTAT_SET_FLAGS | WASI_RIGHT_FD_FILESTAT_GET | WASI_RIGHT_POLL_FD_READWRITE; + if readable { + base |= WASI_RIGHT_FD_READ; + } + if writable { + base |= WASI_RIGHT_FD_WRITE | WASI_RIGHT_FD_DATASYNC | WASI_RIGHT_FD_SYNC; + } + if seekable { + base |= WASI_RIGHT_FD_SEEK | WASI_RIGHT_FD_TELL | WASI_RIGHT_FD_ADVISE; + } + if filetype == FILETYPE_REGULAR_FILE && writable { + base |= WASI_RIGHT_FD_ALLOCATE + | WASI_RIGHT_FD_FILESTAT_SET_SIZE + | WASI_RIGHT_FD_FILESTAT_SET_TIMES; + } + // AgentOS' Linux extensions use Preview1's closest metadata-mutation + // capability for fchmod/fchown. Anonymous pipes and sockets own mutable + // inode metadata even though neither resource is seekable and a pipe's + // read end is O_RDONLY. Grant the synthesized capability at creation; + // Preview1 path_open still replaces synthesized rights with the guest's + // exact explicit request. + if matches!( + filetype, + FILETYPE_PIPE | FILETYPE_SOCKET_DGRAM | FILETYPE_SOCKET_STREAM + ) { + base |= WASI_RIGHT_FD_FILESTAT_SET_TIMES; + } + if filetype == FILETYPE_DIRECTORY { + base |= WASI_RIGHT_FD_READDIR + | WASI_RIGHT_PATH_OPEN + | WASI_RIGHT_PATH_READLINK + | WASI_RIGHT_PATH_FILESTAT_GET; + } + (base, 0) +} + pub type FdResult = Result; pub type SharedFileDescription = Arc; @@ -96,13 +223,24 @@ impl AnonymousFileUsage { pub struct AnonymousFile { pub data: Vec, pub stat: VirtualStat, + pub xattrs: BTreeMap>, usage: Arc, } impl AnonymousFile { - pub fn new(data: Vec, stat: VirtualStat, usage: Arc) -> Self { + pub fn new( + data: Vec, + stat: VirtualStat, + xattrs: BTreeMap>, + usage: Arc, + ) -> Self { usage.add_file(stat.size); - Self { data, stat, usage } + Self { + data, + stat, + xattrs, + usage, + } } } @@ -128,6 +266,7 @@ enum FileBacking { DetachedDirectory { former_path: String, stat: VirtualStat, + xattrs: BTreeMap>, }, } @@ -138,6 +277,13 @@ pub struct FdTableError { } impl FdTableError { + fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + pub fn code(&self) -> &'static str { self.code } @@ -163,6 +309,20 @@ impl FdTableError { } } + fn already_exists(message: impl Into) -> Self { + Self { + code: "EEXIST", + message: message.into(), + } + } + + fn no_data(message: impl Into) -> Self { + Self { + code: "ENODATA", + message: message.into(), + } + } + fn invalid_argument(message: impl Into) -> Self { Self { code: "EINVAL", @@ -193,6 +353,60 @@ impl fmt::Display for FdTableError { impl Error for FdTableError {} +fn set_detached_xattr( + xattrs: &mut BTreeMap>, + name: &str, + value: Vec, + flags: u32, +) -> FdResult<()> { + validate_detached_xattr_name(name)?; + if value.len() > 64 * 1024 { + return Err(FdTableError::new( + "E2BIG", + format!( + "extended attribute value is {} bytes; limit is 65536", + value.len() + ), + )); + } + if flags & !3 != 0 || flags == 3 { + return Err(FdTableError::invalid_argument(format!( + "invalid xattr flags {flags}" + ))); + } + let exists = xattrs.contains_key(name); + if flags == 1 && exists { + return Err(FdTableError::already_exists(format!( + "xattr {name} already exists" + ))); + } + if flags == 2 && !exists { + return Err(FdTableError::no_data(format!( + "xattr {name} does not exist" + ))); + } + xattrs.insert(name.to_owned(), value); + Ok(()) +} + +fn remove_detached_xattr(xattrs: &mut BTreeMap>, name: &str) -> FdResult<()> { + validate_detached_xattr_name(name)?; + xattrs + .remove(name) + .map(|_| ()) + .ok_or_else(|| FdTableError::no_data(format!("xattr {name} does not exist"))) +} + +fn validate_detached_xattr_name(name: &str) -> FdResult<()> { + if name.is_empty() || name.len() > 255 || !name.contains('.') || name.contains('\0') { + return Err(FdTableError::new( + "ERANGE", + format!("invalid extended attribute name: {name:?}"), + )); + } + Ok(()) +} + #[derive(Debug)] pub struct FileDescription { id: u64, @@ -323,13 +537,22 @@ impl FileDescription { true } - pub fn detach_directory(&self, _expected: &str, stat: VirtualStat) -> bool { + pub fn detach_directory( + &self, + _expected: &str, + stat: VirtualStat, + xattrs: BTreeMap>, + ) -> bool { let mut backing = lock_or_recover(&self.backing); let FileBacking::Path(former_path) = &*backing else { return false; }; let former_path = former_path.clone(); - *backing = FileBacking::DetachedDirectory { former_path, stat }; + *backing = FileBacking::DetachedDirectory { + former_path, + stat, + xattrs, + }; true } @@ -468,6 +691,55 @@ impl FileDescription { } } + pub fn detached_xattrs(&self) -> Option>> { + match &*lock_or_recover(&self.backing) { + FileBacking::Anonymous { file, .. } => Some(lock_or_recover(file).xattrs.clone()), + FileBacking::DetachedDirectory { xattrs, .. } => Some(xattrs.clone()), + FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => None, + } + } + + pub fn detached_get_xattr(&self, name: &str) -> Option>> { + let xattrs = self.detached_xattrs()?; + Some( + xattrs + .get(name) + .cloned() + .ok_or_else(|| FdTableError::no_data(format!("xattr {name} does not exist"))), + ) + } + + pub fn detached_set_xattr( + &self, + name: &str, + value: Vec, + flags: u32, + ) -> Option> { + let mut backing = lock_or_recover(&self.backing); + let xattrs = match &mut *backing { + FileBacking::Anonymous { file, .. } => { + let mut file = lock_or_recover(file); + return Some(set_detached_xattr(&mut file.xattrs, name, value, flags)); + } + FileBacking::DetachedDirectory { xattrs, .. } => xattrs, + FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => return None, + }; + Some(set_detached_xattr(xattrs, name, value, flags)) + } + + pub fn detached_remove_xattr(&self, name: &str) -> Option> { + let mut backing = lock_or_recover(&self.backing); + let xattrs = match &mut *backing { + FileBacking::Anonymous { file, .. } => { + let mut file = lock_or_recover(file); + return Some(remove_detached_xattr(&mut file.xattrs, name)); + } + FileBacking::DetachedDirectory { xattrs, .. } => xattrs, + FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => return None, + }; + Some(remove_detached_xattr(xattrs, name)) + } + pub fn lock_target(&self) -> Option { self.lock_target } @@ -525,18 +797,53 @@ impl FileDescription { pub struct FdEntry { pub fd: u32, pub description: SharedFileDescription, - pub status_flags: u32, + pub status_flags: SharedStatusFlags, pub fd_flags: u32, pub rights: u64, + pub rights_inheriting: u64, pub filetype: u8, + pub wasi_preopen_path: Option, +} + +/// Mutable open-file status shared by every descriptor alias of the same +/// open file description. Linux shares `O_NONBLOCK` across `dup`, `fork`, and +/// SCM_RIGHTS while keeping descriptor flags such as `FD_CLOEXEC` per slot. +#[derive(Debug, Clone)] +pub struct SharedStatusFlags(Arc); + +impl SharedStatusFlags { + fn new(flags: u32) -> Self { + Self(Arc::new(AtomicU32::new(flags & ENTRY_STATUS_FLAG_MASK))) + } + + pub fn get(&self) -> u32 { + self.0.load(Ordering::SeqCst) + } + + fn set(&self, flags: u32) { + self.0 + .store(flags & ENTRY_STATUS_FLAG_MASK, Ordering::SeqCst); + } } +impl PartialEq for SharedStatusFlags { + fn eq(&self, other: &Self) -> bool { + self.get() == other.get() + } +} + +impl Eq for SharedStatusFlags {} + #[derive(Debug)] pub struct TransferredFd { description: SharedFileDescription, - status_flags: u32, + status_flags: SharedStatusFlags, rights: u64, + rights_inheriting: u64, filetype: u8, + // Descriptor-local capability metadata is captured for trusted spawn + // staging, but ordinary SCM_RIGHTS installation deliberately drops it. + wasi_preopen_path: Option, } impl Clone for TransferredFd { @@ -544,9 +851,11 @@ impl Clone for TransferredFd { self.description.increment_ref_count(); Self { description: Arc::clone(&self.description), - status_flags: self.status_flags, + status_flags: self.status_flags.clone(), rights: self.rights, + rights_inheriting: self.rights_inheriting, filetype: self.filetype, + wasi_preopen_path: self.wasi_preopen_path.clone(), } } } @@ -556,7 +865,9 @@ impl PartialEq for TransferredFd { self.description.id() == other.description.id() && self.status_flags == other.status_flags && self.rights == other.rights + && self.rights_inheriting == other.rights_inheriting && self.filetype == other.filetype + && self.wasi_preopen_path == other.wasi_preopen_path } } @@ -577,24 +888,37 @@ impl TransferredFd { self.description.id() } + /// Number of live kernel fd-table/transfer references to this canonical + /// open description. A sidecar registry may retain one dedicated transfer + /// lease and retire external resources when only that lease remains. + pub fn ref_count(&self) -> usize { + self.description.ref_count() + } + pub fn status_flags(&self) -> u32 { - self.status_flags + self.status_flags.get() } pub fn rights(&self) -> u64 { self.rights } + pub fn rights_inheriting(&self) -> u64 { + self.rights_inheriting + } + pub fn filetype(&self) -> u8 { self.filetype } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FdStat { pub filetype: u8, pub flags: u32, pub rights: u64, + pub rights_inheriting: u64, + pub wasi_preopen_path: Option, } #[derive(Debug, Clone)] @@ -723,6 +1047,7 @@ pub struct ProcessFdTable { next_fd: u32, alloc_desc: DescriptionFactory, max_fds: usize, + wasi_preopens_initialized: bool, } impl ProcessFdTable { @@ -732,6 +1057,7 @@ impl ProcessFdTable { next_fd: 3, alloc_desc, max_fds, + wasi_preopens_initialized: false, } } @@ -739,6 +1065,16 @@ impl ProcessFdTable { self.max_fds } + /// Change the per-process allocation ceiling without closing descriptors. + /// Linux permits lowering RLIMIT_NOFILE below the current open count; new + /// allocations fail until the table falls back below the limit. + pub fn set_max_fds(&mut self, max_fds: usize) { + self.max_fds = max_fds; + if self.next_fd as usize >= max_fds { + self.next_fd = 0; + } + } + pub fn available_fd_capacity(&self) -> usize { self.max_fds.saturating_sub(self.entries.len()) } @@ -754,10 +1090,12 @@ impl ProcessFdTable { FdEntry { fd: 0, description: stdin_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_READ_RIGHTS, + rights_inheriting: 0, filetype: FILETYPE_CHARACTER_DEVICE, + wasi_preopen_path: None, }, ); self.entries.insert( @@ -765,10 +1103,12 @@ impl ProcessFdTable { FdEntry { fd: 1, description: stdout_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_WRITE_RIGHTS, + rights_inheriting: 0, filetype: FILETYPE_CHARACTER_DEVICE, + wasi_preopen_path: None, }, ); self.entries.insert( @@ -776,10 +1116,12 @@ impl ProcessFdTable { FdEntry { fd: 2, description: stderr_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_WRITE_RIGHTS, + rights_inheriting: 0, filetype: FILETYPE_CHARACTER_DEVICE, + wasi_preopen_path: None, }, ); } @@ -801,10 +1143,12 @@ impl ProcessFdTable { FdEntry { fd: 0, description: stdin_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_READ_RIGHTS, + rights_inheriting: 0, filetype: stdin_type, + wasi_preopen_path: None, }, ); self.entries.insert( @@ -812,10 +1156,12 @@ impl ProcessFdTable { FdEntry { fd: 1, description: stdout_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_WRITE_RIGHTS, + rights_inheriting: 0, filetype: stdout_type, + wasi_preopen_path: None, }, ); self.entries.insert( @@ -823,10 +1169,12 @@ impl ProcessFdTable { FdEntry { fd: 2, description: stderr_desc, - status_flags: 0, + status_flags: SharedStatusFlags::new(0), fd_flags: 0, - rights: 0, + rights: WASI_STDIO_WRITE_RIGHTS, + rights_inheriting: 0, filetype: stderr_type, + wasi_preopen_path: None, }, ); } @@ -850,15 +1198,18 @@ impl ProcessFdTable { let description = self.alloc_desc .allocate_with_lock(path, description_flags(flags), lock_target); + let (rights, rights_inheriting) = wasi_rights_for_open(flags, filetype); self.entries.insert( fd, FdEntry { fd, description, - status_flags: status_flags(flags), + status_flags: SharedStatusFlags::new(status_flags(flags)), fd_flags: 0, - rights: 0, + rights, + rights_inheriting, filetype, + wasi_preopen_path: None, }, ); Ok(fd) @@ -876,21 +1227,26 @@ impl ProcessFdTable { self.validate_fd_bounds(fd)?; if self.entries.contains_key(&fd) { self.close(fd); + } else if self.entries.len() >= self.max_fds { + return Err(FdTableError::too_many_open_files()); } fd } None => self.allocate_fd()?, }; description.increment_ref_count(); + let (rights, rights_inheriting) = wasi_rights_for_open(description.flags(), filetype); self.entries.insert( fd, FdEntry { fd, description, - status_flags: entry_status_flags, + status_flags: SharedStatusFlags::new(entry_status_flags), fd_flags: 0, - rights: 0, + rights, + rights_inheriting, filetype, + wasi_preopen_path: None, }, ); Ok(fd) @@ -917,6 +1273,7 @@ impl ProcessFdTable { }; let first = self.alloc_desc.allocate(first_path, O_RDWR); let second = self.alloc_desc.allocate(second_path, O_RDWR); + let (rights, rights_inheriting) = wasi_rights_for_open(O_RDWR, filetype); for (fd, description) in [ (first_fd, Arc::clone(&first)), (second_fd, Arc::clone(&second)), @@ -926,10 +1283,12 @@ impl ProcessFdTable { FdEntry { fd, description, - status_flags: status_flags & ENTRY_STATUS_FLAG_MASK, + status_flags: SharedStatusFlags::new(status_flags), fd_flags: fd_flags & FD_CLOEXEC, - rights: 0, + rights, + rights_inheriting, filetype, + wasi_preopen_path: None, }, ); } @@ -944,9 +1303,11 @@ impl ProcessFdTable { entry.description.increment_ref_count(); Ok(TransferredFd { description: Arc::clone(&entry.description), - status_flags: entry.status_flags, + status_flags: entry.status_flags.clone(), rights: entry.rights, + rights_inheriting: entry.rights_inheriting, filetype: entry.filetype, + wasi_preopen_path: entry.wasi_preopen_path.clone(), }) } @@ -956,11 +1317,14 @@ impl ProcessFdTable { /// sending process. Keeping this separate from `open_with_details` avoids /// spuriously returning EMFILE when the sender's descriptor table is full. pub fn create_transfer(&self, path: &str, flags: u32, filetype: u8) -> TransferredFd { + let (rights, rights_inheriting) = wasi_rights_for_open(flags, filetype); TransferredFd { description: self.alloc_desc.allocate(path, description_flags(flags)), - status_flags: status_flags(flags), - rights: 0, + status_flags: SharedStatusFlags::new(status_flags(flags)), + rights, + rights_inheriting, filetype, + wasi_preopen_path: None, } } @@ -990,10 +1354,12 @@ impl ProcessFdTable { FdEntry { fd, description: Arc::clone(&transfer.description), - status_flags: transfer.status_flags, + status_flags: transfer.status_flags.clone(), fd_flags: if close_on_exec { FD_CLOEXEC } else { 0 }, rights: transfer.rights, + rights_inheriting: transfer.rights_inheriting, filetype: transfer.filetype, + wasi_preopen_path: None, }, ); } @@ -1009,10 +1375,34 @@ impl ProcessFdTable { transfer: &TransferredFd, fd: u32, fd_flags: u32, + ) -> FdResult<()> { + self.install_transferred_at_with_preopen(transfer, fd, fd_flags, false) + } + + /// Restore a descriptor captured during trusted fork/exec staging. Unlike + /// SCM_RIGHTS receipt, this retains the kernel-owned WASI capability-root + /// marker attached to the inherited descriptor slot. + pub(crate) fn install_spawn_transferred_at( + &mut self, + transfer: &TransferredFd, + fd: u32, + fd_flags: u32, + ) -> FdResult<()> { + self.install_transferred_at_with_preopen(transfer, fd, fd_flags, true) + } + + fn install_transferred_at_with_preopen( + &mut self, + transfer: &TransferredFd, + fd: u32, + fd_flags: u32, + preserve_wasi_preopen: bool, ) -> FdResult<()> { self.validate_fd_bounds(fd)?; if self.entries.contains_key(&fd) { self.close(fd); + } else if self.entries.len() >= self.max_fds { + return Err(FdTableError::too_many_open_files()); } transfer.description.increment_ref_count(); self.entries.insert( @@ -1020,10 +1410,14 @@ impl ProcessFdTable { FdEntry { fd, description: Arc::clone(&transfer.description), - status_flags: transfer.status_flags, + status_flags: transfer.status_flags.clone(), fd_flags: fd_flags & FD_CLOEXEC, rights: transfer.rights, + rights_inheriting: transfer.rights_inheriting, filetype: transfer.filetype, + wasi_preopen_path: preserve_wasi_preopen + .then(|| transfer.wasi_preopen_path.clone()) + .flatten(), }, ); Ok(()) @@ -1037,6 +1431,86 @@ impl ProcessFdTable { self.entries.values() } + /// Mark the one-time installation of WASI capability roots for this + /// process image. Forked processes inherit this state so spawn file + /// actions that deliberately close a preopen cannot cause it to be + /// silently recreated later by an executor. + pub fn begin_wasi_preopen_initialization(&mut self) -> bool { + if self.wasi_preopens_initialized { + return false; + } + self.wasi_preopens_initialized = true; + true + } + + pub fn mark_wasi_preopen( + &mut self, + fd: u32, + guest_path: String, + rights_base: u64, + rights_inheriting: u64, + ) -> FdResult<()> { + let entry = self + .entries + .get_mut(&fd) + .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; + if entry.filetype != FILETYPE_DIRECTORY { + return Err(FdTableError::invalid_argument(format!( + "WASI preopen fd {fd} is not a directory" + ))); + } + entry.rights = rights_base; + entry.rights_inheriting = rights_inheriting; + entry.wasi_preopen_path = Some(guest_path); + Ok(()) + } + + pub fn set_rights( + &mut self, + fd: u32, + rights_base: u64, + rights_inheriting: u64, + ) -> FdResult<()> { + let entry = self + .entries + .get_mut(&fd) + .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; + entry.rights = rights_base; + entry.rights_inheriting = rights_inheriting; + Ok(()) + } + + /// Restrict inherited capability roots after a child tier drop or exec. + /// Isolated processes close inherited preopen descriptors entirely so + /// neither preopen enumeration nor fd snapshots reveal a hidden root. + pub fn restrict_wasi_preopens( + &mut self, + rights_base: Option, + rights_inheriting: Option, + ) { + if rights_base.is_none() || rights_inheriting.is_none() { + let preopen_fds = self + .entries + .values() + .filter_map(|entry| entry.wasi_preopen_path.as_ref().map(|_| entry.fd)) + .collect::>(); + for fd in preopen_fds { + let closed = self.close(fd); + debug_assert!(closed); + } + return; + } + let base = rights_base.expect("checked above"); + let inheriting = rights_inheriting.expect("checked above"); + for entry in self.entries.values_mut() { + if entry.wasi_preopen_path.is_none() { + continue; + } + entry.rights &= base; + entry.rights_inheriting &= inheriting; + } + } + pub fn close(&mut self, fd: u32) -> bool { let Some(entry) = self.entries.remove(&fd) else { return false; @@ -1074,7 +1548,9 @@ impl ProcessFdTable { self.duplicate_entry( &entry, new_fd, - status_flags_override.unwrap_or(entry.status_flags), + status_flags_override + .map(SharedStatusFlags::new) + .unwrap_or_else(|| entry.status_flags.clone()), 0, ) } @@ -1092,9 +1568,11 @@ impl ProcessFdTable { if self.entries.contains_key(&new_fd) { self.close(new_fd); + } else if self.entries.len() >= self.max_fds { + return Err(FdTableError::too_many_open_files()); } - self.duplicate_entry(&entry, new_fd, entry.status_flags, 0)?; + self.duplicate_entry(&entry, new_fd, entry.status_flags.clone(), 0)?; Ok(()) } @@ -1105,8 +1583,10 @@ impl ProcessFdTable { .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; Ok(FdStat { filetype: entry.filetype, - flags: visible_fd_flags(entry.description.flags(), entry.status_flags), + flags: visible_fd_flags(entry.description.flags(), entry.status_flags.get()), rights: entry.rights, + rights_inheriting: entry.rights_inheriting, + wasi_preopen_path: entry.wasi_preopen_path.clone(), }) } @@ -1120,7 +1600,7 @@ impl ProcessFdTable { .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; let min_fd = self.validate_fcntl_dup_min(arg)?; let new_fd = self.allocate_fd_from(min_fd)?; - self.duplicate_entry(&entry, new_fd, entry.status_flags, 0) + self.duplicate_entry(&entry, new_fd, entry.status_flags.clone(), 0) } F_GETFD => { let entry = self @@ -1144,7 +1624,7 @@ impl ProcessFdTable { .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; Ok(visible_fd_flags( entry.description.flags(), - entry.status_flags, + entry.status_flags.get(), )) } F_SETFL => { @@ -1152,7 +1632,7 @@ impl ProcessFdTable { .entries .get_mut(&fd) .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; - entry.status_flags = arg & ENTRY_STATUS_FLAG_MASK; + entry.status_flags.set(arg); entry.description.update_flags(SHARED_STATUS_FLAG_MASK, arg); Ok(0) } @@ -1178,6 +1658,7 @@ impl ProcessFdTable { fn fork_with_cloexec(&self, preserve_cloexec: bool) -> Self { let mut child = Self::new(self.alloc_desc.clone(), self.max_fds); child.next_fd = self.next_fd; + child.wasi_preopens_initialized = self.wasi_preopens_initialized; for (fd, entry) in &self.entries { // Kernel process creation is spawn (fork + exec combined), so @@ -1195,10 +1676,12 @@ impl ProcessFdTable { FdEntry { fd: *fd, description: Arc::clone(&entry.description), - status_flags: entry.status_flags, + status_flags: entry.status_flags.clone(), fd_flags: entry.fd_flags, rights: entry.rights, + rights_inheriting: entry.rights_inheriting, filetype: entry.filetype, + wasi_preopen_path: entry.wasi_preopen_path.clone(), }, ); } @@ -1274,7 +1757,7 @@ impl ProcessFdTable { &mut self, entry: &FdEntry, new_fd: u32, - status_flags: u32, + status_flags: SharedStatusFlags, fd_flags: u32, ) -> FdResult { entry.description.increment_ref_count(); @@ -1286,7 +1769,9 @@ impl ProcessFdTable { status_flags, fd_flags, rights: entry.rights, + rights_inheriting: entry.rights_inheriting, filetype: entry.filetype, + wasi_preopen_path: entry.wasi_preopen_path.clone(), }, ); Ok(new_fd) @@ -1940,3 +2425,108 @@ fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexG .wait(guard) .unwrap_or_else(|poisoned| poisoned.into_inner()) } + +#[cfg(test)] +mod wasi_preopen_tests { + use super::*; + + #[test] + fn kernel_preopen_metadata_follows_descriptor_lifecycle() { + let mut manager = FdTableManager::new(); + let table = manager.create(7); + assert!(table.begin_wasi_preopen_initialization()); + assert!(!table.begin_wasi_preopen_initialization()); + + let fd = table + .open_with_details("/workspace", O_DIRECTORY, FILETYPE_DIRECTORY, None) + .expect("open preopen directory"); + table + .mark_wasi_preopen( + fd, + String::from("/workspace"), + WASI_PREOPEN_READ_RIGHTS_BASE, + WASI_PREOPEN_READ_RIGHTS_INHERITING, + ) + .expect("mark preopen"); + + let stat = table.stat(fd).expect("stat preopen"); + assert_eq!(stat.rights, WASI_PREOPEN_READ_RIGHTS_BASE); + assert_eq!(stat.rights_inheriting, WASI_PREOPEN_READ_RIGHTS_INHERITING); + assert_eq!(stat.wasi_preopen_path.as_deref(), Some("/workspace")); + + let duplicate = table.dup(fd).expect("duplicate preopen"); + assert_eq!( + table + .stat(duplicate) + .expect("stat duplicate") + .wasi_preopen_path + .as_deref(), + Some("/workspace") + ); + assert!(table.close(fd)); + assert!(table.stat(fd).is_err()); + } + + #[test] + fn descriptor_rights_survive_dup_fork_and_transfer_exactly() { + let mut manager = FdTableManager::new(); + let table = manager.create(11); + let fd = table.open("/file", O_RDWR).expect("open descriptor"); + let base = WASI_RIGHT_FD_READ | WASI_RIGHT_FD_FILESTAT_GET; + table.set_rights(fd, base, 0).expect("set exact rights"); + + let duplicate = table.dup(fd).expect("duplicate descriptor"); + assert_eq!(table.stat(duplicate).expect("dup stat").rights, base); + + let child = table.fork(); + assert_eq!(child.stat(fd).expect("fork stat").rights, base); + + let transfer = table.transfer(fd).expect("transfer descriptor"); + let receiver = manager.create(12); + let installed = receiver + .install_transferred(&[transfer], false) + .expect("install transfer"); + assert_eq!( + receiver.stat(installed[0]).expect("transfer stat").rights, + base + ); + + let socket = receiver + .open_with_details("socket:test", O_RDWR, FILETYPE_SOCKET_STREAM, None) + .expect("open socket descriptor"); + let socket_default = receiver.stat(socket).expect("socket stat").rights; + assert_eq!( + socket_default & (WASI_RIGHT_FD_READ | WASI_RIGHT_FD_WRITE), + WASI_RIGHT_FD_READ | WASI_RIGHT_FD_WRITE + ); + receiver + .set_rights(socket, WASI_RIGHT_FD_READ, 0) + .expect("restrict socket rights"); + let socket_duplicate = receiver.dup(socket).expect("duplicate socket"); + assert_eq!( + receiver + .stat(socket_duplicate) + .expect("socket duplicate stat") + .rights, + WASI_RIGHT_FD_READ + ); + } + + #[test] + fn linux_pipe_and_socket_descriptors_synthesize_metadata_mutation_rights() { + for (flags, filetype) in [ + (O_RDONLY, FILETYPE_PIPE), + (O_WRONLY, FILETYPE_PIPE), + (O_RDWR, FILETYPE_SOCKET_STREAM), + (O_RDWR, FILETYPE_SOCKET_DGRAM), + ] { + let (rights, inheriting) = wasi_rights_for_open(flags, filetype); + assert_ne!( + rights & WASI_RIGHT_FD_FILESTAT_SET_TIMES, + 0, + "Linux descriptor type {filetype} must permit fchmod/fchown" + ); + assert_eq!(inheriting, 0); + } + } +} diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 793bad5cfe..2694658120 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -1,18 +1,28 @@ use crate::bridge::LifecycleState; -use crate::command_registry::{CommandDriver, CommandRegistry}; +use crate::command_registry::{CommandDriver, CommandRegistry, COMMAND_STUB}; use crate::device_layer::{create_device_layer, DeviceLayer}; use crate::dns::{ - format_dns_resource, resolve_dns, resolve_dns_records, DnsConfig, DnsLookupPolicy, - DnsRecordResolution, DnsResolution, DnsResolverErrorKind, HickoryDnsResolver, - SharedDnsResolver, + format_dns_resource, resolve_dns, resolve_dns_async, resolve_dns_records, + resolve_dns_records_async, DnsConfig, DnsLookupPolicy, DnsRecordResolution, DnsResolution, + DnsResolverErrorKind, SharedDnsResolver, UnavailableDnsResolver, }; +#[cfg(test)] +use crate::fd_table::WASI_RIGHT_FD_FILESTAT_GET; use crate::fd_table::{ AnonymousFile, AnonymousFileUsage, FdEntry, FdStat, FdTableError, FdTableManager, FileDescription, FileLockManager, FileLockTarget, FlockOperation, ProcessFdTable, RecordLock, RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SOCKET_DGRAM, - FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, O_APPEND, O_CREAT, O_DIRECT, + FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, F_SETFD, O_APPEND, O_CREAT, O_DIRECT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, + WASI_PREOPEN_READ_RIGHTS_BASE, WASI_PREOPEN_READ_RIGHTS_INHERITING, + WASI_PREOPEN_READ_WRITE_RIGHTS_BASE, WASI_PREOPEN_READ_WRITE_RIGHTS_INHERITING, + WASI_PREOPEN_WRITE_RIGHTS_BASE, WASI_PREOPEN_WRITE_RIGHTS_INHERITING, WASI_RIGHT_FD_ALLOCATE, + WASI_RIGHT_FD_DATASYNC, WASI_RIGHT_FD_FILESTAT_SET_SIZE, WASI_RIGHT_FD_FILESTAT_SET_TIMES, + WASI_RIGHT_FD_READ, WASI_RIGHT_FD_SYNC, WASI_RIGHT_FD_WRITE, WASI_RIGHT_PATH_LINK_SOURCE, + WASI_RIGHT_PATH_LINK_TARGET, WASI_RIGHT_PATH_OPEN, WASI_RIGHT_PATH_REMOVE_DIRECTORY, + WASI_RIGHT_PATH_RENAME_SOURCE, WASI_RIGHT_PATH_RENAME_TARGET, WASI_RIGHT_PATH_SYMLINK, + WASI_RIGHT_PATH_UNLINK_FILE, }; use crate::mount_table::{MountEntry, MountOptions, MountTable, MountedFileSystem}; use crate::network_policy::format_tcp_resource; @@ -23,12 +33,16 @@ use crate::permissions::{ use crate::pipe_manager::{PipeError, PipeManager}; use crate::poll::{ PollEvents, PollFd, PollNotifier, PollResult, PollTarget, PollTargetEntry, PollTargetResult, - POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, + POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLRDNORM, POLLWRNORM, +}; +use crate::process_runtime::{ + ProcessControlWake, ProcessExit, ProcessExitReporter, ProcessRuntimeEndpoint, + ProcessRuntimeFault, ProcessRuntimeIdentity, RuntimeControlCell, RuntimeControlReceiver, }; use crate::process_table::{ - DriverProcess, ProcessContext, ProcessExitCallback, ProcessInfo, ProcessStatus, ProcessTable, - ProcessTableError, ProcessWaitResult, SigmaskHow, SignalSet, DEFAULT_PROCESS_UMASK, SIGCONT, - SIGPIPE, SIGSTOP, SIGTSTP, SIGWINCH, + ProcessContext, ProcessInfo, ProcessPermissionTier, ProcessStatus, ProcessTable, + ProcessTableError, ProcessWaitResult, SigmaskHow, SignalAction, SignalDelivery, SignalSet, + DEFAULT_PROCESS_UMASK, SIGPIPE, SIGWINCH, }; use crate::pty::{ LineDisciplineConfig, PartialTermios, PtyError, PtyManager, PtyWindowSize, Termios, @@ -45,7 +59,11 @@ use crate::socket_table::{ SocketMulticastMembership, SocketReadiness, SocketRecord, SocketShutdown, SocketSpec, SocketState, SocketTable, SocketTableError, SocketType, TransferredSocketRight, }; -use crate::user::{ProcessIdentity, UserConfig, UserManager}; +use crate::system::{realtime_now_ns, KernelClockId, SystemIdentity}; +use crate::user::{ + group_record_at, group_record_by_gid, group_record_by_name, passwd_record_at, + passwd_record_by_name, passwd_record_by_uid, ProcessIdentity, UserConfig, UserManager, +}; use crate::vfs::{ normalize_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualTimeSpec, VirtualUtimeSpec, MAX_PATH_LENGTH, RENAME_EXCHANGE, S_IFDIR, S_IFLNK, S_IFREG, @@ -56,13 +74,17 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; #[cfg(test)] +use std::sync::Condvar; +#[cfg(test)] use std::sync::OnceLock; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult}; +use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; use web_time::{Instant, SystemTime, UNIX_EPOCH}; pub type KernelResult = Result; -pub use crate::process_table::{ProcessWaitEvent as WaitPidEvent, WaitPidFlags}; +pub use crate::process_table::{ + ProcessResourceLimit, ProcessResourceLimitKind, ProcessWaitEvent as WaitPidEvent, WaitPidFlags, +}; pub const SEEK_SET: u8 = 0; pub const SEEK_CUR: u8 = 1; @@ -81,11 +103,50 @@ pub struct KernelError { message: String, } +/// A runtime image read by the trusted executor loader after its launch +/// authority has been established. This bypasses guest `fs.read` policy only; +/// guest process launches must use [`KernelVm::load_process_runtime_image`], +/// which enforces pathname traversal and execute DAC first. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeLaunchImage { + pub canonical_path: String, + pub bytes: Vec, + pub mode: u32, +} + +/// Bounded prefix used to classify a runtime image before the engine-specific +/// full-image loader applies its own named size limit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeLaunchImagePrefix { + pub canonical_path: String, + pub bytes: Vec, +} + +/// Exact userspace argv/env image committed in the process table. The kernel +/// validates aggregate payload limits before returning this owned snapshot; +/// executor adapters may then apply their own bounded wire representation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KernelProcessImage { + pub argv: Vec, + pub env: Vec<(String, String)>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KernelFileExtent { + pub start: u64, + pub end: u64, + pub unwritten: bool, +} + impl KernelError { pub fn code(&self) -> &'static str { self.code } + pub fn message(&self) -> &str { + &self.message + } + fn new(code: &'static str, message: impl Into) -> Self { Self { code, @@ -169,6 +230,7 @@ fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult, pub cwd: String, pub user: UserConfig, @@ -178,21 +240,24 @@ pub struct KernelVmConfig { pub dns_resolver: SharedDnsResolver, pub resources: ResourceLimits, pub zombie_ttl: Duration, + pub system_identity: SystemIdentity, } impl KernelVmConfig { pub fn new(vm_id: impl Into) -> Self { Self { vm_id: vm_id.into(), + vm_generation: 0, env: BTreeMap::new(), cwd: String::from("/workspace"), user: UserConfig::default(), permissions: Permissions::default(), loopback_exempt_ports: BTreeSet::new(), dns: DnsConfig::default(), - dns_resolver: Arc::new(HickoryDnsResolver::default()), + dns_resolver: Arc::new(UnavailableDnsResolver), resources: ResourceLimits::default(), zombie_ttl: Duration::from_secs(60), + system_identity: SystemIdentity::default(), } } } @@ -203,6 +268,9 @@ pub struct SpawnOptions { pub parent_pid: Option, pub env: BTreeMap, pub cwd: Option, + /// Trusted process-creation ceiling. A child can only retain or reduce its + /// parent's kernel-owned authority; omitting this field inherits exactly. + pub permission_tier: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -210,6 +278,8 @@ pub struct VirtualProcessOptions { pub parent_pid: Option, pub env: BTreeMap, pub cwd: Option, + /// Trusted virtual-process ceiling. Omission inherits the parent's tier. + pub permission_tier: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -250,6 +320,14 @@ pub struct WaitPidEventResult { pub event: WaitPidEvent, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WaitPidDetailedResult { + pub pid: u32, + pub status: i32, + pub event: WaitPidEvent, + pub termination: Option, +} + #[derive(Debug)] pub struct ReceivedFdMessage { pub payload: Vec, @@ -297,14 +375,61 @@ impl fmt::Debug for ReceivedFdRight { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProcessFdSnapshotEntry { pub fd: u32, + pub description_id: u64, pub fd_flags: u32, pub status_flags: u32, pub filetype: u8, + pub rights_base: u64, + pub rights_inheriting: u64, pub is_socket: bool, pub is_pipe: bool, pub is_pty: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessWasiPreopen { + pub fd: u32, + pub guest_path: String, + pub rights_base: u64, + pub rights_inheriting: u64, +} + +fn wasi_preopen_rights(tier: ProcessPermissionTier, mount_writable: bool) -> Option<(u64, u64)> { + match tier { + ProcessPermissionTier::Isolated => None, + ProcessPermissionTier::ReadOnly => Some(( + WASI_PREOPEN_READ_RIGHTS_BASE, + WASI_PREOPEN_READ_RIGHTS_INHERITING, + )), + ProcessPermissionTier::ReadWrite if mount_writable => Some(( + WASI_PREOPEN_READ_WRITE_RIGHTS_BASE, + WASI_PREOPEN_READ_WRITE_RIGHTS_INHERITING, + )), + ProcessPermissionTier::Full if mount_writable => Some(( + WASI_PREOPEN_WRITE_RIGHTS_BASE, + WASI_PREOPEN_WRITE_RIGHTS_INHERITING, + )), + ProcessPermissionTier::ReadWrite | ProcessPermissionTier::Full => Some(( + WASI_PREOPEN_READ_RIGHTS_BASE, + WASI_PREOPEN_READ_RIGHTS_INHERITING, + )), + } +} + +const WASI_WRITE_RIGHTS: u64 = WASI_RIGHT_FD_DATASYNC + | WASI_RIGHT_FD_SYNC + | WASI_RIGHT_FD_WRITE + | WASI_RIGHT_FD_ALLOCATE + | WASI_RIGHT_FD_FILESTAT_SET_SIZE + | WASI_RIGHT_FD_FILESTAT_SET_TIMES; +const WASI_NAMESPACE_DESTRUCTIVE_RIGHTS: u64 = WASI_RIGHT_PATH_LINK_SOURCE + | WASI_RIGHT_PATH_LINK_TARGET + | WASI_RIGHT_PATH_RENAME_SOURCE + | WASI_RIGHT_PATH_RENAME_TARGET + | WASI_RIGHT_PATH_SYMLINK + | WASI_RIGHT_PATH_REMOVE_DIRECTORY + | WASI_RIGHT_PATH_UNLINK_FILE; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProcessFdDirEntry { pub name: String, @@ -369,7 +494,9 @@ struct ShebangCommand { pub struct KernelProcessHandle { pid: u32, driver: String, - process: Arc, + processes: ProcessTable, + runtime_control: RuntimeControlCell, + exit_reporter: ProcessExitReporter, } impl fmt::Debug for KernelProcessHandle { @@ -391,19 +518,105 @@ impl KernelProcessHandle { } pub fn finish(&self, exit_code: i32) { - self.process.finish(exit_code); + if let Err(error) = self + .exit_reporter + .report_exit(ProcessExit::Exited(exit_code)) + { + eprintln!("failed to report exit for kernel pid {}: {error}", self.pid); + } + } + + pub fn finish_signaled(&self, signal: i32, core_dumped: bool) { + if let Err(error) = self.exit_reporter.report_exit(ProcessExit::Signaled { + signal, + core_dumped, + }) { + eprintln!( + "failed to report signal exit for kernel pid {}: {error}", + self.pid + ); + } + } + + pub fn finish_runtime_fault(&self, fault: ProcessRuntimeFault) { + if let Err(error) = self.exit_reporter.report_runtime_fault(fault) { + eprintln!( + "failed to report runtime fault for kernel pid {}: {error}", + self.pid + ); + } + } + + pub fn exit_reporter(&self) -> ProcessExitReporter { + self.exit_reporter.clone() } pub fn kill(&self, signal: i32) { - self.process.kill(signal); + if let Err(error) = self.processes.kill(self.pid as i32, signal) { + eprintln!("failed to signal kernel pid {}: {error}", self.pid); + } } pub fn wait(&self, timeout: Duration) -> Option { - self.process.wait(timeout) + self.processes + .wait_for_exit(self.pid, timeout) + .ok() + .flatten() + .map(ProcessExit::shell_status) + } + + pub fn attach_runtime_control( + &self, + wake: ProcessControlWake, + ) -> Result { + self.runtime_control.attach(wake) + } + + pub fn runtime_identity(&self) -> ProcessRuntimeIdentity { + self.runtime_control + .identity() + .expect("a kernel process handle is created after endpoint binding") + } + + pub fn signal_action( + &self, + signal: i32, + action: Option, + ) -> KernelResult { + Ok(self.processes.signal_action(self.pid, signal, action)?) + } + + pub fn begin_signal_delivery(&self) -> KernelResult> { + Ok(self.processes.begin_signal_delivery(self.pid)?) + } + + pub fn end_signal_delivery(&self, token: u64) -> KernelResult<()> { + Ok(self.processes.end_signal_delivery(self.pid, token)?) } - pub fn kill_signals(&self) -> Vec { - self.process.kill_signals() + pub fn sigprocmask(&self, how: SigmaskHow, set: SignalSet) -> KernelResult { + Ok(self.processes.sigprocmask(self.pid, how, set)?) + } + + pub fn sigpending(&self) -> KernelResult { + Ok(self.processes.sigpending(self.pid)?) + } + + pub fn begin_temporary_signal_mask(&self, mask: SignalSet) -> KernelResult { + Ok(self.processes.begin_temporary_signal_mask(self.pid, mask)?) + } + + pub fn end_temporary_signal_mask(&self, token: u64) -> KernelResult<()> { + Ok(self.processes.end_temporary_signal_mask(self.pid, token)?) + } + + pub fn end_temporary_signal_mask_and_begin_signal_delivery( + &self, + token: u64, + ) -> KernelResult> { + Ok(self + .processes + .end_temporary_signal_mask_and_begin_signal_delivery(self.pid, token)?) } } @@ -439,8 +652,10 @@ impl OpenShellHandle { pub struct KernelVm { vm_id: String, + vm_generation: u64, boot_time_ms: u64, boot_instant: Instant, + system_identity: SystemIdentity, filesystem: PermissionedFileSystem>, permissions: Permissions, loopback_exempt_ports: BTreeSet, @@ -590,7 +805,7 @@ fn close_special_resource_if_needed( sockets: &SocketTable, fd_sockets: &FdSocketRegistry, description: &Arc, - filetype: u8, + _filetype: u8, ) { if description.ref_count() != 0 { return; @@ -598,7 +813,7 @@ fn close_special_resource_if_needed( file_locks.release_owner(description.id()); - if filetype == FILETYPE_PIPE && pipes.is_pipe(description.id()) { + if pipes.is_pipe(description.id()) { pipes.close(description.id()); } @@ -664,8 +879,10 @@ enum ProcNode { impl KernelVm { pub fn new(filesystem: F, config: KernelVmConfig) -> Self { let vm_id = config.vm_id; + let vm_generation = config.vm_generation; let boot_time_ms = now_ms(); let boot_instant = Instant::now(); + let system_identity = config.system_identity; let permissions = config.permissions.clone(); let users = UserManager::from_config(config.user); let process_table = ProcessTable::with_zombie_ttl(config.zombie_ttl); @@ -685,7 +902,14 @@ impl KernelVm { let pipes = PipeManager::with_notifier(poll_notifier.clone()); let ptys = PtyManager::with_signal_handler_and_notifier( Arc::new(move |pgid, signal| { - let _ = process_table_for_pty.kill(-(pgid as i32), signal); + if let Err(error) = process_table_for_pty.kill(-(pgid as i32), signal) { + if !pty_signal_error_is_stale(&error) { + eprintln!( + "[agentos] PTY foreground process-group signal delivery failed: pgid={pgid} signal={signal} code={} error={error}", + error.code() + ); + } + } }), poll_notifier.clone(), ); @@ -725,8 +949,10 @@ impl KernelVm { Self { vm_id: vm_id.clone(), + vm_generation, boot_time_ms, boot_instant, + system_identity, filesystem, permissions, loopback_exempt_ports: config.loopback_exempt_ports, @@ -802,10 +1028,72 @@ impl KernelVm { .identity) } + pub fn process_image( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + let entry = self + .processes + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + self.resources + .check_process_argv_bytes(&entry.command, &entry.args)?; + self.resources + .check_process_env_bytes(&BTreeMap::new(), &entry.env)?; + + let mut argv = Vec::with_capacity(entry.args.len().saturating_add(1)); + argv.push(entry.command); + argv.extend(entry.args); + Ok(KernelProcessImage { + argv, + env: entry.env.into_iter().collect(), + }) + } + pub fn user_profile(&self) -> UserManager { self.users.clone() } + pub fn system_identity(&self) -> &SystemIdentity { + &self.system_identity + } + + pub fn clock_time_ns( + &self, + clock: KernelClockId, + deterministic_realtime_ns: Option, + ) -> KernelResult { + match clock { + KernelClockId::Realtime => deterministic_realtime_ns + .or_else(realtime_now_ns) + .ok_or_else(|| { + KernelError::new("EOVERFLOW", "realtime clock exceeds u64 nanoseconds") + }), + KernelClockId::Monotonic => u64::try_from(self.boot_instant.elapsed().as_nanos()) + .map_err(|_| { + KernelError::new("EOVERFLOW", "monotonic clock exceeds u64 nanoseconds") + }), + KernelClockId::ProcessCpu | KernelClockId::ThreadCpu => Err(KernelError::new( + "ENOTSUP", + "virtual process CPU clocks are not implemented", + )), + } + } + + pub fn clock_resolution_ns(&self, clock: KernelClockId) -> KernelResult { + match clock { + // Deterministic realtime is configured at millisecond precision. + KernelClockId::Realtime => Ok(1_000_000), + KernelClockId::Monotonic => Ok(1), + KernelClockId::ProcessCpu | KernelClockId::ThreadCpu => Err(KernelError::new( + "ENOTSUP", + "virtual process CPU clock resolution is not implemented", + )), + } + } + pub fn getuid(&self, requester_driver: &str, pid: u32) -> KernelResult { Ok(self.process_identity(requester_driver, pid)?.uid) } @@ -1069,6 +1357,104 @@ impl KernelVm { .ok_or_else(|| KernelError::new("ENOENT", "end of group database")) } + /// Resolve a passwd entry through the process-visible Linux account database. + /// + /// A live `/etc/passwd` file is authoritative when present. VM user + /// configuration remains the fallback for minimal filesystems that do not + /// project an account database at all. + pub fn getpwuid_for_process( + &mut self, + requester_driver: &str, + pid: u32, + uid: u32, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/passwd")? { + Some(database) => passwd_record_by_uid(&database, uid) + .ok_or_else(|| KernelError::new("ENOENT", format!("unknown uid {uid}"))), + None => self.getpwuid(uid), + } + } + + pub fn getpwnam_for_process( + &mut self, + requester_driver: &str, + pid: u32, + username: &str, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/passwd")? { + Some(database) => passwd_record_by_name(&database, username) + .ok_or_else(|| KernelError::new("ENOENT", format!("unknown user {username}"))), + None => self.getpwnam(username), + } + } + + pub fn getpwent_for_process( + &mut self, + requester_driver: &str, + pid: u32, + index: usize, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/passwd")? { + Some(database) => passwd_record_at(&database, index) + .ok_or_else(|| KernelError::new("ENOENT", "end of passwd database")), + None => self.getpwent(index), + } + } + + pub fn getgrgid_for_process( + &mut self, + requester_driver: &str, + pid: u32, + gid: u32, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/group")? { + Some(database) => group_record_by_gid(&database, gid) + .ok_or_else(|| KernelError::new("ENOENT", format!("unknown gid {gid}"))), + None => self.getgrgid(gid), + } + } + + pub fn getgrnam_for_process( + &mut self, + requester_driver: &str, + pid: u32, + name: &str, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/group")? { + Some(database) => group_record_by_name(&database, name) + .ok_or_else(|| KernelError::new("ENOENT", format!("unknown group {name}"))), + None => self.getgrnam(name), + } + } + + pub fn getgrent_for_process( + &mut self, + requester_driver: &str, + pid: u32, + index: usize, + ) -> KernelResult { + match self.read_account_database_for_process(requester_driver, pid, "/etc/group")? { + Some(database) => group_record_at(&database, index) + .ok_or_else(|| KernelError::new("ENOENT", "end of group database")), + None => self.getgrent(index), + } + } + + fn read_account_database_for_process( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + ) -> KernelResult>> { + match self.lstat_for_process(requester_driver, pid, path) { + Ok(_) => self + .read_file_for_process(requester_driver, pid, path) + .map(Some), + Err(error) if error.code() == "ENOENT" => Ok(None), + Err(error) => Err(error), + } + } + pub fn resource_snapshot(&self) -> ResourceSnapshot { let fd_tables = lock_or_recover(&self.fd_tables); self.resources.snapshot( @@ -1116,6 +1502,37 @@ impl KernelVm { resolve_dns(&self.dns, self.dns_resolver.as_ref(), hostname).map_err(map_dns_resolver_error) } + pub fn resolve_dns_async( + &self, + hostname: &str, + policy: DnsLookupPolicy, + ) -> std::pin::Pin> + Send>> + { + let prepared: KernelResult<(DnsConfig, SharedDnsResolver, String)> = (|| { + self.assert_not_terminated()?; + if matches!(policy, DnsLookupPolicy::CheckPermissions) { + let resource = format_dns_resource(hostname).map_err(map_dns_resolver_error)?; + check_network_access( + &self.vm_id, + &self.permissions, + NetworkOperation::Dns, + &resource, + )?; + } + Ok(( + self.dns.clone(), + Arc::clone(&self.dns_resolver), + hostname.to_owned(), + )) + })(); + Box::pin(async move { + let (dns, resolver, hostname) = prepared?; + resolve_dns_async(&dns, resolver.as_ref(), &hostname) + .await + .map_err(map_dns_resolver_error) + }) + } + pub fn resolve_dns_records( &self, hostname: &str, @@ -1137,6 +1554,39 @@ impl KernelVm { .map_err(map_dns_resolver_error) } + pub fn resolve_dns_records_async( + &self, + hostname: &str, + record_type: RecordType, + policy: DnsLookupPolicy, + ) -> std::pin::Pin< + Box> + Send>, + > { + let prepared: KernelResult<(DnsConfig, SharedDnsResolver, String)> = (|| { + self.assert_not_terminated()?; + if matches!(policy, DnsLookupPolicy::CheckPermissions) { + let resource = format_dns_resource(hostname).map_err(map_dns_resolver_error)?; + check_network_access( + &self.vm_id, + &self.permissions, + NetworkOperation::Dns, + &resource, + )?; + } + Ok(( + self.dns.clone(), + Arc::clone(&self.dns_resolver), + hostname.to_owned(), + )) + })(); + Box::pin(async move { + let (dns, resolver, hostname) = prepared?; + resolve_dns_records_async(&dns, resolver.as_ref(), &hostname, record_type) + .await + .map_err(map_dns_resolver_error) + }) + } + pub fn register_driver(&mut self, driver: CommandDriver) -> KernelResult<()> { self.assert_not_terminated()?; let driver_name = driver.name().to_owned(); @@ -1150,6 +1600,31 @@ impl KernelVm { Ok(()) } + /// Atomically replace the command names owned by a runtime driver and + /// retire only obsolete `/bin` files that are still the exact generated + /// kernel stub. Guest/package files that replaced a stub are preserved. + pub fn replace_driver(&mut self, driver: CommandDriver) -> KernelResult<()> { + self.assert_not_terminated()?; + let driver_name = driver.name().to_owned(); + let populate_driver = driver.clone(); + let obsolete = self.commands.replace(driver)?; + lock_or_recover(&self.driver_pids) + .entry(driver_name) + .or_default(); + for command in obsolete { + let path = format!("/bin/{command}"); + if self.commands.resolve(&command).is_none() && self.filesystem.exists(&path)? { + let stat = self.filesystem.lstat(&path)?; + if !stat.is_symbolic_link && self.filesystem.read_file(&path)? == COMMAND_STUB { + self.filesystem.remove_file(&path)?; + } + } + } + self.commands + .populate_driver_bin(&mut self.filesystem, &populate_driver)?; + Ok(()) + } + pub fn exec( &mut self, command: &str, @@ -1163,6 +1638,7 @@ impl KernelVm { parent_pid: options.parent_pid, env: options.env, cwd: options.cwd, + permission_tier: None, }, ) } @@ -1178,6 +1654,7 @@ impl KernelVm { parent_pid: None, env: options.env, cwd: options.cwd, + permission_tier: None, }, )?; let owner = requester_driver.as_deref().unwrap_or(process.driver()); @@ -1239,7 +1716,85 @@ impl KernelVm { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; self.check_dac_access(pid, path, DAC_READ)?; - self.read_file_internal(Some(pid), path) + if is_proc_path(path) { + if let Some( + node @ (ProcNode::SelfLink { .. } + | ProcNode::PidCwdLink { .. } + | ProcNode::PidFdLink { .. }), + ) = self.resolve_proc_node(path, Some(pid))? + { + let target = self.proc_symlink_target(&node)?; + return self.read_file_for_process(requester_driver, pid, &target); + } + let bytes = self.read_file_internal(Some(pid), path)?; + self.resources.check_pread_length(bytes.len())?; + return Ok(bytes); + } + if is_virtual_device_storage_path(path) { + let bytes = self.read_file_internal(Some(pid), path)?; + self.resources.check_pread_length(bytes.len())?; + return Ok(bytes); + } + + self.reject_unix_socket_data_path(path, "ENXIO")?; + let stat = self.stat_internal(Some(pid), path)?; + let length = usize::try_from(stat.size).map_err(|_| { + KernelError::new( + "EOVERFLOW", + format!("file '{path}' size does not fit host address space"), + ) + })?; + self.resources.check_pread_length(length)?; + Ok(VirtualFileSystem::pread( + &mut self.filesystem, + path, + 0, + length, + )?) + } + + /// Validate a stable-size regular file read without allocating its body. + /// Queue-owning adapters use this before reserving by the admitted size, + /// then call `read_file_for_process` after admission and compare lengths to + /// detect a change between the two operations. + pub fn preflight_regular_file_read_for_process( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + self.check_dac_access(pid, path, DAC_READ)?; + self.reject_unix_socket_data_path(path, "ENXIO")?; + let canonical_path = self.raw_filesystem_mut().realpath(path)?; + if is_proc_path(&canonical_path) || is_virtual_device_storage_path(&canonical_path) { + return Err(KernelError::new( + "EINVAL", + format!("stable regular-file read does not support '{path}'"), + )); + } + let stat = self.stat_internal(Some(pid), &canonical_path)?; + if stat.is_directory || stat.mode & 0o170000 == S_IFDIR { + return Err(KernelError::new( + "EISDIR", + format!("cannot read directory '{path}' as a regular file"), + )); + } + if stat.mode & 0o170000 != S_IFREG { + return Err(KernelError::new( + "EINVAL", + format!("path '{path}' is not a regular file"), + )); + } + let length = usize::try_from(stat.size).map_err(|_| { + KernelError::new( + "EOVERFLOW", + format!("file '{path}' size does not fit host address space"), + ) + })?; + self.resources.check_pread_length(length)?; + Ok(length) } pub fn write_file(&mut self, path: &str, content: impl Into>) -> KernelResult<()> { @@ -1645,18 +2200,79 @@ impl KernelVm { } } - pub fn exists(&self, path: &str) -> KernelResult { - self.assert_not_terminated()?; - self.exists_internal(None, path) + /// Return the kernel-owned runtime-neutral authority attached to a process. + pub fn process_permission_tier( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.permission_tier(pid)?) } - pub fn exists_for_process( - &mut self, + /// Preview the immutable tier that an exec commit will install. Adapters + /// use this kernel calculation while preparing the replacement image, then + /// pass the same trusted requested tier to the commit operation. + pub fn effective_exec_permission_tier( + &self, requester_driver: &str, pid: u32, - path: &str, - ) -> KernelResult { - self.assert_not_terminated()?; + requested: ProcessPermissionTier, + ) -> KernelResult { + Ok(self + .process_permission_tier(requester_driver, pid)? + .restrict(requested)) + } + + pub fn get_resource_limit( + &self, + requester_driver: &str, + pid: u32, + kind: ProcessResourceLimitKind, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.get_resource_limit(pid, kind)?) + } + + pub fn set_resource_limit( + &self, + requester_driver: &str, + pid: u32, + kind: ProcessResourceLimitKind, + value: ProcessResourceLimit, + ) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + if kind == ProcessResourceLimitKind::OpenFiles { + if let Some(soft) = value.soft { + usize::try_from(soft).map_err(|_| { + KernelError::new("EINVAL", "RLIMIT_NOFILE does not fit the host fd index") + })?; + } + } + self.processes.set_resource_limit(pid, kind, value)?; + if kind == ProcessResourceLimitKind::OpenFiles { + let soft = value.soft.map_or(usize::MAX, |value| value as usize); + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + table.set_max_fds(soft); + } + Ok(()) + } + + pub fn exists(&self, path: &str) -> KernelResult { + self.assert_not_terminated()?; + self.exists_internal(None, path) + } + + pub fn exists_for_process( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + ) -> KernelResult { + self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; if let Err(error) = self.check_dac_traversal(pid, path) { if matches!(error.code(), "EACCES" | "ENOENT" | "ENOTDIR" | "ELOOP") { @@ -1950,11 +2566,11 @@ impl KernelVm { self.assert_not_terminated()?; self.reject_read_only_entry_write_path(path)?; let removed = self.storage_lstat(path)?; - let detached = self.prepare_detached_directory_backing(path, removed.as_ref()); + let detached = self.prepare_detached_directory_backing(path, removed.as_ref())?; self.filesystem.remove_dir(path)?; - if let Some((descriptions, stat)) = detached { + if let Some((descriptions, stat, xattrs)) = detached { for description in descriptions { - description.detach_directory(path, stat.clone()); + description.detach_directory(path, stat.clone(), xattrs.clone()); } } if removed.as_ref().is_some_and(|stat| stat.is_directory) { @@ -1988,7 +2604,7 @@ impl KernelVm { let detached_destination = self.prepare_anonymous_file_backing(new_path, replaced.as_ref())?; let detached_directory_destination = - self.prepare_detached_directory_backing(new_path, replaced.as_ref()); + self.prepare_detached_directory_backing(new_path, replaced.as_ref())?; self.filesystem.rename_at2(old_path, new_path, flags)?; if flags == RENAME_EXCHANGE { let temporary = format!( @@ -2021,9 +2637,9 @@ impl KernelVm { } None => {} } - if let Some((descriptions, stat)) = detached_directory_destination { + if let Some((descriptions, stat, xattrs)) = detached_directory_destination { for description in descriptions { - description.detach_directory(new_path, stat.clone()); + description.detach_directory(new_path, stat.clone(), xattrs.clone()); } } self.rename_open_file_descriptions(old_path, new_path); @@ -2216,23 +2832,11 @@ impl KernelVm { uid: u32, gid: u32, ) -> KernelResult<()> { - let identity = self.process_identity(requester_driver, pid)?; - self.check_dac_traversal(pid, path)?; - let stat = self.filesystem.lstat(path)?; - if identity.euid != 0 { - let owns_file = identity.euid == stat.uid; - let keeps_owner = uid == stat.uid; - let allowed_group = gid == identity.egid || identity.supplementary_gids.contains(&gid); - if !owns_file || !keeps_owner || !allowed_group { - return Err(KernelError::new( - "EPERM", - format!("lchown is not permitted for {path}"), - )); - } - } - self.reject_read_only_entry_write_path(path)?; - self.filesystem.lchown(path, uid, gid)?; - Ok(()) + // Keep one Linux ownership policy for chown/lchown/fchown. In + // particular, uid/gid UINT32_MAX are independent "unchanged" + // sentinels and must be resolved against the no-follow stat before + // permission checks or mutation. + self.chown_for_process(requester_driver, pid, path, uid, gid, false) } pub fn get_xattr( @@ -2409,6 +3013,157 @@ impl KernelVm { self.remove_xattr(path, name, follow_symlinks) } + pub fn fd_get_xattr_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + name: &str, + ) -> KernelResult> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_xattrs().is_none() { + return self.get_xattr_for_process( + requester_driver, + pid, + &description.path(), + name, + true, + ); + } + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + let label = format!("fd {fd}"); + check_xattr_namespace(&identity, name, false, &label)?; + self.check_detached_fd_dac(&identity, &stat, &description, DAC_READ, &label)?; + description + .detached_get_xattr(name) + .expect("detached xattr state was checked") + .map_err(KernelError::from) + } + + pub fn fd_list_xattrs_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + ) -> KernelResult> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_xattrs().is_none() { + return self.list_xattrs_for_process(requester_driver, pid, &description.path(), true); + } + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + let label = format!("fd {fd}"); + self.check_detached_fd_dac(&identity, &stat, &description, DAC_READ, &label)?; + let mut names = description + .detached_xattrs() + .expect("detached xattr state was checked") + .into_keys() + .collect::>(); + if identity.euid != 0 { + names.retain(|name| !name.starts_with("trusted.") && !name.starts_with("security.")); + } + Ok(names) + } + + pub fn fd_set_xattr_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + name: &str, + value: Vec, + flags: u32, + ) -> KernelResult<()> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_xattrs().is_none() { + return self.set_xattr_for_process( + requester_driver, + pid, + &description.path(), + name, + value, + flags, + true, + ); + } + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + let label = format!("fd {fd}"); + check_xattr_namespace(&identity, name, true, &label)?; + check_xattr_inode_write_policy(&stat, name, &label)?; + self.reject_read_only_resolved_write_path(&description.path())?; + if name.starts_with("system.posix_acl_") { + if identity.euid != 0 && identity.euid != stat.uid { + return Err(KernelError::new( + "EPERM", + format!("setting {name} requires ownership of {label}"), + )); + } + } else { + self.check_detached_fd_dac(&identity, &stat, &description, DAC_WRITE, &label)?; + } + let acl = if name == POSIX_ACL_ACCESS || name == POSIX_ACL_DEFAULT { + let acl = PosixAcl::parse(&value, &label)?; + if name == POSIX_ACL_DEFAULT && !stat.is_directory { + return Err(KernelError::new( + "EACCES", + format!("default ACL requires a directory: {label}"), + )); + } + Some(acl) + } else { + None + }; + description + .detached_set_xattr(name, value, flags) + .expect("detached xattr state was checked")?; + if name == POSIX_ACL_ACCESS { + let mode = acl.expect("access ACL was parsed").mode(stat.mode); + description.detached_chmod(mode); + } + Ok(()) + } + + pub fn fd_remove_xattr_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + name: &str, + ) -> KernelResult<()> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_xattrs().is_none() { + return self.remove_xattr_for_process( + requester_driver, + pid, + &description.path(), + name, + true, + ); + } + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + let label = format!("fd {fd}"); + check_xattr_namespace(&identity, name, true, &label)?; + check_xattr_inode_write_policy(&stat, name, &label)?; + self.reject_read_only_resolved_write_path(&description.path())?; + if name.starts_with("system.posix_acl_") { + if identity.euid != 0 && identity.euid != stat.uid { + return Err(KernelError::new( + "EPERM", + format!("removing {name} requires ownership of {label}"), + )); + } + } else { + self.check_detached_fd_dac(&identity, &stat, &description, DAC_WRITE, &label)?; + } + description + .detached_remove_xattr(name) + .expect("detached xattr state was checked")?; + Ok(()) + } + pub fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> KernelResult<()> { self.utimes_spec( path, @@ -2761,6 +3516,37 @@ impl KernelVm { Ok(self.filesystem.unwritten_ranges(&path)?) } + /// Return one classified FIEMAP extent. The query remains indexed through + /// the VFS stack so implementations can scan authoritative extent metadata + /// without materializing adapter-local range lists. + pub fn fd_extent_at( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + index: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let path = { + let tables = lock_or_recover(&self.fd_tables); + tables + .get(pid) + .and_then(|table| table.get(fd)) + .map(|entry| entry.description.path()) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))? + }; + let wanted = usize::try_from(index) + .map_err(|_| KernelError::new("EOVERFLOW", "extent index exceeds usize"))?; + Ok(self + .filesystem + .extent_at(&path, wanted)? + .map(|extent| KernelFileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })) + } + pub fn check_execute_for_process( &mut self, requester_driver: &str, @@ -2768,45 +3554,421 @@ impl KernelVm { path: &str, ) -> KernelResult<()> { self.assert_driver_owns(requester_driver, pid)?; - let stat = self.filesystem.stat(path)?; + let stat = self.raw_filesystem_mut().stat(path)?; if stat.is_directory { return Err(KernelError::new( "EACCES", format!("permission denied, execute '{path}'"), )); } - self.check_dac_access(pid, path, DAC_EXECUTE) + self.check_execute_dac_traversal(pid, path)?; + // Registered command projections are executable kernel objects. Their + // backing WASM blobs may be stored as 0644 because the trusted runtime + // driver, rather than the host filesystem, performs the image launch. + // Keep directory traversal checks above, but do not require a host-style + // execute bit on the projected blob itself. + if self.resolve_registered_command_path(path).is_some() { + return Ok(()); + } + let identity = self + .processes + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .identity; + self.check_execute_dac_mode_with_acl(&identity, &stat, DAC_EXECUTE, path) } - pub fn list_processes(&self) -> BTreeMap { - self.processes.list_processes() + /// Load the initial runtime image selected by a trusted client Execute + /// request. Callers must bound and admit any external source before placing + /// it in the kernel VFS; this method never consults ambient host paths. + pub fn load_trusted_initial_runtime_image( + &mut self, + path: &str, + maximum_bytes: u64, + ) -> KernelResult { + self.load_runtime_image_after_authorization(path, maximum_bytes) } - pub fn zombie_timer_count(&self) -> usize { - self.processes.zombie_timer_count() + pub fn load_trusted_initial_runtime_image_prefix( + &mut self, + path: &str, + prefix_bytes: usize, + ) -> KernelResult { + self.load_runtime_image_prefix_after_authorization(path, prefix_bytes) } - pub fn reap_due_zombies(&self) { - self.processes.reap_due_zombies(); - } + /// Admit the exact bounded source selected by a trusted initial Execute + /// request into the authoritative kernel VFS. This is deliberately the + /// only policy-bypassing write used by runtime launch staging: it rejects + /// traversal spellings and protected/read-only paths, accounts all new + /// bytes and inodes before mutation, and rolls back partial creation. + pub fn admit_trusted_initial_runtime_image( + &mut self, + path: &str, + bytes: Vec, + mode: u32, + maximum_bytes: u64, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + if path.is_empty() || !path.starts_with('/') { + return Err(KernelError::new( + "EINVAL", + "trusted runtime launch image path must be absolute", + )); + } + if path.as_bytes().contains(&0) { + return Err(KernelError::new( + "EINVAL", + "trusted runtime launch image path contains a NUL byte", + )); + } + if path.len() >= MAX_PATH_LENGTH { + return Err(KernelError::new( + "ENAMETOOLONG", + format!( + "trusted runtime launch image path is {} bytes; maximum is {}", + path.len(), + MAX_PATH_LENGTH - 1 + ), + )); + } + let normalized = normalize_path(path); + if normalized != path { + return Err(KernelError::new( + "EINVAL", + format!( + "trusted runtime launch image path must be normalized without traversal: {path}" + ), + )); + } + if is_proc_path(path) || is_agentos_path(path) { + return Err(read_only_filesystem_error(path)); + } + if bytes.len() as u64 > maximum_bytes { + return Err(KernelError::new( + "E2BIG", + format!( + "trusted runtime launch image '{path}' is {} bytes, exceeding maximum {maximum_bytes}", + bytes.len() + ), + )); + } + let admitted_size = bytes.len() as u64; + match self.raw_filesystem_mut().lstat(path) { + Ok(_) => { + return Err(KernelError::new( + "EEXIST", + format!("trusted runtime launch image already exists: {path}"), + )); + } + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(error.into()), + } - pub fn next_zombie_reap_deadline(&self) -> Option { - self.processes.next_zombie_reap_deadline() + let parent = parent_path(path); + let created_directories = self.missing_directory_paths(&parent, true)?; + let usage = self.filesystem_usage()?; + self.resources.check_filesystem_usage( + &usage, + usage.total_bytes.saturating_add(bytes.len() as u64), + usage + .inode_count + .saturating_add(created_directories.len()) + .saturating_add(1), + )?; + + if let Err(error) = self.raw_filesystem_mut().mkdir(&parent, true) { + let error = KernelError::from(error); + return match self.rollback_runtime_image_admission(path, &created_directories, false) { + Ok(()) => Err(error), + Err(rollback) => Err(KernelError::new( + "EIO", + format!("{error}; runtime image admission rollback failed: {rollback}"), + )), + }; + } + if let Err(error) = self.raw_filesystem_mut().create_file_exclusive(path, bytes) { + let error = KernelError::from(error); + return match self.rollback_runtime_image_admission(path, &created_directories, false) { + Ok(()) => Err(error), + Err(rollback) => Err(KernelError::new( + "EIO", + format!("{error}; runtime image admission rollback failed: {rollback}"), + )), + }; + } + if let Err(error) = self.raw_filesystem_mut().chmod(path, mode & 0o7777) { + let error = KernelError::from(error); + return match self.rollback_runtime_image_admission(path, &created_directories, true) { + Ok(()) => Err(error), + Err(rollback) => Err(KernelError::new( + "EIO", + format!("{error}; runtime image admission rollback failed: {rollback}"), + )), + }; + } + + self.update_filesystem_usage_cache_for_inode_creates(&parent, created_directories.len()); + self.update_filesystem_usage_cache_for_inode_create(path, admitted_size); + Ok(()) } - pub fn spawn_process( + fn rollback_runtime_image_admission( &mut self, - command: &str, - args: Vec, - options: SpawnOptions, - ) -> KernelResult { - self.spawn_process_with_process_group(command, args, options, None) + path: &str, + created_directories: &[String], + file_created: bool, + ) -> Result<(), String> { + let mut failures = Vec::new(); + if file_created { + if let Err(error) = self.raw_filesystem_mut().remove_file(path) { + if error.code() != "ENOENT" { + failures.push(format!("remove {path}: {error}")); + } + } + } + for directory in created_directories.iter().rev() { + if let Err(error) = self.raw_filesystem_mut().remove_dir(directory) { + if error.code() != "ENOENT" { + failures.push(format!("remove {directory}: {error}")); + } + } + } + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("; ")) + } } - pub fn spawn_process_with_process_group( + /// Load a guest-selected runtime image. Unlike an ordinary file read, + /// Linux exec requires search/execute permission but not read permission, + /// so authorization is checked before the trusted loader reads the bytes. + pub fn load_process_runtime_image( &mut self, - command: &str, - args: Vec, + requester_driver: &str, + pid: u32, + path: &str, + maximum_bytes: u64, + ) -> KernelResult { + self.check_execute_for_process(requester_driver, pid, path)?; + self.load_runtime_image_after_authorization(path, maximum_bytes) + } + + /// Load an executable image through the exact open file description + /// selected by fexecve(2), without advancing its file offset. + pub fn load_process_runtime_image_from_fd( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + maximum_bytes: u64, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let entry = { + let tables = lock_or_recover(&self.fd_tables); + tables + .get(pid) + .and_then(|table| table.get(fd)) + .cloned() + .ok_or_else(|| KernelError::bad_file_descriptor(fd))? + }; + let path = entry.description.path(); + let anonymous = entry.description.anonymous_stat().is_some(); + let stat = if let Some(stat) = entry.description.anonymous_stat() { + stat + } else { + self.raw_filesystem_mut().stat(&path)? + }; + if stat.is_directory || stat.mode & 0o170000 != S_IFREG { + return Err(KernelError::new( + "EACCES", + format!("file descriptor {fd} is not a regular executable file"), + )); + } + + let projected = !anonymous && self.resolve_registered_command_path(&path).is_some(); + if !projected { + let identity = self + .processes + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .identity; + if anonymous { + self.check_detached_fd_dac( + &identity, + &stat, + &entry.description, + DAC_EXECUTE, + &format!("fd {fd}"), + )?; + } else { + self.check_execute_dac_mode_with_acl(&identity, &stat, DAC_EXECUTE, &path)?; + } + } + + if stat.size > maximum_bytes { + return Err(KernelError::new( + "E2BIG", + format!( + "runtime launch image from fd {fd} is {} bytes, exceeding maximum {maximum_bytes}", + stat.size + ), + )); + } + if stat.size >= maximum_bytes.saturating_mul(4) / 5 { + eprintln!( + "WARN_AGENTOS_RUNTIME_LAUNCH_IMAGE_NEAR_LIMIT: fd={fd} observed={} maximum={maximum_bytes}", + stat.size + ); + } + let size = usize::try_from(stat.size).map_err(|_| { + KernelError::new( + "EOVERFLOW", + format!("runtime launch image from fd {fd} exceeds host address space"), + ) + })?; + let bytes = if let Some(bytes) = entry.description.anonymous_pread(0, size) { + bytes + } else { + VirtualFileSystem::pread(&mut self.filesystem, &path, 0, size)? + }; + if bytes.len() != size { + return Err(KernelError::new( + "EIO", + format!( + "runtime launch image from fd {fd} changed while being snapshotted: expected {size} bytes, read {}", + bytes.len() + ), + )); + } + let canonical_path = if anonymous { + entry.description.proc_display_path() + } else { + self.raw_filesystem_mut().realpath(&path)? + }; + Ok(RuntimeLaunchImage { + canonical_path, + bytes, + mode: stat.mode, + }) + } + + pub fn load_process_runtime_image_prefix( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + prefix_bytes: usize, + ) -> KernelResult { + self.check_execute_for_process(requester_driver, pid, path)?; + self.load_runtime_image_prefix_after_authorization(path, prefix_bytes) + } + + fn load_runtime_image_prefix_after_authorization( + &mut self, + path: &str, + prefix_bytes: usize, + ) -> KernelResult { + self.assert_not_terminated()?; + self.resources.check_pread_length(prefix_bytes)?; + let canonical_path = self.raw_filesystem_mut().realpath(path)?; + let stat = self.raw_filesystem_mut().stat(&canonical_path)?; + if stat.is_directory || stat.mode & 0o170000 != S_IFREG { + return Err(KernelError::new( + "EACCES", + format!("permission denied, execute '{path}'"), + )); + } + let available = usize::try_from(stat.size.min(prefix_bytes as u64)).map_err(|_| { + KernelError::new( + "EOVERFLOW", + format!("runtime launch image '{path}' size does not fit host address space"), + ) + })?; + let bytes = VirtualFileSystem::pread(&mut self.filesystem, &canonical_path, 0, available)?; + Ok(RuntimeLaunchImagePrefix { + canonical_path, + bytes, + }) + } + + fn load_runtime_image_after_authorization( + &mut self, + path: &str, + maximum_bytes: u64, + ) -> KernelResult { + self.assert_not_terminated()?; + let canonical_path = self.raw_filesystem_mut().realpath(path)?; + let stat = self.raw_filesystem_mut().stat(&canonical_path)?; + if stat.is_directory || stat.mode & 0o170000 != S_IFREG { + return Err(KernelError::new( + "EACCES", + format!("permission denied, execute '{path}'"), + )); + } + if stat.size > maximum_bytes { + return Err(KernelError::new( + "E2BIG", + format!( + "runtime launch image '{path}' is {} bytes, exceeding maximum {maximum_bytes}", + stat.size + ), + )); + } + if stat.size >= maximum_bytes.saturating_mul(4) / 5 { + eprintln!( + "WARN_AGENTOS_RUNTIME_LAUNCH_IMAGE_NEAR_LIMIT: path={path} observed={} maximum={maximum_bytes}", + stat.size + ); + } + let bytes = self.raw_filesystem_mut().read_file(&canonical_path)?; + if bytes.len() as u64 > maximum_bytes { + return Err(KernelError::new( + "E2BIG", + format!( + "runtime launch image '{path}' grew to {} bytes, exceeding maximum {maximum_bytes}", + bytes.len() + ), + )); + } + Ok(RuntimeLaunchImage { + canonical_path, + bytes, + mode: stat.mode, + }) + } + + pub fn list_processes(&self) -> BTreeMap { + self.processes.list_processes() + } + + pub fn zombie_timer_count(&self) -> usize { + self.processes.zombie_timer_count() + } + + pub fn reap_due_zombies(&self) { + self.processes.reap_due_zombies(); + } + + pub fn next_zombie_reap_deadline(&self) -> Option { + self.processes.next_zombie_reap_deadline() + } + + pub fn spawn_process( + &mut self, + command: &str, + args: Vec, + options: SpawnOptions, + ) -> KernelResult { + self.spawn_process_with_process_group(command, args, options, None) + } + + pub fn spawn_process_with_process_group( + &mut self, + command: &str, + args: Vec, options: SpawnOptions, requested_pgid: Option, ) -> KernelResult { @@ -2901,14 +4063,27 @@ impl KernelVm { None => DEFAULT_PROCESS_UMASK, }; + let root_open_files = self + .resource_limits() + .max_open_fds + .unwrap_or(DEFAULT_MAX_OPEN_FDS) as u64; let mut context = parent_context.unwrap_or_else(|| ProcessContext { identity: self.users.identity(), + resource_limits: crate::process_table::ProcessResourceLimits::with_open_files( + root_open_files, + ), ..ProcessContext::default() }); context.ppid = options.parent_pid.unwrap_or(0); context.env = env; context.cwd = cwd; context.umask = process_umask; + if let Some(requested_tier) = options.permission_tier { + context.permission_tier = context.permission_tier.restrict(requested_tier); + } + if let Some(requested_tier) = options.permission_tier { + context.permission_tier = context.permission_tier.restrict(requested_tier); + } self.register_process( resolved.driver.name().to_owned(), @@ -2944,6 +4119,7 @@ impl KernelVm { &[], &[], None, + None, ) } @@ -2984,6 +4160,7 @@ impl KernelVm { retained_internal_fds: &[u32], additional_cloexec_fds: &[u32], image_command: Option<&str>, + requested_permission_tier: Option, ) -> KernelResult<()> { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; @@ -3053,8 +4230,16 @@ impl KernelVm { committed_args, env, cwd, + requested_permission_tier, )?; + let effective_tier = self.processes.permission_tier(pid)?; + let (rights_base, rights_inheriting) = wasi_preopen_rights(effective_tier, true) + .map_or((None, None), |(base, inheriting)| { + (Some(base), Some(inheriting)) + }); + table.restrict_wasi_preopens(rights_base, rights_inheriting); + let mut closed_entries = Vec::with_capacity(fds.len()); for fd in fds { if retained_internal_fds.contains(&fd) { @@ -3153,8 +4338,15 @@ impl KernelVm { None => DEFAULT_PROCESS_UMASK, }; + let root_open_files = self + .resource_limits() + .max_open_fds + .unwrap_or(DEFAULT_MAX_OPEN_FDS) as u64; let mut context = parent_context.unwrap_or_else(|| ProcessContext { identity: self.users.identity(), + resource_limits: crate::process_table::ProcessResourceLimits::with_open_files( + root_open_files, + ), ..ProcessContext::default() }); context.ppid = options.parent_pid.unwrap_or(0); @@ -3208,7 +4400,7 @@ impl KernelVm { exit_code: i32, ) -> KernelResult<()> { self.assert_driver_owns(requester_driver, pid)?; - self.processes.mark_exited(pid, exit_code); + self.processes.mark_exited(pid, exit_code)?; Ok(()) } @@ -3246,14 +4438,20 @@ impl KernelVm { } } - let process = Arc::new(StubDriverProcess::default()); + let runtime_control = RuntimeControlCell::new_with_ack_sink( + self.vm_generation, + Arc::new(self.processes.clone()), + ); + runtime_control + .bind_pid(pid) + .map_err(|error| KernelError::new(error.code(), error.message()))?; self.processes.register_with_process_group( pid, driver_name.clone(), command, args, ctx.clone(), - process.clone(), + Arc::new(runtime_control.clone()), requested_pgid, )?; @@ -3269,6 +4467,14 @@ impl KernelVm { } else { tables.create(pid); } + let (rights_base, rights_inheriting) = wasi_preopen_rights(ctx.permission_tier, true) + .map_or((None, None), |(base, inheriting)| { + (Some(base), Some(inheriting)) + }); + tables + .get_mut(pid) + .expect("registered process fd table exists") + .restrict_wasi_preopens(rights_base, rights_inheriting); } let mut owners = lock_or_recover(&self.driver_pids); @@ -3283,7 +4489,15 @@ impl KernelVm { Ok(KernelProcessHandle { pid, driver: driver_name, - process, + processes: self.processes.clone(), + runtime_control, + exit_reporter: ProcessExitReporter::new( + ProcessRuntimeIdentity { + generation: self.vm_generation, + pid, + }, + Arc::new(self.processes.clone()), + ), }) } @@ -3305,6 +4519,31 @@ impl KernelVm { Ok(result.map(|result| self.finish_waitpid_event(result))) } + pub fn waitpid_detailed_with_options( + &mut self, + requester_driver: &str, + waiter_pid: u32, + pid: i32, + flags: WaitPidFlags, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, waiter_pid)?; + let transition = self + .processes + .waitpid_for_detailed(waiter_pid, pid, flags)?; + Ok(transition.map(|transition| { + let result = transition.result; + if result.event == WaitPidEvent::Exited { + self.cleanup_process_resources(result.pid); + } + WaitPidDetailedResult { + pid: result.pid, + status: result.status, + event: result.event, + termination: transition.termination, + } + })) + } + pub fn take_nonterminal_wait_event( &self, requester_driver: &str, @@ -3377,6 +4616,123 @@ impl KernelVm { })) } + /// Install and return the process's WASI capability roots. The kernel owns + /// both descriptor allocation and rights; an executor may only project + /// this metadata into its engine-specific ABI. + pub fn initialize_wasi_preopens( + &mut self, + requester_driver: &str, + pid: u32, + ) -> KernelResult> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let permission_tier = self.processes.permission_tier(pid)?; + let should_initialize = { + let mut tables = lock_or_recover(&self.fd_tables); + tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .begin_wasi_preopen_initialization() + }; + + if should_initialize && permission_tier != ProcessPermissionTier::Isolated { + let cwd = self.processes.inherited_context(pid)?.cwd; + let mut candidates = vec![normalize_path(&cwd), String::from("/")]; + candidates.dedup(); + + for guest_path in candidates { + // Preopens are kernel-owned process capability roots, not a + // guest read of the directory. Install stable descriptors from + // trusted VFS metadata even when the guest's dynamic fs.read + // policy denies directory operations; each later operation on + // the descriptor still passes through policy, DAC, and rights. + let stat = self.raw_filesystem_mut().stat(&guest_path)?; + if !stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("WASI preopen path is not a directory: {guest_path}"), + )); + } + + let writable = self + .filesystem + .check_virtual_path(FsOperation::Write, &guest_path) + .is_ok() + && self + .reject_read_only_resolved_write_path(&guest_path) + .is_ok(); + let (rights_base, rights_inheriting) = + wasi_preopen_rights(permission_tier, writable) + .expect("non-isolated tiers always expose preopens"); + self.resources + .check_fd_allocation(&self.resource_snapshot(), 1)?; + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let fd = table.open_with_details( + &guest_path, + O_DIRECTORY | O_RDONLY, + FILETYPE_DIRECTORY, + None, + )?; + table.mark_wasi_preopen(fd, guest_path, rights_base, rights_inheriting)?; + } + } + + self.wasi_preopens(requester_driver, pid) + } + + pub fn wasi_preopens( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + Ok(table + .values() + .filter_map(|entry| { + entry + .wasi_preopen_path + .as_ref() + .map(|guest_path| ProcessWasiPreopen { + fd: entry.fd, + guest_path: guest_path.clone(), + rights_base: entry.rights, + rights_inheriting: entry.rights_inheriting, + }) + }) + .collect()) + } + + pub fn wasi_preopen( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let entry = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .get(fd) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; + Ok(entry + .wasi_preopen_path + .as_ref() + .map(|guest_path| ProcessWasiPreopen { + fd, + guest_path: guest_path.clone(), + rights_base: entry.rights, + rights_inheriting: entry.rights_inheriting, + })) + } + pub fn fd_snapshot( &self, requester_driver: &str, @@ -3391,16 +4747,32 @@ impl KernelVm { .values() .map(|entry| ProcessFdSnapshotEntry { fd: entry.fd, + description_id: entry.description.id(), fd_flags: entry.fd_flags, - status_flags: entry.status_flags | entry.description.flags(), + status_flags: entry.status_flags.get() | entry.description.flags(), filetype: entry.filetype, - is_socket: self.fd_socket_id(&entry.description).is_some(), + rights_base: entry.rights, + rights_inheriting: entry.rights_inheriting, + is_socket: matches!( + entry.filetype, + FILETYPE_SOCKET_STREAM | FILETYPE_SOCKET_DGRAM + ), is_pipe: self.pipes.is_pipe(entry.description.id()), is_pty: self.ptys.is_pty(entry.description.id()), }) .collect()) } + pub fn fd_is_pipe(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + let entry = lock_or_recover(&self.fd_tables) + .get(pid) + .and_then(|table| table.get(fd)) + .cloned() + .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; + Ok(self.pipes.is_pipe(entry.description.id())) + } + /// Create a connected AF_UNIX socket pair whose endpoints live in the /// process descriptor table. The socket records are owned by their open /// file descriptions rather than by a PID so SCM_RIGHTS and spawn @@ -3566,7 +4938,7 @@ impl KernelVm { .ok_or_else(|| KernelError::no_such_process(pid))?; let fd = table.open_with_details( &format!("socket:{socket_id}"), - status_flags, + status_flags | O_RDWR, filetype, None, )?; @@ -3592,30 +4964,164 @@ impl KernelVm { Ok(fd) } - /// Attach an existing kernel socket directly to a transferable open file - /// description. Unlike `fd_adopt_socket`, this does not allocate a - /// temporary descriptor in the sender, matching SCM_RIGHTS behavior when - /// the sender is already at its per-process fd limit. - pub fn fd_adopt_socket_transfer( + /// Allocate a canonical descriptor for a sidecar-owned socket transport. + /// + /// The transport itself remains in the sidecar reactor; this description + /// exists so dup/fork/exec/SCM_RIGHTS use the same kernel-owned Linux fd + /// semantics as every other resource. It deliberately does not create a + /// dummy `SocketId` or enter `fd_sockets`, which would double-account and + /// double-own the sidecar transport. + pub fn fd_open_external_socket( &mut self, requester_driver: &str, pid: u32, - socket_id: SocketId, - status_flags: u32, - ) -> KernelResult { + datagram: bool, + nonblocking: bool, + close_on_exec: bool, + ) -> KernelResult<(u32, u64)> { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; - let identity = self.process_identity(requester_driver, pid)?; - let socket = self - .sockets - .get(socket_id) - .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if socket.owner_pid() != pid && socket.owner_pid() != 0 { - return Err(KernelError::permission_denied(format!( - "process {pid} does not own socket {socket_id}" - ))); - } - let filetype = if socket.spec().socket_type == SocketType::Datagram { + self.resources + .check_fd_allocation(&self.resource_snapshot(), 1)?; + let filetype = if datagram { + FILETYPE_SOCKET_DGRAM + } else { + FILETYPE_SOCKET_STREAM + }; + let status_flags = if nonblocking { O_NONBLOCK } else { 0 }; + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let fd = + table.open_with_details("hostnet:external", status_flags | O_RDWR, filetype, None)?; + if close_on_exec { + table.fcntl(fd, F_SETFD, FD_CLOEXEC)?; + } + let description_id = table + .get(fd) + .expect("new external socket fd must exist") + .description + .id(); + Ok((fd, description_id)) + } + + /// Return the open-file-description identity and number of aliases in this + /// process. Sidecar transports use this to key mutable reactor metadata by + /// description rather than treating a runner-local fd map as authoritative. + pub fn fd_description_identity( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + ) -> KernelResult<(u64, usize)> { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let entry = table + .get(fd) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; + let description_id = entry.description.id(); + let aliases = table + .values() + .filter(|candidate| candidate.description.id() == description_id) + .count(); + Ok((description_id, aliases)) + } + + /// Validate a live descriptor as a socket, optionally requiring the + /// kernel-owned socket to be in listening state. Managed external sockets + /// are recognizable by file type but their listener state remains in the + /// sidecar registry, which validates them before using this fallback. + pub fn fd_validate_socket( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + require_listening: bool, + ) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + let (description, filetype) = { + let tables = lock_or_recover(&self.fd_tables); + let entry = tables + .get(pid) + .and_then(|table| table.get(fd)) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; + (Arc::clone(&entry.description), entry.filetype) + }; + if !matches!(filetype, FILETYPE_SOCKET_DGRAM | FILETYPE_SOCKET_STREAM) { + return Err(KernelError::new( + "ENOTSOCK", + format!("file descriptor {fd} is not a socket"), + )); + } + if !require_listening { + return Ok(()); + } + let Some(socket_id) = self.fd_socket_id(&description) else { + return Err(KernelError::new( + "EINVAL", + format!("socket file descriptor {fd} is not listening"), + )); + }; + let socket = self.sockets.get(socket_id).ok_or_else(|| { + KernelError::new( + "ENOTSOCK", + format!("socket file descriptor {fd} no longer has a live socket"), + ) + })?; + if socket.state() != SocketState::Listening { + return Err(KernelError::new( + "EINVAL", + format!("socket file descriptor {fd} is not listening"), + )); + } + Ok(()) + } + + pub fn fd_description_alias_count( + &self, + requester_driver: &str, + pid: u32, + description_id: u64, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + Ok(table + .values() + .filter(|entry| entry.description.id() == description_id) + .count()) + } + + /// Attach an existing kernel socket directly to a transferable open file + /// description. Unlike `fd_adopt_socket`, this does not allocate a + /// temporary descriptor in the sender, matching SCM_RIGHTS behavior when + /// the sender is already at its per-process fd limit. + pub fn fd_adopt_socket_transfer( + &mut self, + requester_driver: &str, + pid: u32, + socket_id: SocketId, + status_flags: u32, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let identity = self.process_identity(requester_driver, pid)?; + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; + if socket.owner_pid() != pid && socket.owner_pid() != 0 { + return Err(KernelError::permission_denied(format!( + "process {pid} does not own socket {socket_id}" + ))); + } + let filetype = if socket.spec().socket_type == SocketType::Datagram { FILETYPE_SOCKET_DGRAM } else { FILETYPE_SOCKET_STREAM @@ -3625,7 +5131,11 @@ impl KernelVm { let table = tables .get(pid) .ok_or_else(|| KernelError::no_such_process(pid))?; - table.create_transfer(&format!("socket:{socket_id}"), status_flags, filetype) + table.create_transfer( + &format!("socket:{socket_id}"), + status_flags | O_RDWR, + filetype, + ) }; self.sockets.reassign_owner(socket_id, 0)?; lock_or_recover(&self.fd_sockets).insert( @@ -3686,6 +5196,108 @@ impl KernelVm { Ok(()) } + /// Install a descriptor captured by the kernel's trusted POSIX-spawn + /// staging path, retaining descriptor-local WASI preopen metadata. Guest + /// descriptor passing must continue to use [`Self::fd_install_transfer_at`], + /// which intentionally does not confer capability-root status. + pub fn fd_install_spawn_transfer_at( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + fd_flags: u32, + transfer: &TransferredFd, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let replaced = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let replaced = table + .get(fd) + .map(|entry| (Arc::clone(&entry.description), entry.filetype)); + table.install_spawn_transferred_at(transfer, fd, fd_flags)?; + replaced + }; + if let Some((description, filetype)) = replaced { + self.close_special_resource_if_needed(&description, filetype); + } + Ok(()) + } + + /// Install one queued open-file description at the receiver's next free + /// descriptor. This is the sidecar-resource counterpart to the kernel's + /// ordinary SCM_RIGHTS installation path. + pub fn fd_install_transfer( + &mut self, + requester_driver: &str, + pid: u32, + transfer: &TransferredFd, + close_on_exec: bool, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + self.resources + .check_fd_allocation(&self.resource_snapshot(), 1)?; + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let mut installed = + table.install_transferred(std::slice::from_ref(transfer), close_on_exec)?; + Ok(installed + .pop() + .expect("one transferred description must install one fd")) + } + + /// Atomically commit a runner-namespace `fd_renumber` without making the + /// runner authoritative for descriptor state. If the guest destination + /// already projects a kernel descriptor, replace that backing descriptor; + /// otherwise retain the source backing slot and only clear `FD_CLOEXEC`. + /// The caller updates guest-to-kernel identity projections after success. + pub fn fd_renumber_projection( + &mut self, + requester_driver: &str, + pid: u32, + source_fd: u32, + target_fd: Option, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + { + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + table + .get(source_fd) + .ok_or_else(|| KernelError::bad_file_descriptor(source_fd))?; + if let Some(target_fd) = target_fd { + table + .get(target_fd) + .ok_or_else(|| KernelError::bad_file_descriptor(target_fd))?; + } + } + + let Some(target_fd) = target_fd else { + self.fd_fcntl(requester_driver, pid, source_fd, F_SETFD, 0)?; + return Ok(source_fd); + }; + if source_fd == target_fd { + self.fd_fcntl(requester_driver, pid, source_fd, F_SETFD, 0)?; + return Ok(source_fd); + } + + // fd_dup2 validates and replaces the destination before the source is + // consumed. Therefore every recoverable error leaves both original + // descriptors intact; the remaining close cannot fail without an + // internal concurrent mutation of this process's synchronous table. + self.fd_dup2(requester_driver, pid, source_fd, target_fd)?; + self.fd_close(requester_driver, pid, source_fd)?; + Ok(target_fd) + } + pub fn fd_socket_sendmsg( &mut self, requester_driver: &str, @@ -3789,7 +5401,8 @@ impl KernelVm { ( socket_id, table.available_fd_capacity(), - (socket_entry.description.flags() | socket_entry.status_flags) & O_NONBLOCK != 0, + (socket_entry.description.flags() | socket_entry.status_flags.get()) & O_NONBLOCK + != 0, self.sockets .get(socket_id) .ok_or_else(|| KernelError::bad_file_descriptor(socket_fd))? @@ -3798,6 +5411,9 @@ impl KernelVm { ) }; + let mut blocking_deadline = (!nonblocking && !dontwait) + .then(|| self.resources.blocking_read_deadline()) + .flatten(); let deadline = (!nonblocking && !dontwait) .then(|| { self.blocking_read_timeout() @@ -3831,11 +5447,22 @@ impl KernelVm { } Ok(None) => break None, Err(error) if error.code() == "EAGAIN" && !nonblocking && !dontwait => { - let remaining = - deadline.map(|deadline| deadline.saturating_duration_since(Instant::now())); - if matches!(remaining, Some(duration) if duration.is_zero()) - || !self.poll_notifier.wait_for_change(generation, remaining) - { + let remaining = blocking_deadline + .as_mut() + .and_then(|deadline| deadline.wait_slice()) + .or_else(|| { + deadline + .map(|deadline| deadline.saturating_duration_since(Instant::now())) + }); + let changed = !matches!(remaining, Some(duration) if duration.is_zero()) + && self.poll_notifier.wait_for_change(generation, remaining); + if !changed { + if blocking_deadline + .as_mut() + .is_some_and(|deadline| !deadline.expired()) + { + continue; + } return Err(KernelError::new( "EAGAIN", "blocking socket receive timed out; raise limits.resources.maxBlockingReadMs", @@ -3957,7 +5584,7 @@ impl KernelVm { #[cfg(not(target_arch = "wasm32"))] pub fn set_socket_resource_ledger( &mut self, - resources: Arc, + resources: Arc, ) -> KernelResult<()> { self.sockets.set_resource_ledger(resources)?; Ok(()) @@ -4449,7 +6076,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -4682,6 +6309,20 @@ impl KernelVm { ) -> KernelResult { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; + match self.processes.permission_tier(pid)? { + ProcessPermissionTier::Isolated => { + return Err(KernelError::new( + "EACCES", + "isolated process tier has no filesystem path capability", + )); + } + ProcessPermissionTier::ReadOnly if open_requires_write_access(flags) => { + return Err(read_only_filesystem_error(path)); + } + ProcessPermissionTier::ReadOnly + | ProcessPermissionTier::ReadWrite + | ProcessPermissionTier::Full => {} + } self.validate_fd_open_flags(pid, path, flags)?; if let Some(existing_fd) = parse_dev_fd_path(path)? { { @@ -4705,7 +6346,7 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(existing_fd))?; return Ok(table.dup_with_status_flags( existing_fd, - Some(entry.status_flags | (flags & O_NONBLOCK)), + Some(entry.status_flags.get() | (flags & O_NONBLOCK)), )?); } @@ -4777,7 +6418,13 @@ impl KernelVm { self.resources .check_fd_allocation(&self.resource_snapshot(), 1)?; let timeout = self.blocking_read_timeout(); - let pipe = self.pipes.open_named_pipe(key, path, flags, timeout)?; + let pipe = self.pipes.open_named_pipe_with_deadline( + key, + path, + flags, + timeout, + self.resources.blocking_read_deadline(), + )?; let mut tables = lock_or_recover(&self.fd_tables); let table = tables .get_mut(pid) @@ -4812,94 +6459,256 @@ impl KernelVm { Ok(table.open_with_details(&description_path, flags, filetype, lock_target)?) } - pub fn fd_open_tmpfile( + /// Open a descriptor under the kernel-owned Preview1 capability model. + /// Parent/tier/access validation is complete before `fd_open` can create + /// or truncate a path. `None` requests Linux-style synthesized rights. + pub fn fd_open_with_rights( &mut self, requester_driver: &str, pid: u32, - directory: &str, + parent_fd: Option, + path: &str, flags: u32, - mode: u32, - linkable: bool, + mode: Option, + requested_rights: Option<(u64, u64)>, ) -> KernelResult { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; - if flags & 0b11 == crate::fd_table::O_RDONLY { - return Err(KernelError::new( - "EINVAL", - "O_TMPFILE requires write access", - )); - } - let directory_stat = self.stat_for_process(requester_driver, pid, directory)?; - if !directory_stat.is_directory { + let tier = self.processes.permission_tier(pid)?; + + if requested_rights.is_some() && parent_fd.is_none() { return Err(KernelError::new( - "ENOTDIR", - format!("O_TMPFILE target is not a directory: {directory}"), + "EACCES", + "explicit WASI open rights require a kernel directory capability", )); } - self.check_dac_access(pid, directory, DAC_WRITE | DAC_EXECUTE)?; - self.reject_read_only_resolved_write_path(directory)?; - let hidden_path = loop { - let id = self.next_unnamed_file_id; - self.next_unnamed_file_id = - self.next_unnamed_file_id.checked_add(1).ok_or_else(|| { - KernelError::new("EMFILE", "unnamed-file identifier space exhausted") - })?; - let candidate = normalize_path(&format!("{directory}/{UNNAMED_FILE_PREFIX}{pid}-{id}")); - if !self.exists_internal(Some(pid), &candidate)? { - break candidate; + if let Some(parent_fd) = parent_fd { + let parent = self.fd_stat(requester_driver, pid, parent_fd)?; + if parent.filetype != FILETYPE_DIRECTORY { + return Err(KernelError::new( + "ENOTDIR", + format!("path_open parent fd {parent_fd} is not a directory"), + )); } - }; - - let fd = self.fd_open( - requester_driver, - pid, - &hidden_path, - (flags & !O_TRUNC) | O_CREAT | O_EXCL, - Some(mode), - )?; - let description_id = self.description_for_fd(requester_driver, pid, fd)?.id(); - self.unnamed_files.insert( - description_id, - UnnamedFile { - path: hidden_path, - linkable, - }, - ); - Ok(fd) - } - - pub fn fd_link_tmpfile_for_process( - &mut self, - requester_driver: &str, - pid: u32, - fd: u32, - destination: &str, - ) -> KernelResult<()> { - self.assert_not_terminated()?; - self.assert_driver_owns(requester_driver, pid)?; - let description = self.description_for_fd(requester_driver, pid, fd)?; - let source = if let Some(unnamed) = self.unnamed_files.get(&description.id()) { - if !unnamed.linkable { + if parent.rights & WASI_RIGHT_PATH_OPEN == 0 { return Err(KernelError::new( - "ENOENT", - "O_EXCL unnamed files cannot be linked", + "EACCES", + format!("path_open parent fd {parent_fd} lacks PATH_OPEN"), )); } - unnamed.path.clone() - } else { - description.path().to_string() - }; - self.check_dac_parent_access(pid, destination, DAC_WRITE | DAC_EXECUTE)?; - self.link(&source, destination) - } + if let Some((base, inheriting)) = requested_rights { + let unavailable = (base | inheriting) & !parent.rights_inheriting; + if unavailable != 0 { + return Err(KernelError::new("EACCES", format!( + "path_open requested rights {unavailable:#x} outside parent fd {parent_fd} inheriting rights" + ))); + } + } - pub fn fd_read( - &mut self, - requester_driver: &str, - pid: u32, - fd: u32, - length: usize, + let capability_root = self + .realpath_internal(Some(pid), &self.fd_path(requester_driver, pid, parent_fd)?)?; + let candidate = normalize_path(path); + if !path_is_within(&capability_root, &candidate) { + return Err(KernelError::new("EACCES", format!( + "path_open target '{candidate}' escapes directory capability '{capability_root}'" + ))); + } + let resolved_candidate = if self.exists_internal(Some(pid), &candidate)? { + self.realpath_internal(Some(pid), &candidate)? + } else { + let candidate_parent = + self.realpath_internal(Some(pid), &parent_path(&candidate))?; + normalize_path(&format!( + "{candidate_parent}/{}", + candidate.rsplit('/').next().unwrap_or_default() + )) + }; + if !path_is_within(&capability_root, &resolved_candidate) { + return Err(KernelError::new("EACCES", format!( + "path_open resolved target '{resolved_candidate}' escapes directory capability '{capability_root}'" + ))); + } + } + + if let Some((base, inheriting)) = requested_rights { + let requested = base | inheriting; + let (tier_base, tier_inheriting) = + wasi_preopen_rights(tier, true).ok_or_else(|| { + KernelError::new( + "EACCES", + "isolated process tier has no filesystem path capability", + ) + })?; + let unavailable = (base & !tier_base) | (inheriting & !tier_inheriting); + if unavailable != 0 { + return Err(KernelError::new( + "EACCES", + format!( + "permission tier {tier:?} denies requested WASI rights {unavailable:#x}" + ), + )); + } + if tier == ProcessPermissionTier::ReadOnly && requested & WASI_WRITE_RIGHTS != 0 { + return Err(read_only_filesystem_error(path)); + } + if tier == ProcessPermissionTier::ReadWrite + && requested & WASI_NAMESPACE_DESTRUCTIVE_RIGHTS != 0 + { + return Err(KernelError::new( + "EACCES", + format!( + "read-write permission tier denies namespace rights {:#x}", + requested & WASI_NAMESPACE_DESTRUCTIVE_RIGHTS + ), + )); + } + if flags & (O_WRONLY | O_RDWR) == O_RDONLY && base & WASI_RIGHT_FD_WRITE != 0 { + return Err(KernelError::new( + "EACCES", + "read-only open flags cannot grant FD_WRITE", + )); + } + if flags & (O_WRONLY | O_RDWR) == O_WRONLY && base & WASI_RIGHT_FD_READ != 0 { + return Err(KernelError::new( + "EACCES", + "write-only open flags cannot grant FD_READ", + )); + } + } + + let source_rights = if parse_dev_fd_path(path)?.is_some() { + let source_fd = parse_dev_fd_path(path)?.expect("checked above"); + let source = self.fd_stat(requester_driver, pid, source_fd)?; + Some((source.rights, source.rights_inheriting)) + } else { + None + }; + let fd = self.fd_open(requester_driver, pid, path, flags, mode)?; + if let Some((requested_base, requested_inheriting)) = requested_rights { + let stat = self.fd_stat(requester_driver, pid, fd)?; + let (source_base, source_inheriting) = source_rights.unwrap_or((u64::MAX, u64::MAX)); + let effective_base = requested_base & source_base; + let effective_inheriting = if stat.filetype == FILETYPE_DIRECTORY { + requested_inheriting & source_inheriting + } else { + 0 + }; + let result = { + let mut tables = lock_or_recover(&self.fd_tables); + tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .set_rights(fd, effective_base, effective_inheriting) + }; + if let Err(error) = result { + let _closed = self.fd_close(requester_driver, pid, fd); + return Err(error.into()); + } + } + Ok(fd) + } + + pub fn fd_open_tmpfile( + &mut self, + requester_driver: &str, + pid: u32, + directory: &str, + flags: u32, + mode: u32, + linkable: bool, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + match self.processes.permission_tier(pid)? { + ProcessPermissionTier::Isolated => { + return Err(KernelError::new( + "EACCES", + "isolated process tier has no filesystem path capability", + )); + } + ProcessPermissionTier::ReadOnly => { + return Err(read_only_filesystem_error(directory)); + } + ProcessPermissionTier::ReadWrite | ProcessPermissionTier::Full => {} + } + if flags & 0b11 == crate::fd_table::O_RDONLY { + return Err(KernelError::new( + "EINVAL", + "O_TMPFILE requires write access", + )); + } + let directory_stat = self.stat_for_process(requester_driver, pid, directory)?; + if !directory_stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("O_TMPFILE target is not a directory: {directory}"), + )); + } + self.check_dac_access(pid, directory, DAC_WRITE | DAC_EXECUTE)?; + self.reject_read_only_resolved_write_path(directory)?; + + let hidden_path = loop { + let id = self.next_unnamed_file_id; + self.next_unnamed_file_id = + self.next_unnamed_file_id.checked_add(1).ok_or_else(|| { + KernelError::new("EMFILE", "unnamed-file identifier space exhausted") + })?; + let candidate = normalize_path(&format!("{directory}/{UNNAMED_FILE_PREFIX}{pid}-{id}")); + if !self.exists_internal(Some(pid), &candidate)? { + break candidate; + } + }; + + let fd = self.fd_open( + requester_driver, + pid, + &hidden_path, + (flags & !O_TRUNC) | O_CREAT | O_EXCL, + Some(mode), + )?; + let description_id = self.description_for_fd(requester_driver, pid, fd)?.id(); + self.unnamed_files.insert( + description_id, + UnnamedFile { + path: hidden_path, + linkable, + }, + ); + Ok(fd) + } + + pub fn fd_link_tmpfile_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + destination: &str, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let description = self.description_for_fd(requester_driver, pid, fd)?; + let source = if let Some(unnamed) = self.unnamed_files.get(&description.id()) { + if !unnamed.linkable { + return Err(KernelError::new( + "ENOENT", + "O_EXCL unnamed files cannot be linked", + )); + } + unnamed.path.clone() + } else { + description.path().to_string() + }; + self.check_dac_parent_access(pid, destination, DAC_WRITE | DAC_EXECUTE)?; + self.link(&source, destination) + } + + pub fn fd_read( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + length: usize, ) -> KernelResult> { Ok(self .fd_read_with_timeout_result(requester_driver, pid, fd, length, None)? @@ -4929,23 +6738,39 @@ impl KernelVm { if let Some(socket_id) = self.fd_socket_id(&entry.description) { self.resources.check_pread_length(length)?; - let nonblocking = (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0; + let nonblocking = + (entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK != 0; let wait = if nonblocking { Some(Duration::ZERO) } else { timeout.or_else(|| self.blocking_read_timeout()) }; + let mut blocking_deadline = (!nonblocking && timeout.is_none()) + .then(|| self.resources.blocking_read_deadline()) + .flatten(); let deadline = wait.map(|wait| Instant::now() + wait); let result = loop { let generation = self.poll_notifier.snapshot(); match self.sockets.read(socket_id, length) { Ok(result) => break result, Err(error) if error.code() == "EAGAIN" && !nonblocking => { - let remaining = deadline - .map(|deadline| deadline.saturating_duration_since(Instant::now())); - if matches!(remaining, Some(duration) if duration.is_zero()) - || !self.poll_notifier.wait_for_change(generation, remaining) - { + let remaining = blocking_deadline + .as_mut() + .and_then(|deadline| deadline.wait_slice()) + .or_else(|| { + deadline.map(|deadline| { + deadline.saturating_duration_since(Instant::now()) + }) + }); + let changed = !matches!(remaining, Some(duration) if duration.is_zero()) + && self.poll_notifier.wait_for_change(generation, remaining); + if !changed { + if blocking_deadline + .as_mut() + .is_some_and(|deadline| !deadline.expired()) + { + continue; + } return Err(KernelError::new( "EAGAIN", "blocking socket read timed out; raise limits.resources.maxBlockingReadMs", @@ -4963,27 +6788,39 @@ impl KernelVm { } if self.pipes.is_pipe(entry.description.id()) { - let result = self.pipes.read_with_timeout( + let blocking_deadline = + ((entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK == 0 + && timeout.is_none()) + .then(|| self.resources.blocking_read_deadline()) + .flatten(); + let result = self.pipes.read_with_timeout_and_deadline( entry.description.id(), length, - if (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0 { + if (entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK != 0 { Some(Duration::ZERO) } else { timeout.or_else(|| self.blocking_read_timeout()) }, + blocking_deadline, )?; return Ok(result); } if self.ptys.is_pty(entry.description.id()) { - return Ok(self.ptys.read_with_timeout( + let blocking_deadline = + ((entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK == 0 + && timeout.is_none()) + .then(|| self.resources.blocking_read_deadline()) + .flatten(); + return Ok(self.ptys.read_with_timeout_and_deadline( entry.description.id(), length, - if (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0 { + if (entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK != 0 { Some(Duration::ZERO) } else { timeout.or_else(|| self.blocking_read_timeout()) }, + blocking_deadline, )?); } @@ -5089,7 +6926,7 @@ impl KernelVm { entry.description.id(), data, force_nonblocking - || (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0, + || (entry.description.flags() | entry.status_flags.get()) & O_NONBLOCK != 0, ) { Ok(bytes) => Ok(bytes), Err(error) => { @@ -5236,6 +7073,10 @@ impl KernelVm { crate::poll::PollWaitHandle::new(self.poll_notifier.clone()) } + pub fn process_wait_handle(&self) -> crate::process_table::ProcessWaitHandle { + self.processes.wait_handle() + } + pub fn poll_targets( &self, requester_driver: &str, @@ -5624,6 +7465,91 @@ impl KernelVm { Ok(()) } + /// Close every descriptor at or above `min_fd` under one descriptor-table + /// lock. Linux closefrom(2) ignores holes; special-resource retirement is + /// performed after the atomic table mutation so no concurrent observer can + /// see a partially closed descriptor range. + pub fn fd_close_from( + &mut self, + requester_driver: &str, + pid: u32, + min_fd: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + self.fd_close_matching(pid, |entry| entry.fd >= min_fd) + } + + /// Close one exact set of canonical kernel descriptors as a single table + /// mutation. Missing descriptors are ignored, and private preopen roots + /// remain installed. Executor adapters use this when their display-fd + /// namespace is not identical to the kernel descriptor namespace. + pub fn fd_close_exact( + &mut self, + requester_driver: &str, + pid: u32, + fds: impl IntoIterator, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let fds = fds.into_iter().collect::>(); + self.fd_close_matching(pid, |entry| fds.contains(&entry.fd)) + } + + fn fd_close_matching( + &mut self, + pid: u32, + mut should_close: impl FnMut(&FdEntry) -> bool, + ) -> KernelResult> { + let closed_entries = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let fds = table + .iter() + // WASI preopens are private capability roots used to service + // pathname operations, not guest-visible Linux descriptors. + // The executor retires only their untagged public aliases; + // closefrom must preserve the backing roots. + .filter_map(|entry| { + (should_close(entry) && entry.wasi_preopen_path.is_none()).then_some(entry.fd) + }) + .collect::>(); + let mut closed = Vec::with_capacity(fds.len()); + for fd in fds { + let entry = table + .get(fd) + .cloned() + .expect("closefrom snapshot must reference an open descriptor"); + let removed = table.close(fd); + debug_assert!(removed); + closed.push((fd, entry.description, entry.filetype)); + } + closed + }; + + let mut first_cleanup_error = None; + let mut closed_fds = Vec::with_capacity(closed_entries.len()); + for (fd, description, filetype) in closed_entries { + closed_fds.push(fd); + if let Some(target) = description.lock_target() { + self.file_locks.release_process_target(pid, target); + } + self.close_special_resource_if_needed(&description, filetype); + if let Err(error) = self.cleanup_unnamed_file_if_closed(&description) { + eprintln!( + "ERR_AGENTOS_CLOSE_MULTIPLE_CLEANUP: pid={pid} fd={fd} cleanup failed: {error}" + ); + if first_cleanup_error.is_none() { + first_cleanup_error = Some(error); + } + } + } + if let Some(error) = first_cleanup_error { + return Err(error); + } + Ok(closed_fds) + } + /// Commit the descriptor half of exec by closing every descriptor still /// marked `FD_CLOEXEC` after POSIX spawn file actions have completed. pub fn close_process_cloexec_fds( @@ -5710,7 +7636,7 @@ impl KernelVm { .and_then(|table| table.get(fd)) .cloned() .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; - if entry.filetype != FILETYPE_PIPE { + if !self.pipes.is_pipe(entry.description.id()) { return Err(KernelError::new( "EINVAL", format!("fd {fd} is not a named pipe"), @@ -5967,7 +7893,7 @@ impl KernelVm { .cloned() .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - Ok(self.ptys.is_slave(entry.description.id())) + Ok(self.ptys.is_pty(entry.description.id())) } pub fn pty_window_size( @@ -6077,6 +8003,11 @@ impl KernelVm { Ok(self.ptys.get_foreground_pgid(description.id())?) } + pub fn tcgetsid(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { + let _ = self.description_for_fd(requester_driver, pid, fd)?; + Ok(self.processes.getsid(pid)?) + } + pub fn pty_resize( &self, requester_driver: &str, @@ -6191,6 +8122,45 @@ impl KernelVm { Ok(self.processes.sigpending(pid)?) } + pub fn signal_action( + &self, + requester_driver: &str, + pid: u32, + signal: i32, + action: Option, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.signal_action(pid, signal, action)?) + } + + pub fn begin_signal_delivery( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.begin_signal_delivery(pid)?) + } + + pub fn end_signal_delivery( + &self, + requester_driver: &str, + pid: u32, + token: u64, + ) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.end_signal_delivery(pid, token)?) + } + + pub fn reset_signal_actions_for_exec( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + Ok(self.processes.reset_signal_actions_for_exec(pid)?) + } + pub fn getppid(&self, requester_driver: &str, pid: u32) -> KernelResult { self.assert_driver_owns(requester_driver, pid)?; Ok(self.processes.getppid(pid)?) @@ -6650,41 +8620,85 @@ impl KernelVm { entry: &crate::fd_table::FdEntry, requested: PollEvents, ) -> KernelResult { + // Linux exposes POLLRDNORM/POLLWRNORM as distinct public bits even + // though they use the same underlying readiness as POLLIN/POLLOUT. + // Normalize for every kernel object, then project readiness back onto + // exactly the aliases the caller requested (plus unconditional + // POLLERR/POLLHUP). + let normalized_requested = PollEvents::from_bits( + requested.bits() + | if requested.intersects(POLLRDNORM) { + POLLIN.bits() + } else { + 0 + } + | if requested.intersects(POLLWRNORM) { + POLLOUT.bits() + } else { + 0 + }, + ); + let project_aliases = |events: PollEvents| { + let mut bits = events.bits(); + if events.intersects(POLLIN) { + if requested.intersects(POLLRDNORM) { + bits |= POLLRDNORM.bits(); + } + if !requested.intersects(POLLIN) { + bits &= !POLLIN.bits(); + } + } + if events.intersects(POLLOUT) { + if requested.intersects(POLLWRNORM) { + bits |= POLLWRNORM.bits(); + } + if !requested.intersects(POLLOUT) { + bits &= !POLLOUT.bits(); + } + } + PollEvents::from_bits(bits) + }; if let Some(socket_id) = self.fd_socket_id(&entry.description) { let socket = self .sockets .get(socket_id) .ok_or_else(|| KernelError::bad_file_descriptor(entry.fd))?; - let mut events = self.sockets.poll(socket_id, requested)?; + let mut events = self.sockets.poll(socket_id, normalized_requested)?; if events.intersects(POLLOUT) && !self.socket_pollout_has_resource_capacity(&socket) { events = PollEvents::from_bits(events.bits() & !POLLOUT.bits()); } - return Ok(events); + return Ok(project_aliases(events)); } if self.pipes.is_pipe(entry.description.id()) { - return Ok(self.pipes.poll(entry.description.id(), requested)?); + return Ok(project_aliases( + self.pipes + .poll(entry.description.id(), normalized_requested)?, + )); } if self.ptys.is_pty(entry.description.id()) { - return Ok(self.ptys.poll(entry.description.id(), requested)?); + return Ok(project_aliases( + self.ptys + .poll(entry.description.id(), normalized_requested)?, + )); } let access_mode = entry.description.flags() & 0b11; let mut events = PollEvents::empty(); - if requested.intersects(POLLIN) && access_mode != crate::fd_table::O_WRONLY { + if normalized_requested.intersects(POLLIN) && access_mode != crate::fd_table::O_WRONLY { events |= POLLIN; } - if requested.intersects(POLLOUT) && access_mode != crate::fd_table::O_RDONLY { + if normalized_requested.intersects(POLLOUT) && access_mode != crate::fd_table::O_RDONLY { events |= POLLOUT; } - if entry.filetype == FILETYPE_DIRECTORY && requested.intersects(POLLOUT) { + if entry.filetype == FILETYPE_DIRECTORY && normalized_requested.intersects(POLLOUT) { events |= POLLERR; } if self.terminated { events |= POLLHUP; } - Ok(events) + Ok(project_aliases(events)) } fn description_for_fd( @@ -6764,9 +8778,11 @@ impl KernelVm { } stat.nlink = 0; let data = self.filesystem.read_file(path)?; + let xattrs = self.snapshot_xattrs(path)?; let backing: SharedAnonymousFile = Arc::new(Mutex::new(AnonymousFile::new( data, stat, + xattrs, Arc::clone(&self.anonymous_file_usage), ))); Ok(Some(OpenFileRemovalBacking::Anonymous { @@ -6787,9 +8803,18 @@ impl KernelVm { while let Some((directory, depth)) = queue.pop_front() { self.resources.check_recursive_fs_depth(depth)?; - let names = self + let names_result = self .raw_filesystem_mut() - .read_dir_limited(&directory, per_directory_limit)?; + .read_dir_limited(&directory, per_directory_limit); + let names = match names_result { + Ok(names) => names, + Err(error) if error.code() == "ENOMEM" => { + self.resources + .check_readdir_entries(per_directory_limit.saturating_add(1))?; + return Err(error.into()); + } + Err(error) => return Err(error.into()), + }; self.resources.check_readdir_entries(names.len())?; for name in names { if matches!(name.as_str(), "." | "..") { @@ -6815,11 +8840,19 @@ impl KernelVm { } fn prepare_detached_directory_backing( - &self, + &mut self, path: &str, stat: Option<&VirtualStat>, - ) -> Option<(Vec>, VirtualStat)> { - let mut stat = stat.filter(|stat| stat.is_directory)?.clone(); + ) -> KernelResult< + Option<( + Vec>, + VirtualStat, + BTreeMap>, + )>, + > { + let Some(mut stat) = stat.filter(|stat| stat.is_directory).cloned() else { + return Ok(None); + }; stat.nlink = 0; let descriptions = self .open_file_descriptions() @@ -6831,7 +8864,20 @@ impl KernelVm { .is_some_and(|target| target.ino() == stat.ino) }) .collect::>(); - (!descriptions.is_empty()).then_some((descriptions, stat)) + if descriptions.is_empty() { + return Ok(None); + } + let xattrs = self.snapshot_xattrs(path)?; + Ok(Some((descriptions, stat, xattrs))) + } + + fn snapshot_xattrs(&mut self, path: &str) -> KernelResult>> { + let mut snapshot = BTreeMap::new(); + for name in self.filesystem.list_xattrs(path, true)? { + let value = self.filesystem.get_xattr(path, &name, true)?; + snapshot.insert(name, value); + } + Ok(snapshot) } fn rename_open_file_descriptions(&self, old_path: &str, new_path: &str) { @@ -7300,7 +9346,15 @@ impl KernelVm { } if let Some(limit) = self.resources.max_readdir_entries() { - Ok(self.filesystem.read_dir_limited(path, limit)?) + match self.filesystem.read_dir_limited(path, limit) { + Ok(entries) => Ok(entries), + Err(error) if error.code() == "ENOMEM" => { + self.resources + .check_readdir_entries(limit.saturating_add(1))?; + Err(error.into()) + } + Err(error) => Err(error.into()), + } } else { Ok(self.filesystem.read_dir(path)?) } @@ -7767,8 +9821,12 @@ impl KernelVm { fn proc_version_bytes(&self) -> Vec { format!( - "Linux version 6.8.0-agentos (agentos@localhost) #1 SMP boot={}\n", - self.boot_time_ms + "{} version {} (agentos@{}) {} boot={}\n", + self.system_identity.os_type, + self.system_identity.os_release, + self.system_identity.hostname, + self.system_identity.os_version, + self.boot_time_ms, ) .into_bytes() } @@ -8001,7 +10059,60 @@ impl KernelVm { Ok(()) } - fn check_dac_access(&mut self, pid: u32, path: &str, access: u32) -> KernelResult<()> { + fn check_execute_dac_traversal(&mut self, pid: u32, path: &str) -> KernelResult<()> { + if is_proc_path(path) { + return Ok(()); + } + let identity = self + .processes + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .identity; + let normalized = normalize_path(path); + let components = normalized + .split('/') + .filter(|component| !component.is_empty()) + .collect::>(); + let mut current = String::from("/"); + for component in components.iter().take(components.len().saturating_sub(1)) { + current = join_child_path(¤t, component); + let stat = self.raw_filesystem_mut().stat(¤t)?; + if !stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("path component is not a directory: {current}"), + )); + } + self.check_execute_dac_mode_with_acl(&identity, &stat, DAC_EXECUTE, ¤t)?; + } + Ok(()) + } + + fn check_execute_dac_mode_with_acl( + &mut self, + identity: &ProcessIdentity, + stat: &VirtualStat, + access: u32, + path: &str, + ) -> KernelResult<()> { + if identity.euid == 0 { + return check_dac_mode(identity, stat, access, path); + } + match self.read_posix_acl_raw(path, POSIX_ACL_ACCESS)? { + Some(acl) => acl.check_access(identity, stat, access, path), + None => check_dac_mode(identity, stat, access, path), + } + } + + fn read_posix_acl_raw(&mut self, path: &str, name: &str) -> KernelResult> { + match self.raw_filesystem_mut().get_xattr(path, name, true) { + Ok(value) => PosixAcl::parse(&value, path).map(Some), + Err(error) if matches!(error.code(), "ENODATA" | "EOPNOTSUPP") => Ok(None), + Err(error) => Err(error.into()), + } + } + + fn check_dac_access(&mut self, pid: u32, path: &str, access: u32) -> KernelResult<()> { if is_proc_path(path) { return Ok(()); } @@ -8062,6 +10173,28 @@ impl KernelVm { } } + fn check_detached_fd_dac( + &mut self, + identity: &ProcessIdentity, + stat: &VirtualStat, + description: &FileDescription, + access: u32, + label: &str, + ) -> KernelResult<()> { + if identity.euid == 0 { + return check_dac_mode(identity, stat, access, label); + } + let acl = description + .detached_xattrs() + .and_then(|xattrs| xattrs.get(POSIX_ACL_ACCESS).cloned()) + .map(|value| PosixAcl::parse(&value, label)) + .transpose()?; + match acl { + Some(acl) => acl.check_access(identity, stat, access, label), + None => check_dac_mode(identity, stat, access, label), + } + } + fn read_posix_acl(&mut self, path: &str, name: &str) -> KernelResult> { match self.filesystem.get_xattr(path, name, true) { Ok(value) => PosixAcl::parse(&value, path).map(Some), @@ -8442,6 +10575,10 @@ impl KernelVm { } } +fn pty_signal_error_is_stale(error: &ProcessTableError) -> bool { + error.code() == "ESRCH" +} + impl KernelVm { fn check_mount_permissions(&self, path: &str) -> KernelResult<()> { self.filesystem @@ -8535,6 +10672,26 @@ impl KernelVm { .root_virtual_filesystem_mut::() } + /// Complete trusted root bootstrap and activate the guest-visible + /// read-only mount policy in one kernel-owned transition. + pub fn finish_root_filesystem_bootstrap(&mut self) -> KernelResult<()> { + let read_only = match self.root_filesystem_mut() { + Some(root) => root.is_read_only_mode(), + None => return Ok(()), + }; + if read_only { + self.filesystem + .inner_mut() + .inner_mut() + .remount("/", "remount,ro") + .map_err(KernelError::from)?; + } + self.root_filesystem_mut() + .expect("root filesystem remained available across remount") + .finish_bootstrap(); + Ok(()) + } + pub fn snapshot_root_filesystem(&mut self) -> KernelResult { let usage = self.filesystem_usage()?; self.resources @@ -8588,79 +10745,6 @@ impl KernelVm { } } -#[derive(Default)] -struct StubDriverState { - exit_code: Option, - on_exit: Option, - kill_signals: Vec, -} - -#[derive(Default)] -struct StubDriverProcess { - state: Mutex, - waiters: Condvar, -} - -impl StubDriverProcess { - fn finish(&self, exit_code: i32) { - let callback = { - let mut state = lock_or_recover(&self.state); - if state.exit_code.is_some() { - return; - } - state.exit_code = Some(exit_code); - self.waiters.notify_all(); - state.on_exit.clone() - }; - - if let Some(callback) = callback { - callback(exit_code); - } - } - - fn kill_signals(&self) -> Vec { - lock_or_recover(&self.state).kill_signals.clone() - } -} - -impl DriverProcess for StubDriverProcess { - fn kill(&self, signal: i32) { - { - let mut state = lock_or_recover(&self.state); - state.kill_signals.push(signal); - } - if matches!( - signal, - crate::process_table::SIGCHLD | SIGCONT | SIGSTOP | SIGTSTP | SIGWINCH - ) { - return; - } - self.finish(128 + signal); - } - - fn wait(&self, timeout: Duration) -> Option { - let state = lock_or_recover(&self.state); - if let Some(code) = state.exit_code { - return Some(code); - } - - let (state, _) = wait_timeout_or_recover(&self.waiters, state, timeout); - state.exit_code - } - - fn set_on_exit(&self, callback: ProcessExitCallback) { - let maybe_exit = { - let mut state = lock_or_recover(&self.state); - state.on_exit = Some(callback.clone()); - state.exit_code - }; - - if let Some(code) = maybe_exit { - callback(code); - } - } -} - fn unix_socket_absolute_components( cwd: &str, path: &str, @@ -8954,17 +11038,6 @@ fn lock_or_recover<'a, T>(mutex: &'a Mutex) -> MutexGuard<'a, T> { } } -fn wait_timeout_or_recover<'a, T>( - condvar: &Condvar, - guard: MutexGuard<'a, T>, - timeout: Duration, -) -> (MutexGuard<'a, T>, WaitTimeoutResult) { - match condvar.wait_timeout(guard, timeout) { - Ok(result) => result, - Err(poisoned) => poisoned.into_inner(), - } -} - fn is_sensitive_mount_path(path: &str) -> bool { let normalized = crate::vfs::normalize_path(path); normalized == "/" @@ -9117,6 +11190,16 @@ fn parent_path(path: &str) -> String { } } +fn path_is_within(root: &str, candidate: &str) -> bool { + let root = normalize_path(root); + let candidate = normalize_path(candidate); + root == "/" + || candidate == root + || candidate + .strip_prefix(&root) + .is_some_and(|suffix| suffix.starts_with('/')) +} + fn required_dirent_ino(path: &str, ino: u64) -> KernelResult { if ino == 0 { return Err(KernelError::new( @@ -9707,52 +11790,1175 @@ fn proc_not_found_error(path: &str) -> KernelError { ) } -fn read_only_filesystem_error(path: &str) -> KernelError { - KernelError::new("EROFS", format!("read-only filesystem: {path}")) -} +fn read_only_filesystem_error(path: &str) -> KernelError { + KernelError::new("EROFS", format!("read-only filesystem: {path}")) +} + +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} + +impl Drop for KernelVm { + fn drop(&mut self) { + if !self.terminated { + dispose_kernel_vm_resources(self); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fd_table::{FD_CLOEXEC, F_GETFD, F_SETFD, O_RDONLY}; + use crate::process_table::SIGTERM; + use crate::root_fs::{FilesystemEntry, RootFilesystemDescriptor, RootFilesystemMode}; + use crate::vfs::MemoryFileSystem; + use std::fs; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::thread; + + #[test] + fn pty_signal_error_classification_only_suppresses_stale_process_groups() { + let process_table = ProcessTable::new(); + let stale_group = process_table + .kill(-7, SIGTERM) + .expect_err("missing foreground process group must be stale"); + assert_eq!(stale_group.code(), "ESRCH"); + assert!(pty_signal_error_is_stale(&stale_group)); + + let invalid_signal = process_table + .kill(-7, i32::MAX) + .expect_err("invalid signal must remain diagnostic"); + assert_eq!(invalid_signal.code(), "EINVAL"); + assert!(!pty_signal_error_is_stale(&invalid_signal)); + } + + #[test] + fn finishing_read_only_root_bootstrap_seals_storage_and_mount_policy() { + let root = RootFileSystem::from_descriptor(RootFilesystemDescriptor { + mode: RootFilesystemMode::ReadOnly, + disable_default_base_layer: true, + lowers: Vec::new(), + bootstrap_entries: vec![FilesystemEntry::file("/bin/node", b"bootstrap".to_vec())], + }) + .expect("build read-only root"); + let mut config = KernelVmConfig::new("vm-read-only-bootstrap-transition"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MountTable::new(root), config); + + kernel + .write_file("/bin/node", b"trusted-bootstrap".to_vec()) + .expect("trusted bootstrap remains writable before finish"); + kernel + .finish_root_filesystem_bootstrap() + .expect("finish root bootstrap"); + + assert!( + kernel + .mounted_filesystems() + .into_iter() + .find(|mount| mount.path == "/") + .expect("root mount") + .read_only + ); + assert_eq!( + kernel.read_file("/bin/node").expect("read sealed root"), + b"trusted-bootstrap".to_vec() + ); + assert_eq!( + kernel + .write_file("/bin/node", b"guest-write".to_vec()) + .expect_err("sealed root rejects writes") + .code(), + "EROFS" + ); + } + + fn kernel_with_process() -> (KernelVm, KernelProcessHandle) { + let mut config = KernelVmConfig::new("vm-fd-socket-test"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("wasm", ["socket-test"])) + .expect("register wasm driver"); + let process = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + ..SpawnOptions::default() + }, + ) + .expect("spawn socket test process"); + (kernel, process) + } + + #[test] + fn replace_driver_retires_only_obsolete_generated_command_stubs() { + let mut config = KernelVmConfig::new("vm-replace-driver"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("runtime", ["old", "keep"])) + .expect("register initial runtime driver"); + assert!(kernel.exists("/bin/old").expect("stat old stub")); + kernel + .write_file("/bin/keep", b"guest replacement".to_vec()) + .expect("replace generated stub"); + + kernel + .replace_driver(CommandDriver::new("runtime", ["keep", "new"])) + .expect("replace runtime driver command set"); + assert!(!kernel.exists("/bin/old").expect("stat retired stub")); + assert!(kernel.exists("/bin/new").expect("stat new stub")); + assert_eq!( + kernel.read_file("/bin/keep").expect("read preserved file"), + b"guest replacement".to_vec() + ); + assert!(kernel.resolve_registered_command_path("/bin/old").is_none()); + assert_eq!( + kernel + .commands + .resolve("new") + .expect("new command resolves") + .name(), + "runtime" + ); + + kernel + .replace_driver(CommandDriver::new("runtime", std::iter::empty::<&str>())) + .expect("remove runtime driver commands"); + assert!( + kernel.exists("/bin/keep").expect("stat preserved file"), + "guest replacement is preserved" + ); + assert!(!kernel.exists("/bin/new").expect("stat removed new stub")); + } + + #[test] + fn closefrom_closes_one_atomic_sorted_descriptor_range() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .write_file("/one", b"1".to_vec()) + .expect("create first file"); + kernel + .write_file("/two", b"2".to_vec()) + .expect("create second file"); + let first = kernel + .fd_open("wasm", pid, "/one", O_RDONLY, None) + .expect("open first fd"); + let second = kernel + .fd_open("wasm", pid, "/two", O_RDONLY, None) + .expect("open second fd"); + let high = kernel + .fd_fcntl("wasm", pid, first, F_DUPFD, 12) + .expect("duplicate high fd"); + + let closed = kernel + .fd_close_from("wasm", pid, second) + .expect("close descriptor range"); + assert_eq!(closed, vec![second, high]); + assert!(kernel.fd_stat("wasm", pid, first).is_ok()); + assert_eq!( + kernel.fd_stat("wasm", pid, second).unwrap_err().code(), + "EBADF" + ); + assert_eq!( + kernel.fd_stat("wasm", pid, high).unwrap_err().code(), + "EBADF" + ); + } + + #[test] + fn closefrom_preserves_private_wasi_preopen_roots() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .mkdir("/workspace", true) + .expect("create default workspace preopen"); + let preopens = kernel + .initialize_wasi_preopens("wasm", pid) + .expect("initialize private preopen roots"); + assert!(!preopens.is_empty()); + kernel + .write_file("/ordinary", b"data".to_vec()) + .expect("create ordinary file"); + let ordinary = kernel + .fd_open("wasm", pid, "/ordinary", O_RDONLY, None) + .expect("open ordinary descriptor"); + + let closed = kernel + .fd_close_from("wasm", pid, 3) + .expect("close guest descriptor range"); + assert_eq!(closed, vec![ordinary]); + for preopen in preopens { + kernel + .fd_stat("wasm", pid, preopen.fd) + .expect("private preopen backing remains live"); + } + } + + #[test] + fn exact_bulk_close_does_not_translate_a_display_fd_cutoff() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .write_file("/canonical-three", b"3".to_vec()) + .expect("create low canonical descriptor file"); + kernel + .write_file("/unrelated-high", b"64".to_vec()) + .expect("create high canonical descriptor file"); + let canonical_three = kernel + .fd_open("wasm", pid, "/canonical-three", O_RDONLY, None) + .expect("open low canonical descriptor"); + assert_eq!(canonical_three, 3); + let unrelated_high = kernel + .fd_fcntl("wasm", pid, canonical_three, F_DUPFD, 64) + .expect("create unrelated high descriptor"); + + let closed = kernel + .fd_close_exact("wasm", pid, [canonical_three]) + .expect("close exact canonical target for display fd 64"); + assert_eq!(closed, vec![canonical_three]); + assert_eq!( + kernel + .fd_stat("wasm", pid, canonical_three) + .unwrap_err() + .code(), + "EBADF" + ); + kernel + .fd_stat("wasm", pid, unrelated_high) + .expect("numeric cutoff must not close unrelated canonical fd 64"); + } + + #[test] + fn descriptor_runtime_image_is_exact_and_does_not_advance_offset() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let image = b"\0asm-executable".to_vec(); + kernel + .write_file_for_process("wasm", pid, "/program", image.clone(), Some(0o755)) + .expect("create executable image"); + let fd = kernel + .fd_open("wasm", pid, "/program", O_RDONLY, None) + .expect("open executable image"); + kernel + .fd_seek("wasm", pid, fd, 3, SEEK_SET) + .expect("move descriptor cursor"); + + let loaded = kernel + .load_process_runtime_image_from_fd("wasm", pid, fd, 1024) + .expect("load exact descriptor image"); + assert_eq!(loaded.bytes, image); + assert_eq!(loaded.canonical_path, "/program"); + assert_eq!( + kernel + .fd_seek("wasm", pid, fd, 0, SEEK_CUR) + .expect("observe unchanged cursor"), + 3 + ); + + kernel + .remove_file_for_process("wasm", pid, "/program") + .expect("unlink executable after open"); + let unlinked = kernel + .load_process_runtime_image_from_fd("wasm", pid, fd, 1024) + .expect("open description survives unlink"); + assert_eq!(unlinked.bytes, image); + assert!(unlinked.canonical_path.ends_with(" (deleted)")); + } + + #[test] + fn socket_validation_distinguishes_fd_type_and_listener_state() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .write_file("/regular", b"x".to_vec()) + .expect("create regular file"); + let regular = kernel + .fd_open("wasm", pid, "/regular", O_RDONLY, None) + .expect("open regular file"); + assert_eq!( + kernel + .fd_validate_socket("wasm", pid, regular, false) + .unwrap_err() + .code(), + "ENOTSOCK" + ); + + let socket_id = kernel + .socket_create("wasm", pid, SocketSpec::unix_stream()) + .expect("create socket"); + let socket_fd = kernel + .fd_adopt_socket("wasm", pid, socket_id, 0) + .expect("adopt socket fd"); + kernel + .fd_validate_socket("wasm", pid, socket_fd, false) + .expect("created socket validates"); + assert_eq!( + kernel + .fd_validate_socket("wasm", pid, socket_fd, true) + .unwrap_err() + .code(), + "EINVAL" + ); + kernel + .socket_bind_unix("wasm", pid, socket_id, "/listener") + .expect("bind listener"); + kernel + .socket_listen("wasm", pid, socket_id, 8) + .expect("listen"); + kernel + .fd_validate_socket("wasm", pid, socket_fd, true) + .expect("listening socket validates"); + } + + #[test] + fn explicit_wasi_open_requires_and_cannot_escape_a_kernel_directory_capability() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel.mkdir("/cap", true).expect("create capability root"); + kernel + .mkdir("/outside", true) + .expect("create outside directory"); + kernel + .write_file("/cap/inside", b"inside".to_vec()) + .expect("create inside file"); + kernel + .write_file("/outside/secret", b"secret".to_vec()) + .expect("create outside file"); + let cap_fd = kernel + .fd_open("wasm", pid, "/cap", O_DIRECTORY | O_RDONLY, None) + .expect("open capability root"); + { + let mut tables = lock_or_recover(&kernel.fd_tables); + tables + .get_mut(pid) + .expect("process fd table") + .set_rights( + cap_fd, + WASI_RIGHT_PATH_OPEN, + WASI_RIGHT_FD_READ | WASI_RIGHT_FD_FILESTAT_GET, + ) + .expect("grant capability rights"); + } + + let direct = kernel + .fd_open_with_rights( + "wasm", + pid, + None, + "/outside/secret", + O_RDONLY, + None, + Some((0, 0)), + ) + .expect_err("explicit rights cannot use an ambient path"); + assert_eq!(direct.code(), "EACCES"); + + let escape = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(cap_fd), + "/outside/secret", + O_RDONLY, + None, + Some((WASI_RIGHT_FD_READ, 0)), + ) + .expect_err("directory capability cannot escape"); + assert_eq!(escape.code(), "EACCES"); + + let zero = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(cap_fd), + "/cap/inside", + O_RDONLY, + None, + Some((0, 0)), + ) + .expect("explicit zero rights are valid"); + let zero_stat = kernel.fd_stat("wasm", pid, zero).expect("zero-right stat"); + assert_eq!(zero_stat.rights, 0); + assert_eq!(zero_stat.rights_inheriting, 0); + + let denied_create = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(cap_fd), + "/cap/not-created", + O_CREAT | O_WRONLY, + Some(0o600), + Some((WASI_RIGHT_FD_WRITE, 0)), + ) + .expect_err("parent inheriting rights reject write before create"); + assert_eq!(denied_create.code(), "EACCES"); + assert!(!kernel + .exists("/cap/not-created") + .expect("query rollback path")); + } + + #[test] + fn wasi_preopen_rights_are_monotonic_and_propagate_path_authority() { + let read_only = wasi_preopen_rights(ProcessPermissionTier::ReadOnly, true) + .expect("read-only preopen rights"); + assert_eq!(read_only.0, WASI_PREOPEN_READ_RIGHTS_BASE); + assert_eq!(read_only.1, WASI_PREOPEN_READ_RIGHTS_INHERITING); + assert_eq!(read_only.0, read_only.1); + assert_eq!(read_only.0 & WASI_WRITE_RIGHTS, 0); + assert_eq!(read_only.0 & WASI_NAMESPACE_DESTRUCTIVE_RIGHTS, 0); + + let read_write = wasi_preopen_rights(ProcessPermissionTier::ReadWrite, true) + .expect("read-write preopen rights"); + assert_eq!(read_write.0, WASI_PREOPEN_READ_WRITE_RIGHTS_BASE); + assert_eq!(read_write.1, WASI_PREOPEN_READ_WRITE_RIGHTS_INHERITING); + assert_eq!(read_write.0, read_write.1); + assert_ne!(read_write.0 & WASI_RIGHT_FD_WRITE, 0); + assert_eq!(read_write.0 & WASI_NAMESPACE_DESTRUCTIVE_RIGHTS, 0); + + let full = + wasi_preopen_rights(ProcessPermissionTier::Full, true).expect("full preopen rights"); + assert_eq!(full.0, WASI_PREOPEN_WRITE_RIGHTS_BASE); + assert_eq!(full.1, WASI_PREOPEN_WRITE_RIGHTS_INHERITING); + assert_eq!(full.0, full.1); + assert_eq!( + full.0 & WASI_NAMESPACE_DESTRUCTIVE_RIGHTS, + WASI_NAMESPACE_DESTRUCTIVE_RIGHTS + ); + assert_eq!( + wasi_preopen_rights(ProcessPermissionTier::Isolated, true), + None + ); + } + + #[test] + fn preview1_opened_directory_retains_nested_posix_path_rights() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel.mkdir("/cap", true).expect("create capability root"); + let root_fd = kernel + .fd_open("wasm", pid, "/", O_DIRECTORY | O_RDONLY, None) + .expect("open root capability"); + { + let mut tables = lock_or_recover(&kernel.fd_tables); + tables + .get_mut(pid) + .expect("process fd table") + .set_rights( + root_fd, + WASI_PREOPEN_READ_RIGHTS_BASE, + WASI_PREOPEN_WRITE_RIGHTS_INHERITING, + ) + .expect("grant full root inheriting rights"); + } + + let directory_fd = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(root_fd), + "/cap", + O_DIRECTORY | O_RDONLY, + None, + Some(( + WASI_PREOPEN_READ_RIGHTS_BASE, + WASI_PREOPEN_WRITE_RIGHTS_INHERITING, + )), + ) + .expect("open nested directory capability"); + let directory_stat = kernel + .fd_stat("wasm", pid, directory_fd) + .expect("stat nested directory capability"); + assert_ne!(directory_stat.rights & WASI_RIGHT_PATH_OPEN, 0); + assert_eq!( + directory_stat.rights_inheriting, + WASI_PREOPEN_WRITE_RIGHTS_INHERITING + ); + + let child_fd = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(directory_fd), + "/cap/child", + O_CREAT | O_RDWR, + Some(0o600), + Some((WASI_RIGHT_FD_READ | WASI_RIGHT_FD_WRITE, 0)), + ) + .expect("openat through ordinary POSIX directory fd"); + assert!( + kernel + .fd_stat("wasm", pid, child_fd) + .expect("stat nested child") + .rights + & WASI_RIGHT_FD_WRITE + != 0 + ); + } + + #[test] + fn isolated_child_closes_inherited_preopens_from_enumeration_and_snapshot() { + let (mut kernel, parent) = kernel_with_process(); + kernel + .mkdir("/workspace", true) + .expect("create default workspace preopen"); + let parent_preopens = kernel + .initialize_wasi_preopens("wasm", parent.pid()) + .expect("initialize parent preopens"); + assert!(!parent_preopens.is_empty()); + let parent_preopen_fds = parent_preopens + .iter() + .map(|entry| entry.fd) + .collect::>(); + + let child = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + parent_pid: Some(parent.pid()), + permission_tier: Some(ProcessPermissionTier::Isolated), + ..SpawnOptions::default() + }, + ) + .expect("spawn isolated child"); + assert!(kernel + .wasi_preopens("wasm", child.pid()) + .expect("enumerate child preopens") + .is_empty()); + let child_fds = kernel + .fd_snapshot("wasm", child.pid()) + .expect("snapshot child fds") + .into_iter() + .map(|entry| entry.fd) + .collect::>(); + assert!(parent_preopen_fds.is_disjoint(&child_fds)); + } + + #[test] + fn dev_fd_open_intersects_explicit_requested_rights() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .write_file("/source", b"source".to_vec()) + .expect("create source"); + let root_fd = kernel + .fd_open("wasm", pid, "/", O_DIRECTORY | O_RDONLY, None) + .expect("open root capability"); + let source_fd = kernel + .fd_open("wasm", pid, "/source", O_RDWR, None) + .expect("open source"); + { + let mut tables = lock_or_recover(&kernel.fd_tables); + let table = tables.get_mut(pid).expect("process fd table"); + table + .set_rights( + root_fd, + WASI_RIGHT_PATH_OPEN, + WASI_RIGHT_FD_READ | WASI_RIGHT_FD_WRITE, + ) + .expect("set root rights"); + table + .set_rights(source_fd, WASI_RIGHT_FD_READ | WASI_RIGHT_FD_WRITE, 0) + .expect("set source rights"); + } + + let read_alias = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(root_fd), + &format!("/dev/fd/{source_fd}"), + O_RDONLY, + None, + Some((WASI_RIGHT_FD_READ, 0)), + ) + .expect("open read-only alias"); + assert_eq!( + kernel + .fd_stat("wasm", pid, read_alias) + .expect("alias stat") + .rights, + WASI_RIGHT_FD_READ + ); + let snapshot = kernel.fd_snapshot("wasm", pid).expect("fd snapshot"); + let source_description = snapshot + .iter() + .find(|entry| entry.fd == source_fd) + .expect("source snapshot") + .description_id; + assert_eq!( + snapshot + .iter() + .find(|entry| entry.fd == read_alias) + .expect("alias snapshot") + .description_id, + source_description, + "/dev/fd alias must share the open description" + ); + kernel + .fd_seek("wasm", pid, source_fd, 1, SEEK_SET) + .expect("seek source description"); + assert_eq!( + kernel + .fd_read("wasm", pid, read_alias, 1) + .expect("read through alias"), + b"o" + ); + assert_eq!( + kernel + .fd_seek("wasm", pid, source_fd, 0, SEEK_CUR) + .expect("observe shared offset"), + 2 + ); + + let zero_alias = kernel + .fd_open_with_rights( + "wasm", + pid, + Some(root_fd), + &format!("/dev/fd/{source_fd}"), + O_RDONLY, + None, + Some((0, 0)), + ) + .expect("open zero-right alias"); + assert_eq!( + kernel + .fd_stat("wasm", pid, zero_alias) + .expect("zero alias stat") + .rights, + 0 + ); + } + + #[test] + fn trusted_runtime_image_admission_is_bounded_policy_independent_and_accounted() { + let mut config = KernelVmConfig::new("vm-trusted-runtime-image"); + config.permissions = Permissions { + filesystem: Some(Arc::new(|_| { + crate::permissions::PermissionDecision::deny("guest filesystem denied") + })), + ..Permissions::allow_all() + }; + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + let before = kernel.filesystem_usage().expect("measure initial usage"); + let image = b"\0asmfixture".to_vec(); + + kernel + .admit_trusted_initial_runtime_image( + "/tmp/runtime/guest.wasm", + image.clone(), + 0o755, + image.len() as u64, + ) + .expect("trusted admission bypasses guest fs policy"); + let loaded = kernel + .load_trusted_initial_runtime_image("/tmp/runtime/guest.wasm", image.len() as u64) + .expect("trusted loader reads admitted image"); + assert_eq!(loaded.bytes, image); + assert_eq!(loaded.mode & 0o777, 0o755); + let after = kernel.filesystem_usage().expect("measure admitted usage"); + assert_eq!( + after.total_bytes - before.total_bytes, + loaded.bytes.len() as u64 + ); + assert_eq!(after.inode_count - before.inode_count, 3); + assert_eq!( + kernel + .read_file("/tmp/runtime/guest.wasm") + .expect_err("ordinary guest-attributable read remains denied") + .code(), + "EACCES" + ); + + let too_large = kernel + .admit_trusted_initial_runtime_image("/too-large.wasm", vec![0; 5], 0o755, 4) + .expect_err("oversized trusted image must fail before mutation"); + assert_eq!(too_large.code(), "E2BIG"); + assert_eq!( + kernel + .load_trusted_initial_runtime_image("/too-large.wasm", 4) + .expect_err("oversized rejection must leave no inode") + .code(), + "ENOENT" + ); + + let traversal = kernel + .admit_trusted_initial_runtime_image("/tmp/../escape.wasm", vec![0], 0o755, 1) + .expect_err("traversal spelling must be rejected"); + assert_eq!(traversal.code(), "EINVAL"); + let protected = kernel + .admit_trusted_initial_runtime_image("/etc/agentos/guest.wasm", vec![0], 0o755, 1) + .expect_err("protected package tree must remain read-only"); + assert_eq!(protected.code(), "EROFS"); + + // Admission preflights the complete directory+file inode delta. A + // quota failure must therefore leave neither the file nor a partial + // parent hierarchy behind, and must not perturb cached accounting. + let mut baseline_config = KernelVmConfig::new("vm-trusted-runtime-quota-baseline"); + baseline_config.permissions = Permissions::allow_all(); + let mut baseline_kernel = KernelVm::new(MemoryFileSystem::new(), baseline_config); + let baseline = baseline_kernel + .filesystem_usage() + .expect("measure trusted admission quota baseline"); + + let mut limited_config = KernelVmConfig::new("vm-trusted-runtime-quota"); + limited_config.permissions = Permissions::allow_all(); + limited_config.resources = ResourceLimits { + max_inode_count: Some(baseline.inode_count + 1), + ..ResourceLimits::default() + }; + let mut limited_kernel = KernelVm::new(MemoryFileSystem::new(), limited_config); + let before_rejection = limited_kernel + .filesystem_usage() + .expect("measure limited kernel before admission"); + let quota = limited_kernel + .admit_trusted_initial_runtime_image("/quota/guest.wasm", b"image".to_vec(), 0o755, 5) + .expect_err("directory plus image must exceed the one-inode allowance"); + assert_eq!(quota.code(), "ENOSPC"); + assert!(!limited_kernel + .exists("/quota") + .expect("query partial parent")); + assert!(!limited_kernel + .exists("/quota/guest.wasm") + .expect("query rejected image")); + assert_eq!( + limited_kernel + .filesystem_usage() + .expect("measure limited kernel after rejection"), + before_rejection + ); + } + + #[test] + fn process_file_read_preflight_and_runtime_prefix_are_bounded_and_authorized() { + let mut config = KernelVmConfig::new("vm-bounded-read-preflight"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_pread_bytes: Some(4), + ..ResourceLimits::default() + }; + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("wasm", ["reader"])) + .expect("register reader"); + let process = kernel + .spawn_process( + "reader", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + ..SpawnOptions::default() + }, + ) + .expect("spawn reader"); + let pid = process.pid(); + + kernel + .write_file("/exact", b"four".to_vec()) + .expect("write exact fixture"); + kernel + .write_file("/over", b"five!".to_vec()) + .expect("write oversized fixture"); + assert_eq!( + kernel + .preflight_regular_file_read_for_process("wasm", pid, "/exact") + .expect("exact preflight"), + 4 + ); + let over = kernel + .preflight_regular_file_read_for_process("wasm", pid, "/over") + .expect_err("oversized preflight must fail before allocation"); + assert_eq!(over.code(), "EINVAL"); + assert!(over.message().contains("limits.resources.maxPreadBytes")); + + kernel.chmod("/exact", 0).expect("deny exact fixture"); + assert_eq!( + kernel + .preflight_regular_file_read_for_process("wasm", pid, "/exact") + .expect_err("read DAC must be enforced") + .code(), + "EACCES" + ); + kernel.chmod("/exact", 0o644).expect("restore exact mode"); + + kernel + .symlink("/loop-b", "/loop-a") + .expect("first loop link"); + kernel + .symlink("/loop-a", "/loop-b") + .expect("second loop link"); + assert_eq!( + kernel + .preflight_regular_file_read_for_process("wasm", pid, "/loop-a") + .expect_err("symlink loop must stay typed") + .code(), + "ELOOP" + ); + + kernel + .write_file("/program", b"\0asmrest".to_vec()) + .expect("write executable fixture"); + kernel.chmod("/program", 0o755).expect("make executable"); + let prefix = kernel + .load_process_runtime_image_prefix("wasm", pid, "/program", 4) + .expect("authorized prefix"); + assert_eq!(prefix.canonical_path, "/program"); + assert_eq!(prefix.bytes, b"\0asm"); + kernel + .chmod("/program", 0o644) + .expect("remove execute mode"); + assert_eq!( + kernel + .load_process_runtime_image_prefix("wasm", pid, "/program", 4) + .expect_err("process prefix must enforce execute DAC") + .code(), + "EACCES" + ); + assert_eq!( + kernel + .load_trusted_initial_runtime_image_prefix("/program", 4) + .expect("trusted prefix bypasses process execute DAC") + .bytes, + b"\0asm" + ); + } + + #[test] + fn registered_projected_runtime_image_does_not_require_host_execute_bits() { + let (mut kernel, process) = kernel_with_process(); + kernel + .set_resource_limit( + "wasm", + process.pid(), + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(3), + hard: Some(3), + }, + ) + .expect("saturate the guest descriptor range with stdio"); + assert_eq!(kernel.fd_snapshot("wasm", process.pid()).unwrap().len(), 3); + kernel + .register_driver(CommandDriver::new("wasm", ["projected-tool"])) + .expect("register projected command"); + kernel + .mkdir("/__secure_exec/commands/0", true) + .expect("create projected command root"); + kernel + .write_file( + "/__secure_exec/commands/0/projected-tool", + b"\0asmprojected".to_vec(), + ) + .expect("write projected image"); + + let stat = kernel + .stat("/__secure_exec/commands/0/projected-tool") + .expect("stat projected image"); + assert_eq!(stat.mode & EXECUTABLE_PERMISSION_BITS, 0); + let image = kernel + .load_process_runtime_image( + "wasm", + process.pid(), + "/__secure_exec/commands/0/projected-tool", + 64, + ) + .expect("registered projection should be executable"); + assert_eq!(image.bytes, b"\0asmprojected"); + assert_eq!( + kernel.fd_snapshot("wasm", process.pid()).unwrap().len(), + 3, + "trusted runtime-image loading must not consume a guest descriptor" + ); + } + + #[test] + fn guest_runtime_image_loader_never_imports_ambient_host_paths() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after Unix epoch") + .as_nanos(); + let host_directory = std::env::temp_dir().join(format!( + "agentos-kernel-ambient-runtime-image-{}-{unique}", + std::process::id() + )); + fs::create_dir_all(&host_directory).expect("create ambient host fixture directory"); + let host_image = host_directory.join("guest.wasm"); + fs::write(&host_image, b"\0asmhost-only").expect("write ambient host-only image"); + + let (mut kernel, process) = kernel_with_process(); + let guest_path = host_image + .to_str() + .expect("temporary host fixture path should be UTF-8"); + let error = kernel + .load_process_runtime_image("wasm", process.pid(), guest_path, 1024) + .expect_err("guest runtime image lookup must remain inside the kernel VFS"); + assert_eq!(error.code(), "ENOENT"); + assert!(host_image.is_file(), "ambient fixture itself must exist"); + + fs::remove_dir_all(host_directory).expect("remove ambient host fixture directory"); + } + + #[test] + fn kernel_owns_system_identity_and_clock_semantics() { + let mut config = KernelVmConfig::new("vm-system-services"); + config.system_identity = SystemIdentity { + hostname: String::from("configured-host"), + os_type: String::from("Linux"), + os_release: String::from("test-release"), + os_version: String::from("test-version"), + machine: String::from("test-machine"), + domain_name: String::from("test-domain"), + }; + let kernel = KernelVm::new(MemoryFileSystem::new(), config); + + assert_eq!(kernel.system_identity().hostname, "configured-host"); + assert_eq!( + kernel + .clock_time_ns(KernelClockId::Realtime, Some(123_456_789)) + .expect("deterministic realtime"), + 123_456_789 + ); + assert!(kernel.clock_time_ns(KernelClockId::Monotonic, None).is_ok()); + assert_eq!( + kernel + .clock_resolution_ns(KernelClockId::Realtime) + .expect("realtime resolution"), + 1_000_000 + ); + assert_eq!( + kernel + .clock_resolution_ns(KernelClockId::Monotonic) + .expect("monotonic resolution"), + 1 + ); + let error = kernel + .clock_time_ns(KernelClockId::ProcessCpu, None) + .expect_err("unsupported CPU clock must be typed"); + assert_eq!(error.code(), "ENOTSUP"); + } + + #[test] + fn external_socket_descriptions_are_canonical_without_dummy_kernel_sockets() { + let (mut kernel, parent) = kernel_with_process(); + let pid = parent.pid(); + let sockets_before = kernel.sockets.snapshot().sockets; + let (fd, description_id) = kernel + .fd_open_external_socket("wasm", pid, false, true, false) + .expect("open sidecar-owned socket description"); + assert_eq!(kernel.sockets.snapshot().sockets, sockets_before); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, fd) + .expect("external description identity"), + (description_id, 1) + ); + assert!(kernel + .fd_snapshot("wasm", pid) + .expect("fd snapshot") + .iter() + .any(|entry| { + entry.fd == fd && entry.description_id == description_id && entry.is_socket + })); -fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} + let duplicate = kernel + .fd_dup("wasm", pid, fd) + .expect("duplicate external fd"); + assert_eq!( + kernel + .fd_description_alias_count("wasm", pid, description_id) + .expect("count parent aliases"), + 2 + ); + let transfer = kernel + .fd_transfer("wasm", pid, duplicate) + .expect("capture canonical transfer"); + let received = kernel + .fd_install_transfer("wasm", pid, &transfer, true) + .expect("install canonical transfer"); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, received) + .expect("received identity") + .0, + description_id + ); -impl Drop for KernelVm { - fn drop(&mut self) { - if !self.terminated { - dispose_kernel_vm_resources(self); - } + let child = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + parent_pid: Some(pid), + ..SpawnOptions::default() + }, + ) + .expect("spawn child with external description"); + assert_eq!( + kernel + .fd_description_identity("wasm", child.pid(), fd) + .expect("child inherited identity") + .0, + description_id + ); + kernel + .fd_close("wasm", pid, fd) + .expect("close one parent alias"); + assert_eq!( + kernel + .fd_description_alias_count("wasm", pid, description_id) + .expect("count remaining parent aliases"), + 2 + ); } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::fd_table::{FD_CLOEXEC, F_GETFD, F_SETFD, O_RDONLY}; - use crate::process_table::SIGTERM; - use crate::vfs::MemoryFileSystem; - use std::panic::{catch_unwind, AssertUnwindSafe}; - use std::thread; - fn kernel_with_process() -> (KernelVm, KernelProcessHandle) { - let mut config = KernelVmConfig::new("vm-fd-socket-test"); - config.permissions = Permissions::allow_all(); - let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + #[test] + fn rlimit_nofile_is_dynamic_and_shared_by_fd_allocation_paths() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (rights_sender, rights_receiver) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .expect("create SCM_RIGHTS channel"); kernel - .register_driver(CommandDriver::new("wasm", ["socket-test"])) - .expect("register wasm driver"); - let process = kernel + .fd_socket_sendmsg("wasm", pid, rights_sender, b"x", &[0]) + .expect("queue transferred fd"); + kernel + .set_resource_limit( + "wasm", + pid, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(5), + hard: Some(8), + }, + ) + .expect("lower RLIMIT_NOFILE to current descriptor count"); + let received = kernel + .fd_socket_recvmsg( + "wasm", + pid, + rights_receiver, + 1, + 1, + false, + false, + false, + false, + ) + .expect("receive queued message") + .expect("message available"); + assert!(received.rights.is_empty()); + assert!(received.control_truncated); + kernel + .fd_close("wasm", pid, rights_sender) + .expect("close rights sender"); + kernel + .fd_close("wasm", pid, rights_receiver) + .expect("close rights receiver"); + + let child = kernel .spawn_process( "socket-test", Vec::new(), SpawnOptions { requester_driver: Some(String::from("wasm")), + parent_pid: Some(pid), ..SpawnOptions::default() }, ) - .expect("spawn socket test process"); - (kernel, process) + .expect("spawn child with inherited resource limits"); + assert_eq!( + kernel + .get_resource_limit("wasm", child.pid(), ProcessResourceLimitKind::OpenFiles) + .expect("read child limit") + .soft, + Some(5) + ); + kernel + .fd_dup("wasm", child.pid(), 0) + .expect("child fourth fd"); + kernel + .fd_dup("wasm", child.pid(), 0) + .expect("child fifth fd"); + assert_eq!( + kernel.fd_dup("wasm", child.pid(), 0).unwrap_err().code(), + "EMFILE", + "the inherited limit must also configure the child's fd table" + ); + + kernel + .set_resource_limit( + "wasm", + pid, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(4), + hard: Some(8), + }, + ) + .expect("lower RLIMIT_NOFILE"); + + let duplicate = kernel.fd_dup("wasm", pid, 0).expect("allocate fourth fd"); + assert_eq!(kernel.fd_dup("wasm", pid, 0).unwrap_err().code(), "EMFILE"); + assert_eq!( + kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .unwrap_err() + .code(), + "EMFILE" + ); + assert_eq!(kernel.open_pty("wasm", pid).unwrap_err().code(), "EMFILE"); + + kernel + .fd_close("wasm", pid, duplicate) + .expect("free fourth fd"); + let transfer = kernel.fd_transfer("wasm", pid, 0).expect("transfer stdio"); + kernel + .set_resource_limit( + "wasm", + pid, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(3), + hard: Some(8), + }, + ) + .expect("lower limit to existing stdio count"); + assert_eq!( + kernel + .fd_install_transfer_at("wasm", pid, 3, 0, &transfer) + .unwrap_err() + .code(), + "EBADF", + "an exact descriptor at RLIMIT_NOFILE is outside the descriptor range" + ); + assert_eq!( + kernel + .set_resource_limit( + "wasm", + pid, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(9), + hard: Some(9), + }, + ) + .unwrap_err() + .code(), + "EPERM" + ); } #[test] @@ -9916,6 +13122,54 @@ mod tests { assert_eq!(kernel.fd_read("wasm", pid, right, 8).unwrap(), b"ef"); } + #[test] + fn fd_socketpair_applies_seqpacket_nonblocking_and_cloexec_options() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::SeqPacket, true, true) + .expect("create nonblocking close-on-exec seqpacket socketpair"); + + for fd in [left, right] { + let stat = kernel + .fd_stat("wasm", pid, fd) + .expect("stat seqpacket endpoint"); + assert_eq!(stat.filetype, FILETYPE_SOCKET_STREAM); + assert_ne!(stat.flags & O_NONBLOCK, 0); + assert_eq!( + kernel + .fd_fcntl("wasm", pid, fd, F_GETFD, 0) + .expect("get seqpacket descriptor flags"), + FD_CLOEXEC + ); + } + + kernel + .fd_write("wasm", pid, left, b"abcd") + .expect("write first seqpacket message"); + kernel + .fd_write("wasm", pid, left, b"ef") + .expect("write second seqpacket message"); + let truncated = kernel + .fd_socket_recvmsg("wasm", pid, right, 2, 0, false, false, false, false) + .expect("receive first seqpacket message") + .expect("first seqpacket message is available"); + assert_eq!(truncated.payload, b"ab"); + assert!(truncated.payload_truncated); + assert_eq!(truncated.full_length, 4); + assert_eq!( + kernel + .fd_read("wasm", pid, right, 8) + .expect("read second seqpacket message"), + b"ef" + ); + + kernel.fd_close("wasm", pid, left).expect("close left fd"); + kernel.fd_close("wasm", pid, right).expect("close right fd"); + assert_eq!(kernel.sockets.snapshot().sockets, 0); + assert!(lock_or_recover(&kernel.fd_sockets).is_empty()); + } + #[test] fn fd_socketpair_peek_duplicates_rights_without_consuming_message() { let (mut kernel, process) = kernel_with_process(); @@ -10001,6 +13255,119 @@ mod tests { kernel.waitpid(parent_pid).unwrap(); } + #[test] + fn fd_renumber_projection_without_backing_target_preserves_description_state() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel.write_file("/move-shared", b"abc").unwrap(); + let original = kernel + .fd_open("wasm", pid, "/move-shared", O_RDONLY, None) + .unwrap(); + let source = kernel.fd_dup("wasm", pid, original).unwrap(); + let alias = kernel.fd_dup("wasm", pid, source).unwrap(); + let description_id = kernel + .fd_description_identity("wasm", pid, source) + .unwrap() + .0; + kernel + .fd_fcntl("wasm", pid, source, F_SETFD, FD_CLOEXEC) + .unwrap(); + kernel.fd_close("wasm", pid, original).unwrap(); + + let moved = kernel + .fd_renumber_projection("wasm", pid, source, None) + .unwrap(); + assert_eq!( + moved, source, + "the runner can re-project the same backing fd" + ); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, moved) + .unwrap() + .0, + description_id + ); + assert_eq!(kernel.fd_fcntl("wasm", pid, moved, F_GETFD, 0).unwrap(), 0); + assert_eq!(kernel.fd_read("wasm", pid, moved, 1).unwrap(), b"a"); + assert_eq!(kernel.fd_read("wasm", pid, alias, 1).unwrap(), b"b"); + + kernel.fd_close("wasm", pid, moved).unwrap(); + kernel.fd_close("wasm", pid, alias).unwrap(); + process.finish(0); + kernel.waitpid(pid).unwrap(); + } + + #[test] + fn fd_renumber_projection_atomically_replaces_backing_target() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel.write_file("/renumber-source", b"source").unwrap(); + kernel.write_file("/renumber-target", b"target").unwrap(); + let source = kernel + .fd_open("wasm", pid, "/renumber-source", O_RDONLY, None) + .unwrap(); + let target = kernel + .fd_open("wasm", pid, "/renumber-target", O_RDONLY, None) + .unwrap(); + let source_description = kernel + .fd_description_identity("wasm", pid, source) + .unwrap() + .0; + let target_description = kernel + .fd_description_identity("wasm", pid, target) + .unwrap() + .0; + kernel + .fd_fcntl("wasm", pid, source, F_SETFD, FD_CLOEXEC) + .unwrap(); + + let error = kernel + .fd_renumber_projection("wasm", pid, source, Some(999)) + .expect_err("an invalid projected target must fail before mutation"); + assert_eq!(error.code(), "EBADF"); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, source) + .unwrap() + .0, + source_description + ); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, target) + .unwrap() + .0, + target_description + ); + assert_eq!( + kernel.fd_fcntl("wasm", pid, source, F_GETFD, 0).unwrap(), + FD_CLOEXEC + ); + + let moved = kernel + .fd_renumber_projection("wasm", pid, source, Some(target)) + .unwrap(); + assert_eq!(moved, target); + assert_eq!( + kernel.fd_stat("wasm", pid, source).unwrap_err().code(), + "EBADF" + ); + assert_eq!( + kernel + .fd_description_identity("wasm", pid, target) + .unwrap() + .0, + source_description + ); + assert_eq!(kernel.fd_fcntl("wasm", pid, target, F_GETFD, 0).unwrap(), 0); + assert_eq!(kernel.fd_read("wasm", pid, target, 6).unwrap(), b"source"); + + kernel.fd_close("wasm", pid, target).unwrap(); + process.finish(0); + kernel.waitpid(pid).unwrap(); + } + #[test] fn dev_fd_stat_reports_linux_pipe_and_socket_modes() { let (mut kernel, process) = kernel_with_process(); @@ -10028,6 +13395,35 @@ mod tests { } } + #[test] + fn pipe_fdstat_is_unknown_without_losing_kernel_pipe_identity() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (pipe_read, pipe_write) = kernel.open_pipe("wasm", pid).unwrap(); + + for fd in [pipe_read, pipe_write] { + assert_eq!( + kernel.fd_stat("wasm", pid, fd).unwrap().filetype, + crate::fd_table::FILETYPE_UNKNOWN, + "Preview1 must not expose a pipe as a socket" + ); + } + + let snapshot = kernel.fd_snapshot("wasm", pid).unwrap(); + for fd in [pipe_read, pipe_write] { + let entry = snapshot + .iter() + .find(|entry| entry.fd == fd) + .expect("pipe fd in process snapshot"); + assert!( + entry.is_pipe, + "PipeManager identity must remain authoritative" + ); + assert!(!entry.is_socket, "pipe must not be classified as a socket"); + assert!(kernel.fd_is_pipe("wasm", pid, fd).unwrap()); + } + } + #[test] fn adopted_kernel_socket_lives_while_queued_transfer_guard_exists() { let (mut kernel, process) = kernel_with_process(); @@ -10064,6 +13460,53 @@ mod tests { ); } + #[test] + fn adopted_udp_socket_allows_charged_receive_for_transfer_owner() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let socket_id = kernel + .socket_create("wasm", pid, SocketSpec::udp()) + .expect("create transferable UDP socket"); + kernel + .socket_bind_inet( + "wasm", + pid, + socket_id, + InetSocketAddress::new("127.0.0.1", 41000), + ) + .expect("bind transferable UDP socket"); + let _guard = kernel + .fd_adopt_socket_transfer("wasm", pid, socket_id, 0) + .expect("retain UDP socket description"); + let sender_id = kernel + .socket_create("wasm", pid, SocketSpec::udp()) + .expect("create UDP sender"); + kernel + .socket_bind_inet( + "wasm", + pid, + sender_id, + InetSocketAddress::new("127.0.0.1", 41001), + ) + .expect("bind UDP sender"); + kernel + .socket_send_to_inet_loopback( + "wasm", + pid, + sender_id, + InetSocketAddress::new("127.0.0.1", 41000), + b"x", + ) + .expect("send to description-owned UDP socket"); + + let received = kernel + .socket_recv_datagram_charged("wasm", pid, socket_id, 1) + .expect("description-owned UDP socket remains usable") + .expect("queued datagram remains receivable"); + let (_, payload, _reservations) = received.into_parts(); + assert_eq!(payload, b"x"); + } + #[test] fn adopting_socket_for_transfer_does_not_require_a_free_sender_fd() { let mut config = KernelVmConfig::new("vm-full-fd-transfer-test"); @@ -10184,6 +13627,7 @@ mod tests { parent_pid: Some(parent.pid()), cwd: Some(String::from("/before-exec")), env: BTreeMap::from([(String::from("STALE"), String::from("value"))]), + ..SpawnOptions::default() }, ) .expect("spawn child"); @@ -10233,9 +13677,17 @@ mod tests { &[], &[forwarded_cloexec_fd], Some("/literal/not-executable"), + Some(ProcessPermissionTier::ReadOnly), ) .expect_err("pathname validation must fail before exec commits"); assert_eq!(error.code(), "EACCES"); + assert_eq!( + kernel + .process_permission_tier("runtime", child.pid()) + .expect("permission tier after rejected exec"), + ProcessPermissionTier::Full, + "a rejected exec must not commit its requested tier" + ); assert_eq!( kernel.processes.get(child.pid()).expect("child entry"), before, @@ -10264,10 +13716,18 @@ mod tests { &[], &[forwarded_cloexec_fd], Some("/literal/new"), + Some(ProcessPermissionTier::ReadOnly), ) .expect("replace process image"); let after = kernel.processes.get(child.pid()).expect("exec child entry"); + assert_eq!( + kernel + .process_permission_tier("runtime", child.pid()) + .expect("permission tier after exec"), + ProcessPermissionTier::ReadOnly, + "exec must atomically drop to the trusted image tier" + ); assert_eq!(after.pid, before.pid); assert_eq!(after.ppid, before.ppid); assert_eq!(after.pgid, before.pgid); @@ -10277,6 +13737,16 @@ mod tests { assert_eq!(after.cwd, before.cwd, "execve must preserve cwd"); assert_eq!(after.command, ""); assert_eq!(after.args, vec![String::from("argument")]); + assert_eq!( + kernel + .process_image("runtime", child.pid()) + .expect("read committed process image"), + KernelProcessImage { + argv: vec![String::new(), String::from("argument")], + env: vec![(String::from("ONLY"), String::from("new"))], + }, + "the kernel image query must observe the replacement image exactly" + ); assert_eq!( kernel .read_file_for_process( @@ -10625,8 +14095,7 @@ mod tests { } fn assert_kernel_drop_released_resources(retained: &RetainedKernelResources) { - assert_eq!(retained.process.wait(Duration::from_millis(50)), Some(143)); - assert_eq!(retained.process.kill_signals(), vec![15]); + assert_eq!(retained.process.wait(Duration::from_millis(50)), Some(137)); assert!( lock_or_recover(retained.fd_tables.as_ref()).is_empty(), "kernel drop should remove fd tables" @@ -10672,8 +14141,14 @@ mod tests { identity: ProcessIdentity::default(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), + resource_limits: Default::default(), + permission_tier: Default::default(), + }, + { + let endpoint = RuntimeControlCell::new(0); + endpoint.bind_pid(leader_pid).expect("bind leader endpoint"); + Arc::new(endpoint) }, - Arc::new(StubDriverProcess::default()), ); let peer_pid = kernel.processes.allocate_pid().expect("allocate pid"); @@ -10692,8 +14167,14 @@ mod tests { identity: ProcessIdentity::default(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), + resource_limits: Default::default(), + permission_tier: Default::default(), + }, + { + let endpoint = RuntimeControlCell::new(0); + endpoint.bind_pid(peer_pid).expect("bind peer endpoint"); + Arc::new(endpoint) }, - Arc::new(StubDriverProcess::default()), ); lock_or_recover(&kernel.driver_pids) @@ -10729,6 +14210,8 @@ mod tests { identity: ProcessIdentity::default(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), + resource_limits: Default::default(), + permission_tier: Default::default(), }, None, None, diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 36f2506684..ced05e5a66 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -3,6 +3,7 @@ //! Shared per-VM kernel plane for the secure-exec runtime migration. pub use agentos_bridge as bridge; +pub use agentos_resource as admission; pub mod command_registry; pub mod device_layer; pub mod dns; @@ -12,10 +13,12 @@ pub mod network_policy; pub mod permissions; pub mod pipe_manager; pub mod poll; +pub mod process_runtime; pub mod process_table; pub mod pty; pub mod resource_accounting; pub mod socket_table; +pub mod system; pub mod user; pub use ::vfs::posix as vfs; diff --git a/crates/kernel/src/permissions.rs b/crates/kernel/src/permissions.rs index 333b146735..17a7b3b29f 100644 --- a/crates/kernel/src/permissions.rs +++ b/crates/kernel/src/permissions.rs @@ -1,6 +1,6 @@ use crate::vfs::{ - validate_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, - VirtualUtimeSpec, + validate_path, FileExtent, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, + VirtualStat, VirtualUtimeSpec, }; use std::collections::{BTreeMap, HashMap}; use std::error::Error; @@ -856,6 +856,11 @@ impl VirtualFileSystem for PermissionedFileSystem { self.inner.unwritten_ranges(path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + self.check_subject(FsOperation::Read, path)?; + self.inner.extent_at(path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { self.check_subject(FsOperation::Read, path)?; self.inner.pread(path, offset, length) diff --git a/crates/kernel/src/pipe_manager.rs b/crates/kernel/src/pipe_manager.rs index 8f19350809..f460a0e594 100644 --- a/crates/kernel/src/pipe_manager.rs +++ b/crates/kernel/src/pipe_manager.rs @@ -3,6 +3,7 @@ use crate::fd_table::{ FILETYPE_PIPE, O_NONBLOCK, O_RDONLY, O_RDWR, O_WRONLY, }; use crate::poll::{PollEvents, PollNotifier, POLLERR, POLLHUP, POLLIN, POLLOUT}; +use crate::resource_accounting::BlockingReadDeadline; use std::collections::{BTreeMap, VecDeque}; use std::error::Error; use std::fmt; @@ -53,6 +54,20 @@ impl PipeError { message: message.into(), } } + + fn io(message: impl Into) -> Self { + Self { + code: "EIO", + message: message.into(), + } + } + + fn overflow(message: impl Into) -> Self { + Self { + code: "EOVERFLOW", + message: message.into(), + } + } } impl fmt::Display for PipeError { @@ -267,6 +282,24 @@ impl PipeManager { flags: u32, timeout: Option, ) -> PipeResult { + self.open_named_pipe_with_deadline(key, path, flags, timeout, None) + } + + pub(crate) fn open_named_pipe_with_deadline( + &self, + key: (u64, u64), + path: &str, + flags: u32, + timeout: Option, + mut blocking_deadline: Option, + ) -> PipeResult { + let timeout_deadline = timeout + .map(|timeout| { + Instant::now().checked_add(timeout).ok_or_else(|| { + PipeError::overflow("FIFO open timeout exceeds the supported deadline range") + }) + }) + .transpose()?; let access_mode = flags & 0b11; if !matches!(access_mode, O_RDONLY | O_WRONLY | O_RDWR) { return Err(PipeError::bad_file_descriptor("invalid FIFO access mode")); @@ -333,26 +366,44 @@ impl PipeManager { PipeSide::ReadWrite => true, }) }; - if let Some(timeout) = timeout { - let (next, result) = self - .inner - .waiters - .wait_timeout_while(state, timeout, |state| !ready(state)) - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state = next; - if result.timed_out() && !ready(&state) { - drop(state); - self.close(description_id); - return Err(PipeError::would_block(format!( - "FIFO open timed out: {path}" - ))); + if let Some(deadline) = timeout_deadline { + while !ready(&state) { + let remaining = blocking_deadline + .as_mut() + .and_then(BlockingReadDeadline::wait_slice) + .unwrap_or_else(|| deadline.saturating_duration_since(Instant::now())); + let (next, result) = self + .inner + .waiters + .wait_timeout_while(state, remaining, |state| !ready(state)) + .map_err(|_| { + PipeError::io("FIFO wait state was poisoned by a prior panic") + })?; + state = next; + if ready(&state) { + break; + } + if result.timed_out() + && blocking_deadline + .as_mut() + .is_some_and(|deadline| !deadline.expired()) + { + continue; + } + if result.timed_out() || Instant::now() >= deadline { + drop(state); + self.close(description_id); + return Err(PipeError::would_block(format!( + "FIFO open timed out: {path}" + ))); + } } } else { state = self .inner .waiters .wait_while(state, |state| !ready(state)) - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .map_err(|_| PipeError::io("FIFO wait state was poisoned by a prior panic"))?; } } drop(state); @@ -523,6 +574,16 @@ impl PipeManager { description_id: u64, length: usize, timeout: Option, + ) -> PipeResult>> { + self.read_with_timeout_and_deadline(description_id, length, timeout, None) + } + + pub(crate) fn read_with_timeout_and_deadline( + &self, + description_id: u64, + length: usize, + timeout: Option, + mut blocking_deadline: Option, ) -> PipeResult>> { let mut state = lock_or_recover(&self.inner.state); let pipe_ref = state @@ -607,6 +668,9 @@ impl PipeManager { let now = Instant::now(); if now >= deadline { + if let Some(blocking_deadline) = &mut blocking_deadline { + blocking_deadline.expired(); + } if let Some(id) = waiter_id.take() { state.waiters.remove(&id); if let Some(pipe) = state.pipes.get_mut(&pipe_ref.pipe_id) { @@ -617,7 +681,10 @@ impl PipeManager { return Err(PipeError::would_block("pipe read timed out")); } - let remaining = deadline.saturating_duration_since(now); + let remaining = blocking_deadline + .as_mut() + .and_then(BlockingReadDeadline::wait_slice) + .unwrap_or_else(|| deadline.saturating_duration_since(now)); let (next_state, wait_result) = wait_timeout_or_recover(&self.inner.waiters, state, remaining); state = next_state; @@ -625,6 +692,12 @@ impl PipeManager { waiter_id = None; } if wait_result.timed_out() { + if blocking_deadline + .as_mut() + .is_some_and(|deadline| !deadline.expired()) + { + continue; + } if let Some(id) = waiter_id.take() { state.waiters.remove(&id); if let Some(pipe) = state.pipes.get_mut(&pipe_ref.pipe_id) { @@ -869,14 +942,22 @@ fn drain_buffer(buffer: &mut VecDeque>, length: usize) -> Vec { fn lock_or_recover<'a, T>(mutex: &'a Mutex) -> MutexGuard<'a, T> { match mutex.lock() { Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), + Err(poisoned) => { + eprintln!("ERR_AGENTOS_PIPE_STATE_POISONED: recovering pipe state after a prior panic"); + poisoned.into_inner() + } } } fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> { match condvar.wait(guard) { Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_PIPE_WAIT_POISONED: recovering pipe wait state after a prior panic" + ); + poisoned.into_inner() + } } } @@ -887,7 +968,12 @@ fn wait_timeout_or_recover<'a, T>( ) -> (MutexGuard<'a, T>, std::sync::WaitTimeoutResult) { match condvar.wait_timeout(guard, timeout) { Ok(result) => result, - Err(poisoned) => poisoned.into_inner(), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_PIPE_WAIT_POISONED: recovering timed pipe wait state after a prior panic" + ); + poisoned.into_inner() + } } } @@ -895,6 +981,21 @@ fn wait_timeout_or_recover<'a, T>( mod tests { use super::*; + #[test] + fn oversized_named_pipe_timeout_fails_before_allocating_state() { + let manager = PipeManager::new(); + + let error = manager + .open_named_pipe((1, 2), "/fifo", O_RDONLY, Some(Duration::MAX)) + .expect_err("an unrepresentable deadline must fail"); + + assert_eq!(error.code(), "EOVERFLOW"); + assert_eq!(manager.pipe_count(), 0); + assert!(lock_or_recover(&manager.inner.state) + .desc_to_pipe + .is_empty()); + } + #[test] fn zero_timeout_empty_read_does_not_publish_false_readiness() { let notifier = PollNotifier::default(); diff --git a/crates/kernel/src/poll.rs b/crates/kernel/src/poll.rs index 90ed18cbc8..99f44b1f43 100644 --- a/crates/kernel/src/poll.rs +++ b/crates/kernel/src/poll.rs @@ -53,6 +53,8 @@ pub const POLLOUT: PollEvents = PollEvents(0x0004); pub const POLLERR: PollEvents = PollEvents(0x0008); pub const POLLHUP: PollEvents = PollEvents(0x0010); pub const POLLNVAL: PollEvents = PollEvents(0x0020); +pub const POLLRDNORM: PollEvents = PollEvents(0x0040); +pub const POLLWRNORM: PollEvents = PollEvents(0x0100); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PollFd { diff --git a/crates/kernel/src/process_runtime.rs b/crates/kernel/src/process_runtime.rs new file mode 100644 index 0000000000..0085a3445e --- /dev/null +++ b/crates/kernel/src/process_runtime.rs @@ -0,0 +1,931 @@ +use std::error::Error; +use std::fmt; +use std::io::{self, Write}; +use std::sync::{Arc, Mutex, MutexGuard}; + +use serde_json::Value; + +const MAX_RUNTIME_FAULT_CODE_BYTES: usize = 128; +const MAX_RUNTIME_FAULT_MESSAGE_BYTES: usize = 4 * 1024; +const MAX_RUNTIME_FAULT_DETAILS_BYTES: usize = 64 * 1024; + +/// Identifies the one VM generation and kernel process an endpoint may control. +/// +/// The generation is allocated by the sidecar. It prevents a retained endpoint +/// from being reused after a VM id is destroyed and recreated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProcessRuntimeIdentity { + pub generation: u64, + pub pid: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessTermination { + Signal { signal: i32, force: bool }, + RuntimeFault, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessCancellationReason { + VmTeardown, + Deadline, + HostRequest, + RuntimeFault, +} + +/// Exact terminal state reported by an execution. The kernel never infers a +/// signal from an exit code such as `128 + signal`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessExit { + Exited(i32), + Signaled { signal: i32, core_dumped: bool }, +} + +/// Stable, bounded diagnostic for an executor failure that is not a guest +/// `exit(2)` or signal termination. +/// +/// The kernel keeps this beside the synthetic Linux exit status used by +/// `waitpid`; callers never parse an engine string to recover its category. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessRuntimeFault { + code: String, + message: String, + details: Option, +} + +impl ProcessRuntimeFault { + pub fn try_new( + code: impl Into, + message: impl Into, + details: Option, + ) -> Result { + let code = code.into(); + let message = message.into(); + if code.is_empty() { + return Err(ProcessRuntimeEndpointError::new( + "EINVAL", + "runtime fault code must not be empty", + )); + } + if code.len() > MAX_RUNTIME_FAULT_CODE_BYTES { + return Err(ProcessRuntimeEndpointError::new( + "E2BIG", + format!( + "runtime fault code is {} bytes; maximum is {MAX_RUNTIME_FAULT_CODE_BYTES}", + code.len() + ), + )); + } + if message.len() > MAX_RUNTIME_FAULT_MESSAGE_BYTES { + return Err(ProcessRuntimeEndpointError::new( + "E2BIG", + format!( + "runtime fault message is {} bytes; maximum is {MAX_RUNTIME_FAULT_MESSAGE_BYTES}", + message.len() + ), + )); + } + if let Some(value) = &details { + let mut counter = BoundedJsonCounter::new(MAX_RUNTIME_FAULT_DETAILS_BYTES); + if let Err(error) = serde_json::to_writer(&mut counter, value) { + if counter.exceeded { + return Err(ProcessRuntimeEndpointError::new( + "E2BIG", + format!( + "runtime fault details exceed {MAX_RUNTIME_FAULT_DETAILS_BYTES} bytes" + ), + )); + } + return Err(ProcessRuntimeEndpointError::new( + "EINVAL", + format!("runtime fault details are not encodable: {error}"), + )); + } + } + Ok(Self { + code, + message, + details, + }) + } + + pub fn code(&self) -> &str { + &self.code + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn details(&self) -> Option<&Value> { + self.details.as_ref() + } +} + +struct BoundedJsonCounter { + written: usize, + limit: usize, + exceeded: bool, +} + +impl BoundedJsonCounter { + fn new(limit: usize) -> Self { + Self { + written: 0, + limit, + exceeded: false, + } + } +} + +impl Write for BoundedJsonCounter { + fn write(&mut self, bytes: &[u8]) -> io::Result { + let Some(next) = self.written.checked_add(bytes.len()) else { + self.exceeded = true; + return Err(io::Error::new( + io::ErrorKind::FileTooLarge, + "runtime fault details exceed their encoded limit", + )); + }; + if next > self.limit { + self.exceeded = true; + return Err(io::Error::new( + io::ErrorKind::FileTooLarge, + "runtime fault details exceed their encoded limit", + )); + } + self.written = next; + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl ProcessExit { + pub fn shell_status(self) -> i32 { + match self { + Self::Exited(code) => code, + Self::Signaled { signal, .. } => 128 + signal, + } + } +} + +/// Runtime-neutral control requested by the kernel. +/// +/// Requests update durable state only. An endpoint must never enter guest code +/// or wait for the runtime while servicing this call. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessControlRequest { + Checkpoint, + Stop { signal: i32 }, + Continue, + Terminate(ProcessTermination), + Cancel(ProcessCancellationReason), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessRuntimeEndpointError { + code: &'static str, + message: String, +} + +impl ProcessRuntimeEndpointError { + pub(crate) fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + pub fn code(&self) -> &'static str { + self.code + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for ProcessRuntimeEndpointError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.code, self.message) + } +} + +impl Error for ProcessRuntimeEndpointError {} + +pub(crate) trait ProcessExitSink: Send + Sync { + fn report_exit( + &self, + identity: ProcessRuntimeIdentity, + termination: ProcessExit, + ) -> Result<(), ProcessRuntimeEndpointError>; + + fn report_runtime_fault( + &self, + identity: ProcessRuntimeIdentity, + fault: ProcessRuntimeFault, + ) -> Result<(), ProcessRuntimeEndpointError>; +} + +pub(crate) trait ProcessControlAckSink: Send + Sync { + fn acknowledge_stop_state( + &self, + identity: ProcessRuntimeIdentity, + stopped: bool, + stop_signal: Option, + ) -> Result<(), ProcessRuntimeEndpointError>; +} + +/// Narrow capability returned to an executor for reporting its one terminal +/// result. It carries no process-table or control authority and is bound to the +/// VM generation and PID allocated for that execution. +#[derive(Clone)] +pub struct ProcessExitReporter { + identity: ProcessRuntimeIdentity, + sink: Arc, +} + +impl fmt::Debug for ProcessExitReporter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProcessExitReporter") + .field("identity", &self.identity) + .finish_non_exhaustive() + } +} + +impl ProcessExitReporter { + pub(crate) fn new(identity: ProcessRuntimeIdentity, sink: Arc) -> Self { + Self { identity, sink } + } + + pub fn identity(&self) -> ProcessRuntimeIdentity { + self.identity + } + + pub fn report_exit(&self, termination: ProcessExit) -> Result<(), ProcessRuntimeEndpointError> { + self.sink.report_exit(self.identity, termination) + } + + pub fn report_runtime_fault( + &self, + fault: ProcessRuntimeFault, + ) -> Result<(), ProcessRuntimeEndpointError> { + self.sink.report_runtime_fault(self.identity, fault) + } +} + +pub trait ProcessRuntimeEndpoint: Send + Sync { + fn identity(&self) -> Option; + + /// Whether a backend receiver is attached and able to make progress. The + /// kernel uses this only to avoid teardown grace waits for deliberately + /// virtual or never-started processes. + fn has_control_consumer(&self) -> bool { + true + } + + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError>; +} + +/// Coalesced controls observed by an execution at one safe point. +/// +/// Stop/continue is last-writer-wins. Terminal controls are never stored in an +/// ordinary bounded event queue and therefore cannot be rejected by output or +/// host-call backpressure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ProcessControlBatch { + pub checkpoint: bool, + pub stopped: Option, + pub stop_signal: Option, + pub termination: Option, + pub cancellation: Option, + checkpoint_revision: u64, + stopped_revision: u64, + termination_revision: u64, + cancellation_revision: u64, +} + +impl ProcessControlBatch { + pub fn is_empty(self) -> bool { + !self.checkpoint + && self.stopped.is_none() + && self.termination.is_none() + && self.cancellation.is_none() + } +} + +pub type ProcessControlWake = Arc; + +/// Two-part endpoint used while process allocation and backend construction +/// depend on one another. +/// +/// The producer is registered with the kernel before a backend exists. The +/// sidecar binds the allocated PID and attaches exactly one receiver before it +/// starts guest instructions. Controls requested in between remain durable. +#[derive(Clone)] +pub struct RuntimeControlCell { + inner: Arc, +} + +struct RuntimeControlCellInner { + generation: u64, + ack_sink: Option>, + state: Mutex, +} + +#[derive(Default)] +struct RuntimeControlState { + pid: Option, + receiver_attached: bool, + wake: Option, + wake_pending: bool, + checkpoint: bool, + checkpoint_revision: u64, + stopped: Option, + stop_signal: Option, + stopped_revision: u64, + termination: Option, + termination_revision: u64, + cancellation: Option, + cancellation_revision: u64, + next_revision: u64, +} + +impl fmt::Debug for RuntimeControlCell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeControlCell") + .field("identity", &self.identity()) + .finish_non_exhaustive() + } +} + +impl RuntimeControlCell { + pub fn new(generation: u64) -> Self { + Self::with_ack_sink(generation, None) + } + + pub(crate) fn new_with_ack_sink( + generation: u64, + ack_sink: Arc, + ) -> Self { + Self::with_ack_sink(generation, Some(ack_sink)) + } + + fn with_ack_sink(generation: u64, ack_sink: Option>) -> Self { + Self { + inner: Arc::new(RuntimeControlCellInner { + generation, + ack_sink, + state: Mutex::new(RuntimeControlState::default()), + }), + } + } + + /// Binds the PID allocated by the kernel. Binding is idempotent only for + /// the same PID; rebinding would let a stale capability target a new + /// process and is rejected. + pub fn bind_pid(&self, pid: u32) -> Result<(), ProcessRuntimeEndpointError> { + let mut state = lock_or_recover(&self.inner.state); + match state.pid { + None => { + state.pid = Some(pid); + Ok(()) + } + Some(bound) if bound == pid => Ok(()), + Some(bound) => Err(ProcessRuntimeEndpointError::new( + "ESTALE", + format!("runtime endpoint is bound to pid {bound}, not pid {pid}"), + )), + } + } + + /// Attaches the backend-side consumer. If control was requested during + /// construction, the supplied wake is called after the lock is released. + pub fn attach( + &self, + wake: ProcessControlWake, + ) -> Result { + let should_wake = { + let mut state = lock_or_recover(&self.inner.state); + if state.pid.is_none() { + return Err(ProcessRuntimeEndpointError::new( + "EINVAL", + "runtime endpoint must be bound to a kernel pid before attachment", + )); + } + if state.receiver_attached { + return Err(ProcessRuntimeEndpointError::new( + "EALREADY", + "runtime endpoint already has a control receiver", + )); + } + state.receiver_attached = true; + state.wake = Some(Arc::clone(&wake)); + state.wake_pending + }; + if should_wake { + wake(); + } + Ok(RuntimeControlReceiver { + inner: Arc::clone(&self.inner), + }) + } +} + +impl ProcessRuntimeEndpoint for RuntimeControlCell { + fn identity(&self) -> Option { + lock_or_recover(&self.inner.state) + .pid + .map(|pid| ProcessRuntimeIdentity { + generation: self.inner.generation, + pid, + }) + } + + fn has_control_consumer(&self) -> bool { + lock_or_recover(&self.inner.state).receiver_attached + } + + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError> { + let wake = { + let mut state = lock_or_recover(&self.inner.state); + if state.pid.is_none() { + return Err(ProcessRuntimeEndpointError::new( + "EINVAL", + "runtime endpoint is not bound to a kernel pid", + )); + } + match request { + ProcessControlRequest::Checkpoint => { + state.checkpoint = true; + state.checkpoint_revision = take_next_revision(&mut state); + } + ProcessControlRequest::Stop { signal } => { + state.stopped = Some(true); + state.stop_signal = Some(signal); + state.stopped_revision = take_next_revision(&mut state); + } + ProcessControlRequest::Continue => { + state.stopped = Some(false); + state.stop_signal = None; + state.stopped_revision = take_next_revision(&mut state); + } + ProcessControlRequest::Terminate(termination) => { + state.termination = Some(prefer_termination(state.termination, termination)); + state.termination_revision = take_next_revision(&mut state); + } + ProcessControlRequest::Cancel(reason) => { + if state.cancellation.is_none() { + state.cancellation = Some(reason); + state.cancellation_revision = take_next_revision(&mut state); + } + } + } + if state.wake_pending { + None + } else { + state.wake_pending = true; + state.wake.clone() + } + }; + if let Some(wake) = wake { + wake(); + } + Ok(()) + } +} + +fn take_next_revision(state: &mut RuntimeControlState) -> u64 { + state.next_revision = state.next_revision.wrapping_add(1).max(1); + state.next_revision +} + +fn prefer_termination( + current: Option, + requested: ProcessTermination, +) -> ProcessTermination { + match (current, requested) { + ( + Some(ProcessTermination::Signal { + signal, + force: true, + }), + _, + ) => ProcessTermination::Signal { + signal, + force: true, + }, + (_, forced @ ProcessTermination::Signal { force: true, .. }) => forced, + (Some(current), _) => current, + (None, requested) => requested, + } +} + +pub struct RuntimeControlReceiver { + inner: Arc, +} + +impl fmt::Debug for RuntimeControlReceiver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeControlReceiver") + .field( + "identity", + &lock_or_recover(&self.inner.state) + .pid + .map(|pid| ProcessRuntimeIdentity { + generation: self.inner.generation, + pid, + }), + ) + .finish_non_exhaustive() + } +} + +impl RuntimeControlReceiver { + pub fn identity(&self) -> ProcessRuntimeIdentity { + let state = lock_or_recover(&self.inner.state); + ProcessRuntimeIdentity { + generation: self.inner.generation, + pid: state + .pid + .expect("a control receiver is only created after PID binding"), + } + } + + /// Replaces the coalesced wake target when lifecycle ownership moves to a + /// shared process pump. Pending control is replayed to the new target. + pub fn set_wake(&self, wake: ProcessControlWake) { + let should_wake = { + let mut state = lock_or_recover(&self.inner.state); + state.wake = Some(Arc::clone(&wake)); + state.wake_pending + }; + if should_wake { + wake(); + } + } + + /// Leases the complete current control snapshot without clearing it. + /// Call [`Self::acknowledge`] only after every adapter action succeeds. + pub fn pending(&self) -> ProcessControlBatch { + let state = lock_or_recover(&self.inner.state); + ProcessControlBatch { + checkpoint: state.checkpoint, + stopped: state.stopped, + stop_signal: state.stop_signal, + termination: state.termination, + cancellation: state.cancellation, + checkpoint_revision: state.checkpoint_revision, + stopped_revision: state.stopped_revision, + termination_revision: state.termination_revision, + cancellation_revision: state.cancellation_revision, + } + } + + /// Acknowledges only the leased revisions. Controls requested concurrently + /// remain durable and schedule another coalesced wake. + pub fn acknowledge( + &self, + batch: ProcessControlBatch, + ) -> Result<(), ProcessRuntimeEndpointError> { + // The acknowledgement sink may lock the process table, which validates + // this endpoint's identity by locking the cell again. Never invoke it + // while holding the cell mutex. + if let (Some(sink), Some(stopped)) = (&self.inner.ack_sink, batch.stopped) { + sink.acknowledge_stop_state(self.identity(), stopped, batch.stop_signal)?; + } + + let wake = { + let mut state = lock_or_recover(&self.inner.state); + if state.stopped_revision == batch.stopped_revision && state.stopped == batch.stopped { + state.stopped = None; + state.stop_signal = None; + } + if state.checkpoint_revision == batch.checkpoint_revision { + state.checkpoint = false; + } + if state.termination_revision == batch.termination_revision + && state.termination == batch.termination + { + state.termination = None; + } + if state.cancellation_revision == batch.cancellation_revision + && state.cancellation == batch.cancellation + { + state.cancellation = None; + } + state.wake_pending = state.checkpoint + || state.stopped.is_some() + || state.termination.is_some() + || state.cancellation.is_some(); + state.wake_pending.then(|| state.wake.clone()).flatten() + }; + if let Some(wake) = wake { + wake(); + } + Ok(()) + } + + /// Replays the coalesced wake after a failed adapter action. The leased + /// controls remain unchanged. + pub fn retry_pending(&self) { + let wake = { + let mut state = lock_or_recover(&self.inner.state); + let has_pending = state.checkpoint + || state.stopped.is_some() + || state.termination.is_some() + || state.cancellation.is_some(); + state.wake_pending = has_pending; + has_pending.then(|| state.wake.clone()).flatten() + }; + if let Some(wake) = wake { + wake(); + } + } +} + +impl Drop for RuntimeControlReceiver { + fn drop(&mut self) { + let mut state = lock_or_recover(&self.inner.state); + state.wake = None; + state.receiver_attached = false; + } +} + +fn lock_or_recover(mutex: &Mutex) -> MutexGuard<'_, T> { + mutex.lock().unwrap_or_else(|poisoned| { + eprintln!("ERR_AGENTOS_PROCESS_RUNTIME_CONTROL_POISONED: recovering runtime-control state"); + poisoned.into_inner() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Mutex; + + #[derive(Default)] + struct RecordingAckSink { + transitions: Mutex)>>, + } + + impl ProcessControlAckSink for RecordingAckSink { + fn acknowledge_stop_state( + &self, + identity: ProcessRuntimeIdentity, + stopped: bool, + stop_signal: Option, + ) -> Result<(), ProcessRuntimeEndpointError> { + self.transitions + .lock() + .expect("ack transitions lock poisoned") + .push((identity, stopped, stop_signal)); + Ok(()) + } + } + + #[test] + fn runtime_fault_payloads_are_bounded_before_reporting() { + let fault = ProcessRuntimeFault::try_new( + "ERR_AGENTOS_WASM_TRAP", + "unreachable", + Some(serde_json::json!({ "trap": "unreachable" })), + ) + .expect("bounded fault"); + assert_eq!(fault.code(), "ERR_AGENTOS_WASM_TRAP"); + assert_eq!(fault.message(), "unreachable"); + assert_eq!(fault.details().expect("details")["trap"], "unreachable"); + + assert_eq!( + ProcessRuntimeFault::try_new("", "missing code", None) + .expect_err("empty code must fail") + .code(), + "EINVAL" + ); + assert_eq!( + ProcessRuntimeFault::try_new( + "ERR_AGENTOS_WASM_TRAP", + "x".repeat(MAX_RUNTIME_FAULT_MESSAGE_BYTES + 1), + None, + ) + .expect_err("oversized message must fail") + .code(), + "E2BIG" + ); + assert_eq!( + ProcessRuntimeFault::try_new( + "ERR_AGENTOS_WASM_TRAP", + "details", + Some(serde_json::json!({ + "payload": "x".repeat(MAX_RUNTIME_FAULT_DETAILS_BYTES) + })), + ) + .expect_err("oversized details must fail") + .code(), + "E2BIG" + ); + } + + #[test] + fn pre_attach_controls_are_durable_and_wake_once() { + let cell = RuntimeControlCell::new(7); + cell.bind_pid(41).expect("bind pid"); + cell.request_control(ProcessControlRequest::Checkpoint) + .expect("checkpoint"); + cell.request_control(ProcessControlRequest::Stop { signal: 20 }) + .expect("stop"); + cell.request_control(ProcessControlRequest::Checkpoint) + .expect("coalesced checkpoint"); + + let wakes = Arc::new(AtomicUsize::new(0)); + let wake_count = Arc::clone(&wakes); + let receiver = cell + .attach(Arc::new(move || { + wake_count.fetch_add(1, Ordering::Relaxed); + })) + .expect("attach receiver"); + + assert_eq!(wakes.load(Ordering::Relaxed), 1); + assert_eq!( + receiver.identity(), + ProcessRuntimeIdentity { + generation: 7, + pid: 41 + } + ); + let controls = receiver.pending(); + assert!(controls.checkpoint); + assert_eq!(controls.stopped, Some(true)); + assert_eq!(controls.stop_signal, Some(20)); + receiver + .acknowledge(controls) + .expect("acknowledge controls"); + assert!(receiver.pending().is_empty()); + } + + #[test] + fn one_wake_covers_a_coalesced_batch_and_rearms_after_take() { + let cell = RuntimeControlCell::new(9); + cell.bind_pid(3).expect("bind pid"); + let wakes = Arc::new(AtomicUsize::new(0)); + let wake_count = Arc::clone(&wakes); + let receiver = cell + .attach(Arc::new(move || { + wake_count.fetch_add(1, Ordering::Relaxed); + })) + .expect("attach receiver"); + + cell.request_control(ProcessControlRequest::Stop { signal: 20 }) + .expect("stop"); + cell.request_control(ProcessControlRequest::Continue) + .expect("continue"); + cell.request_control(ProcessControlRequest::Cancel( + ProcessCancellationReason::VmTeardown, + )) + .expect("cancel"); + assert_eq!(wakes.load(Ordering::Relaxed), 1); + let controls = receiver.pending(); + assert_eq!(controls.stopped, Some(false)); + receiver + .acknowledge(controls) + .expect("acknowledge controls"); + + cell.request_control(ProcessControlRequest::Checkpoint) + .expect("checkpoint"); + assert_eq!(wakes.load(Ordering::Relaxed), 2); + } + + #[test] + fn forced_termination_cannot_be_downgraded_or_dropped() { + let cell = RuntimeControlCell::new(1); + cell.bind_pid(99).expect("bind pid"); + let receiver = cell.attach(Arc::new(|| {})).expect("attach receiver"); + cell.request_control(ProcessControlRequest::Terminate( + ProcessTermination::Signal { + signal: 15, + force: false, + }, + )) + .expect("term"); + cell.request_control(ProcessControlRequest::Terminate( + ProcessTermination::Signal { + signal: 9, + force: true, + }, + )) + .expect("kill"); + cell.request_control(ProcessControlRequest::Terminate( + ProcessTermination::RuntimeFault, + )) + .expect("later fault"); + assert_eq!( + receiver.pending().termination, + Some(ProcessTermination::Signal { + signal: 9, + force: true, + }) + ); + } + + #[test] + fn identity_cannot_be_rebound_or_attached_twice() { + let cell = RuntimeControlCell::new(11); + cell.bind_pid(5).expect("bind pid"); + assert_eq!( + cell.bind_pid(6).expect_err("reject rebind").code(), + "ESTALE" + ); + let receiver = cell.attach(Arc::new(|| {})).expect("attach receiver"); + assert_eq!( + cell.attach(Arc::new(|| {})) + .expect_err("reject second receiver") + .code(), + "EALREADY" + ); + drop(receiver); + cell.attach(Arc::new(|| {})).expect("reattach after drop"); + } + + #[test] + fn failed_application_replays_wake_without_clearing_controls() { + let cell = RuntimeControlCell::new(13); + cell.bind_pid(8).expect("bind pid"); + let wakes = Arc::new(AtomicUsize::new(0)); + let wake_count = Arc::clone(&wakes); + let receiver = cell + .attach(Arc::new(move || { + wake_count.fetch_add(1, Ordering::Relaxed); + })) + .expect("attach receiver"); + + cell.request_control(ProcessControlRequest::Terminate( + ProcessTermination::RuntimeFault, + )) + .expect("request termination"); + let controls = receiver.pending(); + receiver.retry_pending(); + + assert_eq!(wakes.load(Ordering::Relaxed), 2); + assert_eq!(receiver.pending(), controls); + receiver.acknowledge(controls).expect("acknowledge retry"); + assert!(receiver.pending().is_empty()); + } + + #[test] + fn acknowledgement_clears_only_leased_revisions() { + let sink = Arc::new(RecordingAckSink::default()); + let cell = RuntimeControlCell::new_with_ack_sink(17, sink.clone()); + cell.bind_pid(12).expect("bind pid"); + let receiver = cell.attach(Arc::new(|| {})).expect("attach receiver"); + + cell.request_control(ProcessControlRequest::Stop { signal: 20 }) + .expect("request stop"); + let stopped = receiver.pending(); + cell.request_control(ProcessControlRequest::Continue) + .expect("request continue concurrently"); + + receiver + .acknowledge(stopped) + .expect("acknowledge applied stop"); + let continued = receiver.pending(); + assert_eq!(continued.stopped, Some(false)); + receiver + .acknowledge(continued) + .expect("acknowledge applied continue"); + assert!(receiver.pending().is_empty()); + assert_eq!( + sink.transitions + .lock() + .expect("ack transitions lock poisoned") + .as_slice(), + &[ + ( + ProcessRuntimeIdentity { + generation: 17, + pid: 12, + }, + true, + Some(20), + ), + ( + ProcessRuntimeIdentity { + generation: 17, + pid: 12, + }, + false, + None, + ), + ] + ); + } +} diff --git a/crates/kernel/src/process_table.rs b/crates/kernel/src/process_table.rs index 1beed30ba0..07b122b6ec 100644 --- a/crates/kernel/src/process_table.rs +++ b/crates/kernel/src/process_table.rs @@ -1,4 +1,10 @@ +use crate::process_runtime::{ + ProcessControlAckSink, ProcessControlRequest, ProcessExit, ProcessExitSink, + ProcessRuntimeEndpoint, ProcessRuntimeEndpointError, ProcessRuntimeFault, + ProcessRuntimeIdentity, ProcessTermination, +}; use crate::user::ProcessIdentity; +use event_listener::Event; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; @@ -21,14 +27,37 @@ pub const SIGKILL: i32 = 9; pub const SIGPIPE: i32 = 13; pub const SIGWINCH: i32 = 28; const MAX_SIGNAL: i32 = 64; +const MAX_SIGNAL_HANDLER_DEPTH: usize = 64; +const SIGTTIN: i32 = 21; +const SIGTTOU: i32 = 22; +const SIGURG: i32 = 23; + +pub const SA_RESTART: u32 = 0x1000_0000; +pub const SA_NODEFER: u32 = 0x4000_0000; +pub const SA_RESETHAND: u32 = 0x8000_0000; pub type ProcessResult = Result; -pub type ProcessExitCallback = Arc; -pub trait DriverProcess: Send + Sync { - fn kill(&self, signal: i32); - fn wait(&self, timeout: Duration) -> Option; - fn set_on_exit(&self, callback: ProcessExitCallback); +/// Runtime-neutral capability tier attached to one kernel process. +/// +/// Protocol and executor-specific tier enums are converted to this type once, +/// before registration. Host operations read this kernel-owned, monotonically +/// restricted state; a guest request can never select or raise its authority. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ProcessPermissionTier { + Isolated, + ReadOnly, + ReadWrite, + #[default] + Full, +} + +impl ProcessPermissionTier { + /// Apply a requested child-process ceiling without allowing an inherited + /// process to regain authority. + pub fn restrict(self, requested: Self) -> Self { + self.min(requested) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -83,6 +112,99 @@ impl ProcessTableError { message: message.into(), } } + + fn invalid_argument(message: impl Into) -> Self { + Self { + code: "EINVAL", + message: message.into(), + } + } + + fn interrupted(message: impl Into) -> Self { + Self { + code: "EINTR", + message: message.into(), + } + } + + fn signal_delivery_depth_exceeded(pid: u32) -> Self { + Self { + code: "EAGAIN", + message: format!( + "process {pid} exceeded {MAX_SIGNAL_HANDLER_DEPTH} nested signal handlers" + ), + } + } + + fn invalid_signal_delivery_token(pid: u32, token: u64) -> Self { + Self { + code: "EINVAL", + message: format!("invalid signal delivery token {token} for process {pid}"), + } + } + + fn stale_runtime_identity(expected: ProcessRuntimeIdentity) -> Self { + Self { + code: "ESTALE", + message: format!( + "runtime reporter for VM generation {} pid {} no longer owns that process", + expected.generation, expected.pid + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ProcessResourceLimitKind { + AddressSpace, + Core, + Cpu, + Data, + FileSize, + LockedMemory, + OpenFiles, + Processes, + ResidentSet, + Stack, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ProcessResourceLimit { + /// `None` is Linux `RLIM_INFINITY`. + pub soft: Option, + /// `None` is Linux `RLIM_INFINITY`. + pub hard: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ProcessResourceLimits { + values: BTreeMap, +} + +impl ProcessResourceLimits { + pub fn with_open_files(limit: u64) -> Self { + let mut limits = Self::default(); + limits.values.insert( + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimit { + soft: Some(limit), + hard: Some(limit), + }, + ); + limits + } + + pub fn get(&self, kind: ProcessResourceLimitKind) -> ProcessResourceLimit { + self.values.get(&kind).copied().unwrap_or_default() + } + + fn set(&mut self, kind: ProcessResourceLimitKind, value: ProcessResourceLimit) { + if value == ProcessResourceLimit::default() { + self.values.remove(&kind); + } else { + self.values.insert(kind, value); + } + } } impl fmt::Display for ProcessTableError { @@ -174,6 +296,48 @@ pub enum SigmaskHow { SetMask, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SignalDisposition { + #[default] + Default, + Ignore, + User, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct SignalAction { + pub disposition: SignalDisposition, + pub mask: SignalSet, + pub flags: u32, +} + +impl SignalAction { + pub const DEFAULT: Self = Self { + disposition: SignalDisposition::Default, + mask: SignalSet::empty(), + flags: 0, + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignalDelivery { + pub token: u64, + pub signal: i32, + pub action: SignalAction, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct InProgressSignalDelivery { + token: u64, + previous_mask: SignalSet, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct TemporarySignalMask { + token: u64, + previous_mask: SignalSet, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WaitPidFlags { bits: u32, @@ -229,6 +393,12 @@ pub struct ProcessWaitResult { pub event: ProcessWaitEvent, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessWaitTransition { + pub result: ProcessWaitResult, + pub termination: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProcessFileDescriptors { pub stdin: u32, @@ -257,6 +427,8 @@ pub struct ProcessContext { pub identity: ProcessIdentity, pub blocked_signals: SignalSet, pub pending_signals: SignalSet, + pub resource_limits: ProcessResourceLimits, + pub permission_tier: ProcessPermissionTier, } impl Default for ProcessContext { @@ -271,6 +443,8 @@ impl Default for ProcessContext { identity: ProcessIdentity::default(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), + resource_limits: ProcessResourceLimits::default(), + permission_tier: ProcessPermissionTier::default(), } } } @@ -286,6 +460,9 @@ pub struct ProcessEntry { pub args: Vec, pub status: ProcessStatus, pub exit_code: Option, + pub pending_termination: Option, + pub termination: Option, + pub runtime_fault: Option, pub exit_time_ms: Option, pub env: BTreeMap, pub cwd: String, @@ -303,6 +480,9 @@ pub struct ProcessInfo { pub command: String, pub status: ProcessStatus, pub exit_code: Option, + pub pending_termination: Option, + pub termination: Option, + pub runtime_fault: Option, pub identity: ProcessIdentity, } @@ -314,22 +494,66 @@ pub struct ProcessTable { struct ProcessTableInner { state: Mutex, waiters: Condvar, + wait_generation: Mutex, + async_waiters: Event, reaper: Arc, } +/// Cloneable async notification capability for process-table wait state. +/// +/// Callers snapshot before probing `waitpid(..., WNOHANG)`, then await a +/// generation change off the kernel-owning thread. The process table remains +/// the source of truth; a wake only authorizes another nonblocking probe. +#[derive(Clone)] +pub struct ProcessWaitHandle { + inner: Arc, +} + +impl fmt::Debug for ProcessWaitHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProcessWaitHandle").finish_non_exhaustive() + } +} + +impl ProcessWaitHandle { + pub fn snapshot(&self) -> u64 { + *lock_or_recover(&self.inner.wait_generation) + } + + pub async fn wait_for_change_async(&self, observed: u64) { + loop { + let listener = self.inner.async_waiters.listen(); + if self.snapshot() != observed { + return; + } + listener.await; + if self.snapshot() != observed { + return; + } + } + } +} + struct ProcessRecord { entry: ProcessEntry, - driver_process: Arc, + runtime_endpoint: Arc, pending_wait_events: VecDeque, blocked_signals: SignalSet, pending_signals: SignalSet, + signal_actions: [SignalAction; MAX_SIGNAL as usize], + signal_deliveries: Vec, + temporary_signal_masks: Vec, + next_signal_delivery_token: u64, + next_signal_mask_token: u64, + resource_limits: ProcessResourceLimits, + permission_tier: ProcessPermissionTier, } struct ScheduledSignalDelivery { pid: u32, signal: i32, - status: ProcessStatus, - driver_process: Arc, + runtime_endpoint: Arc, + controls: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -381,6 +605,8 @@ impl Default for ProcessTable { inner: Arc::new(ProcessTableInner { state: Mutex::new(ProcessTableState::default()), waiters: Condvar::new(), + wait_generation: Mutex::new(0), + async_waiters: Event::new(), reaper, }), } @@ -398,6 +624,12 @@ impl ProcessTable { table } + pub fn wait_handle(&self) -> ProcessWaitHandle { + ProcessWaitHandle { + inner: Arc::clone(&self.inner), + } + } + pub fn allocate_pid(&self) -> ProcessResult { let mut state = self.inner.lock_state(); let start = normalize_next_pid(state.next_pid); @@ -427,9 +659,9 @@ impl ProcessTable { command: impl Into, args: Vec, ctx: ProcessContext, - driver_process: Arc, + runtime_endpoint: Arc, ) -> ProcessEntry { - self.register_with_process_group(pid, driver, command, args, ctx, driver_process, None) + self.register_with_process_group(pid, driver, command, args, ctx, runtime_endpoint, None) .expect("inheriting a process group cannot fail") } @@ -443,15 +675,23 @@ impl ProcessTable { command: impl Into, args: Vec, ctx: ProcessContext, - driver_process: Arc, + runtime_endpoint: Arc, requested_pgid: Option, ) -> ProcessResult { let driver = driver.into(); let command = command.into(); let mut state = self.inner.lock_state(); - let (inherited_pgid, sid) = match state.entries.get(&ctx.ppid) { - Some(parent) => (parent.entry.pgid, parent.entry.sid), - None => (pid, pid), + let (inherited_pgid, sid, inherited_signal_actions) = match state.entries.get(&ctx.ppid) { + Some(parent) => { + let mut actions = [SignalAction::DEFAULT; MAX_SIGNAL as usize]; + for (target, source) in actions.iter_mut().zip(parent.signal_actions) { + if source.disposition == SignalDisposition::Ignore { + *target = source; + } + } + (parent.entry.pgid, parent.entry.sid, actions) + } + None => (pid, pid, [SignalAction::DEFAULT; MAX_SIGNAL as usize]), }; let pgid = requested_pgid.map_or(inherited_pgid, |pgid| if pgid == 0 { pid } else { pgid }); if requested_pgid.is_some() && pgid != pid { @@ -485,6 +725,9 @@ impl ProcessTable { args, status: ProcessStatus::Running, exit_code: None, + pending_termination: None, + termination: None, + runtime_fault: None, exit_time_ms: None, env: ctx.env, cwd: ctx.cwd, @@ -497,21 +740,19 @@ impl ProcessTable { pid, ProcessRecord { entry: entry.clone(), - driver_process: driver_process.clone(), + runtime_endpoint, pending_wait_events: VecDeque::new(), blocked_signals: ctx.blocked_signals, pending_signals: ctx.pending_signals, + signal_actions: inherited_signal_actions, + signal_deliveries: Vec::new(), + temporary_signal_masks: Vec::new(), + next_signal_delivery_token: 1, + next_signal_mask_token: 1, + resource_limits: ctx.resource_limits, + permission_tier: ctx.permission_tier, }, ); - drop(state); - - let weak = Arc::downgrade(&self.inner); - driver_process.set_on_exit(Arc::new(move |code| { - if let Some(inner) = weak.upgrade() { - mark_exited_inner(&inner, pid, code); - } - })); - Ok(entry) } @@ -550,12 +791,70 @@ impl ProcessTable { identity: parent.entry.identity.clone(), blocked_signals: parent.blocked_signals, pending_signals: SignalSet::empty(), + resource_limits: parent.resource_limits.clone(), + permission_tier: parent.permission_tier, }) } + pub fn permission_tier(&self, pid: u32) -> ProcessResult { + let state = self.inner.lock_state(); + let record = state + .entries + .get(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + Ok(record.permission_tier) + } + + pub fn get_resource_limit( + &self, + pid: u32, + kind: ProcessResourceLimitKind, + ) -> ProcessResult { + let state = self.inner.lock_state(); + let record = state + .entries + .get(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + Ok(record.resource_limits.get(kind)) + } + + pub fn set_resource_limit( + &self, + pid: u32, + kind: ProcessResourceLimitKind, + value: ProcessResourceLimit, + ) -> ProcessResult<()> { + if matches!((value.soft, value.hard), (Some(soft), Some(hard)) if soft > hard) { + return Err(ProcessTableError::invalid_argument( + "resource-limit soft value exceeds hard value", + )); + } + + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let current_hard = record.resource_limits.get(kind).hard; + let raises_hard = match (current_hard, value.hard) { + (Some(_), None) => true, + (Some(current), Some(requested)) => requested > current, + (None, _) => false, + }; + if raises_hard { + return Err(ProcessTableError::permission_denied( + "resource-limit hard value cannot be raised", + )); + } + record.resource_limits.set(kind, value); + Ok(()) + } + /// Replace the userspace image metadata while retaining Linux process /// identity (PID/PPID/PGID/SID), wait relationships, signal mask, pending - /// signals, and the driver process used to report the eventual exit. + /// signals, and the runtime endpoint used to report the eventual exit. + /// Caught dispositions reset to default while ignored dispositions survive, + /// matching execve(2). pub fn exec( &self, pid: u32, @@ -564,6 +863,7 @@ impl ProcessTable { args: Vec, env: BTreeMap, cwd: String, + requested_permission_tier: Option, ) -> ProcessResult<()> { let mut state = self.inner.lock_state(); let record = state @@ -573,6 +873,11 @@ impl ProcessTable { if record.entry.status == ProcessStatus::Exited { return Err(ProcessTableError::no_such_process(pid)); } + if record.entry.pending_termination.is_some() { + return Err(ProcessTableError::interrupted(format!( + "process {pid} cannot replace its image after termination was requested" + ))); + } record.entry.driver = driver.into(); record.entry.command = command.into(); record.entry.args = args; @@ -580,8 +885,19 @@ impl ProcessTable { record.entry.cwd = cwd; record.entry.status = ProcessStatus::Running; record.entry.exit_code = None; + record.entry.termination = None; record.entry.exit_time_ms = None; - self.inner.waiters.notify_all(); + if let Some(requested_tier) = requested_permission_tier { + record.permission_tier = record.permission_tier.restrict(requested_tier); + } + for action in &mut record.signal_actions { + if action.disposition == SignalDisposition::User { + *action = SignalAction::DEFAULT; + } + } + record.signal_deliveries.clear(); + record.temporary_signal_masks.clear(); + self.inner.notify_waiters(); Ok(()) } @@ -618,32 +934,40 @@ impl ProcessTable { .count() } - pub fn mark_exited(&self, pid: u32, exit_code: i32) { - mark_exited_inner(&self.inner, pid, exit_code); + pub fn mark_exited(&self, pid: u32, exit_code: i32) -> ProcessResult<()> { + mark_exited_inner(&self.inner, pid, None, ProcessExit::Exited(exit_code), None) + } + + /// Reports one exact terminal result. The first report wins; duplicate or + /// late adapter reports cannot rewrite wait status. + pub fn report_exit(&self, pid: u32, termination: ProcessExit) -> ProcessResult<()> { + mark_exited_inner(&self.inner, pid, None, termination, None) } - pub fn mark_stopped(&self, pid: u32, signal: i32) { + pub fn mark_stopped(&self, pid: u32, signal: i32) -> ProcessResult<()> { mark_wait_event_inner( &self.inner, pid, + None, ProcessStatus::Stopped, PendingWaitEvent { status: signal, event: ProcessWaitEvent::Stopped, }, - ); + ) } - pub fn mark_continued(&self, pid: u32) { + pub fn mark_continued(&self, pid: u32) -> ProcessResult<()> { mark_wait_event_inner( &self.inner, pid, + None, ProcessStatus::Running, PendingWaitEvent { status: SIGCONT, event: ProcessWaitEvent::Continued, }, - ); + ) } pub fn waitpid(&self, pid: u32) -> ProcessResult<(u32, i32)> { @@ -658,7 +982,7 @@ impl ProcessTable { state.entries.remove(&pid); drop(state); self.inner.reaper.cancel(pid); - self.inner.waiters.notify_all(); + self.inner.notify_waiters(); return Ok((pid, status)); } @@ -666,12 +990,46 @@ impl ProcessTable { } } + /// Wait for terminal state without consuming the zombie record. + pub fn wait_for_exit(&self, pid: u32, timeout: Duration) -> ProcessResult> { + let deadline = Instant::now().checked_add(timeout).ok_or_else(|| { + ProcessTableError::invalid_argument( + "process wait timeout exceeds the supported deadline range", + ) + })?; + let mut state = self.inner.lock_state(); + loop { + let Some(record) = state.entries.get(&pid) else { + return Err(ProcessTableError::no_such_process(pid)); + }; + if record.entry.status == ProcessStatus::Exited { + return Ok(record.entry.termination); + } + let now = Instant::now(); + if now >= deadline { + return Ok(None); + } + state = wait_timeout_or_recover(&self.inner.waiters, state, deadline - now); + } + } + pub fn waitpid_for( &self, waiter_pid: u32, pid: i32, flags: WaitPidFlags, ) -> ProcessResult> { + Ok(self + .waitpid_for_detailed(waiter_pid, pid, flags)? + .map(|transition| transition.result)) + } + + pub fn waitpid_for_detailed( + &self, + waiter_pid: u32, + pid: i32, + flags: WaitPidFlags, + ) -> ProcessResult> { let mut state = self.inner.lock_state(); loop { let selector = resolve_wait_selector(&state, waiter_pid, pid)?; @@ -680,14 +1038,16 @@ impl ProcessTable { return Err(ProcessTableError::no_matching_child(waiter_pid, pid)); } - if let Some(result) = take_waitable_event(&mut state, &matching_children, flags) { - let should_reap = result.event == ProcessWaitEvent::Exited; + if let Some(transition) = + take_waitable_transition(&mut state, &matching_children, flags) + { + let should_reap = transition.result.event == ProcessWaitEvent::Exited; drop(state); if should_reap { - self.inner.reaper.cancel(result.pid); - self.inner.waiters.notify_all(); + self.inner.reaper.cancel(transition.result.pid); + self.inner.notify_waiters(); } - return Ok(Some(result)); + return Ok(Some(transition)); } if flags.contains(WaitPidFlags::WNOHANG) { @@ -749,7 +1109,9 @@ impl ProcessTable { let grouped = state .entries .values() - .filter(|record| record.entry.pgid == pgid) + .filter(|record| { + record.entry.pgid == pgid && record.entry.status != ProcessStatus::Exited + }) .map(|record| record.entry.pid) .collect::>(); if grouped.is_empty() { @@ -776,6 +1138,7 @@ impl ProcessTable { } deliver_signals(&self.inner, deliveries); + self.inner.notify_waiters(); Ok(()) } @@ -887,26 +1250,44 @@ impl ProcessTable { } pub fn terminate_all(&self) { + let graceful_termination = ProcessTermination::Signal { + signal: SIGTERM, + force: false, + }; let running = { let mut state = self.inner.lock_state(); state.terminating_all = true; self.inner.reaper.clear(); state .entries - .values() - .filter(|record| record.entry.status == ProcessStatus::Running) - .map(|record| (record.entry.pid, Arc::clone(&record.driver_process))) + .values_mut() + .filter(|record| record.entry.status != ProcessStatus::Exited) + .map(|record| { + record.entry.pending_termination = Some(graceful_termination); + (record.entry.pid, Arc::clone(&record.runtime_endpoint)) + }) .collect::>() }; - for (_, driver) in &running { - driver.kill(SIGTERM); - } - for (pid, driver) in &running { - if let Some(exit_code) = driver.wait(Duration::from_secs(1)) { - self.mark_exited(*pid, exit_code); + for (pid, endpoint) in &running { + if let Err(error) = + endpoint.request_control(ProcessControlRequest::Terminate(graceful_termination)) + { + eprintln!( + "ERR_AGENTOS_PROCESS_CONTROL: pid={pid} control=terminate-graceful code={} error={}", + error.code(), + error.message() + ); } } + self.wait_for_terminal_state( + &running + .iter() + .filter(|(_, endpoint)| endpoint.has_control_consumer()) + .map(|(pid, _)| *pid) + .collect::>(), + Duration::from_secs(1), + ); let survivors = { let state = self.inner.lock_state(); @@ -916,92 +1297,443 @@ impl ProcessTable { state .entries .get(pid) - .map(|record| record.entry.status == ProcessStatus::Running) + .map(|record| record.entry.status != ProcessStatus::Exited) .unwrap_or(false) }) .cloned() .collect::>() }; - for (_, driver) in &survivors { - driver.kill(SIGKILL); + let forced_termination = ProcessTermination::Signal { + signal: SIGKILL, + force: true, + }; + { + let mut state = self.inner.lock_state(); + for (pid, _) in &survivors { + if let Some(record) = state.entries.get_mut(pid) { + record.entry.pending_termination = Some(forced_termination); + } + } + } + + for (pid, endpoint) in &survivors { + if let Err(error) = + endpoint.request_control(ProcessControlRequest::Terminate(forced_termination)) + { + eprintln!( + "ERR_AGENTOS_PROCESS_CONTROL: pid={pid} control=terminate-forced code={} error={}", + error.code(), + error.message() + ); + } } - for (pid, driver) in &survivors { - if let Some(exit_code) = driver.wait(Duration::from_millis(500)) { - self.mark_exited(*pid, exit_code); + self.wait_for_terminal_state( + &survivors + .iter() + .filter(|(_, endpoint)| endpoint.has_control_consumer()) + .map(|(pid, _)| *pid) + .collect::>(), + Duration::from_millis(500), + ); + for (pid, _) in &survivors { + let still_running = self + .get(*pid) + .is_some_and(|entry| entry.status != ProcessStatus::Exited); + if still_running { + if let Err(error) = self.report_exit( + *pid, + ProcessExit::Signaled { + signal: SIGKILL, + core_dumped: false, + }, + ) { + eprintln!( + "ERR_AGENTOS_PROCESS_EXIT: pid={pid} control=terminate-forced code={} error={}", + error.code(), + error + ); + } } } self.inner.lock_state().terminating_all = false; } - pub fn sigprocmask( + fn wait_for_terminal_state(&self, pids: &[u32], timeout: Duration) { + let deadline = Instant::now() + timeout; + let mut state = self.inner.lock_state(); + loop { + let all_terminal = pids.iter().all(|pid| { + state + .entries + .get(pid) + .map(|record| record.entry.status == ProcessStatus::Exited) + .unwrap_or(true) + }); + if all_terminal { + return; + } + let now = Instant::now(); + if now >= deadline { + return; + } + state = wait_timeout_or_recover(&self.inner.waiters, state, deadline - now); + } + } + + pub fn signal_action( &self, pid: u32, - how: SigmaskHow, - set: SignalSet, - ) -> ProcessResult { - let (previous, deliveries) = { + signal: i32, + action: Option, + ) -> ProcessResult { + if !(1..=MAX_SIGNAL).contains(&signal) { + return Err(ProcessTableError::invalid_signal(signal)); + } + if action.is_some() && matches!(signal, SIGKILL | SIGSTOP) { + return Err(ProcessTableError::invalid_signal(signal)); + } + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let slot = &mut record.signal_actions[(signal - 1) as usize]; + let previous = *slot; + if let Some(action) = action { + *slot = action; + if action.disposition == SignalDisposition::Ignore { + record.pending_signals.remove(signal)?; + } + } + Ok(previous) + } + + pub fn reset_signal_actions_for_exec(&self, pid: u32) -> ProcessResult<()> { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + for action in &mut record.signal_actions { + if action.disposition == SignalDisposition::User { + *action = SignalAction::DEFAULT; + } + } + record.signal_deliveries.clear(); + record.temporary_signal_masks.clear(); + Ok(()) + } + + /// Atomically installs the temporary process mask used by `ppoll`. + pub fn begin_temporary_signal_mask(&self, pid: u32, mut mask: SignalSet) -> ProcessResult { + mask.remove(SIGKILL)?; + mask.remove(SIGSTOP)?; + let (token, deliveries) = { let mut state = self.inner.lock_state(); let record = state .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - let previous = record.blocked_signals; - record.blocked_signals = match how { - SigmaskHow::Block => previous.union(set), - SigmaskHow::Unblock => previous.difference(set), - SigmaskHow::SetMask => set, - }; - - let unblocked_pending = record.pending_signals.difference(record.blocked_signals); - let deliveries = collect_pending_signal_deliveries(record, unblocked_pending)?; - (previous, deliveries) + if record.temporary_signal_masks.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } + let token = record.next_signal_mask_token; + record.next_signal_mask_token = + record.next_signal_mask_token.checked_add(1).unwrap_or(1); + record.temporary_signal_masks.push(TemporarySignalMask { + token, + previous_mask: record.blocked_signals, + }); + record.blocked_signals = mask; + (token, collect_pending_signal_deliveries(record)?) }; + deliver_signals(&self.inner, deliveries); + Ok(token) + } + /// Restores the previous mask for a `ppoll` scope and schedules any signal + /// that became deliverable as part of restoration. + pub fn end_temporary_signal_mask(&self, pid: u32, token: u64) -> ProcessResult<()> { + let deliveries = { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let Some(scope) = record.temporary_signal_masks.last().copied() else { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + }; + if scope.token != token { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + } + record.temporary_signal_masks.pop(); + record.blocked_signals = scope.previous_mask; + collect_pending_signal_deliveries(record)? + }; deliver_signals(&self.inner, deliveries); - Ok(previous) + Ok(()) } - pub fn sigpending(&self, pid: u32) -> ProcessResult { - self.inner - .lock_state() + /// Atomically selects one caught signal that became deliverable under a + /// `ppoll` mask, restores the caller's mask, and starts its handler using + /// that restored mask as the handler frame's previous state. + /// + /// Selection must happen before restoration or a signal unblocked only by + /// `ppoll` would disappear from the deliverable set. Handler setup must + /// happen after restoration so nested handlers and `sigprocmask` observe + /// the real caller mask, matching Linux's atomic ppoll return semantics. + pub fn end_temporary_signal_mask_and_begin_signal_delivery( + &self, + pid: u32, + token: u64, + ) -> ProcessResult> { + let mut state = self.inner.lock_state(); + let record = state .entries - .get(&pid) - .map(|record| record.pending_signals) - .ok_or_else(|| ProcessTableError::no_such_process(pid)) - } -} + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let Some(scope) = record.temporary_signal_masks.last().copied() else { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + }; + if scope.token != token { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + } + if record.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } -fn to_process_info(entry: &ProcessEntry) -> ProcessInfo { - ProcessInfo { - pid: entry.pid, - ppid: entry.ppid, - pgid: entry.pgid, - sid: entry.sid, + let selected = record + .pending_signals + .difference(record.blocked_signals) + .signals() + .into_iter() + .find(|signal| { + record.signal_actions[(*signal - 1) as usize].disposition == SignalDisposition::User + }); + record.temporary_signal_masks.pop(); + record.blocked_signals = scope.previous_mask; + + let Some(signal) = selected else { + return Ok(None); + }; + record.pending_signals.remove(signal)?; + let action = record.signal_actions[(signal - 1) as usize]; + let previous_mask = record.blocked_signals; + record.blocked_signals = record.blocked_signals.union(action.mask); + if action.flags & SA_NODEFER == 0 { + record.blocked_signals.insert(signal)?; + } + record.blocked_signals.remove(SIGKILL)?; + record.blocked_signals.remove(SIGSTOP)?; + let delivery_token = record.next_signal_delivery_token; + record.next_signal_delivery_token = record + .next_signal_delivery_token + .checked_add(1) + .unwrap_or(1); + record.signal_deliveries.push(InProgressSignalDelivery { + token: delivery_token, + previous_mask, + }); + if action.flags & SA_RESETHAND != 0 { + record.signal_actions[(signal - 1) as usize] = SignalAction::DEFAULT; + } + Ok(Some(SignalDelivery { + token: delivery_token, + signal, + action, + })) + } + + /// Claims one caught, unblocked signal and applies its handler mask. + pub fn begin_signal_delivery(&self, pid: u32) -> ProcessResult> { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + if record.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } + let Some(signal) = record + .pending_signals + .difference(record.blocked_signals) + .signals() + .into_iter() + .find(|signal| { + record.signal_actions[(*signal - 1) as usize].disposition == SignalDisposition::User + }) + else { + return Ok(None); + }; + record.pending_signals.remove(signal)?; + let action = record.signal_actions[(signal - 1) as usize]; + let previous_mask = record.blocked_signals; + record.blocked_signals = record.blocked_signals.union(action.mask); + if action.flags & SA_NODEFER == 0 { + record.blocked_signals.insert(signal)?; + } + record.blocked_signals.remove(SIGKILL)?; + record.blocked_signals.remove(SIGSTOP)?; + let token = record.next_signal_delivery_token; + record.next_signal_delivery_token = record + .next_signal_delivery_token + .checked_add(1) + .unwrap_or(1); + record.signal_deliveries.push(InProgressSignalDelivery { + token, + previous_mask, + }); + if action.flags & SA_RESETHAND != 0 { + record.signal_actions[(signal - 1) as usize] = SignalAction::DEFAULT; + } + Ok(Some(SignalDelivery { + token, + signal, + action, + })) + } + + pub fn end_signal_delivery(&self, pid: u32, token: u64) -> ProcessResult<()> { + let deliveries = { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let Some(delivery) = record.signal_deliveries.last().copied() else { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + }; + if delivery.token != token { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + } + record.signal_deliveries.pop(); + record.blocked_signals = delivery.previous_mask; + collect_pending_signal_deliveries(record)? + }; + deliver_signals(&self.inner, deliveries); + Ok(()) + } + + pub fn sigprocmask( + &self, + pid: u32, + how: SigmaskHow, + set: SignalSet, + ) -> ProcessResult { + let (previous, deliveries) = { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let previous = record.blocked_signals; + let mut next = match how { + SigmaskHow::Block => previous.union(set), + SigmaskHow::Unblock => previous.difference(set), + SigmaskHow::SetMask => set, + }; + next.remove(SIGKILL)?; + next.remove(SIGSTOP)?; + record.blocked_signals = next; + + let deliveries = collect_pending_signal_deliveries(record)?; + (previous, deliveries) + }; + + deliver_signals(&self.inner, deliveries); + Ok(previous) + } + + pub fn sigpending(&self, pid: u32) -> ProcessResult { + self.inner + .lock_state() + .entries + .get(&pid) + .map(|record| record.pending_signals) + .ok_or_else(|| ProcessTableError::no_such_process(pid)) + } +} + +impl ProcessExitSink for ProcessTable { + fn report_exit( + &self, + identity: ProcessRuntimeIdentity, + termination: ProcessExit, + ) -> Result<(), ProcessRuntimeEndpointError> { + mark_exited_inner(&self.inner, identity.pid, Some(identity), termination, None) + .map_err(|error| ProcessRuntimeEndpointError::new(error.code, error.message)) + } + + fn report_runtime_fault( + &self, + identity: ProcessRuntimeIdentity, + fault: ProcessRuntimeFault, + ) -> Result<(), ProcessRuntimeEndpointError> { + mark_exited_inner( + &self.inner, + identity.pid, + Some(identity), + ProcessExit::Exited(1), + Some(fault), + ) + .map_err(|error| ProcessRuntimeEndpointError::new(error.code, error.message)) + } +} + +fn to_process_info(entry: &ProcessEntry) -> ProcessInfo { + ProcessInfo { + pid: entry.pid, + ppid: entry.ppid, + pgid: entry.pgid, + sid: entry.sid, driver: entry.driver.clone(), command: entry.command.clone(), status: entry.status, exit_code: entry.exit_code, + pending_termination: entry.pending_termination, + termination: entry.termination, + runtime_fault: entry.runtime_fault.clone(), identity: entry.identity.clone(), } } -fn mark_exited_inner(inner: &Arc, pid: u32, exit_code: i32) { +fn mark_exited_inner( + inner: &Arc, + pid: u32, + expected_identity: Option, + termination: ProcessExit, + runtime_fault: Option, +) -> ProcessResult<()> { let (callback, zombie_ttl, should_schedule, deliveries) = { let mut state = inner.lock_state(); let (ppid, pgid) = { let Some(record) = state.entries.get_mut(&pid) else { - return; + return expected_identity.map_or(Ok(()), |identity| { + Err(ProcessTableError::stale_runtime_identity(identity)) + }); }; + if let Some(expected_identity) = expected_identity { + if record.runtime_endpoint.identity() != Some(expected_identity) { + return Err(ProcessTableError::stale_runtime_identity(expected_identity)); + } + } + if record.entry.status == ProcessStatus::Exited { - return; + return Ok(()); } record.entry.status = ProcessStatus::Exited; - record.entry.exit_code = Some(exit_code); + record.entry.exit_code = Some(termination.shell_status()); + record.entry.pending_termination = None; + record.entry.termination = Some(termination); + record.entry.runtime_fault = runtime_fault; record.entry.exit_time_ms = Some(now_ms()); + // Child wait state is level-like: a terminal transition supersedes + // an unconsumed stop/continue notification for the same child. + record.pending_wait_events.clear(); let ppid = record.entry.ppid; let pgid = record.entry.pgid; (ppid, pgid) @@ -1062,7 +1794,8 @@ fn mark_exited_inner(inner: &Arc, pid: u32, exit_code: i32) { on_process_exit(pid); } - inner.waiters.notify_all(); + inner.notify_waiters(); + Ok(()) } fn reparent_children_to_init( @@ -1157,21 +1890,34 @@ fn process_group_has_stopped_member(state: &ProcessTableState, pgid: u32) -> boo fn mark_wait_event_inner( inner: &Arc, pid: u32, + expected_identity: Option, next_status: ProcessStatus, event: PendingWaitEvent, -) { +) -> ProcessResult<()> { let deliveries = { let mut state = inner.lock_state(); let ppid = { let Some(record) = state.entries.get_mut(&pid) else { - return; + return expected_identity.map_or(Ok(()), |identity| { + Err(ProcessTableError::stale_runtime_identity(identity)) + }); }; + if let Some(expected_identity) = expected_identity { + if record.runtime_endpoint.identity() != Some(expected_identity) { + return Err(ProcessTableError::stale_runtime_identity(expected_identity)); + } + } + if record.entry.status == ProcessStatus::Exited || record.entry.status == next_status { - return; + return Ok(()); } record.entry.status = next_status; + // Wait state is level-like per child: only the latest unconsumed + // nonterminal transition is observable. Ordering across children + // remains the process-table iteration order used by waitpid. + record.pending_wait_events.clear(); record.pending_wait_events.push_back(event); record.entry.ppid }; @@ -1192,7 +1938,8 @@ fn mark_wait_event_inner( deliver_signals(inner, deliveries); - inner.waiters.notify_all(); + inner.notify_waiters(); + Ok(()) } fn signal_bit(signal: i32) -> ProcessResult { @@ -1232,23 +1979,92 @@ fn next_pid_after_registered(current: u32, registered: u32) -> u32 { } fn signal_can_be_blocked(signal: i32) -> bool { - !matches!(signal, SIGKILL | SIGSTOP | SIGCONT) + !matches!(signal, SIGKILL | SIGSTOP) } fn queue_or_schedule_signal( record: &mut ProcessRecord, signal: i32, ) -> ProcessResult> { + let action = if matches!(signal, SIGKILL | SIGSTOP) { + SignalAction::DEFAULT + } else { + record.signal_actions[(signal - 1) as usize] + }; + let mut controls = Vec::with_capacity(2); + + // SIGCONT must also supersede a stop that has been requested but not yet + // acknowledged by the runtime. Sending an idempotent Continue while the + // process is still running lets the endpoint's last-writer-wins control + // cell cancel that in-flight Stop. + if signal == SIGCONT { + controls.push(ProcessControlRequest::Continue); + } + + if action.disposition == SignalDisposition::Ignore { + return scheduled_signal_delivery(record, signal, controls); + } + if signal_can_be_blocked(signal) && record.blocked_signals.contains(signal) { record.pending_signals.insert(signal)?; - return Ok(None); + return scheduled_signal_delivery(record, signal, controls); } + match action.disposition { + SignalDisposition::Ignore => unreachable!("ignore handled above"), + SignalDisposition::User => { + record.pending_signals.insert(signal)?; + controls.push(ProcessControlRequest::Checkpoint); + } + SignalDisposition::Default => match signal { + SIGCHLD | SIGWINCH | SIGURG => {} + SIGCONT => {} + SIGSTOP | SIGTSTP | SIGTTIN | SIGTTOU => { + controls.push(ProcessControlRequest::Stop { signal }) + } + signal => { + let termination = ProcessTermination::Signal { + signal, + force: signal == SIGKILL, + }; + record.entry.pending_termination = Some(prefer_pending_termination( + record.entry.pending_termination, + termination, + )); + controls.push(ProcessControlRequest::Terminate(termination)); + } + }, + } + + scheduled_signal_delivery(record, signal, controls) +} + +fn prefer_pending_termination( + current: Option, + requested: ProcessTermination, +) -> ProcessTermination { + match (current, requested) { + ( + Some(current @ ProcessTermination::Signal { force: true, .. }), + ProcessTermination::Signal { force: false, .. }, + ) => current, + (_, requested) => requested, + } +} + +fn scheduled_signal_delivery( + record: &ProcessRecord, + signal: i32, + controls: Vec, +) -> ProcessResult> { + if controls.is_empty() { + return Ok(None); + } Ok(Some(ScheduledSignalDelivery { pid: record.entry.pid, signal, - status: record.entry.status, - driver_process: Arc::clone(&record.driver_process), + runtime_endpoint: Arc::clone(&record.runtime_endpoint), + controls, })) } @@ -1271,57 +2087,63 @@ fn collect_signal_deliveries( fn collect_pending_signal_deliveries( record: &mut ProcessRecord, - signals: SignalSet, ) -> ProcessResult> { let mut deliveries = Vec::new(); + let signals = record.pending_signals.difference(record.blocked_signals); for signal in signals.signals() { record.pending_signals.remove(signal)?; - deliveries.push(ScheduledSignalDelivery { - pid: record.entry.pid, - signal, - status: record.entry.status, - driver_process: Arc::clone(&record.driver_process), - }); + if let Some(delivery) = queue_or_schedule_signal(record, signal)? { + deliveries.push(delivery); + } } Ok(deliveries) } -fn deliver_signals(inner: &Arc, deliveries: Vec) { - let mut stopped = Vec::new(); - let mut continued = Vec::new(); - +fn deliver_signals(_inner: &Arc, deliveries: Vec) { for delivery in &deliveries { - match delivery.signal { - SIGSTOP | SIGTSTP if delivery.status == ProcessStatus::Running => { - stopped.push((delivery.pid, delivery.signal)) + for request in &delivery.controls { + if let Err(error) = delivery.runtime_endpoint.request_control(*request) { + eprintln!( + "failed to request runtime control for kernel pid {} signal {}: {}", + delivery.pid, delivery.signal, error + ); } - SIGCONT if delivery.status == ProcessStatus::Stopped => continued.push(delivery.pid), - _ => {} } - delivery.driver_process.kill(delivery.signal); } +} - for (pid, signal) in stopped { - mark_wait_event_inner( - inner, - pid, - ProcessStatus::Stopped, - PendingWaitEvent { - status: signal, - event: ProcessWaitEvent::Stopped, - }, - ); - } - for pid in continued { - mark_wait_event_inner( - inner, - pid, - ProcessStatus::Running, - PendingWaitEvent { - status: SIGCONT, - event: ProcessWaitEvent::Continued, - }, - ); +impl ProcessControlAckSink for ProcessTable { + fn acknowledge_stop_state( + &self, + identity: ProcessRuntimeIdentity, + stopped: bool, + stop_signal: Option, + ) -> Result<(), ProcessRuntimeEndpointError> { + let (status, event) = if stopped { + let signal = stop_signal.ok_or_else(|| { + ProcessRuntimeEndpointError::new( + "EINVAL", + "stopped runtime control acknowledgement is missing its signal", + ) + })?; + ( + ProcessStatus::Stopped, + PendingWaitEvent { + status: signal, + event: ProcessWaitEvent::Stopped, + }, + ) + } else { + ( + ProcessStatus::Running, + PendingWaitEvent { + status: SIGCONT, + event: ProcessWaitEvent::Continued, + }, + ) + }; + mark_wait_event_inner(&self.inner, identity.pid, Some(identity), status, event) + .map_err(|error| ProcessRuntimeEndpointError::new(error.code, error.message)) } } @@ -1361,11 +2183,11 @@ fn matching_child_pids( .collect() } -fn take_waitable_event( +fn take_waitable_transition( state: &mut ProcessTableState, matching_children: &[u32], flags: WaitPidFlags, -) -> Option { +) -> Option { for child_pid in matching_children { let mut non_exit_result = None; let mut should_reap = false; @@ -1380,10 +2202,13 @@ fn take_waitable_event( .pending_wait_events .remove(index) .expect("pending wait event should exist"); - non_exit_result = Some(ProcessWaitResult { - pid: *child_pid, - status: event.status, - event: event.event, + non_exit_result = Some(ProcessWaitTransition { + result: ProcessWaitResult { + pid: *child_pid, + status: event.status, + event: event.event, + }, + termination: None, }); } else if record.entry.status == ProcessStatus::Exited { should_reap = true; @@ -1399,10 +2224,13 @@ fn take_waitable_event( .entries .remove(child_pid) .expect("exited child should still exist"); - return Some(ProcessWaitResult { - pid: *child_pid, - status: record.entry.exit_code.unwrap_or_default(), - event: ProcessWaitEvent::Exited, + return Some(ProcessWaitTransition { + result: ProcessWaitResult { + pid: *child_pid, + status: record.entry.exit_code.unwrap_or_default(), + event: ProcessWaitEvent::Exited, + }, + termination: record.entry.termination, }); } } @@ -1441,7 +2269,7 @@ fn reap_due_pid(inner: &ProcessTableInner, reaper: &ZombieReaper, pid: u32) { reaper.schedule(pid, state.zombie_ttl); } drop(state); - inner.waiters.notify_all(); + inner.notify_waiters(); } fn has_living_parent(state: &ProcessTableState, ppid: u32) -> bool { @@ -1454,6 +2282,15 @@ fn has_living_parent(state: &ProcessTableState, ppid: u32) -> bool { } impl ProcessTableInner { + fn notify_waiters(&self) { + { + let mut generation = lock_or_recover(&self.wait_generation); + *generation = generation.wrapping_add(1); + } + self.waiters.notify_all(); + self.async_waiters.notify(usize::MAX); + } + fn lock_state(&self) -> MutexGuard<'_, ProcessTableState> { lock_or_recover(&self.state) } @@ -1539,52 +2376,72 @@ fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexG } } +fn wait_timeout_or_recover<'a, T>( + condvar: &Condvar, + guard: MutexGuard<'a, T>, + timeout: Duration, +) -> MutexGuard<'a, T> { + match condvar.wait_timeout(guard, timeout) { + Ok((guard, _)) => guard, + Err(poisoned) => poisoned.into_inner().0, + } +} + #[cfg(test)] mod tests { use super::*; - #[derive(Default)] - struct TestDriverProcess { - on_exit: Mutex>, - } + #[test] + fn oversized_wait_timeout_fails_before_process_lookup() { + let error = ProcessTable::new() + .wait_for_exit(42, Duration::MAX) + .expect_err("an unrepresentable deadline must fail"); - impl TestDriverProcess { - fn exit(&self, exit_code: i32) { - let callback = self - .on_exit - .lock() - .expect("test driver lock poisoned") - .clone(); - if let Some(callback) = callback { - callback(exit_code); - } - } + assert_eq!(error.code(), "EINVAL"); } + use crate::process_runtime::{ + ProcessExitReporter, ProcessRuntimeEndpointError, ProcessRuntimeFault, + ProcessRuntimeIdentity, RuntimeControlCell, + }; - impl DriverProcess for TestDriverProcess { - fn kill(&self, _signal: i32) {} + #[derive(Default)] + struct TestRuntimeEndpoint { + identity: Option, + controls: Mutex>, + } - fn wait(&self, _timeout: Duration) -> Option { - None + impl ProcessRuntimeEndpoint for TestRuntimeEndpoint { + fn identity(&self) -> Option { + self.identity } - fn set_on_exit(&self, callback: ProcessExitCallback) { - *self.on_exit.lock().expect("test driver lock poisoned") = Some(callback); + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError> { + self.controls + .lock() + .expect("test endpoint lock poisoned") + .push(request); + Ok(()) } } - struct AlreadyExitedDriverProcess(i32); - - impl DriverProcess for AlreadyExitedDriverProcess { - fn kill(&self, _signal: i32) {} - - fn wait(&self, _timeout: Duration) -> Option { - Some(self.0) + impl TestRuntimeEndpoint { + fn take_controls(&self) -> Vec { + std::mem::take(&mut *self.controls.lock().expect("test endpoint lock poisoned")) } + } - fn set_on_exit(&self, callback: ProcessExitCallback) { - callback(self.0); - } + fn endpoint() -> Arc { + Arc::new(TestRuntimeEndpoint::default()) + } + + fn identified_endpoint(identity: ProcessRuntimeIdentity) -> Arc { + Arc::new(TestRuntimeEndpoint { + identity: Some(identity), + controls: Mutex::new(Vec::new()), + }) } fn context(ppid: u32) -> ProcessContext { @@ -1595,7 +2452,81 @@ mod tests { } #[test] - fn register_accepts_synchronous_already_exited_callback() { + fn async_wait_generation_closes_the_probe_to_listener_lost_wake() { + use std::future::Future; + use std::task::{Context, Poll, Waker}; + + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register(10, "test", "parent", Vec::new(), context(0), endpoint()); + table.register(11, "test", "child", Vec::new(), context(10), endpoint()); + + let wait_handle = table.wait_handle(); + let observed = wait_handle.snapshot(); + assert!(table + .waitpid_for(10, 11, WaitPidFlags::WNOHANG) + .expect("nonblocking probe") + .is_none()); + + // Deliberately publish the only transition before constructing the + // listener. A notification-only design would now sleep forever; the + // pre-probe generation must make the future immediately ready. + table.mark_exited(11, 0).expect("publish child exit"); + let mut future = Box::pin(wait_handle.wait_for_change_async(observed)); + let waker = Waker::noop(); + let mut task_context = Context::from_waker(waker); + assert_eq!(future.as_mut().poll(&mut task_context), Poll::Ready(())); + assert!(table + .waitpid_for(10, 11, WaitPidFlags::WNOHANG) + .expect("ready probe") + .is_some()); + } + + #[test] + fn child_wait_state_is_coalesced_and_terminal_supersedes_it() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register(10, "test", "parent", Vec::new(), context(0), endpoint()); + table.register(11, "test", "child", Vec::new(), context(10), endpoint()); + + for _ in 0..1_000 { + table.mark_stopped(11, SIGTSTP).expect("stop child"); + table.mark_continued(11).expect("continue child"); + } + { + let state = table.inner.lock_state(); + let child = state.entries.get(&11).expect("child record"); + assert_eq!(child.pending_wait_events.len(), 1); + assert_eq!( + child.pending_wait_events.front().map(|event| event.event), + Some(ProcessWaitEvent::Continued) + ); + } + + table + .report_exit(11, ProcessExit::Exited(23)) + .expect("publish terminal status"); + { + let state = table.inner.lock_state(); + assert!(state + .entries + .get(&11) + .expect("child zombie") + .pending_wait_events + .is_empty()); + } + let transition = table + .waitpid_for( + 10, + 11, + WaitPidFlags::WNOHANG | WaitPidFlags::WUNTRACED | WaitPidFlags::WCONTINUED, + ) + .expect("wait for terminal child") + .expect("terminal transition"); + assert_eq!(transition.event, ProcessWaitEvent::Exited); + assert_eq!(transition.status, 23); + } + + #[test] + fn first_exact_exit_report_wins() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); table.register( 10, @@ -1603,26 +2534,116 @@ mod tests { "already-exited", Vec::new(), context(0), - Arc::new(AlreadyExitedDriverProcess(27)), + endpoint(), ); + table + .report_exit(10, ProcessExit::Exited(27)) + .expect("publish first exit"); + table + .report_exit( + 10, + ProcessExit::Signaled { + signal: SIGKILL, + core_dumped: false, + }, + ) + .expect("ignore later exact exit without losing first"); let entry = table.get(10).expect("registered process remains a zombie"); assert_eq!(entry.status, ProcessStatus::Exited); assert_eq!(entry.exit_code, Some(27)); + assert_eq!(entry.termination, Some(ProcessExit::Exited(27))); + assert_eq!(entry.runtime_fault, None); } #[test] - fn spawn_process_group_is_applied_atomically() { + fn first_runtime_fault_report_wins_and_stays_typed() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let identity = ProcessRuntimeIdentity { + generation: 19, + pid: 10, + }; table.register( 10, "test", - "parent", + "faulted", + Vec::new(), + context(0), + identified_endpoint(identity), + ); + let reporter = ProcessExitReporter::new(identity, Arc::new(table.clone())); + let fault = ProcessRuntimeFault::try_new( + "ERR_AGENTOS_WASM_TRAP", + "integer divide by zero", + Some(serde_json::json!({ "trap": "integer_division_by_zero" })), + ) + .expect("bounded typed fault"); + reporter + .report_runtime_fault(fault.clone()) + .expect("current reporter should fault its process"); + reporter + .report_exit(ProcessExit::Exited(0)) + .expect("late terminal report is idempotent"); + + let entry = table.get(10).expect("faulted process remains a zombie"); + assert_eq!(entry.status, ProcessStatus::Exited); + assert_eq!(entry.exit_code, Some(1)); + assert_eq!(entry.termination, Some(ProcessExit::Exited(1))); + assert_eq!(entry.runtime_fault, Some(fault)); + } + + #[test] + fn stale_exit_reporter_cannot_exit_reused_pid() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let first_identity = ProcessRuntimeIdentity { + generation: 41, + pid: 10, + }; + table.register( + 10, + "test", + "first", Vec::new(), context(0), - Arc::new(TestDriverProcess::default()), + identified_endpoint(first_identity), + ); + let reporter = ProcessExitReporter::new(first_identity, Arc::new(table.clone())); + reporter + .report_exit(ProcessExit::Exited(7)) + .expect("current reporter should finish its process"); + table.waitpid(10).expect("reap first process"); + + let replacement_identity = ProcessRuntimeIdentity { + generation: 42, + pid: 10, + }; + table.register( + 10, + "test", + "replacement", + Vec::new(), + context(0), + identified_endpoint(replacement_identity), ); + let error = reporter + .report_exit(ProcessExit::Signaled { + signal: SIGKILL, + core_dumped: false, + }) + .expect_err("stale reporter must not target a reused pid"); + assert_eq!(error.code(), "ESTALE"); + assert_eq!( + table.get(10).expect("replacement remains live").status, + ProcessStatus::Running + ); + } + + #[test] + fn spawn_process_group_is_applied_atomically() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register(10, "test", "parent", Vec::new(), context(0), endpoint()); + let leader = table .register_with_process_group( 11, @@ -1630,7 +2651,7 @@ mod tests { "leader", Vec::new(), context(10), - Arc::new(TestDriverProcess::default()), + endpoint(), Some(0), ) .expect("spawn should create a new process group"); @@ -1643,7 +2664,7 @@ mod tests { "peer", Vec::new(), context(10), - Arc::new(TestDriverProcess::default()), + endpoint(), Some(11), ) .expect("spawn should join an existing group in the same session"); @@ -1656,7 +2677,7 @@ mod tests { "invalid", Vec::new(), context(10), - Arc::new(TestDriverProcess::default()), + endpoint(), Some(999), ) .expect_err("spawn must reject a nonexistent process group"); @@ -1672,7 +2693,7 @@ mod tests { "other-session", Vec::new(), context(0), - Arc::new(TestDriverProcess::default()), + endpoint(), ); let error = table .register_with_process_group( @@ -1681,7 +2702,7 @@ mod tests { "cross-session", Vec::new(), context(10), - Arc::new(TestDriverProcess::default()), + endpoint(), Some(20), ) .expect_err("spawn must reject a process group in another session"); @@ -1692,9 +2713,9 @@ mod tests { #[test] fn allocate_pid_wraps_without_reusing_live_or_zombie_processes() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); - let live_high = Arc::new(TestDriverProcess::default()); - let zombie_high = Arc::new(TestDriverProcess::default()); - let live_one = Arc::new(TestDriverProcess::default()); + let live_high = endpoint(); + let zombie_high = endpoint(); + let live_one = endpoint(); let max_pid = MAX_ALLOCATED_PID; table.register( @@ -1714,11 +2735,620 @@ mod tests { zombie_high.clone(), ); table.register(1, "test", "live-one", Vec::new(), context(0), live_one); - zombie_high.exit(0); + table + .report_exit(max_pid, ProcessExit::Exited(0)) + .expect("publish high-pid exit"); table.inner.lock_state().next_pid = max_pid - 1; assert_eq!(table.allocate_pid().expect("allocate pid"), 2); assert_eq!(table.allocate_pid().expect("allocate pid"), 3); } + + #[test] + fn caught_signal_is_kernel_pending_until_handler_checkpoint() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let endpoint = endpoint(); + table.register( + 10, + "test", + "signals", + Vec::new(), + context(0), + endpoint.clone(), + ); + let handler_mask = SignalSet::from_signal(SIGTERM).expect("handler mask"); + table + .signal_action( + 10, + SIGPIPE, + Some(SignalAction { + disposition: SignalDisposition::User, + mask: handler_mask, + flags: SA_RESETHAND, + }), + ) + .expect("install action"); + + table.kill(10, SIGPIPE).expect("queue caught signal"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Checkpoint] + ); + assert!(table.sigpending(10).expect("pending").contains(SIGPIPE)); + + let delivery = table + .begin_signal_delivery(10) + .expect("begin delivery") + .expect("caught signal"); + assert_eq!(delivery.signal, SIGPIPE); + assert!(!table.sigpending(10).expect("pending").contains(SIGPIPE)); + assert_eq!( + table + .signal_action(10, SIGPIPE, None) + .expect("query") + .disposition, + SignalDisposition::Default + ); + table + .end_signal_delivery(10, delivery.token) + .expect("end delivery"); + } + + #[test] + fn different_caught_signals_keep_delivery_tokens_strictly_lifo() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register(10, "test", "signals", Vec::new(), context(0), endpoint()); + for signal in [SIGPIPE, SIGTERM] { + table + .signal_action( + 10, + signal, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught action"); + table.kill(10, signal).expect("queue caught signal"); + } + + let first = table + .begin_signal_delivery(10) + .expect("claim first signal") + .expect("first delivery"); + assert_eq!(first.signal, SIGPIPE, "standard signals use numeric order"); + let nested = table + .begin_signal_delivery(10) + .expect("claim nested signal") + .expect("nested delivery"); + assert_eq!(nested.signal, SIGTERM); + assert_eq!( + table + .end_signal_delivery(10, first.token) + .expect_err("outer token cannot end before nested delivery") + .code(), + "EINVAL" + ); + table + .end_signal_delivery(10, nested.token) + .expect("end nested delivery"); + table + .end_signal_delivery(10, first.token) + .expect("end outer delivery"); + } + + #[test] + fn blocked_caught_signal_wakes_only_after_unblock() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let endpoint = endpoint(); + table.register( + 10, + "test", + "signals", + Vec::new(), + context(0), + endpoint.clone(), + ); + table + .signal_action( + 10, + SIGTERM, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install action"); + let mask = SignalSet::from_signal(SIGTERM).expect("mask"); + table + .sigprocmask(10, SigmaskHow::Block, mask) + .expect("block"); + table.kill(10, SIGTERM).expect("queue blocked signal"); + assert!(endpoint.take_controls().is_empty()); + assert!(table.sigpending(10).expect("pending").contains(SIGTERM)); + + table + .sigprocmask(10, SigmaskHow::Unblock, mask) + .expect("unblock"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Checkpoint] + ); + } + + #[test] + fn temporary_ppoll_mask_restores_atomically_and_releases_pending_signal() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let endpoint = endpoint(); + table.register( + 10, + "test", + "ppoll", + Vec::new(), + context(0), + endpoint.clone(), + ); + table + .signal_action( + 10, + SIGTERM, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); + + let token = table + .begin_temporary_signal_mask( + 10, + SignalSet::from_signal(SIGTERM).expect("temporary mask"), + ) + .expect("begin ppoll mask"); + table.kill(10, SIGTERM).expect("queue during ppoll"); + assert!(endpoint.take_controls().is_empty()); + assert!(table.sigpending(10).expect("pending").contains(SIGTERM)); + + let error = table + .end_temporary_signal_mask(10, token + 1) + .expect_err("out-of-order token must not restore the mask"); + assert_eq!(error.code(), "EINVAL"); + assert!(endpoint.take_controls().is_empty()); + + table + .end_temporary_signal_mask(10, token) + .expect("restore ppoll mask"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Checkpoint] + ); + } + + #[test] + fn ppoll_claims_signal_under_temporary_mask_but_builds_handler_from_original_mask() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let endpoint = endpoint(); + table.register( + 10, + "test", + "ppoll-delivery", + Vec::new(), + context(0), + endpoint.clone(), + ); + table + .signal_action( + 10, + SIGTERM, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); + let original = SignalSet::from_signals([SIGPIPE, SIGTERM]).expect("original mask"); + table + .sigprocmask(10, SigmaskHow::Block, original) + .expect("block caught signal in caller mask"); + table.kill(10, SIGTERM).expect("queue blocked signal"); + assert!(endpoint.take_controls().is_empty()); + + let token = table + .begin_temporary_signal_mask(10, SignalSet::empty()) + .expect("install unblocking ppoll mask"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Checkpoint] + ); + let delivery = table + .end_temporary_signal_mask_and_begin_signal_delivery(10, token) + .expect("restore mask and claim ppoll signal") + .expect("caught signal delivery"); + assert_eq!(delivery.signal, SIGTERM); + assert!(!table + .sigpending(10) + .expect("pending signals") + .contains(SIGTERM)); + let handler_mask = table + .sigprocmask(10, SigmaskHow::Block, SignalSet::empty()) + .expect("query handler mask"); + assert!(handler_mask.contains(SIGPIPE)); + assert!(handler_mask.contains(SIGTERM)); + + table + .end_signal_delivery(10, delivery.token) + .expect("complete handler"); + let restored = table + .sigprocmask(10, SigmaskHow::Block, SignalSet::empty()) + .expect("query restored mask"); + assert_eq!(restored, original); + } + + #[test] + fn spawn_and_exec_preserve_only_ignored_signal_dispositions() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register(10, "test", "parent", Vec::new(), context(0), endpoint()); + table + .signal_action( + 10, + SIGPIPE, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught action"); + table + .signal_action( + 10, + SIGTERM, + Some(SignalAction { + disposition: SignalDisposition::Ignore, + ..SignalAction::DEFAULT + }), + ) + .expect("install ignored action"); + + table.register(11, "test", "child", Vec::new(), context(10), endpoint()); + assert_eq!( + table + .signal_action(11, SIGPIPE, None) + .expect("query caught action") + .disposition, + SignalDisposition::Default + ); + assert_eq!( + table + .signal_action(11, SIGTERM, None) + .expect("query ignored action") + .disposition, + SignalDisposition::Ignore + ); + + table + .signal_action( + 11, + SIGPIPE, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install child handler"); + table + .exec( + 11, + "test", + "replacement", + Vec::new(), + BTreeMap::new(), + String::from("/"), + None, + ) + .expect("exec replacement image"); + assert_eq!( + table + .signal_action(11, SIGPIPE, None) + .expect("query reset action") + .disposition, + SignalDisposition::Default + ); + assert_eq!( + table + .signal_action(11, SIGTERM, None) + .expect("query retained ignore") + .disposition, + SignalDisposition::Ignore + ); + } + + #[test] + fn permission_tier_inherits_and_exec_can_only_restrict() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let mut parent_context = context(0); + parent_context.permission_tier = ProcessPermissionTier::ReadWrite; + table.register( + 40, + "runtime", + "parent", + Vec::new(), + parent_context, + endpoint(), + ); + + let child_context = table.inherited_context(40).expect("inherit parent context"); + assert_eq!( + child_context.permission_tier, + ProcessPermissionTier::ReadWrite + ); + table.register( + 41, + "runtime", + "child", + Vec::new(), + child_context, + endpoint(), + ); + assert_eq!( + table.permission_tier(41).expect("child tier"), + ProcessPermissionTier::ReadWrite + ); + + table + .exec( + 41, + "runtime", + "restricted", + Vec::new(), + BTreeMap::new(), + String::from("/"), + Some(ProcessPermissionTier::ReadOnly), + ) + .expect("restrict tier on exec"); + assert_eq!( + table.permission_tier(41).expect("restricted tier"), + ProcessPermissionTier::ReadOnly + ); + + table + .exec( + 41, + "runtime", + "cannot-escalate", + Vec::new(), + BTreeMap::new(), + String::from("/"), + Some(ProcessPermissionTier::Full), + ) + .expect("exec with broader image ceiling"); + assert_eq!( + table.permission_tier(41).expect("non-escalated tier"), + ProcessPermissionTier::ReadOnly + ); + } + + #[test] + fn default_signal_actions_are_decided_by_kernel() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let identity = ProcessRuntimeIdentity { + generation: 7, + pid: 10, + }; + let endpoint = identified_endpoint(identity); + table.register( + 10, + "test", + "signals", + Vec::new(), + context(0), + endpoint.clone(), + ); + + table.kill(10, SIGCHLD).expect("default ignored signal"); + assert!(endpoint.take_controls().is_empty()); + table.kill(10, SIGTSTP).expect("default stop signal"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Stop { signal: SIGTSTP }] + ); + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Running, + "requesting stop must not publish wait state before runtime acknowledgement" + ); + ProcessControlAckSink::acknowledge_stop_state(&table, identity, true, Some(SIGTSTP)) + .expect("acknowledge stop"); + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Stopped + ); + table.kill(10, SIGCONT).expect("continue signal"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Continue] + ); + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Stopped, + "requesting continue must not publish wait state before runtime acknowledgement" + ); + ProcessControlAckSink::acknowledge_stop_state(&table, identity, false, None) + .expect("acknowledge continue"); + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Running + ); + table.kill(10, SIGKILL).expect("fatal signal"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Terminate( + ProcessTermination::Signal { + signal: SIGKILL, + force: true, + } + )] + ); + assert_eq!( + table + .get(10) + .expect("terminating process") + .pending_termination, + Some(ProcessTermination::Signal { + signal: SIGKILL, + force: true, + }), + "kernel process state must publish the durable termination request" + ); + table.kill(10, SIGTERM).expect("later graceful signal"); + assert_eq!( + table + .get(10) + .expect("terminating process") + .pending_termination, + Some(ProcessTermination::Signal { + signal: SIGKILL, + force: true, + }), + "a forced termination request cannot be downgraded" + ); + assert_eq!( + table + .exec( + 10, + "test", + "replacement", + Vec::new(), + BTreeMap::new(), + String::from("/"), + None, + ) + .expect_err("termination must prevent image replacement") + .code(), + "EINTR" + ); + ProcessExitReporter::new(identity, Arc::new(table.clone())) + .report_exit(ProcessExit::Signaled { + signal: SIGKILL, + core_dumped: false, + }) + .expect("runtime reports terminal signal"); + assert_eq!( + table.get(10).expect("exited process").pending_termination, + None, + "terminal state replaces the pending termination request" + ); + } + + #[test] + fn continue_supersedes_unacknowledged_stop_without_phantom_wait_state() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let identity = ProcessRuntimeIdentity { + generation: 9, + pid: 10, + }; + let cell = RuntimeControlCell::new_with_ack_sink(9, Arc::new(table.clone())); + cell.bind_pid(10).expect("bind runtime endpoint"); + let receiver = cell.attach(Arc::new(|| {})).expect("attach runtime"); + table.register( + 10, + "test", + "signals", + Vec::new(), + context(0), + Arc::new(cell), + ); + + table.kill(10, SIGTSTP).expect("request stop"); + table.kill(10, SIGCONT).expect("supersede stop"); + + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Running + ); + let controls = receiver.pending(); + assert_eq!(controls.stopped, Some(false)); + receiver + .acknowledge(controls) + .expect("acknowledge final running state"); + assert_eq!( + table.get(10).expect("process").status, + ProcessStatus::Running + ); + assert!(table + .inner + .lock_state() + .entries + .get(&identity.pid) + .expect("process record") + .pending_wait_events + .is_empty()); + } + + #[test] + fn resource_limits_cover_all_kinds_inherit_and_survive_exec() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let mut parent_context = context(0); + parent_context.resource_limits = ProcessResourceLimits::with_open_files(256); + table.register(10, "test", "parent", Vec::new(), parent_context, endpoint()); + + let kinds = [ + ProcessResourceLimitKind::AddressSpace, + ProcessResourceLimitKind::Core, + ProcessResourceLimitKind::Cpu, + ProcessResourceLimitKind::Data, + ProcessResourceLimitKind::FileSize, + ProcessResourceLimitKind::LockedMemory, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimitKind::Processes, + ProcessResourceLimitKind::ResidentSet, + ProcessResourceLimitKind::Stack, + ]; + for (index, kind) in kinds.into_iter().enumerate() { + let hard = if kind == ProcessResourceLimitKind::OpenFiles { + 200 + } else { + 1_000 + index as u64 + }; + table + .set_resource_limit( + 10, + kind, + ProcessResourceLimit { + soft: Some(hard - 1), + hard: Some(hard), + }, + ) + .expect("set resource limit"); + } + + let child_context = table.inherited_context(10).expect("inherit context"); + table.register(11, "test", "child", Vec::new(), child_context, endpoint()); + table + .exec( + 11, + "test", + "replacement", + Vec::new(), + BTreeMap::new(), + String::from("/"), + None, + ) + .expect("exec child"); + + for (index, kind) in kinds.into_iter().enumerate() { + let hard = if kind == ProcessResourceLimitKind::OpenFiles { + 200 + } else { + 1_000 + index as u64 + }; + assert_eq!( + table.get_resource_limit(11, kind).expect("get child limit"), + ProcessResourceLimit { + soft: Some(hard - 1), + hard: Some(hard), + } + ); + } + } } diff --git a/crates/kernel/src/pty.rs b/crates/kernel/src/pty.rs index 6c0a06fb1e..5e0aa09431 100644 --- a/crates/kernel/src/pty.rs +++ b/crates/kernel/src/pty.rs @@ -3,6 +3,7 @@ use crate::fd_table::{ FILETYPE_CHARACTER_DEVICE, O_RDWR, }; use crate::poll::{PollEvents, PollNotifier, POLLHUP, POLLIN, POLLOUT}; +use crate::resource_accounting::BlockingReadDeadline; use std::collections::{BTreeMap, VecDeque}; use std::error::Error; use std::fmt; @@ -12,6 +13,10 @@ use web_time::Instant; pub const MAX_PTY_BUFFER_BYTES: usize = 65_536; pub const MAX_CANON: usize = 4_096; +pub const MAX_PTY_READ_BYTES: usize = MAX_PTY_BUFFER_BYTES; +pub const MAX_PTY_WRITE_BYTES: usize = MAX_PTY_BUFFER_BYTES; +pub const MAX_PTY_READ_WAITERS: usize = 1_024; +pub const MAX_PTY_RETAINED_READ_BYTES: usize = 1024 * 1024; pub const SIGINT: i32 = 2; pub const SIGQUIT: i32 = 3; pub const SIGTSTP: i32 = 20; @@ -32,9 +37,16 @@ impl PtyError { self.code } - fn bad_file_descriptor(message: impl Into) -> Self { + fn not_a_tty(message: impl Into) -> Self { Self { - code: "EBADF", + code: "ENOTTY", + message: message.into(), + } + } + + fn dangling_pty(message: impl Into) -> Self { + Self { + code: "EIO", message: message.into(), } } @@ -52,6 +64,13 @@ impl PtyError { message: message.into(), } } + + fn too_big(message: impl Into) -> Self { + Self { + code: "E2BIG", + message: message.into(), + } + } } impl fmt::Display for PtyError { @@ -230,9 +249,10 @@ enum PtyEndKind { Slave, } -#[derive(Debug, Default)] +#[derive(Debug, Clone)] struct PendingRead { length: usize, + end: PtyEndKind, result: Option>>, } @@ -270,6 +290,8 @@ struct PtyManagerState { waiters: BTreeMap, next_pty_id: u64, next_waiter_id: u64, + warned_waiter_limit: bool, + warned_retained_read_limit: bool, } impl Default for PtyManagerState { @@ -280,6 +302,8 @@ impl Default for PtyManagerState { waiters: BTreeMap::new(), next_pty_id: 0, next_waiter_id: 1, + warned_waiter_limit: false, + warned_retained_read_limit: false, } } } @@ -422,11 +446,13 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; + let retained_read_capacity_available = + retained_read_bytes(&state.waiters) < MAX_PTY_RETAINED_READ_BYTES; let mut events = PollEvents::empty(); match pty_ref.end { @@ -437,8 +463,11 @@ impl PtyManager { if pty.closed_slave { events |= POLLHUP; } else if requested.intersects(POLLOUT) - && (available_capacity(&pty.input_buffer) > 0 - || !pty.waiting_input_reads.is_empty()) + && if pty.waiting_input_reads.is_empty() { + available_capacity(&pty.input_buffer) > 0 + } else { + retained_read_capacity_available + } { events |= POLLOUT; } @@ -452,8 +481,11 @@ impl PtyManager { if pty.closed_master { events |= POLLHUP; } else if requested.intersects(POLLOUT) - && (available_capacity(&pty.output_buffer) > 0 - || !pty.waiting_output_reads.is_empty()) + && if pty.waiting_output_reads.is_empty() { + available_capacity(&pty.output_buffer) > 0 + } else { + retained_read_capacity_available + } { events |= POLLOUT; } @@ -465,6 +497,12 @@ impl PtyManager { pub fn write(&self, description_id: u64, data: impl AsRef<[u8]>) -> PtyResult { let payload = data.as_ref(); + if payload.len() > MAX_PTY_WRITE_BYTES { + return Err(PtyError::too_big(format!( + "maxPtyWriteBytes is {MAX_PTY_WRITE_BYTES}, requested {} bytes; split the terminal write into bounded chunks", + payload.len() + ))); + } let mut signals = Vec::new(); { @@ -473,11 +511,16 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; - let PtyManagerState { ptys, waiters, .. } = &mut *state; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; + let PtyManagerState { + ptys, + waiters, + warned_retained_read_limit, + .. + } = &mut *state; let pty = ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; match pty_ref.end { PtyEndKind::Master => { @@ -487,7 +530,13 @@ impl PtyManager { if pty.closed_slave { return Err(PtyError::io("slave closed")); } - process_input(pty, waiters, payload, &mut signals)?; + process_input( + pty, + waiters, + payload, + &mut signals, + warned_retained_read_limit, + )?; } PtyEndKind::Slave => { if pty.closed_slave { @@ -497,15 +546,15 @@ impl PtyManager { return Err(PtyError::io("master closed")); } - let processed = process_output(&pty.termios, payload); - deliver_output(pty, waiters, &processed, false)?; + let processed = process_output(&pty.termios, payload)?; + deliver_output(pty, waiters, &processed, false, warned_retained_read_limit)?; // Terminal emulation: answer a Device Status Report cursor-position // query (ESC[6n) with a cursor report (ESC[row;colR) on the slave's // input. A real terminal emulator on the master side does this; the // converged PTY may have no such emulator, so crossterm/reedline guests // that probe the cursor at startup would otherwise stall and abort. if contains_dsr_cursor_query(payload) { - deliver_input(pty, waiters, b"\x1b[1;1R")?; + deliver_input(pty, waiters, b"\x1b[1;1R", warned_retained_read_limit)?; } } } @@ -533,12 +582,30 @@ impl PtyManager { length: usize, timeout: Option, ) -> PtyResult>> { + self.read_with_timeout_and_deadline(description_id, length, timeout, None) + } + + pub(crate) fn read_with_timeout_and_deadline( + &self, + description_id: u64, + length: usize, + timeout: Option, + mut blocking_deadline: Option, + ) -> PtyResult>> { + if length > MAX_PTY_READ_BYTES { + return Err(PtyError::too_big(format!( + "maxPtyReadBytes is {MAX_PTY_READ_BYTES}, requested {length} bytes; lower the terminal read size" + ))); + } let mut state = lock_or_recover(&self.inner.state); let pty_ref = state .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; + if length == 0 { + return Ok(Some(Vec::new())); + } let mut waiter_id = None; let deadline = timeout.map(|duration| Instant::now() + duration); @@ -547,6 +614,9 @@ impl PtyManager { if let Some(waiter) = state.waiters.get_mut(&id) { if let Some(result) = waiter.result.take() { state.waiters.remove(&id); + // Releasing a retained result can make a PTY writable + // again even when its ordinary byte buffer is full. + self.notify_waiters_and_pollers(); return Ok(result); } } @@ -556,7 +626,7 @@ impl PtyManager { let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; match pty_ref.end { PtyEndKind::Master => { @@ -640,18 +710,34 @@ impl PtyManager { let id = if let Some(id) = waiter_id { id } else { + if state.waiters.len() >= MAX_PTY_READ_WAITERS { + return Err(PtyError::would_block(format!( + "maxPtyReadWaiters limit is {MAX_PTY_READ_WAITERS}; retry after another terminal read completes" + ))); + } + let waiter_count = state.waiters.len().saturating_add(1); + warn_near_pty_limit( + &mut state.warned_waiter_limit, + "maxPtyReadWaiters", + waiter_count, + MAX_PTY_READ_WAITERS, + ); let next = state.next_waiter_id; - state.next_waiter_id += 1; + state.next_waiter_id = state + .next_waiter_id + .checked_add(1) + .ok_or_else(|| PtyError::io("PTY read waiter identifier space exhausted"))?; state.waiters.insert( next, PendingRead { length, + end: pty_ref.end, result: None, }, ); let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) else { state.waiters.remove(&next); - return Err(PtyError::bad_file_descriptor("PTY not found")); + return Err(PtyError::dangling_pty("PTY not found")); }; match pty_ref.end { PtyEndKind::Master => pty.waiting_output_reads.push_back(next), @@ -672,6 +758,9 @@ impl PtyManager { let now = Instant::now(); if now >= deadline { + if let Some(blocking_deadline) = &mut blocking_deadline { + blocking_deadline.expired(); + } if let Some(id) = waiter_id.take() { state.waiters.remove(&id); if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) { @@ -683,7 +772,10 @@ impl PtyManager { return Err(PtyError::would_block("PTY read timed out")); } - let remaining = deadline.saturating_duration_since(now); + let remaining = blocking_deadline + .as_mut() + .and_then(BlockingReadDeadline::wait_slice) + .unwrap_or_else(|| deadline.saturating_duration_since(now)); let (next_state, wait_result) = wait_timeout_or_recover(&self.inner.waiters, state, remaining); state = next_state; @@ -691,6 +783,12 @@ impl PtyManager { waiter_id = None; } if wait_result.timed_out() { + if blocking_deadline + .as_mut() + .is_some_and(|deadline| !deadline.expired()) + { + continue; + } if let Some(id) = waiter_id.take() { state.waiters.remove(&id); if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) { @@ -765,11 +863,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; pty.termios_generation = pty .termios_generation .checked_add(1) @@ -812,11 +910,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; if !enabled { if let Some(owner_pid) = lease_owner_pid { @@ -889,11 +987,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; release_raw_mode_lease(pty, owner_pid, Some(generation)) } @@ -903,13 +1001,13 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; state .ptys .get(&pty_ref.pty_id) .cloned() .map(|pty| pty.termios) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found")) + .ok_or_else(|| PtyError::dangling_pty("PTY not found")) } pub fn set_termios(&self, description_id: u64, termios: PartialTermios) -> PtyResult<()> { @@ -918,11 +1016,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; advance_termios_generation(pty)?; pty.termios.merge(termios); Ok(()) @@ -934,11 +1032,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; pty.foreground_pgid = pgid; Ok(()) } @@ -949,12 +1047,12 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; state .ptys .get(&pty_ref.pty_id) .map(|pty| pty.foreground_pgid) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found")) + .ok_or_else(|| PtyError::dangling_pty("PTY not found")) } pub fn window_size(&self, description_id: u64) -> PtyResult { @@ -963,12 +1061,12 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; state .ptys .get(&pty_ref.pty_id) .map(|pty| pty.window_size) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found")) + .ok_or_else(|| PtyError::dangling_pty("PTY not found")) } pub fn resize(&self, description_id: u64, cols: u16, rows: u16) -> PtyResult> { @@ -977,11 +1075,11 @@ impl PtyManager { .desc_to_pty .get(&description_id) .copied() - .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?; + .ok_or_else(|| PtyError::not_a_tty("not a PTY end"))?; let pty = state .ptys .get_mut(&pty_ref.pty_id) - .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?; + .ok_or_else(|| PtyError::dangling_pty("PTY not found"))?; let next_size = PtyWindowSize { cols, rows }; if pty.window_size == next_size { return Ok(None); @@ -995,19 +1093,29 @@ impl PtyManager { } pub fn buffered_input_bytes(&self) -> usize { - lock_or_recover(&self.inner.state) + let state = lock_or_recover(&self.inner.state); + let buffered: usize = state .ptys .values() .map(|pty| buffer_size(&pty.input_buffer)) - .sum() + .sum(); + buffered.saturating_add(retained_read_bytes_for_end( + &state.waiters, + PtyEndKind::Slave, + )) } pub fn buffered_output_bytes(&self) -> usize { - lock_or_recover(&self.inner.state) + let state = lock_or_recover(&self.inner.state); + let buffered: usize = state .ptys .values() .map(|pty| buffer_size(&pty.output_buffer)) - .sum() + .sum(); + buffered.saturating_add(retained_read_bytes_for_end( + &state.waiters, + PtyEndKind::Master, + )) } pub fn pending_read_waiter_count(&self) -> usize { @@ -1043,9 +1151,9 @@ fn contains_dsr_cursor_query(data: &[u8]) -> bool { data.windows(QUERY.len()).any(|window| window == QUERY) } -fn process_output(termios: &Termios, data: &[u8]) -> Vec { +fn process_output(termios: &Termios, data: &[u8]) -> PtyResult> { if !termios.opost || !termios.onlcr || !data.contains(&b'\n') { - return data.to_vec(); + return Ok(data.to_vec()); } let extra_crs = data @@ -1054,17 +1162,25 @@ fn process_output(termios: &Termios, data: &[u8]) -> Vec { .filter(|(index, byte)| **byte == b'\n' && (*index == 0 || data[*index - 1] != b'\r')) .count(); if extra_crs == 0 { - return data.to_vec(); + return Ok(data.to_vec()); } - let mut result = Vec::with_capacity(data.len() + extra_crs); + let transformed_len = data.len().checked_add(extra_crs).ok_or_else(|| { + PtyError::too_big("maxPtyTransformedOutputBytes length calculation overflowed") + })?; + if transformed_len > MAX_PTY_BUFFER_BYTES { + return Err(PtyError::too_big(format!( + "maxPtyTransformedOutputBytes is {MAX_PTY_BUFFER_BYTES}, terminal output processing would produce {transformed_len} bytes; split the write into smaller chunks" + ))); + } + let mut result = Vec::with_capacity(transformed_len); for (index, byte) in data.iter().enumerate() { if *byte == b'\n' && (index == 0 || data[index - 1] != b'\r') { result.push(b'\r'); } result.push(*byte); } - result + Ok(result) } fn process_input( @@ -1072,10 +1188,11 @@ fn process_input( waiters: &mut BTreeMap, data: &[u8], signals: &mut Vec<(u32, i32)>, + warned_retained_read_limit: &mut bool, ) -> PtyResult<()> { if !pty.termios.icanon && !pty.termios.echo && !pty.termios.isig { let translated = translate_input(&pty.termios, data); - deliver_input(pty, waiters, &translated)?; + deliver_input(pty, waiters, &translated, warned_retained_read_limit)?; return Ok(()); } @@ -1096,15 +1213,21 @@ fn process_input( // signal is delivered to it and the char is not echoed, matching // the integration suite's VINTR/VSUSP/VQUIT expectations. if pty.termios.echo && !has_foreground_process_group { - deliver_output(pty, waiters, &echo_control_byte(byte), true)?; + deliver_output( + pty, + waiters, + &echo_control_byte(byte), + true, + warned_retained_read_limit, + )?; if pty.termios.icanon { - deliver_output(pty, waiters, b"\r\n", true)?; + deliver_output(pty, waiters, b"\r\n", true, warned_retained_read_limit)?; } } if has_foreground_process_group { signals.push((pty.foreground_pgid, signal)); } else if pty.termios.icanon { - deliver_input(pty, waiters, b"\n")?; + deliver_input(pty, waiters, b"\n", warned_retained_read_limit)?; } continue; } @@ -1116,7 +1239,7 @@ fn process_input( deliver_input_eof(pty, waiters); } else { let line = pty.line_buffer.clone(); - deliver_input(pty, waiters, &line)?; + deliver_input(pty, waiters, &line, warned_retained_read_limit)?; pty.line_buffer.clear(); } continue; @@ -1125,7 +1248,13 @@ fn process_input( if byte == pty.termios.cc.verase || byte == 0x08 { if let Some(&erased) = pty.line_buffer.last() { if pty.termios.echo { - deliver_output(pty, waiters, &erase_sequence(erased), true)?; + deliver_output( + pty, + waiters, + &erase_sequence(erased), + true, + warned_retained_read_limit, + )?; } pty.line_buffer.pop(); } @@ -1140,7 +1269,7 @@ fn process_input( .iter() .flat_map(|b| erase_sequence(*b)) .collect(); - deliver_output(pty, waiters, &erase, true)?; + deliver_output(pty, waiters, &erase, true, warned_retained_read_limit)?; } pty.line_buffer.clear(); } @@ -1164,7 +1293,7 @@ fn process_input( if pty.termios.echo && !erased.is_empty() { let sequence: Vec = erased.iter().flat_map(|b| erase_sequence(*b)).collect(); - deliver_output(pty, waiters, &sequence, true)?; + deliver_output(pty, waiters, &sequence, true, warned_retained_read_limit)?; } continue; } @@ -1173,9 +1302,9 @@ fn process_input( let mut line = pty.line_buffer.clone(); line.push(b'\n'); if pty.termios.echo { - deliver_output(pty, waiters, b"\r\n", true)?; + deliver_output(pty, waiters, b"\r\n", true, warned_retained_read_limit)?; } - deliver_input(pty, waiters, &line)?; + deliver_input(pty, waiters, &line, warned_retained_read_limit)?; pty.line_buffer.clear(); continue; } @@ -1186,14 +1315,20 @@ fn process_input( if pty.termios.echo { // ECHOCTL: echo control chars in caret form (e.g. 0x01 -> "^A") // so they are visible; printable bytes echo verbatim. - deliver_output(pty, waiters, &echo_control_byte(byte), true)?; + deliver_output( + pty, + waiters, + &echo_control_byte(byte), + true, + warned_retained_read_limit, + )?; } pty.line_buffer.push(byte); } else { if pty.termios.echo { - deliver_output(pty, waiters, &[byte], true)?; + deliver_output(pty, waiters, &[byte], true, warned_retained_read_limit)?; } - deliver_input(pty, waiters, &[byte])?; + deliver_input(pty, waiters, &[byte], warned_retained_read_limit)?; } } @@ -1214,27 +1349,29 @@ fn deliver_input( pty: &mut PtyState, waiters: &mut BTreeMap, data: &[u8], + warned_retained_read_limit: &mut bool, ) -> PtyResult<()> { - if let Some(waiter_id) = pty.waiting_input_reads.pop_front() { - if let Some(waiter) = waiters.get_mut(&waiter_id) { - if data.len() <= waiter.length { - waiter.result = Some(Some(data.to_vec())); - } else { - // The waiter consumes `waiter.length` bytes directly; only the - // tail is buffered, so the buffer cap must be enforced on the - // tail. Otherwise a single large write past a pending reader - // bypasses MAX_PTY_BUFFER_BYTES entirely. - let tail_len = data.len() - waiter.length; - if tail_len > available_capacity(&pty.input_buffer) { - pty.waiting_input_reads.push_front(waiter_id); - return Err(PtyError::would_block("PTY input buffer full")); - } - let (head, tail) = data.split_at(waiter.length); - waiter.result = Some(Some(head.to_vec())); + if let Some(waiter_id) = pty.waiting_input_reads.front().copied() { + if let Some(waiter) = waiters.get(&waiter_id) { + let retained_len = data.len().min(waiter.length); + check_retained_read_capacity(waiters, retained_len, warned_retained_read_limit)?; + let tail_len = data.len().saturating_sub(waiter.length); + if tail_len > available_capacity(&pty.input_buffer) { + return Err(PtyError::would_block("PTY input buffer full")); + } + let split_at = retained_len; + let (head, tail) = data.split_at(split_at); + pty.waiting_input_reads.pop_front(); + waiters + .get_mut(&waiter_id) + .expect("validated PTY input waiter must remain registered") + .result = Some(Some(head.to_vec())); + if !tail.is_empty() { pty.input_buffer.push_front(tail.to_vec()); } return Ok(()); } + pty.waiting_input_reads.pop_front(); } if buffer_size(&pty.input_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES { @@ -1261,29 +1398,33 @@ fn deliver_output( waiters: &mut BTreeMap, data: &[u8], echo: bool, + warned_retained_read_limit: &mut bool, ) -> PtyResult<()> { - if let Some(waiter_id) = pty.waiting_output_reads.pop_front() { - if let Some(waiter) = waiters.get_mut(&waiter_id) { - if data.len() <= waiter.length { - waiter.result = Some(Some(data.to_vec())); - } else { - // Enforce the buffer cap on the tail (see deliver_input). - let tail_len = data.len() - waiter.length; - if tail_len > available_capacity(&pty.output_buffer) { - pty.waiting_output_reads.push_front(waiter_id); - let message = if echo { - "PTY output buffer full (echo backpressure)" - } else { - "PTY output buffer full" - }; - return Err(PtyError::would_block(message)); - } - let (head, tail) = data.split_at(waiter.length); - waiter.result = Some(Some(head.to_vec())); + if let Some(waiter_id) = pty.waiting_output_reads.front().copied() { + if let Some(waiter) = waiters.get(&waiter_id) { + let retained_len = data.len().min(waiter.length); + check_retained_read_capacity(waiters, retained_len, warned_retained_read_limit)?; + let tail_len = data.len().saturating_sub(waiter.length); + if tail_len > available_capacity(&pty.output_buffer) { + let message = if echo { + "PTY output buffer full (echo backpressure)" + } else { + "PTY output buffer full" + }; + return Err(PtyError::would_block(message)); + } + let (head, tail) = data.split_at(retained_len); + pty.waiting_output_reads.pop_front(); + waiters + .get_mut(&waiter_id) + .expect("validated PTY output waiter must remain registered") + .result = Some(Some(head.to_vec())); + if !tail.is_empty() { pty.output_buffer.push_front(tail.to_vec()); } return Ok(()); } + pty.waiting_output_reads.pop_front(); } if buffer_size(&pty.output_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES { @@ -1299,6 +1440,56 @@ fn deliver_output( Ok(()) } +fn check_retained_read_capacity( + waiters: &BTreeMap, + additional: usize, + warned_near_limit: &mut bool, +) -> PtyResult<()> { + let current = retained_read_bytes(waiters); + let observed = current + .checked_add(additional) + .ok_or_else(|| PtyError::would_block("maxPtyRetainedReadBytes accounting overflowed"))?; + if observed > MAX_PTY_RETAINED_READ_BYTES { + return Err(PtyError::would_block(format!( + "maxPtyRetainedReadBytes limit is {MAX_PTY_RETAINED_READ_BYTES}, retaining this result would use {observed} bytes; retry after another terminal reader consumes its result" + ))); + } + warn_near_pty_limit( + warned_near_limit, + "maxPtyRetainedReadBytes", + observed, + MAX_PTY_RETAINED_READ_BYTES, + ); + Ok(()) +} + +fn retained_read_bytes(waiters: &BTreeMap) -> usize { + waiters + .values() + .filter_map(|waiter| waiter.result.as_ref()?.as_ref()) + .map(Vec::len) + .fold(0usize, usize::saturating_add) +} + +fn retained_read_bytes_for_end(waiters: &BTreeMap, end: PtyEndKind) -> usize { + waiters + .values() + .filter(|waiter| waiter.end == end) + .filter_map(|waiter| waiter.result.as_ref()?.as_ref()) + .map(Vec::len) + .fold(0usize, usize::saturating_add) +} + +fn warn_near_pty_limit(warned: &mut bool, limit_name: &str, observed: usize, limit: usize) { + if *warned || observed < limit.saturating_sub(limit / 10) { + return; + } + *warned = true; + eprintln!( + "WARN_AGENTOS_RESOURCE_NEAR_LIMIT: resource={limit_name} used={observed} limit={limit}" + ); +} + fn advance_termios_generation(pty: &mut PtyState) -> PtyResult<()> { pty.termios_generation = pty .termios_generation @@ -1456,6 +1647,164 @@ fn wait_timeout_or_recover<'a, T>( mod tests { use super::*; + #[test] + fn oversized_read_is_rejected_before_waiter_registration() { + let manager = PtyManager::new(); + let pty = manager.create_pty(); + + let error = manager + .read_with_timeout( + pty.slave.description.id(), + MAX_PTY_READ_BYTES + 1, + Some(Duration::from_secs(1)), + ) + .expect_err("oversized PTY read must fail before waiting"); + + assert_eq!(error.code(), "E2BIG"); + assert!(error.to_string().contains("maxPtyReadBytes")); + assert_eq!(manager.pending_read_waiter_count(), 0); + assert_eq!(manager.queued_read_waiter_count(), 0); + assert_eq!(manager.buffered_input_bytes(), 0); + assert_eq!(manager.buffered_output_bytes(), 0); + } + + #[test] + fn oversized_write_is_rejected_before_line_discipline_side_effects() { + let manager = PtyManager::new(); + let pty = manager.create_pty(); + + let error = manager + .write( + pty.master.description.id(), + vec![b'x'; MAX_PTY_WRITE_BYTES + 1], + ) + .expect_err("oversized PTY write must fail before processing input"); + + assert_eq!(error.code(), "E2BIG"); + assert!(error.to_string().contains("maxPtyWriteBytes")); + let state = lock_or_recover(&manager.inner.state); + let pty_ref = state + .desc_to_pty + .get(&pty.master.description.id()) + .expect("master must remain registered"); + let pty_state = state.ptys.get(&pty_ref.pty_id).expect("PTY must exist"); + assert!(pty_state.line_buffer.is_empty()); + assert!(pty_state.input_buffer.is_empty()); + assert!(pty_state.output_buffer.is_empty()); + assert!(state.waiters.is_empty()); + } + + #[test] + fn output_transform_limit_rejects_before_buffering() { + let manager = PtyManager::new(); + let pty = manager.create_pty(); + let newlines = vec![b'\n'; MAX_PTY_BUFFER_BYTES / 2 + 1]; + + let error = manager + .write(pty.slave.description.id(), newlines) + .expect_err("ONLCR expansion beyond the PTY cap must fail"); + + assert_eq!(error.code(), "E2BIG"); + assert!(error.to_string().contains("maxPtyTransformedOutputBytes")); + assert_eq!(manager.buffered_output_bytes(), 0); + let read_error = manager + .read_with_timeout(pty.master.description.id(), 1, Some(Duration::ZERO)) + .expect_err("failed transformed write must publish no output"); + assert_eq!(read_error.code(), "EAGAIN"); + } + + #[test] + fn read_waiter_limit_fails_with_named_typed_error() { + let manager = PtyManager::new(); + let pty = manager.create_pty(); + { + let mut state = lock_or_recover(&manager.inner.state); + for id in 1..=MAX_PTY_READ_WAITERS as u64 { + state.waiters.insert( + id, + PendingRead { + length: 1, + end: PtyEndKind::Slave, + result: None, + }, + ); + } + state.next_waiter_id = MAX_PTY_READ_WAITERS as u64 + 1; + } + + let error = manager + .read_with_timeout(pty.slave.description.id(), 1, Some(Duration::from_secs(1))) + .expect_err("waiter saturation must fail without registering another waiter"); + + assert_eq!(error.code(), "EAGAIN"); + assert!(error.to_string().contains("maxPtyReadWaiters")); + assert_eq!(manager.pending_read_waiter_count(), MAX_PTY_READ_WAITERS); + assert_eq!(manager.queued_read_waiter_count(), 0); + } + + #[test] + fn retained_read_limit_is_accounted_and_fails_before_delivery() { + const RETAINED_CHUNK_BYTES: usize = 64 * 1024; + const RETAINED_CHUNKS: usize = MAX_PTY_RETAINED_READ_BYTES / RETAINED_CHUNK_BYTES; + + let manager = PtyManager::new(); + let pty = manager.create_pty(); + let pending_id = RETAINED_CHUNKS as u64 + 1; + { + let mut state = lock_or_recover(&manager.inner.state); + let pty_id = state + .desc_to_pty + .get(&pty.master.description.id()) + .expect("master must be registered") + .pty_id; + for id in 1..=RETAINED_CHUNKS as u64 { + state.waiters.insert( + id, + PendingRead { + length: RETAINED_CHUNK_BYTES, + end: PtyEndKind::Master, + result: Some(Some(vec![0; RETAINED_CHUNK_BYTES])), + }, + ); + } + state.waiters.insert( + pending_id, + PendingRead { + length: 1, + end: PtyEndKind::Master, + result: None, + }, + ); + state + .ptys + .get_mut(&pty_id) + .expect("PTY must exist") + .waiting_output_reads + .push_back(pending_id); + } + + assert_eq!(manager.buffered_output_bytes(), MAX_PTY_RETAINED_READ_BYTES); + let error = manager + .write(pty.slave.description.id(), b"x") + .expect_err("retained result saturation must block another delivery"); + + assert_eq!(error.code(), "EAGAIN"); + assert!(error.to_string().contains("maxPtyRetainedReadBytes")); + let state = lock_or_recover(&manager.inner.state); + let pending = state + .waiters + .get(&pending_id) + .expect("blocked waiter must remain registered"); + assert!(pending.result.is_none()); + let pty_ref = state + .desc_to_pty + .get(&pty.master.description.id()) + .expect("master must remain registered"); + let pty_state = state.ptys.get(&pty_ref.pty_id).expect("PTY must exist"); + assert_eq!(pty_state.waiting_output_reads.front(), Some(&pending_id)); + assert!(pty_state.output_buffer.is_empty()); + } + #[test] fn zero_timeout_empty_read_does_not_publish_false_readiness() { let notifier = PollNotifier::default(); diff --git a/crates/kernel/src/resource_accounting.rs b/crates/kernel/src/resource_accounting.rs index 344ddb867e..879db76f6d 100644 --- a/crates/kernel/src/resource_accounting.rs +++ b/crates/kernel/src/resource_accounting.rs @@ -9,6 +9,7 @@ use std::error::Error; use std::fmt; use std::sync::Arc; use vfs::posix::usage::RootFilesystemResourceLimits; +use web_time::{Duration, Instant}; pub use vfs::posix::usage::{ measure_filesystem_usage, FileSystemStats, FileSystemUsage, DEFAULT_MAX_FILESYSTEM_BYTES, @@ -16,7 +17,10 @@ pub use vfs::posix::usage::{ }; pub const DEFAULT_MAX_PROCESSES: usize = 256; -pub const DEFAULT_MAX_OPEN_FDS: usize = 256; +// Keep the Linux-visible default high enough for conventional applications +// that deliberately reserve sparse descriptor ranges (for example +// F_DUPFD/closefrom at fd 512), while retaining a fixed bounded table. +pub const DEFAULT_MAX_OPEN_FDS: usize = 1024; pub const DEFAULT_MAX_PIPES: usize = 128; pub const DEFAULT_MAX_PTYS: usize = 128; pub const DEFAULT_MAX_SOCKETS: usize = 256; @@ -34,9 +38,16 @@ pub const DEFAULT_MAX_RECURSIVE_FS_ENTRIES: usize = 65_536; pub const DEFAULT_VIRTUAL_CPU_COUNT: usize = 1; pub const DEFAULT_MAX_WASM_MEMORY_BYTES: u64 = 128 * 1024 * 1024; +const MAX_PREAD_BYTES_LIMIT: &str = "limits.resources.maxPreadBytes"; +const MAX_FD_WRITE_BYTES_LIMIT: &str = "limits.resources.maxFdWriteBytes"; +const MAX_PROCESS_ARGV_BYTES_LIMIT: &str = "limits.resources.maxProcessArgvBytes"; +const MAX_PROCESS_ENV_BYTES_LIMIT: &str = "limits.resources.maxProcessEnvBytes"; +const MAX_READDIR_ENTRIES_LIMIT: &str = "limits.resources.maxReaddirEntries"; + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ResourceSnapshot { pub running_processes: usize, + pub stopped_processes: usize, pub exited_processes: usize, pub fd_tables: usize, pub open_fds: usize, @@ -73,7 +84,6 @@ pub struct ResourceLimits { pub max_readdir_entries: Option, pub max_recursive_fs_depth: Option, pub max_recursive_fs_entries: Option, - pub max_wasm_fuel: Option, pub max_wasm_memory_bytes: Option, pub max_wasm_stack_bytes: Option, } @@ -100,7 +110,6 @@ impl Default for ResourceLimits { max_readdir_entries: Some(DEFAULT_MAX_READDIR_ENTRIES), max_recursive_fs_depth: Some(DEFAULT_MAX_RECURSIVE_FS_DEPTH), max_recursive_fs_entries: Some(DEFAULT_MAX_RECURSIVE_FS_ENTRIES), - max_wasm_fuel: None, // Match the Workers-style default memory envelope where sensible: // guests are bounded unless the trusted VM config raises the cap. max_wasm_memory_bytes: Some(DEFAULT_MAX_WASM_MEMORY_BYTES), @@ -113,6 +122,9 @@ impl Default for ResourceLimits { pub struct ResourceError { code: &'static str, message: String, + limit_name: Option<&'static str>, + limit: Option, + observed: Option, } impl RootFilesystemResourceLimits for ResourceLimits { @@ -134,6 +146,9 @@ impl ResourceError { Self { code: "EAGAIN", message: message.into(), + limit_name: None, + limit: None, + observed: None, } } @@ -141,6 +156,9 @@ impl ResourceError { Self { code: "ENFILE", message: message.into(), + limit_name: None, + limit: None, + observed: None, } } @@ -148,22 +166,52 @@ impl ResourceError { Self { code: "ENOSPC", message: message.into(), + limit_name: None, + limit: None, + observed: None, } } - fn invalid_input(message: impl Into) -> Self { + fn out_of_memory(message: impl Into) -> Self { Self { - code: "EINVAL", + code: "ENOMEM", message: message.into(), + limit_name: None, + limit: None, + observed: None, } } - fn out_of_memory(message: impl Into) -> Self { + fn point_limit( + code: &'static str, + limit_name: &'static str, + limit: usize, + observed: usize, + description: impl Into, + ) -> Self { Self { - code: "ENOMEM", - message: message.into(), + code, + message: format!( + "{}; limitName={limit_name} limit={limit} observed={observed}; raise {limit_name}", + description.into() + ), + limit_name: Some(limit_name), + limit: Some(limit), + observed: Some(observed), } } + + pub fn limit_name(&self) -> Option<&'static str> { + self.limit_name + } + + pub fn limit(&self) -> Option { + self.limit + } + + pub fn observed(&self) -> Option { + self.observed + } } impl fmt::Display for ResourceError { @@ -190,6 +238,12 @@ struct ResourceGauges { socket_datagram_queue_len: Option>, filesystem_bytes: Option>, inodes: Option>, + pread_bytes: Option>, + fd_write_bytes: Option>, + process_argv_bytes: Option>, + process_env_bytes: Option>, + readdir_entries: Option>, + blocking_read_ms: Option>, recursive_fs_depth: Option>, recursive_fs_entries: Option>, } @@ -231,6 +285,30 @@ impl ResourceGauges { limits.max_filesystem_bytes, ), inodes: register_resource_gauge(TrackedLimit::VmInodes, limits.max_inode_count), + pread_bytes: register_resource_gauge( + TrackedLimit::VmPreadBytes, + limits.max_pread_bytes, + ), + fd_write_bytes: register_resource_gauge( + TrackedLimit::VmFdWriteBytes, + limits.max_fd_write_bytes, + ), + process_argv_bytes: register_resource_gauge( + TrackedLimit::VmProcessArgvBytes, + limits.max_process_argv_bytes, + ), + process_env_bytes: register_resource_gauge( + TrackedLimit::VmProcessEnvBytes, + limits.max_process_env_bytes, + ), + readdir_entries: register_resource_gauge( + TrackedLimit::VmReaddirEntries, + limits.max_readdir_entries, + ), + blocking_read_ms: register_resource_gauge_u64( + TrackedLimit::VmBlockingReadMs, + limits.max_blocking_read_ms, + ), recursive_fs_depth: register_resource_gauge( TrackedLimit::VmRecursiveFsDepth, limits.max_recursive_fs_depth, @@ -248,6 +326,69 @@ pub struct ResourceAccountant { gauges: ResourceGauges, } +/// One kernel blocking wait governed by `maxBlockingReadMs`. Callers wait only +/// until the next edge returned by [`Self::wait_slice`], then retry readiness; +/// the 80% edge emits through the same structured limit-warning registry as +/// byte/count caps without restarting or duplicating the operation. +#[derive(Debug)] +pub struct BlockingReadDeadline { + started: Instant, + limit: Duration, + warning_at: Duration, + warning_emitted: bool, + gauge: Arc, +} + +impl BlockingReadDeadline { + fn new(limit: Duration, gauge: Arc) -> Self { + Self { + started: Instant::now(), + limit, + warning_at: limit.saturating_mul(4) / 5, + warning_emitted: false, + gauge, + } + } + + fn elapsed(&self) -> Duration { + self.started.elapsed().min(self.limit) + } + + fn observe_warning_edge(&mut self) { + let elapsed = self.elapsed(); + if !self.warning_emitted && elapsed >= self.warning_at { + self.warning_emitted = true; + self.gauge + .observe_depth(usize::try_from(elapsed.as_millis()).unwrap_or(usize::MAX)); + } + } + + pub fn wait_slice(&mut self) -> Option { + self.observe_warning_edge(); + let elapsed = self.elapsed(); + if elapsed >= self.limit { + return None; + } + let next_edge = if self.warning_emitted { + self.limit + } else { + self.warning_at + }; + Some(next_edge.saturating_sub(elapsed)) + } + + pub fn expired(&mut self) -> bool { + self.observe_warning_edge(); + self.elapsed() >= self.limit + } +} + +impl Drop for BlockingReadDeadline { + fn drop(&mut self) { + self.gauge.observe_depth(0); + } +} + impl ResourceAccountant { pub fn new(limits: ResourceLimits) -> Self { let gauges = ResourceGauges::new(&limits); @@ -258,7 +399,7 @@ impl ResourceAccountant { /// registry tracks usage and warns before any cap is reached. fn observe_resource_gauges(&self, snapshot: &ResourceSnapshot) { if let Some(gauge) = &self.gauges.processes { - gauge.observe_depth(snapshot.running_processes + snapshot.exited_processes); + gauge.observe_depth(tracked_processes(snapshot)); } if let Some(gauge) = &self.gauges.open_fds { gauge.observe_depth(snapshot.open_fds); @@ -287,6 +428,15 @@ impl ResourceAccountant { &self.limits } + pub fn blocking_read_deadline(&self) -> Option { + let limit = self.limits.max_blocking_read_ms?; + let gauge = Arc::clone(self.gauges.blocking_read_ms.as_ref()?); + Some(BlockingReadDeadline::new( + Duration::from_millis(limit), + gauge, + )) + } + pub fn snapshot( &self, processes: &ProcessTable, @@ -304,10 +454,15 @@ impl ResourceAccountant { .values() .filter(|process| process.status == ProcessStatus::Exited) .count(); + let stopped_processes = process_list + .values() + .filter(|process| process.status == ProcessStatus::Stopped) + .count(); let socket_snapshot = sockets.snapshot(); let snapshot = ResourceSnapshot { running_processes, + stopped_processes, exited_processes, fd_tables: fd_tables.len(), open_fds: fd_tables.total_open_fds(), @@ -332,7 +487,7 @@ impl ResourceAccountant { additional_fds: usize, ) -> Result<(), ResourceError> { if let Some(limit) = self.limits.max_processes { - if snapshot.running_processes + snapshot.exited_processes >= limit { + if tracked_processes(snapshot) >= limit { return Err(ResourceError::exhausted("maximum process limit reached")); } } @@ -345,12 +500,19 @@ impl ResourceAccountant { command: &str, args: &[String], ) -> Result<(), ResourceError> { + let total = argv_payload_bytes(command, args); + if let Some(gauge) = &self.gauges.process_argv_bytes { + gauge.observe_depth(total); + } if let Some(limit) = self.limits.max_process_argv_bytes { - let total = argv_payload_bytes(command, args); if total > limit { - return Err(ResourceError::invalid_input(format!( - "process argv payload {total} bytes exceeds configured limit {limit}" - ))); + return Err(ResourceError::point_limit( + "EINVAL", + MAX_PROCESS_ARGV_BYTES_LIMIT, + limit, + total, + format!("process argv payload {total} bytes exceeds configured limit {limit}"), + )); } } @@ -362,12 +524,21 @@ impl ResourceAccountant { inherited_env: &BTreeMap, overrides: &BTreeMap, ) -> Result<(), ResourceError> { + let total = merged_env_payload_bytes(inherited_env, overrides); + if let Some(gauge) = &self.gauges.process_env_bytes { + gauge.observe_depth(total); + } if let Some(limit) = self.limits.max_process_env_bytes { - let total = merged_env_payload_bytes(inherited_env, overrides); if total > limit { - return Err(ResourceError::invalid_input(format!( - "process environment payload {total} bytes exceeds configured limit {limit}" - ))); + return Err(ResourceError::point_limit( + "EINVAL", + MAX_PROCESS_ENV_BYTES_LIMIT, + limit, + total, + format!( + "process environment payload {total} bytes exceeds configured limit {limit}" + ), + )); } } @@ -462,11 +633,18 @@ impl ResourceAccountant { } pub fn check_pread_length(&self, length: usize) -> Result<(), ResourceError> { + if let Some(gauge) = &self.gauges.pread_bytes { + gauge.observe_depth(length); + } if let Some(limit) = self.limits.max_pread_bytes { if length > limit { - return Err(ResourceError::invalid_input(format!( - "pread length {length} exceeds limits.resources.maxPreadBytes {limit}; raise limits.resources.maxPreadBytes to permit a larger single read" - ))); + return Err(ResourceError::point_limit( + "EINVAL", + MAX_PREAD_BYTES_LIMIT, + limit, + length, + format!("pread length {length} exceeds configured limit {limit}"), + )); } } @@ -474,11 +652,18 @@ impl ResourceAccountant { } pub fn check_fd_write_size(&self, size: usize) -> Result<(), ResourceError> { + if let Some(gauge) = &self.gauges.fd_write_bytes { + gauge.observe_depth(size); + } if let Some(limit) = self.limits.max_fd_write_bytes { if size > limit { - return Err(ResourceError::invalid_input(format!( - "write size {size} exceeds limits.resources.maxFdWriteBytes {limit}; raise limits.resources.maxFdWriteBytes to permit a larger single write" - ))); + return Err(ResourceError::point_limit( + "EINVAL", + MAX_FD_WRITE_BYTES_LIMIT, + limit, + size, + format!("write size {size} exceeds configured limit {limit}"), + )); } } @@ -506,11 +691,20 @@ impl ResourceAccountant { } pub fn check_readdir_entries(&self, entries: usize) -> Result<(), ResourceError> { + if let Some(gauge) = &self.gauges.readdir_entries { + gauge.observe_depth(entries); + } if let Some(limit) = self.limits.max_readdir_entries { if entries > limit { - return Err(ResourceError::out_of_memory(format!( - "directory listing with {entries} entries exceeds configured limit {limit}" - ))); + return Err(ResourceError::point_limit( + "ENOMEM", + MAX_READDIR_ENTRIES_LIMIT, + limit, + entries, + format!( + "directory listing with {entries} entries exceeds configured limit {limit}" + ), + )); } } @@ -598,6 +792,13 @@ impl ResourceAccountant { } } +fn tracked_processes(snapshot: &ResourceSnapshot) -> usize { + snapshot + .running_processes + .saturating_add(snapshot.stopped_processes) + .saturating_add(snapshot.exited_processes) +} + fn argv_payload_bytes(command: &str, args: &[String]) -> usize { let command_bytes = command.len().saturating_add(1); command_bytes.saturating_add( @@ -645,13 +846,26 @@ mod gauge_tests { let sink = Arc::clone(&captured); // Filter by name so a gauge from a concurrently-running test can't pollute. set_limit_warning_handler(Box::new(move |warning| { - if warning.name == TrackedLimit::VmOpenFds { + if matches!( + warning.name, + TrackedLimit::VmOpenFds + | TrackedLimit::VmPreadBytes + | TrackedLimit::VmFdWriteBytes + | TrackedLimit::VmProcessArgvBytes + | TrackedLimit::VmProcessEnvBytes + | TrackedLimit::VmReaddirEntries + ) { sink.lock().expect("sink mutex").push(warning.clone()); } })); let limits = ResourceLimits { max_open_fds: Some(10), + max_pread_bytes: Some(100), + max_fd_write_bytes: Some(100), + max_process_argv_bytes: Some(100), + max_process_env_bytes: Some(100), + max_readdir_entries: Some(100), ..ResourceLimits::default() }; let accountant = ResourceAccountant::new(limits); @@ -661,6 +875,61 @@ mod gauge_tests { }; accountant.observe_resource_gauges(&snapshot); + // Point-in-time limits use the same central 80% warning mechanism. + // Exercise each at its exact accepted boundary before checking +1. + accountant + .check_process_argv_bytes(&"a".repeat(99), &[]) + .expect("exact argv byte limit"); + accountant + .check_process_env_bytes( + &BTreeMap::new(), + &BTreeMap::from([(String::new(), "e".repeat(98))]), + ) + .expect("exact environment byte limit"); + accountant + .check_pread_length(100) + .expect("exact pread byte limit"); + accountant + .check_fd_write_size(100) + .expect("exact write byte limit"); + accountant + .check_readdir_entries(100) + .expect("exact readdir entry limit"); + + let errors = [ + accountant + .check_process_argv_bytes(&"a".repeat(100), &[]) + .expect_err("argv limit +1"), + accountant + .check_process_env_bytes( + &BTreeMap::new(), + &BTreeMap::from([(String::new(), "e".repeat(99))]), + ) + .expect_err("environment limit +1"), + accountant + .check_pread_length(101) + .expect_err("pread limit +1"), + accountant + .check_fd_write_size(101) + .expect_err("write limit +1"), + accountant + .check_readdir_entries(101) + .expect_err("readdir limit +1"), + ]; + for (error, (code, name)) in errors.iter().zip([ + ("EINVAL", MAX_PROCESS_ARGV_BYTES_LIMIT), + ("EINVAL", MAX_PROCESS_ENV_BYTES_LIMIT), + ("EINVAL", MAX_PREAD_BYTES_LIMIT), + ("EINVAL", MAX_FD_WRITE_BYTES_LIMIT), + ("ENOMEM", MAX_READDIR_ENTRIES_LIMIT), + ]) { + assert_eq!(error.code(), code); + assert_eq!(error.limit_name(), Some(name)); + assert_eq!(error.limit(), Some(100)); + assert_eq!(error.observed(), Some(101)); + assert!(error.to_string().contains("raise limits.resources.")); + } + // The gauge reflects the sampled usage... let gauge = accountant .gauges @@ -680,6 +949,24 @@ mod gauge_tests { .any(|warning| warning.name == TrackedLimit::VmOpenFds), "open_fds at 90% of cap must emit an approach warning" ); + let warning_names = captured + .lock() + .unwrap() + .iter() + .map(|warning| warning.name) + .collect::>(); + for expected in [ + TrackedLimit::VmPreadBytes, + TrackedLimit::VmFdWriteBytes, + TrackedLimit::VmProcessArgvBytes, + TrackedLimit::VmProcessEnvBytes, + TrackedLimit::VmReaddirEntries, + ] { + assert!( + warning_names.contains(&expected), + "{expected:?} must warn at the exact configured boundary" + ); + } } #[test] diff --git a/crates/kernel/src/socket_table.rs b/crates/kernel/src/socket_table.rs index 68b5551e32..15ed121db3 100644 --- a/crates/kernel/src/socket_table.rs +++ b/crates/kernel/src/socket_table.rs @@ -1,8 +1,8 @@ +#[cfg(not(target_arch = "wasm32"))] +use crate::admission::{Reservation, ResourceClass, ResourceLedger}; use crate::fd_table::TransferredFd; use crate::poll::{PollEvents, POLLERR, POLLHUP, POLLIN, POLLOUT}; use crate::vfs::normalize_path; -#[cfg(not(target_arch = "wasm32"))] -use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger}; use std::any::Any; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; @@ -544,7 +544,7 @@ impl SocketTableError { } #[cfg(not(target_arch = "wasm32"))] - fn resource_limit(error: agentos_runtime::accounting::LimitError) -> Self { + fn resource_limit(error: crate::admission::LimitError) -> Self { Self { code: "EAGAIN", message: error.to_string(), @@ -3114,7 +3114,7 @@ fn lock_or_recover<'a, T>(mutex: &'a Mutex) -> MutexGuard<'a, T> { mod tests { use super::*; #[cfg(not(target_arch = "wasm32"))] - use agentos_runtime::accounting::{ResourceLimit, ResourceUsage}; + use crate::admission::{ResourceLimit, ResourceUsage}; /// Reads the monotonic socket-id counter without advancing it, so a test can /// observe whether a code path consumed an id. diff --git a/crates/kernel/src/system.rs b/crates/kernel/src/system.rs new file mode 100644 index 0000000000..3ed8bc24ea --- /dev/null +++ b/crates/kernel/src/system.rs @@ -0,0 +1,40 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KernelClockId { + Realtime, + Monotonic, + ProcessCpu, + ThreadCpu, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SystemIdentity { + pub hostname: String, + pub os_type: String, + pub os_release: String, + pub os_version: String, + pub machine: String, + pub domain_name: String, +} + +impl Default for SystemIdentity { + fn default() -> Self { + Self { + hostname: String::from("secure-exec"), + os_type: String::from("Linux"), + os_release: String::from("6.8.0-secure-exec"), + os_version: String::from("#1 SMP PREEMPT_DYNAMIC secure-exec"), + machine: String::from("x86_64"), + domain_name: String::from("localdomain"), + } + } +} + +pub(crate) fn realtime_now_ns() -> Option { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok()? + .as_nanos(); + u64::try_from(nanos).ok() +} diff --git a/crates/kernel/src/user.rs b/crates/kernel/src/user.rs index f8e9cbe81b..962e22a79d 100644 --- a/crates/kernel/src/user.rs +++ b/crates/kernel/src/user.rs @@ -124,9 +124,7 @@ impl UserManager { let primary_group_name = config.group_name.unwrap_or_else(|| username.clone()); let mut groups_by_gid = BTreeMap::new(); - let mut group_gids_by_name = BTreeMap::new(); for group in config.groups { - group_gids_by_name.insert(group.name.clone(), group.gid); groups_by_gid.insert(group.gid, group); } groups_by_gid.entry(gid).or_insert_with(|| GroupRecord { @@ -134,7 +132,32 @@ impl UserManager { name: primary_group_name.clone(), members: vec![username.clone()], }); - group_gids_by_name.insert(primary_group_name.clone(), gid); + let mut synthesized_members = BTreeMap::>::new(); + for account in accounts_by_uid.values() { + for account_gid in &account.supplementary_gids { + if groups_by_gid.contains_key(account_gid) { + continue; + } + let members = synthesized_members.entry(*account_gid).or_default(); + if !members.contains(&account.username) { + members.push(account.username.clone()); + } + } + } + for (group_gid, members) in synthesized_members { + groups_by_gid.insert( + group_gid, + GroupRecord { + gid: group_gid, + name: format!("group{group_gid}"), + members, + }, + ); + } + let group_gids_by_name = groups_by_gid + .values() + .map(|group| (group.name.clone(), group.gid)) + .collect(); Self { uid, @@ -181,15 +204,7 @@ impl UserManager { } pub fn getgrgid(&self, gid: u32) -> Option { - self.groups_by_gid.get(&gid).map(render_group).or_else(|| { - let members = self - .accounts_by_uid - .values() - .filter(|account| account.supplementary_gids.contains(&gid)) - .map(|account| account.username.as_str()) - .collect::>(); - (!members.is_empty()).then(|| format!("group{gid}:x:{gid}:{}", members.join(","))) - }) + self.groups_by_gid.get(&gid).map(render_group) } pub fn getgrnam(&self, name: &str) -> Option { @@ -232,3 +247,137 @@ fn normalize_supplementary_gids(primary_gid: u32, supplementary_gids: Vec) } normalized } + +pub(crate) fn passwd_record_by_uid(database: &[u8], uid: u32) -> Option { + passwd_records(database) + .find(|record| record.uid == uid) + .map(|record| record.text.to_owned()) +} + +pub(crate) fn passwd_record_by_name(database: &[u8], name: &str) -> Option { + passwd_records(database) + .find(|record| record.name == name) + .map(|record| record.text.to_owned()) +} + +pub(crate) fn passwd_record_at(database: &[u8], index: usize) -> Option { + passwd_records(database) + .nth(index) + .map(|record| record.text.to_owned()) +} + +pub(crate) fn group_record_by_gid(database: &[u8], gid: u32) -> Option { + group_records(database) + .find(|record| record.gid == gid) + .map(|record| record.text.to_owned()) +} + +pub(crate) fn group_record_by_name(database: &[u8], name: &str) -> Option { + group_records(database) + .find(|record| record.name == name) + .map(|record| record.text.to_owned()) +} + +pub(crate) fn group_record_at(database: &[u8], index: usize) -> Option { + group_records(database) + .nth(index) + .map(|record| record.text.to_owned()) +} + +#[derive(Debug, Clone, Copy)] +struct PasswdDatabaseRecord<'a> { + text: &'a str, + name: &'a str, + uid: u32, +} + +fn passwd_records(database: &[u8]) -> impl Iterator> { + database.split(|byte| *byte == b'\n').filter_map(|line| { + if line.contains(&b'\0') { + return None; + } + let text = std::str::from_utf8(line).ok()?; + let mut fields = text.split(':'); + let name = fields.next()?; + let _password = fields.next()?; + let uid = fields.next()?.parse().ok()?; + let _gid = fields.next()?.parse::().ok()?; + let _gecos = fields.next()?; + let _home = fields.next()?; + let _shell = fields.next()?; + fields + .next() + .is_none() + .then_some(PasswdDatabaseRecord { text, name, uid }) + }) +} + +#[derive(Debug, Clone, Copy)] +struct GroupDatabaseRecord<'a> { + text: &'a str, + name: &'a str, + gid: u32, +} + +fn group_records(database: &[u8]) -> impl Iterator> { + database.split(|byte| *byte == b'\n').filter_map(|line| { + if line.contains(&b'\0') { + return None; + } + let text = std::str::from_utf8(line).ok()?; + let mut fields = text.split(':'); + let name = fields.next()?; + let _password = fields.next()?; + let gid = fields.next()?.parse().ok()?; + let _members = fields.next()?; + fields + .next() + .is_none() + .then_some(GroupDatabaseRecord { text, name, gid }) + }) +} + +#[cfg(test)] +mod database_tests { + use super::*; + + #[test] + fn account_database_parsers_skip_malformed_records_and_preserve_text() { + let passwd = b"\n# comment\nbad\nnul\0suffix:x:3:3::/:/bin/sh\ninvalid-utf8:x:\xff:2::/:/bin/sh\nextra:x:1:2::/:/bin/sh:field\nmissing:x:1:2::/bin/sh\noverflow:x:4294967296:2::/:/bin/sh\nroot:x:0:0:root:/root:/bin/sh\nroot:x:9:9:duplicate:/duplicate:/bin/false\nalias:x:0:0:duplicate:/duplicate:/bin/false\ncr:x:7:7::/:/bin/sh\r\n"; + assert_eq!( + passwd_record_by_name(passwd, "root").as_deref(), + Some("root:x:0:0:root:/root:/bin/sh") + ); + assert_eq!(passwd_record_by_uid(passwd, 0), passwd_record_at(passwd, 0)); + assert_eq!( + passwd_record_at(passwd, 1).as_deref(), + Some("root:x:9:9:duplicate:/duplicate:/bin/false") + ); + assert_eq!( + passwd_record_at(passwd, 2).as_deref(), + Some("alias:x:0:0:duplicate:/duplicate:/bin/false") + ); + assert_eq!( + passwd_record_at(passwd, 3).as_deref(), + Some("cr:x:7:7::/:/bin/sh\r") + ); + assert_eq!(passwd_record_at(passwd, 4), None); + + let group = b"\n# comment\nbad\nnul\0suffix:x:3:user\ninvalid-utf8:x:\xff:user\nextra:x:1:user:field\nmissing:x:1\noverflow:x:4294967296:user\nroot:x:0:root\nroot:x:9:duplicate\nalias:x:0:duplicate\ncr:x:7:user\r\n"; + assert_eq!( + group_record_by_name(group, "root").as_deref(), + Some("root:x:0:root") + ); + assert_eq!(group_record_by_gid(group, 0), group_record_at(group, 0)); + assert_eq!( + group_record_at(group, 1).as_deref(), + Some("root:x:9:duplicate") + ); + assert_eq!( + group_record_at(group, 2).as_deref(), + Some("alias:x:0:duplicate") + ); + assert_eq!(group_record_at(group, 3).as_deref(), Some("cr:x:7:user\r")); + assert_eq!(group_record_at(group, 4), None); + } +} diff --git a/crates/kernel/tests/agentos_read_only.rs b/crates/kernel/tests/agentos_read_only.rs index ae7c0e1a6d..4df5845209 100644 --- a/crates/kernel/tests/agentos_read_only.rs +++ b/crates/kernel/tests/agentos_read_only.rs @@ -22,6 +22,15 @@ fn seeded_kernel() -> KernelVm { filesystem .write_file(INSTRUCTIONS, b"original instructions".to_vec()) .expect("seed instructions before kernel starts"); + filesystem + .mkdir("/etc/agentos/protected-dir", true) + .expect("seed protected directory before kernel starts"); + filesystem + .write_file( + "/etc/agentos/protected-dir/sentinel", + b"protected sentinel".to_vec(), + ) + .expect("seed protected directory contents before kernel starts"); filesystem.mkdir("/tmp", true).expect("seed tmp directory"); let mut config = KernelVmConfig::new("vm-agentos-read-only"); @@ -337,3 +346,242 @@ fn agentos_protection_rejects_creates_through_symlinked_parent() { "original instructions" ); } + +#[test] +fn process_mutation_matrix_rejects_read_only_sources_and_destinations_without_mutation() { + let mut kernel = seeded_kernel(); + let pid = spawn_shell(&mut kernel); + kernel + .write_file("/tmp/link-source", "link source") + .expect("seed writable hard-link source"); + kernel + .write_file("/tmp/rename-source", "rename source") + .expect("seed writable rename source"); + kernel + .write_file("/tmp/replacement", "replacement") + .expect("seed writable replacement"); + + assert_erofs(kernel.link_for_process( + DRIVER, + pid, + INSTRUCTIONS, + "/tmp/protected-source-hardlink", + )); + assert!(!kernel + .exists("/tmp/protected-source-hardlink") + .expect("check rejected hard-link destination")); + assert_eq!( + read_instructions(&mut kernel).expect("read protected hard-link source"), + "original instructions" + ); + + assert_erofs(kernel.link_for_process( + DRIVER, + pid, + "/tmp/link-source", + "/etc/agentos/new-hardlink", + )); + assert_eq!( + kernel + .read_file("/tmp/link-source") + .expect("read writable hard-link source after denial"), + b"link source" + ); + assert!(!kernel + .exists("/etc/agentos/new-hardlink") + .expect("check rejected protected hard-link destination")); + + assert_erofs(kernel.remove_file_for_process(DRIVER, pid, INSTRUCTIONS)); + assert_eq!( + read_instructions(&mut kernel).expect("read protected file after rejected unlink"), + "original instructions" + ); + + assert_erofs(kernel.remove_dir_for_process(DRIVER, pid, "/etc/agentos/protected-dir")); + assert_eq!( + kernel + .read_file("/etc/agentos/protected-dir/sentinel") + .expect("read protected directory contents after rejected removal"), + b"protected sentinel" + ); + + assert_erofs(kernel.rename_for_process(DRIVER, pid, INSTRUCTIONS, "/tmp/moved-instructions")); + assert_eq!( + read_instructions(&mut kernel).expect("read protected rename source"), + "original instructions" + ); + assert!(!kernel + .exists("/tmp/moved-instructions") + .expect("check rejected rename destination")); + + assert_erofs(kernel.rename_for_process( + DRIVER, + pid, + "/tmp/rename-source", + "/etc/agentos/new-name", + )); + assert_eq!( + kernel + .read_file("/tmp/rename-source") + .expect("read writable rename source after denial"), + b"rename source" + ); + assert!(!kernel + .exists("/etc/agentos/new-name") + .expect("check rejected protected rename destination")); + + assert_erofs(kernel.rename_for_process(DRIVER, pid, "/tmp/replacement", INSTRUCTIONS)); + assert_eq!( + kernel + .read_file("/tmp/replacement") + .expect("read rejected replacement source"), + b"replacement" + ); + assert_eq!( + read_instructions(&mut kernel).expect("read rejected replacement destination"), + "original instructions" + ); + + assert_erofs(kernel.symlink_for_process( + DRIVER, + pid, + "/tmp/target-does-not-need-to-exist", + "/etc/agentos/new-symlink", + )); + assert_eq!( + kernel + .lstat("/etc/agentos/new-symlink") + .expect_err("rejected protected symlink destination must not be created") + .code(), + "ENOENT" + ); +} + +#[test] +fn process_mutation_read_only_checks_follow_symlinked_parents_without_mutation() { + let mut kernel = seeded_kernel(); + let pid = spawn_shell(&mut kernel); + kernel + .symlink("/etc/agentos", "/tmp/agentos-alias") + .expect("create writable-path alias to protected directory"); + kernel + .write_file("/tmp/link-source", "link source") + .expect("seed writable hard-link source"); + kernel + .write_file("/tmp/rename-source", "rename source") + .expect("seed writable rename source"); + kernel + .write_file("/tmp/replacement", "replacement") + .expect("seed writable replacement"); + + assert_erofs(kernel.link_for_process( + DRIVER, + pid, + "/tmp/agentos-alias/instructions.md", + "/tmp/aliased-source-hardlink", + )); + assert!(!kernel + .exists("/tmp/aliased-source-hardlink") + .expect("check rejected aliased-source hard link")); + assert_eq!( + read_instructions(&mut kernel).expect("read aliased hard-link source after denial"), + "original instructions" + ); + + assert_erofs(kernel.link_for_process( + DRIVER, + pid, + "/tmp/link-source", + "/tmp/agentos-alias/new-hardlink", + )); + assert_eq!( + kernel + .read_file("/tmp/link-source") + .expect("read hard-link source after aliased destination denial"), + b"link source" + ); + assert!(!kernel + .exists("/etc/agentos/new-hardlink") + .expect("check protected hard-link destination")); + + assert_erofs(kernel.remove_file_for_process(DRIVER, pid, "/tmp/agentos-alias/instructions.md")); + assert_eq!( + read_instructions(&mut kernel).expect("read aliased unlink target after denial"), + "original instructions" + ); + + assert_erofs(kernel.remove_dir_for_process(DRIVER, pid, "/tmp/agentos-alias/protected-dir")); + assert_eq!( + kernel + .read_file("/etc/agentos/protected-dir/sentinel") + .expect("read aliased protected directory after denial"), + b"protected sentinel" + ); + + assert_erofs(kernel.rename_for_process( + DRIVER, + pid, + "/tmp/agentos-alias/instructions.md", + "/tmp/moved-aliased-instructions", + )); + assert_eq!( + read_instructions(&mut kernel).expect("read aliased rename source after denial"), + "original instructions" + ); + assert!(!kernel + .exists("/tmp/moved-aliased-instructions") + .expect("check rejected writable rename destination")); + + assert_erofs(kernel.rename_for_process( + DRIVER, + pid, + "/tmp/rename-source", + "/tmp/agentos-alias/new-name", + )); + assert_eq!( + kernel + .read_file("/tmp/rename-source") + .expect("read rename source after aliased destination denial"), + b"rename source" + ); + assert!(!kernel + .exists("/etc/agentos/new-name") + .expect("check rejected protected rename destination")); + + assert_erofs(kernel.rename_for_process( + DRIVER, + pid, + "/tmp/replacement", + "/tmp/agentos-alias/instructions.md", + )); + assert_eq!( + kernel + .read_file("/tmp/replacement") + .expect("read replacement after aliased destination denial"), + b"replacement" + ); + assert_eq!( + read_instructions(&mut kernel).expect("read aliased replacement destination"), + "original instructions" + ); + + assert_erofs(kernel.symlink_for_process( + DRIVER, + pid, + "/tmp/target-does-not-need-to-exist", + "/tmp/agentos-alias/new-symlink", + )); + assert_eq!( + kernel + .lstat("/etc/agentos/new-symlink") + .expect_err("rejected aliased symlink destination must not be created") + .code(), + "ENOENT" + ); + assert_eq!( + kernel + .read_link("/tmp/agentos-alias") + .expect("read parent alias after rejected mutations"), + "/etc/agentos" + ); +} diff --git a/crates/kernel/tests/api_surface.rs b/crates/kernel/tests/api_surface.rs index 8f8cf0468e..4be3641829 100644 --- a/crates/kernel/tests/api_surface.rs +++ b/crates/kernel/tests/api_surface.rs @@ -11,7 +11,7 @@ use agentos_kernel::kernel::{ use agentos_kernel::mount_table::{MountOptions, MountTable}; use agentos_kernel::permissions::Permissions; use agentos_kernel::pipe_manager::MAX_PIPE_BUFFER_BYTES; -use agentos_kernel::process_table::{ProcessWaitEvent, SIGWINCH}; +use agentos_kernel::process_table::{ProcessWaitEvent, SignalAction, SignalDisposition, SIGWINCH}; use agentos_kernel::socket_table::SocketType; use agentos_kernel::vfs::{ MemoryFileSystem, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, MAX_PATH_LENGTH, @@ -2011,6 +2011,20 @@ fn pty_resize_delivers_sigwinch_to_the_foreground_process_group() { ..OpenShellOptions::default() }) .expect("open shell"); + shell + .process() + .signal_action( + SIGWINCH, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("catch SIGWINCH"); + let controls = shell + .process() + .attach_runtime_control(std::sync::Arc::new(|| {})) + .expect("attach runtime controls"); kernel .pty_resize("shell", shell.pid(), shell.master_fd(), 120, 40) @@ -2019,7 +2033,16 @@ fn pty_resize_delivers_sigwinch_to_the_foreground_process_group() { .pty_resize("shell", shell.pid(), shell.master_fd(), 120, 40) .expect("repeat shell pty resize"); - assert_eq!(shell.process().kill_signals(), vec![SIGWINCH]); + let pending = controls.pending(); + assert!(pending.checkpoint); + controls + .acknowledge(pending) + .expect("acknowledge resize signal checkpoint"); + assert!(shell + .process() + .sigpending() + .expect("pending signals") + .contains(SIGWINCH)); shell.process().finish(0); kernel.waitpid(shell.pid()).expect("wait shell"); diff --git a/crates/kernel/tests/command_registry.rs b/crates/kernel/tests/command_registry.rs index 6188aded6b..985617dba5 100644 --- a/crates/kernel/tests/command_registry.rs +++ b/crates/kernel/tests/command_registry.rs @@ -76,6 +76,42 @@ fn records_warning_when_overriding_existing_command() { assert!(warnings[0].contains("node")); } +#[test] +fn replace_makes_one_driver_command_set_exact() { + let mut registry = CommandRegistry::new(); + registry + .register(CommandDriver::new("runtime", ["old", "keep", "shared"])) + .expect("register initial commands"); + registry + .register(CommandDriver::new("other", ["overridden", "shared"])) + .expect("register other driver"); + + let obsolete = registry + .replace(CommandDriver::new("runtime", ["keep", "new"])) + .expect("replace runtime commands"); + assert_eq!(obsolete, vec![String::from("old")]); + assert!(registry.resolve("old").is_none()); + assert_eq!( + registry.resolve("keep").expect("kept command").name(), + "runtime" + ); + assert_eq!( + registry.resolve("new").expect("new command").name(), + "runtime" + ); + assert_eq!( + registry + .resolve("overridden") + .expect("other command remains") + .name(), + "other" + ); + assert_eq!( + registry.resolve("shared").expect("override remains").name(), + "other" + ); +} + #[test] fn populate_bin_creates_stub_entries() { let mut vfs = MemoryFileSystem::new(); diff --git a/crates/kernel/tests/dac.rs b/crates/kernel/tests/dac.rs index 4fbfb41867..a5eef58bc2 100644 --- a/crates/kernel/tests/dac.rs +++ b/crates/kernel/tests/dac.rs @@ -254,6 +254,310 @@ fn sticky_directory_only_allows_root_directory_owner_or_file_owner() { .unwrap(); } +#[test] +fn process_mutation_dac_denials_follow_symlinked_parents_without_mutation() { + let mut kernel = kernel(); + kernel.mkdir("/open", false).unwrap(); + kernel.chmod("/open", 0o777).unwrap(); + kernel + .write_file("/open/link-source", b"link source".to_vec()) + .unwrap(); + kernel + .write_file("/open/rename-source", b"rename source".to_vec()) + .unwrap(); + + kernel.mkdir("/restricted-source", false).unwrap(); + kernel + .write_file( + "/restricted-source/hidden-link-source", + b"hidden source".to_vec(), + ) + .unwrap(); + kernel.chown("/restricted-source", 1000, 1000).unwrap(); + kernel.chmod("/restricted-source", 0o700).unwrap(); + kernel + .symlink("/restricted-source", "/source-alias") + .unwrap(); + + kernel.mkdir("/restricted-destination", false).unwrap(); + kernel + .write_file( + "/restricted-destination/remove-file", + b"remove file".to_vec(), + ) + .unwrap(); + kernel + .write_file( + "/restricted-destination/rename-source", + b"restricted rename source".to_vec(), + ) + .unwrap(); + kernel + .write_file( + "/restricted-destination/rename-destination", + b"rename destination".to_vec(), + ) + .unwrap(); + kernel + .mkdir("/restricted-destination/remove-dir", false) + .unwrap(); + kernel.chown("/restricted-destination", 1000, 1000).unwrap(); + kernel.chmod("/restricted-destination", 0o555).unwrap(); + kernel + .symlink("/restricted-destination", "/destination-alias") + .unwrap(); + + let bob = process_as(&mut kernel, 1001); + + let source_traversal = kernel + .link_for_process( + DRIVER, + bob, + "/source-alias/hidden-link-source", + "/open/hidden-hardlink", + ) + .expect_err("hard-link source traversal should require search permission"); + assert_eq!(source_traversal.code(), "EACCES"); + assert!(!kernel.exists("/open/hidden-hardlink").unwrap()); + assert_eq!( + kernel + .read_file("/restricted-source/hidden-link-source") + .unwrap(), + b"hidden source" + ); + + let link_destination = kernel + .link_for_process( + DRIVER, + bob, + "/open/link-source", + "/destination-alias/new-hardlink", + ) + .expect_err("hard-link destination parent should require write permission"); + assert_eq!(link_destination.code(), "EACCES"); + assert!(!kernel + .exists("/restricted-destination/new-hardlink") + .unwrap()); + assert_eq!( + kernel.read_file("/open/link-source").unwrap(), + b"link source" + ); + + let remove_file = kernel + .remove_file_for_process(DRIVER, bob, "/destination-alias/remove-file") + .expect_err("unlink parent should require write permission"); + assert_eq!(remove_file.code(), "EACCES"); + assert_eq!( + kernel + .read_file("/restricted-destination/remove-file") + .unwrap(), + b"remove file" + ); + + let remove_dir = kernel + .remove_dir_for_process(DRIVER, bob, "/destination-alias/remove-dir") + .expect_err("rmdir parent should require write permission"); + assert_eq!(remove_dir.code(), "EACCES"); + assert!( + kernel + .stat("/restricted-destination/remove-dir") + .unwrap() + .is_directory + ); + + let rename_source = kernel + .rename_for_process( + DRIVER, + bob, + "/destination-alias/rename-source", + "/open/moved-from-restricted", + ) + .expect_err("rename source parent should require write permission"); + assert_eq!(rename_source.code(), "EACCES"); + assert_eq!( + kernel + .read_file("/restricted-destination/rename-source") + .unwrap(), + b"restricted rename source" + ); + assert!(!kernel.exists("/open/moved-from-restricted").unwrap()); + + let rename_destination = kernel + .rename_for_process( + DRIVER, + bob, + "/open/rename-source", + "/destination-alias/rename-destination", + ) + .expect_err("rename destination parent should require write permission"); + assert_eq!(rename_destination.code(), "EACCES"); + assert_eq!( + kernel.read_file("/open/rename-source").unwrap(), + b"rename source" + ); + assert_eq!( + kernel + .read_file("/restricted-destination/rename-destination") + .unwrap(), + b"rename destination" + ); + + let symlink_destination = kernel + .symlink_for_process( + DRIVER, + bob, + "/target-does-not-need-to-exist", + "/destination-alias/new-symlink", + ) + .expect_err("symlink destination parent should require write permission"); + assert_eq!(symlink_destination.code(), "EACCES"); + assert_eq!( + kernel + .lstat("/restricted-destination/new-symlink") + .expect_err("rejected symlink destination must not be created") + .code(), + "ENOENT" + ); + + kernel + .symlink_for_process( + DRIVER, + bob, + "/source-alias/hidden-target", + "/open/dangling-symlink", + ) + .expect("symlink creation must not traverse or authorize its target"); + assert_eq!( + kernel.read_link("/open/dangling-symlink").unwrap(), + "/source-alias/hidden-target" + ); +} + +#[test] +fn sticky_directory_mutation_matrix_matches_linux_without_partial_changes() { + let mut kernel = kernel(); + kernel.mkdir("/sticky", false).unwrap(); + kernel.chown("/sticky", 0, 0).unwrap(); + kernel.chmod("/sticky", 0o1777).unwrap(); + kernel.symlink("/sticky", "/sticky-alias").unwrap(); + kernel.mkdir("/open", false).unwrap(); + kernel.chmod("/open", 0o777).unwrap(); + + kernel.mkdir("/sticky/alice-dir", false).unwrap(); + kernel.chown("/sticky/alice-dir", 1000, 1000).unwrap(); + kernel + .write_file("/sticky/alice-rename-source", b"alice source".to_vec()) + .unwrap(); + kernel + .chown("/sticky/alice-rename-source", 1000, 1000) + .unwrap(); + kernel + .write_file("/sticky/alice-replacement", b"alice replacement".to_vec()) + .unwrap(); + kernel + .chown("/sticky/alice-replacement", 1000, 1000) + .unwrap(); + kernel + .write_file("/sticky/bob-destination", b"bob destination".to_vec()) + .unwrap(); + kernel.chown("/sticky/bob-destination", 1001, 1001).unwrap(); + kernel + .write_file("/open/bob-link-source", b"bob source".to_vec()) + .unwrap(); + kernel.chown("/open/bob-link-source", 1001, 1001).unwrap(); + + let bob = process_as(&mut kernel, 1001); + let remove_dir = kernel + .remove_dir_for_process(DRIVER, bob, "/sticky-alias/alice-dir") + .expect_err("sticky directory should reject removing another user's directory"); + assert_eq!(remove_dir.code(), "EPERM"); + assert!(kernel.stat("/sticky/alice-dir").unwrap().is_directory); + + let rename_source = kernel + .rename_for_process( + DRIVER, + bob, + "/sticky-alias/alice-rename-source", + "/sticky-alias/bob-moved-source", + ) + .expect_err("sticky directory should reject renaming another user's source"); + assert_eq!(rename_source.code(), "EPERM"); + assert_eq!( + kernel.read_file("/sticky/alice-rename-source").unwrap(), + b"alice source" + ); + assert!(!kernel.exists("/sticky/bob-moved-source").unwrap()); + + kernel + .link_for_process( + DRIVER, + bob, + "/open/bob-link-source", + "/sticky-alias/bob-hardlink", + ) + .expect("sticky directories allow creating new hard-link names"); + assert_eq!( + kernel.read_file("/sticky/bob-hardlink").unwrap(), + b"bob source" + ); + kernel + .symlink_for_process( + DRIVER, + bob, + "/target-does-not-need-to-exist", + "/sticky-alias/bob-symlink", + ) + .expect("sticky directories allow creating new symlink names"); + assert_eq!( + kernel.read_link("/sticky/bob-symlink").unwrap(), + "/target-does-not-need-to-exist" + ); + + let alice = process_as(&mut kernel, 1000); + let replace_destination = kernel + .rename_for_process( + DRIVER, + alice, + "/sticky-alias/alice-replacement", + "/sticky-alias/bob-destination", + ) + .expect_err("sticky directory should reject replacing another user's destination"); + assert_eq!(replace_destination.code(), "EPERM"); + assert_eq!( + kernel.read_file("/sticky/alice-replacement").unwrap(), + b"alice replacement" + ); + assert_eq!( + kernel.read_file("/sticky/bob-destination").unwrap(), + b"bob destination" + ); + + kernel + .remove_dir_for_process(DRIVER, alice, "/sticky-alias/alice-dir") + .expect("sticky entry owner should be able to remove their directory"); + kernel + .rename_for_process( + DRIVER, + alice, + "/sticky-alias/alice-rename-source", + "/sticky-alias/alice-renamed", + ) + .expect("sticky entry owner should be able to rename their source"); + + kernel.mkdir("/alice-sticky", false).unwrap(); + kernel.chown("/alice-sticky", 1000, 1000).unwrap(); + kernel.chmod("/alice-sticky", 0o1777).unwrap(); + kernel + .write_file("/alice-sticky/bob-file", b"owned by bob".to_vec()) + .unwrap(); + kernel.chown("/alice-sticky/bob-file", 1001, 1001).unwrap(); + kernel + .remove_file_for_process(DRIVER, alice, "/alice-sticky/bob-file") + .expect("sticky directory owner should be able to remove another user's entry"); + assert!(!kernel.exists("/alice-sticky/bob-file").unwrap()); + assert_eq!(kernel.read_link("/sticky-alias").unwrap(), "/sticky"); +} + #[test] fn metadata_changes_and_descriptor_modes_enforce_linux_style_errors() { let mut kernel = kernel(); @@ -448,6 +752,126 @@ fn xattrs_enforce_dac_namespaces_and_linux_flags() { ); } +#[test] +fn xattr_value_limit_accepts_linux_boundary_and_rejects_plus_one_transactionally() { + let mut kernel = kernel(); + kernel.mkdir("/work", false).unwrap(); + kernel.chmod("/work", 0o777).unwrap(); + kernel.write_file("/work/file", b"x".to_vec()).unwrap(); + kernel.chown("/work/file", 1000, 1000).unwrap(); + let alice = process_as(&mut kernel, 1000); + let exact = vec![b'x'; 64 * 1024]; + let oversized = vec![b'y'; 64 * 1024 + 1]; + + kernel + .set_xattr_for_process( + DRIVER, + alice, + "/work/file", + "user.path-limit", + exact.clone(), + 1, + true, + ) + .expect("path xattr exact Linux boundary"); + let path_error = kernel + .set_xattr_for_process( + DRIVER, + alice, + "/work/file", + "user.path-limit", + oversized.clone(), + 2, + true, + ) + .expect_err("path xattr limit +1"); + assert_eq!(path_error.code(), "E2BIG"); + assert_eq!( + kernel + .get_xattr_for_process(DRIVER, alice, "/work/file", "user.path-limit", true) + .expect("path xattr survives rejected replacement"), + exact, + "oversized path replacement must not partially mutate the prior value" + ); + + let fd = kernel + .fd_open(DRIVER, alice, "/work/file", O_RDONLY, None) + .expect("open xattr target"); + kernel + .fd_set_xattr_for_process(DRIVER, alice, fd, "user.fd-limit", exact.clone(), 1) + .expect("fd xattr exact Linux boundary"); + let fd_error = kernel + .fd_set_xattr_for_process(DRIVER, alice, fd, "user.fd-limit", oversized, 2) + .expect_err("fd xattr limit +1"); + assert_eq!(fd_error.code(), "E2BIG"); + assert_eq!( + kernel + .fd_get_xattr_for_process(DRIVER, alice, fd, "user.fd-limit") + .expect("fd xattr survives rejected replacement"), + exact, + "oversized fd replacement must not partially mutate the prior value" + ); +} + +#[test] +fn fd_xattrs_follow_the_open_description_after_unlink() { + let mut kernel = kernel(); + kernel.mkdir("/work", false).unwrap(); + kernel.chmod("/work", 0o777).unwrap(); + kernel.write_file("/work/file", b"x".to_vec()).unwrap(); + kernel.chown("/work/file", 1000, 1000).unwrap(); + let alice = process_as(&mut kernel, 1000); + kernel + .set_xattr_for_process( + DRIVER, + alice, + "/work/file", + "user.note", + b"before".to_vec(), + 0, + true, + ) + .unwrap(); + let fd = kernel + .fd_open(DRIVER, alice, "/work/file", O_RDONLY, None) + .unwrap(); + + kernel + .remove_file_for_process(DRIVER, alice, "/work/file") + .unwrap(); + assert_eq!( + kernel + .fd_get_xattr_for_process(DRIVER, alice, fd, "user.note") + .unwrap(), + b"before" + ); + assert_eq!( + kernel + .fd_list_xattrs_for_process(DRIVER, alice, fd) + .unwrap(), + vec![String::from("user.note")] + ); + kernel + .fd_set_xattr_for_process(DRIVER, alice, fd, "user.note", b"after".to_vec(), 2) + .unwrap(); + assert_eq!( + kernel + .fd_get_xattr_for_process(DRIVER, alice, fd, "user.note") + .unwrap(), + b"after" + ); + kernel + .fd_remove_xattr_for_process(DRIVER, alice, fd, "user.note") + .unwrap(); + assert_eq!( + kernel + .fd_get_xattr_for_process(DRIVER, alice, fd, "user.note") + .unwrap_err() + .code(), + "ENODATA" + ); +} + #[test] fn xattrs_enforce_linux_inode_type_and_symlink_rules() { let mut kernel = kernel(); diff --git a/crates/kernel/tests/dns_resolution.rs b/crates/kernel/tests/dns_resolution.rs index a101c31873..e1fe3c3f58 100644 --- a/crates/kernel/tests/dns_resolution.rs +++ b/crates/kernel/tests/dns_resolution.rs @@ -7,9 +7,12 @@ use agentos_kernel::permissions::{ NetworkAccessRequest, NetworkOperation, PermissionDecision, Permissions, }; use agentos_kernel::vfs::MemoryFileSystem; -use hickory_resolver::proto::rr::{Record, RecordType}; +use hickory_proto::rr::{Record, RecordType}; +use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::pin::Pin; use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Wake, Waker}; #[derive(Debug, Clone)] struct MockDnsResolver { @@ -69,10 +72,91 @@ impl DnsResolver for MockDnsResolver { } } +struct AsyncOnlyDnsResolver; + +impl DnsResolver for AsyncOnlyDnsResolver { + fn lookup_ip(&self, _: &DnsLookupRequest) -> Result, DnsResolverError> { + panic!("reactor DNS path called the synchronous resolver") + } + + fn lookup_records(&self, _: &DnsRecordLookupRequest) -> Result, DnsResolverError> { + panic!("reactor DNS path called the synchronous record resolver") + } + + fn lookup_ip_async<'a>( + &'a self, + _: DnsLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async { Ok(vec![IpAddr::V4(Ipv4Addr::new(198, 51, 100, 91))]) }) + } + + fn lookup_records_async<'a>( + &'a self, + _: DnsRecordLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async { Ok(Vec::new()) }) + } +} + fn new_kernel(config: KernelVmConfig) -> KernelVm { KernelVm::new(MemoryFileSystem::new(), config) } +struct NoopWake; + +impl Wake for NoopWake { + fn wake(self: Arc) {} +} + +fn poll_ready(future: F) -> F::Output { + let waker = Waker::from(Arc::new(NoopWake)); + let mut context = Context::from_waker(&waker); + let mut future = std::pin::pin!(future); + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => output, + Poll::Pending => panic!("transport-neutral test resolver unexpectedly returned Pending"), + } +} + +#[test] +fn kernel_async_dns_path_never_calls_the_synchronous_resolver_on_a_runtime_worker() { + let mut config = KernelVmConfig::new("vm-async-dns"); + config.permissions = Permissions::allow_all(); + config.dns_resolver = Arc::new(AsyncOnlyDnsResolver); + let kernel = new_kernel(config); + let resolution = poll_ready( + kernel.resolve_dns_async("async.example.test", DnsLookupPolicy::CheckPermissions), + ) + .expect("async resolver path"); + assert_eq!( + resolution.addresses(), + &[IpAddr::V4(Ipv4Addr::new(198, 51, 100, 91))] + ); +} + +#[test] +fn kernel_default_resolver_is_transport_neutral_and_unavailable() { + let mut config = KernelVmConfig::new("vm-no-host-dns"); + config.permissions = Permissions::allow_all(); + let kernel = new_kernel(config); + + let error = kernel + .resolve_dns("example.test", DnsLookupPolicy::CheckPermissions) + .expect_err("kernel must not perform ambient host DNS without injection"); + assert_eq!(error.code(), "EHOSTUNREACH"); + assert!(error + .to_string() + .contains("host DNS resolver is unavailable")); + + let literal = kernel + .resolve_dns("192.0.2.1", DnsLookupPolicy::CheckPermissions) + .expect("literal resolution remains kernel-owned"); + assert_eq!( + literal.addresses(), + &["192.0.2.1".parse::().expect("IP")] + ); +} + #[test] fn kernel_dns_resolution_prefers_overrides_before_the_resolver() { let resolver = MockDnsResolver::new(vec![IpAddr::V4(Ipv4Addr::new(198, 51, 100, 44))]); diff --git a/crates/kernel/tests/fd_table.rs b/crates/kernel/tests/fd_table.rs index 75ce2c1c25..ec6acfde8b 100644 --- a/crates/kernel/tests/fd_table.rs +++ b/crates/kernel/tests/fd_table.rs @@ -287,7 +287,7 @@ fn stat_returns_fd_metadata() { } #[test] -fn nonblocking_status_flags_are_tracked_per_fd_entry() { +fn nonblocking_status_override_creates_independent_reopen_state() { let mut manager = FdTableManager::new(); manager.create(1); @@ -322,6 +322,29 @@ fn nonblocking_status_flags_are_tracked_per_fd_entry() { ); } +#[test] +fn duplicated_descriptors_share_nonblocking_open_status() { + let mut manager = FdTableManager::new(); + manager.create(1); + + let table = manager.get_mut(1).expect("FD table should exist"); + let fd = table + .open_with_filetype( + "/tmp/test.txt", + O_WRONLY | O_NONBLOCK, + FILETYPE_REGULAR_FILE, + ) + .expect("open regular file"); + let duplicate = table.dup(fd).expect("duplicate regular file"); + + table + .fcntl(duplicate, F_SETFL, 0) + .expect("clear shared nonblocking flag"); + + assert_eq!(table.fcntl(fd, F_GETFL, 0).unwrap(), O_WRONLY); + assert_eq!(table.fcntl(duplicate, F_GETFL, 0).unwrap(), O_WRONLY); +} + #[test] fn shared_description_open_preserves_nonblocking_status() { let mut manager = FdTableManager::new(); @@ -340,7 +363,7 @@ fn shared_description_open_preserves_nonblocking_status() { .expect("open shared pipe description"); assert_eq!( - table.get(fd).expect("opened entry").status_flags, + table.get(fd).expect("opened entry").status_flags.get(), O_NONBLOCK ); assert_eq!( @@ -434,6 +457,28 @@ fn fcntl_dupfd_uses_lowest_available_fd_at_or_above_minimum() { ); } +#[test] +fn fcntl_dupfd_supports_high_minimum_and_skips_alias_collisions() { + let mut manager = FdTableManager::with_max_fds(1024); + manager.create(1); + + let table = manager.get_mut(1).expect("FD table should exist"); + let fd = table + .open("/tmp/test.txt", O_RDONLY) + .expect("open source FD"); + table.dup2(fd, 512).expect("occupy first high alias"); + table.dup2(fd, 513).expect("occupy second high alias"); + + let duplicated = table + .fcntl(fd, F_DUPFD, 512) + .expect("duplicate above configured high minimum"); + assert_eq!(duplicated, 514); + assert_eq!( + table.stat(duplicated).expect("duplicate stat").rights, + table.stat(fd).expect("source stat").rights + ); +} + #[test] fn fcntl_dupfd_rejects_minimum_fd_past_the_process_limit() { let mut manager = FdTableManager::new(); diff --git a/crates/kernel/tests/identity.rs b/crates/kernel/tests/identity.rs index 17f33e619a..326822a505 100644 --- a/crates/kernel/tests/identity.rs +++ b/crates/kernel/tests/identity.rs @@ -1,3 +1,4 @@ +use agentos_kernel::fd_table::O_RDONLY; use agentos_kernel::kernel::{KernelVm, KernelVmConfig, VirtualProcessOptions}; use agentos_kernel::permissions::Permissions; use agentos_kernel::resource_accounting::ResourceLimits; @@ -152,6 +153,227 @@ fn identity_syscalls_and_process_metadata_use_kernel_managed_values() { assert_eq!(unknown_gid.code(), "ENOENT"); } +#[test] +fn process_account_lookups_use_live_vfs_databases_with_config_fallback() { + let mut kernel = configured_kernel(); + let process = kernel + .create_virtual_process( + "identity-driver", + "identity-driver", + "identity-check", + Vec::new(), + VirtualProcessOptions::default(), + ) + .expect("create identity process"); + let pid = process.pid(); + + assert_eq!( + kernel + .getpwnam_for_process("identity-driver", pid, "deploy") + .expect("configured passwd fallback"), + "deploy:x:501:502:Deploy User:/srv/deploy:/bin/bash" + ); + + kernel.mkdir("/etc", true).expect("create /etc"); + kernel + .write_file( + "/etc/passwd", + b"malformed\nroot:x:0:0:root:/root:/bin/sh\nlive:x:42:43::/home/live:/bin/sh\n" + .to_vec(), + ) + .expect("write passwd database"); + kernel + .write_file( + "/etc/group", + b"malformed\nroot:x:0:root\nlive:x:43:live\n".to_vec(), + ) + .expect("write group database"); + + assert_eq!( + kernel + .getpwuid_for_process("identity-driver", pid, 42) + .expect("live passwd id lookup"), + "live:x:42:43::/home/live:/bin/sh" + ); + assert_eq!( + kernel + .getpwent_for_process("identity-driver", pid, 0) + .expect("live passwd enumeration"), + "root:x:0:0:root:/root:/bin/sh" + ); + assert_eq!( + kernel + .getgrgid_for_process("identity-driver", pid, 43) + .expect("live group id lookup"), + "live:x:43:live" + ); + assert_eq!( + kernel + .getgrent_for_process("identity-driver", pid, 0) + .expect("live group enumeration"), + "root:x:0:root" + ); + assert_eq!( + kernel + .getpwnam_for_process("identity-driver", pid, "deploy") + .expect_err("present passwd database is authoritative") + .code(), + "ENOENT" + ); + + kernel + .remove_file("/etc/passwd") + .expect("remove live passwd database"); + kernel + .symlink("/etc/missing-passwd", "/etc/passwd") + .expect("create dangling passwd database symlink"); + assert_eq!( + kernel + .getpwnam_for_process("identity-driver", pid, "deploy") + .expect_err("present dangling database must not expose configured accounts") + .code(), + "ENOENT" + ); + kernel + .remove_file("/etc/passwd") + .expect("remove dangling passwd database symlink"); + + kernel + .write_file("/etc/passwd", Vec::new()) + .expect("replace passwd database with empty file"); + assert_eq!( + kernel + .getpwnam_for_process("identity-driver", pid, "deploy") + .expect_err("empty present passwd database is authoritative") + .code(), + "ENOENT" + ); + + kernel + .write_file( + "/etc/passwd", + b"updated:x:42:99::/srv/updated:/bin/bash\n".to_vec(), + ) + .expect("replace passwd database"); + assert_eq!( + kernel + .getpwuid_for_process("identity-driver", pid, 42) + .expect("lookup observes live replacement"), + "updated:x:42:99::/srv/updated:/bin/bash" + ); +} + +#[test] +fn process_account_database_reads_obey_the_configured_pread_limit() { + let mut config = KernelVmConfig::new("vm-account-database-limit"); + config.permissions = Permissions::allow_all(); + config.resources.max_pread_bytes = Some(64); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + let process = kernel + .create_virtual_process( + "identity-driver", + "identity-driver", + "identity-check", + Vec::new(), + VirtualProcessOptions::default(), + ) + .expect("create identity process"); + let pid = process.pid(); + kernel.mkdir("/etc", true).expect("create /etc"); + + let prefix = "root:x:0:0::/:"; + let exact = format!("{prefix}{}", "s".repeat(64 - prefix.len())); + assert_eq!(exact.len(), 64); + kernel + .write_file("/etc/passwd", exact.as_bytes().to_vec()) + .expect("write exact-limit passwd database"); + assert_eq!( + kernel + .getpwuid_for_process("identity-driver", pid, 0) + .expect("exact-limit database read"), + exact + ); + + kernel + .write_file("/etc/passwd", vec![b'x'; 65]) + .expect("write oversized passwd database"); + let error = kernel + .getpwuid_for_process("identity-driver", pid, 0) + .expect_err("database above pread cap must fail before allocation"); + assert_eq!(error.code(), "EINVAL"); + assert!(error + .to_string() + .contains("limitName=limits.resources.maxPreadBytes")); + assert!(error + .to_string() + .contains("raise limits.resources.maxPreadBytes")); +} + +#[test] +fn process_full_file_reads_bound_regular_proc_and_device_payloads() { + let mut config = KernelVmConfig::new("vm-process-read-limit"); + config.permissions = Permissions::allow_all(); + config.resources.max_pread_bytes = Some(4_096); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + let process = kernel + .create_virtual_process( + "identity-driver", + "identity-driver", + "identity-check", + vec![String::from("argument")], + VirtualProcessOptions::default(), + ) + .expect("create identity process"); + let pid = process.pid(); + kernel.mkdir("/tmp", true).expect("create /tmp"); + + let exact = vec![b'x'; 4_096]; + kernel + .write_file("/tmp/exact", exact.clone()) + .expect("write exact-limit regular file"); + assert_eq!( + kernel + .read_file_for_process("identity-driver", pid, "/tmp/exact") + .expect("read exact-limit regular file"), + exact + ); + + kernel + .write_file("/tmp/oversized", vec![b'x'; 4_097]) + .expect("write oversized regular file"); + let error = kernel + .read_file_for_process("identity-driver", pid, "/tmp/oversized") + .expect_err("regular file above pread cap must fail before allocation"); + assert_eq!(error.code(), "EINVAL"); + assert!(error + .to_string() + .contains("limitName=limits.resources.maxPreadBytes")); + + let fd = kernel + .fd_open("identity-driver", pid, "/tmp/oversized", O_RDONLY, None) + .expect("open oversized file before proc-fd read"); + let proc_error = kernel + .read_file_for_process("identity-driver", pid, &format!("/proc/self/fd/{fd}")) + .expect_err("proc fd must not bypass the bounded regular-file read"); + assert_eq!(proc_error.code(), "EINVAL"); + assert!(proc_error + .to_string() + .contains("limitName=limits.resources.maxPreadBytes")); + + assert_eq!( + kernel + .read_file_for_process("identity-driver", pid, "/proc/self/cmdline") + .expect("dynamic proc file remains readable"), + b"identity-check\0argument\0".to_vec() + ); + assert_eq!( + kernel + .read_file_for_process("identity-driver", pid, "/dev/zero") + .expect("zero-sized virtual device stat does not truncate its payload"), + vec![0; 4_096] + ); +} + #[test] fn identity_queries_require_process_ownership() { let mut kernel = configured_kernel(); @@ -359,7 +581,7 @@ fn procfs_exposes_linux_like_identity_and_system_files() { assert!(idle_seconds >= uptime_seconds); let version = read_utf8(&mut kernel, "/proc/version"); - assert!(version.starts_with("Linux version 6.8.0-agentos")); + assert!(version.starts_with("Linux version 6.8.0-secure-exec")); let status_stat = kernel .stat(&format!("/proc/{pid}/status")) diff --git a/crates/kernel/tests/kernel_integration.rs b/crates/kernel/tests/kernel_integration.rs index 59d155c7bb..95d74d77bb 100644 --- a/crates/kernel/tests/kernel_integration.rs +++ b/crates/kernel/tests/kernel_integration.rs @@ -2,9 +2,11 @@ use agentos_kernel::bridge::LifecycleState; use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::kernel::{KernelVm, KernelVmConfig, SpawnOptions}; use agentos_kernel::permissions::Permissions; +use agentos_kernel::process_runtime::ProcessExit; use agentos_kernel::process_table::SIGPIPE; use agentos_kernel::pty::LineDisciplineConfig; use agentos_kernel::vfs::MemoryFileSystem; +use std::sync::Arc; use std::time::Duration; #[test] @@ -65,6 +67,45 @@ fn minimal_vm_lifecycle_transitions_between_ready_busy_and_terminated() { assert_eq!(kernel.state(), LifecycleState::Terminated); } +#[test] +fn isatty_recognizes_both_kernel_pty_ends() { + let mut config = KernelVmConfig::new("vm-kernel-pty-isatty"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + let (master_fd, slave_fd, _) = kernel + .open_pty("shell", process.pid()) + .expect("open kernel PTY"); + + assert!( + kernel + .isatty("shell", process.pid(), master_fd) + .expect("classify PTY master"), + "Linux PTY masters are terminal descriptors" + ); + assert!( + kernel + .isatty("shell", process.pid(), slave_fd) + .expect("classify PTY slave"), + "PTY slaves are terminal descriptors" + ); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap shell"); +} + #[test] fn raw_mode_recovery_lease_is_limited_to_foreground_process_group() { let mut config = KernelVmConfig::new("vm-pty-raw-owner"); @@ -155,10 +196,21 @@ fn dispose_kills_running_processes_and_cleans_special_resources() { let _ = kernel.open_pipe("shell", process.pid()).expect("open pipe"); let _ = kernel.open_pty("shell", process.pid()).expect("open pty"); + let exit_reporter = process.exit_reporter(); + let _controls = process + .attach_runtime_control(Arc::new(move || { + exit_reporter + .report_exit(ProcessExit::Signaled { + signal: 15, + core_dumped: false, + }) + .expect("report SIGTERM exit"); + })) + .expect("attach runtime controls"); + kernel.dispose().expect("dispose kernel"); assert_eq!(kernel.state(), LifecycleState::Terminated); assert_eq!(process.wait(Duration::from_millis(50)), Some(143)); - assert_eq!(process.kill_signals(), vec![15]); let snapshot = kernel.resource_snapshot(); assert_eq!(snapshot.fd_tables, 0); @@ -254,11 +306,22 @@ fn broken_pipe_writes_deliver_sigpipe_and_return_epipe() { .fd_close("shell", writer.pid(), read_fd) .expect("close inherited read end"); + let exit_reporter = writer.exit_reporter(); + let _controls = writer + .attach_runtime_control(Arc::new(move || { + exit_reporter + .report_exit(ProcessExit::Signaled { + signal: SIGPIPE, + core_dumped: false, + }) + .expect("report SIGPIPE exit"); + })) + .expect("attach runtime controls"); + let error = kernel .fd_write("shell", writer.pid(), write_fd, b"fail") .expect_err("broken pipe writes should fail"); assert_eq!(error.code(), "EPIPE"); - assert_eq!(writer.kill_signals(), vec![SIGPIPE]); assert_eq!(writer.wait(Duration::from_millis(50)), Some(128 + SIGPIPE)); } diff --git a/crates/kernel/tests/pipe_manager.rs b/crates/kernel/tests/pipe_manager.rs index 9753a3fb8f..5b5e9fa3c5 100644 --- a/crates/kernel/tests/pipe_manager.rs +++ b/crates/kernel/tests/pipe_manager.rs @@ -480,7 +480,7 @@ fn create_pipe_fds_allocates_pipe_entries_in_the_fd_table() { #[test] fn create_pipe_fds_propagates_fd_allocation_failures() { let manager = PipeManager::new(); - let mut tables = FdTableManager::new(); + let mut tables = FdTableManager::with_max_fds(256); let table = tables.create(1); for index in 0..253 { diff --git a/crates/kernel/tests/poll.rs b/crates/kernel/tests/poll.rs index 0f7612cef0..bbc141d6c6 100644 --- a/crates/kernel/tests/poll.rs +++ b/crates/kernel/tests/poll.rs @@ -1,7 +1,9 @@ use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::kernel::{KernelVm, KernelVmConfig, SpawnOptions}; use agentos_kernel::permissions::Permissions; -use agentos_kernel::poll::{PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN, POLLOUT}; +use agentos_kernel::poll::{ + PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN, POLLOUT, POLLRDNORM, POLLWRNORM, +}; use agentos_kernel::resource_accounting::ResourceLimits; use agentos_kernel::socket_table::{InetSocketAddress, SocketShutdown, SocketSpec}; use agentos_kernel::vfs::MemoryFileSystem; @@ -79,6 +81,31 @@ fn poll_reports_pipe_readiness_and_hangup() { assert!(ready.fds[0].revents.contains(POLLHUP)); } +#[test] +fn poll_projects_linux_normal_read_and_write_aliases() { + let mut kernel = kernel_vm("vm-poll-normal-aliases"); + let pid = spawn_shell(&mut kernel); + let (read_fd, write_fd) = kernel.open_pipe("shell", pid).expect("open pipe"); + kernel + .fd_write("shell", pid, write_fd, b"x") + .expect("write pipe payload"); + + let ready = kernel + .poll_fds( + "shell", + pid, + vec![ + PollFd::new(read_fd, POLLRDNORM), + PollFd::new(write_fd, POLLWRNORM), + ], + 0, + ) + .expect("poll Linux normal aliases"); + assert_eq!(ready.ready_count, 2); + assert_eq!(ready.fds[0].revents, POLLRDNORM); + assert_eq!(ready.fds[1].revents, POLLWRNORM); +} + #[test] fn poll_reports_pipe_peer_close_as_pollerr_on_writer() { let mut kernel = kernel_vm("vm-poll-pipe-err"); diff --git a/crates/kernel/tests/process_table.rs b/crates/kernel/tests/process_table.rs index cfd445e329..e046d615e9 100644 --- a/crates/kernel/tests/process_table.rs +++ b/crates/kernel/tests/process_table.rs @@ -1,7 +1,11 @@ +use agentos_kernel::process_runtime::{ + ProcessControlRequest, ProcessExit, ProcessRuntimeEndpoint, ProcessRuntimeEndpointError, + ProcessRuntimeIdentity, ProcessTermination, +}; use agentos_kernel::process_table::{ - DriverProcess, ProcessContext, ProcessExitCallback, ProcessResult, ProcessStatus, ProcessTable, - ProcessWaitEvent, SigmaskHow, SignalSet, WaitPidFlags, SIGCHLD, SIGCONT, SIGHUP, SIGSTOP, - SIGTERM, SIGTSTP, + ProcessContext, ProcessEntry, ProcessResult, ProcessStatus, ProcessTable, ProcessWaitEvent, + SigmaskHow, SignalAction, SignalDisposition, SignalSet, WaitPidFlags, SIGCHLD, SIGCONT, SIGHUP, + SIGSTOP, SIGTERM, SIGTSTP, }; use std::collections::BTreeMap; use std::fmt::Debug; @@ -18,14 +22,13 @@ fn assert_error_code(result: ProcessResult, expected: &str) { struct MockProcessState { kills: Vec, exit_code: Option, - on_exit: Option, + binding: Option<(ProcessTable, u32)>, ignore_sigterm: bool, } #[derive(Default)] struct MockDriverProcess { state: Mutex, - exited: Condvar, } impl MockDriverProcess { @@ -39,10 +42,16 @@ impl MockDriverProcess { ignore_sigterm: true, ..MockProcessState::default() }), - exited: Condvar::new(), }) } + fn bind(&self, table: &ProcessTable, pid: u32) { + self.state + .lock() + .expect("mock process lock poisoned") + .binding = Some((table.clone(), pid)); + } + fn schedule_exit(self: &Arc, delay: Duration, exit_code: i32) { let process = Arc::clone(self); thread::spawn(move || { @@ -52,18 +61,19 @@ impl MockDriverProcess { } fn exit(&self, exit_code: i32) { - let callback = { + let binding = { let mut state = self.state.lock().expect("mock process lock poisoned"); if state.exit_code.is_some() { return; } state.exit_code = Some(exit_code); - self.exited.notify_all(); - state.on_exit.clone() + state.binding.clone() }; - if let Some(callback) = callback { - callback(exit_code); + if let Some((table, pid)) = binding { + table + .report_exit(pid, ProcessExit::Exited(exit_code)) + .expect("mock process exit must be reported"); } } @@ -76,38 +86,102 @@ impl MockDriverProcess { } } -impl DriverProcess for MockDriverProcess { - fn kill(&self, signal: i32) { - let should_exit = { +impl ProcessRuntimeEndpoint for MockDriverProcess { + fn identity(&self) -> Option { + None + } + + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError> { + let (binding, stop_transition, termination) = { let mut state = self.state.lock().expect("mock process lock poisoned"); - state.kills.push(signal); - signal == 9 || (signal == 15 && !state.ignore_sigterm) + let signal = match request { + ProcessControlRequest::Checkpoint => state + .binding + .as_ref() + .and_then(|(table, pid)| table.sigpending(*pid).ok()) + .and_then(|pending| pending.signals().into_iter().next()), + ProcessControlRequest::Stop { signal } => Some(signal), + ProcessControlRequest::Continue => Some(SIGCONT), + ProcessControlRequest::Terminate(ProcessTermination::Signal { signal, .. }) => { + Some(signal) + } + ProcessControlRequest::Terminate(ProcessTermination::RuntimeFault) + | ProcessControlRequest::Cancel(_) => None, + }; + if let Some(signal) = signal { + state.kills.push(signal); + } + let termination = match request { + ProcessControlRequest::Terminate(ProcessTermination::Signal { signal, .. }) + if signal == 9 || (signal == SIGTERM && !state.ignore_sigterm) => + { + Some(ProcessExit::Signaled { + signal, + core_dumped: false, + }) + } + ProcessControlRequest::Terminate(ProcessTermination::RuntimeFault) + | ProcessControlRequest::Cancel(_) => Some(ProcessExit::Exited(1)), + _ => None, + }; + let stop_transition = match request { + ProcessControlRequest::Stop { signal } => Some((true, Some(signal))), + ProcessControlRequest::Continue => Some((false, None)), + _ => None, + }; + (state.binding.clone(), stop_transition, termination) }; - if should_exit { - self.exit(128 + signal); + if let Some((table, pid)) = binding { + if let Some((stopped, signal)) = stop_transition { + if stopped { + table + .mark_stopped(pid, signal.expect("stop transition signal")) + .expect("mock stop transition must be recorded"); + } else { + table + .mark_continued(pid) + .expect("mock continue transition must be recorded"); + } + } + if let Some(termination) = termination { + table + .report_exit(pid, termination) + .expect("mock termination must be reported"); + } } + Ok(()) } +} - fn wait(&self, timeout: Duration) -> Option { - let state = self.state.lock().expect("mock process lock poisoned"); - if state.exit_code.is_some() { - return state.exit_code; - } - - let (state, _) = self - .exited - .wait_timeout(state, timeout) - .expect("mock process wait lock poisoned"); - state.exit_code - } +fn register( + table: &ProcessTable, + pid: u32, + driver: impl Into, + command: impl Into, + args: Vec, + context: ProcessContext, + process: Arc, +) -> ProcessEntry { + let entry = ProcessTable::register(table, pid, driver, command, args, context, process.clone()); + process.bind(table, pid); + entry +} - fn set_on_exit(&self, callback: ProcessExitCallback) { - self.state - .lock() - .expect("mock process lock poisoned") - .on_exit = Some(callback); - } +fn catch_signal(table: &ProcessTable, pid: u32, signal: i32) { + table + .signal_action( + pid, + signal, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); } fn create_context(ppid: u32) -> ProcessContext { @@ -145,7 +219,8 @@ fn register_allocates_expected_process_metadata_and_parent_groups() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - let parent_entry = table.register( + let parent_entry = register( + &table, parent_pid, "wasmvm", "grep", @@ -153,7 +228,8 @@ fn register_allocates_expected_process_metadata_and_parent_groups() { create_context(0), parent, ); - let child_entry = table.register( + let child_entry = register( + &table, child_pid, "node", "node", @@ -176,7 +252,8 @@ fn waitpid_resolves_for_exiting_and_already_exited_processes() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); let process = MockDriverProcess::new(); let pid = allocate_pid(&table); - table.register( + register( + &table, pid, "wasmvm", "echo", @@ -197,7 +274,8 @@ fn waitpid_resolves_for_exiting_and_already_exited_processes() { ); let exited_pid = allocate_pid(&table); - table.register( + register( + &table, exited_pid, "wasmvm", "true", @@ -205,7 +283,9 @@ fn waitpid_resolves_for_exiting_and_already_exited_processes() { create_context(0), MockDriverProcess::new(), ); - table.mark_exited(exited_pid, 42); + table + .mark_exited(exited_pid, 42) + .expect("exit must be recorded"); assert_eq!( table @@ -227,7 +307,8 @@ fn long_lived_parent_retains_zombies_until_waited_under_pressure() { let parent_pid = allocate_pid(&table); let mut child_pids = Vec::new(); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -239,7 +320,8 @@ fn long_lived_parent_retains_zombies_until_waited_under_pressure() { for index in 0..100 { let child = MockDriverProcess::new(); let child_pid = allocate_pid(&table); - table.register( + register( + &table, child_pid, "wasmvm", format!("child-{index}"), @@ -287,7 +369,8 @@ fn allocate_pid_wraps_without_reusing_live_or_zombie_entries() { let live_one = MockDriverProcess::new(); // Registering max_pid - 2 after the high PIDs moves the public allocation cursor back to max_pid - 1. - table.register( + register( + &table, max_pid - 1, "wasmvm", "live-high", @@ -295,7 +378,8 @@ fn allocate_pid_wraps_without_reusing_live_or_zombie_entries() { create_context(0), live_high, ); - table.register( + register( + &table, max_pid, "wasmvm", "zombie-high", @@ -303,7 +387,8 @@ fn allocate_pid_wraps_without_reusing_live_or_zombie_entries() { create_context(0), zombie_high.clone(), ); - table.register( + register( + &table, max_pid - 2, "wasmvm", "cursor-seed", @@ -311,7 +396,8 @@ fn allocate_pid_wraps_without_reusing_live_or_zombie_entries() { create_context(0), cursor_seed, ); - table.register( + register( + &table, 1, "wasmvm", "live-one", @@ -344,7 +430,8 @@ fn waitpid_for_supports_wnohang_and_waiting_for_any_child() { let child_a_pid = allocate_pid(&table); let child_b_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -352,7 +439,8 @@ fn waitpid_for_supports_wnohang_and_waiting_for_any_child() { create_context(0), parent, ); - table.register( + register( + &table, child_a_pid, "wasmvm", "child-a", @@ -360,7 +448,8 @@ fn waitpid_for_supports_wnohang_and_waiting_for_any_child() { create_context(parent_pid), child_a, ); - table.register( + register( + &table, child_b_pid, "wasmvm", "child-b", @@ -402,7 +491,8 @@ fn on_process_exit_runs_before_waitpid_waiters_are_notified() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); let process = MockDriverProcess::new(); let pid = allocate_pid(&table); - table.register( + register( + &table, pid, "wasmvm", "sleep", @@ -468,7 +558,8 @@ fn waitpid_for_reports_stopped_and_continued_children_once() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -476,7 +567,8 @@ fn waitpid_for_reports_stopped_and_continued_children_once() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -484,8 +576,11 @@ fn waitpid_for_reports_stopped_and_continued_children_once() { create_context(parent_pid), child, ); + catch_signal(&table, parent_pid, SIGCHLD); - table.mark_stopped(child_pid, SIGSTOP); + table + .mark_stopped(child_pid, SIGSTOP) + .expect("stop must be recorded"); assert_eq!( table .waitpid_for(parent_pid, child_pid as i32, WaitPidFlags::WNOHANG) @@ -514,7 +609,9 @@ fn waitpid_for_reports_stopped_and_continued_children_once() { ProcessStatus::Stopped ); - table.mark_continued(child_pid); + table + .mark_continued(child_pid) + .expect("continue must be recorded"); assert_eq!( table .waitpid_for( @@ -546,7 +643,8 @@ fn nonterminal_wait_query_never_reaps_an_exited_child() { let child = MockDriverProcess::new(); let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -554,7 +652,8 @@ fn nonterminal_wait_query_never_reaps_an_exited_child() { create_context(0), parent, ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -563,17 +662,16 @@ fn nonterminal_wait_query_never_reaps_an_exited_child() { child.clone(), ); - table.mark_stopped(child_pid, SIGSTOP); + table + .mark_stopped(child_pid, SIGSTOP) + .expect("stop must be recorded"); child.exit(17); assert_eq!( table .take_nonterminal_wait_event_for(parent_pid, child_pid as i32, WaitPidFlags::WUNTRACED,) .expect("nonterminal wait should succeed"), - Some(agentos_kernel::process_table::ProcessWaitResult { - pid: child_pid, - status: SIGSTOP, - event: ProcessWaitEvent::Stopped, - }) + None, + "terminal state must supersede an unconsumed stop notification" ); assert_eq!( table @@ -595,12 +693,57 @@ fn nonterminal_wait_query_never_reaps_an_exited_child() { assert!(table.get(child_pid).is_none()); } +#[test] +fn detailed_wait_preserves_exact_signaled_termination() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let parent = MockDriverProcess::new(); + let child = MockDriverProcess::new(); + let parent_pid = allocate_pid(&table); + let child_pid = allocate_pid(&table); + register( + &table, + parent_pid, + "wasmvm", + "parent", + Vec::new(), + create_context(0), + parent, + ); + register( + &table, + child_pid, + "wasmvm", + "child", + Vec::new(), + create_context(parent_pid), + child, + ); + + let termination = ProcessExit::Signaled { + signal: SIGTERM, + core_dumped: true, + }; + table + .report_exit(child_pid, termination) + .expect("termination must be reported"); + let transition = table + .waitpid_for_detailed(parent_pid, child_pid as i32, WaitPidFlags::WNOHANG) + .expect("detailed wait should succeed") + .expect("terminal transition should be ready"); + assert_eq!(transition.result.pid, child_pid); + assert_eq!(transition.result.status, 128 + SIGTERM); + assert_eq!(transition.result.event, ProcessWaitEvent::Exited); + assert_eq!(transition.termination, Some(termination)); + assert!(table.get(child_pid).is_none()); +} + #[test] fn kill_routes_signals_and_validates_process_existence() { let table = ProcessTable::new(); let process = MockDriverProcess::new(); let pid = allocate_pid(&table); - table.register( + register( + &table, pid, "wasmvm", "sleep", @@ -632,7 +775,8 @@ fn kill_updates_job_control_state_for_stop_and_continue_signals() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -640,7 +784,8 @@ fn kill_updates_job_control_state_for_stop_and_continue_signals() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -648,6 +793,7 @@ fn kill_updates_job_control_state_for_stop_and_continue_signals() { create_context(parent_pid), child.clone(), ); + catch_signal(&table, parent_pid, SIGCHLD); table .kill(child_pid as i32, SIGTSTP) @@ -711,7 +857,8 @@ fn exiting_child_delivers_sigchld_to_living_parent() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -719,7 +866,8 @@ fn exiting_child_delivers_sigchld_to_living_parent() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -727,6 +875,7 @@ fn exiting_child_delivers_sigchld_to_living_parent() { create_context(parent_pid), child.clone(), ); + catch_signal(&table, parent_pid, SIGCHLD); child.exit(0); @@ -749,7 +898,8 @@ fn blocked_sigchld_is_queued_until_the_parent_unblocks_it() { let child_pid = allocate_pid(&table); let sigchld_mask = SignalSet::from_signal(SIGCHLD).expect("SIGCHLD should be valid"); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -757,7 +907,8 @@ fn blocked_sigchld_is_queued_until_the_parent_unblocks_it() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -765,6 +916,7 @@ fn blocked_sigchld_is_queued_until_the_parent_unblocks_it() { create_context(parent_pid), child.clone(), ); + catch_signal(&table, parent_pid, SIGCHLD); assert_eq!( table @@ -797,6 +949,19 @@ fn blocked_sigchld_is_queued_until_the_parent_unblocks_it() { || parent.kills() == vec![SIGCHLD], Duration::from_millis(100), ); + assert_eq!( + table.sigpending(parent_pid).expect("pending signals"), + sigchld_mask, + "checkpoint wake does not consume a caught signal" + ); + let delivery = table + .begin_signal_delivery(parent_pid) + .expect("begin signal delivery") + .expect("caught SIGCHLD delivery"); + assert_eq!(delivery.signal, SIGCHLD); + table + .end_signal_delivery(parent_pid, delivery.token) + .expect("finish signal delivery"); assert_eq!( table.sigpending(parent_pid).expect("pending signals"), SignalSet::empty() @@ -811,7 +976,8 @@ fn killed_child_delivers_sigchld_to_living_parent() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -819,7 +985,8 @@ fn killed_child_delivers_sigchld_to_living_parent() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -827,6 +994,7 @@ fn killed_child_delivers_sigchld_to_living_parent() { create_context(parent_pid), child.clone(), ); + catch_signal(&table, parent_pid, SIGCHLD); table .kill(child_pid as i32, 15) @@ -849,7 +1017,8 @@ fn blocked_sigterm_is_delivered_when_the_process_unblocks_it() { let pid = allocate_pid(&table); let sigterm_mask = SignalSet::from_signal(SIGTERM).expect("SIGTERM should be valid"); - table.register( + register( + &table, pid, "wasmvm", "sleep", @@ -894,7 +1063,8 @@ fn process_groups_and_sessions_follow_legacy_rules() { let p3 = allocate_pid(&table); let p4 = allocate_pid(&table); - table.register( + register( + &table, p1, "wasmvm", "sh", @@ -902,7 +1072,8 @@ fn process_groups_and_sessions_follow_legacy_rules() { create_context(0), MockDriverProcess::new(), ); - table.register( + register( + &table, p2, "wasmvm", "child", @@ -910,7 +1081,8 @@ fn process_groups_and_sessions_follow_legacy_rules() { create_context(p1), MockDriverProcess::new(), ); - table.register( + register( + &table, p3, "wasmvm", "peer", @@ -918,7 +1090,8 @@ fn process_groups_and_sessions_follow_legacy_rules() { create_context(p1), MockDriverProcess::new(), ); - table.register( + register( + &table, p4, "wasmvm", "other", @@ -950,7 +1123,8 @@ fn negative_pid_kill_targets_entire_process_groups() { let pid1 = allocate_pid(&table); let pid2 = allocate_pid(&table); - table.register( + register( + &table, pid1, "wasmvm", "leader", @@ -958,7 +1132,8 @@ fn negative_pid_kill_targets_entire_process_groups() { create_context(0), leader.clone(), ); - table.register( + register( + &table, pid2, "wasmvm", "peer", @@ -984,7 +1159,8 @@ fn negative_pid_signal_zero_checks_process_group_liveness() { let leader_pid = allocate_pid(&table); let peer_pid = allocate_pid(&table); - table.register( + register( + &table, leader_pid, "wasmvm", "leader", @@ -992,7 +1168,8 @@ fn negative_pid_signal_zero_checks_process_group_liveness() { create_context(0), leader.clone(), ); - table.register( + register( + &table, peer_pid, "wasmvm", "peer", @@ -1014,7 +1191,7 @@ fn negative_pid_signal_zero_checks_process_group_liveness() { } #[test] -fn negative_pid_kill_reaches_stopped_and_exited_group_members() { +fn negative_pid_kill_reaches_stopped_but_not_exited_group_members() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); let init = MockDriverProcess::new(); let parent = MockDriverProcess::new(); @@ -1027,7 +1204,8 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { let stopped_pid = allocate_pid(&table); let zombie_pid = allocate_pid(&table); - table.register( + register( + &table, init_pid, "wasmvm", "init", @@ -1035,7 +1213,8 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { create_context(0), init, ); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1043,7 +1222,8 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { create_context(init_pid), parent, ); - table.register( + register( + &table, leader_pid, "wasmvm", "leader", @@ -1051,7 +1231,8 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { create_context(parent_pid), leader.clone(), ); - table.register( + register( + &table, stopped_pid, "wasmvm", "stopped", @@ -1059,7 +1240,8 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { create_context(parent_pid), stopped.clone(), ); - table.register( + register( + &table, zombie_pid, "wasmvm", "zombie", @@ -1076,16 +1258,18 @@ fn negative_pid_kill_reaches_stopped_and_exited_group_members() { table .setpgid(zombie_pid, leader_pid) .expect("zombie peer joins leader group"); - table.mark_stopped(stopped_pid, SIGSTOP); + table + .mark_stopped(stopped_pid, SIGSTOP) + .expect("stop must be recorded"); zombie.exit(23); table .kill(-(leader_pid as i32), 15) - .expect("group kill should include stopped and zombie members"); + .expect("group kill should include live stopped members"); assert_eq!(leader.kills(), vec![15]); assert_eq!(stopped.kills(), vec![15]); - assert_eq!(zombie.kills(), vec![15]); + assert!(zombie.kills().is_empty()); } #[test] @@ -1098,7 +1282,8 @@ fn exiting_parent_reparents_children_to_pid_one_when_available() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, init_pid, "wasmvm", "init", @@ -1106,7 +1291,8 @@ fn exiting_parent_reparents_children_to_pid_one_when_available() { create_context(0), init, ); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1114,7 +1300,8 @@ fn exiting_parent_reparents_children_to_pid_one_when_available() { create_context(init_pid), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -1145,7 +1332,8 @@ fn orphaned_stopped_process_groups_receive_sighup_and_sigcont() { let leader_pid = allocate_pid(&table); let stopped_pid = allocate_pid(&table); - table.register( + register( + &table, init_pid, "wasmvm", "init", @@ -1153,7 +1341,8 @@ fn orphaned_stopped_process_groups_receive_sighup_and_sigcont() { create_context(0), init, ); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1161,7 +1350,8 @@ fn orphaned_stopped_process_groups_receive_sighup_and_sigcont() { create_context(init_pid), parent.clone(), ); - table.register( + register( + &table, leader_pid, "wasmvm", "leader", @@ -1169,7 +1359,8 @@ fn orphaned_stopped_process_groups_receive_sighup_and_sigcont() { create_context(parent_pid), leader.clone(), ); - table.register( + register( + &table, stopped_pid, "wasmvm", "stopped", @@ -1183,7 +1374,9 @@ fn orphaned_stopped_process_groups_receive_sighup_and_sigcont() { table .setpgid(stopped_pid, leader_pid) .expect("stopped peer joins leader group"); - table.mark_stopped(stopped_pid, SIGSTOP); + table + .mark_stopped(stopped_pid, SIGSTOP) + .expect("stop must be recorded"); parent.exit(0); @@ -1196,10 +1389,13 @@ fn terminate_all_escalates_from_sigterm_to_sigkill_for_survivors() { let table = ProcessTable::new(); let graceful = MockDriverProcess::new(); let stubborn = MockDriverProcess::stubborn(); + let stopped = MockDriverProcess::new(); let pid1 = allocate_pid(&table); let pid2 = allocate_pid(&table); - table.register( + let pid3 = allocate_pid(&table); + register( + &table, pid1, "wasmvm", "graceful", @@ -1207,7 +1403,8 @@ fn terminate_all_escalates_from_sigterm_to_sigkill_for_survivors() { create_context(0), graceful.clone(), ); - table.register( + register( + &table, pid2, "wasmvm", "stubborn", @@ -1215,11 +1412,24 @@ fn terminate_all_escalates_from_sigterm_to_sigkill_for_survivors() { create_context(0), stubborn.clone(), ); + register( + &table, + pid3, + "wasmvm", + "stopped", + Vec::new(), + create_context(0), + stopped.clone(), + ); + table + .mark_stopped(pid3, SIGSTOP) + .expect("stop must be recorded"); table.terminate_all(); assert_eq!(graceful.kills(), vec![15]); assert_eq!(stubborn.kills(), vec![15, 9]); + assert_eq!(stopped.kills(), vec![15]); assert_eq!( table .get(pid1) @@ -1234,6 +1444,13 @@ fn terminate_all_escalates_from_sigterm_to_sigkill_for_survivors() { .status, ProcessStatus::Exited ); + assert_eq!( + table + .get(pid3) + .expect("stopped process should remain as zombie") + .status, + ProcessStatus::Exited + ); assert_eq!(table.zombie_timer_count(), 0); } @@ -1243,7 +1460,8 @@ fn list_processes_returns_a_snapshot_of_registered_processes() { let pid1 = allocate_pid(&table); let pid2 = allocate_pid(&table); - table.register( + register( + &table, pid1, "wasmvm", "ls", @@ -1251,7 +1469,8 @@ fn list_processes_returns_a_snapshot_of_registered_processes() { create_context(0), MockDriverProcess::new(), ); - table.register( + register( + &table, pid2, "node", "node", @@ -1283,7 +1502,8 @@ fn waitpid_for_supports_pid_zero_and_negative_process_group_selectors() { let same_group_child_pid = allocate_pid(&table); let other_group_child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1291,7 +1511,8 @@ fn waitpid_for_supports_pid_zero_and_negative_process_group_selectors() { create_context(0), parent, ); - table.register( + register( + &table, same_group_child_pid, "wasmvm", "same-group", @@ -1299,7 +1520,8 @@ fn waitpid_for_supports_pid_zero_and_negative_process_group_selectors() { create_context(parent_pid), same_group_child.clone(), ); - table.register( + register( + &table, other_group_child_pid, "wasmvm", "other-group", @@ -1354,7 +1576,8 @@ fn zombie_reaper_is_cooperatively_driven_for_many_exits() { for index in 0..100 { let process = MockDriverProcess::new(); let pid = allocate_pid(&table); - table.register( + register( + &table, pid, "wasmvm", format!("proc-{index}"), @@ -1389,7 +1612,8 @@ fn zombie_reaper_preserves_child_exit_code_while_parent_is_alive() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1397,7 +1621,8 @@ fn zombie_reaper_preserves_child_exit_code_while_parent_is_alive() { create_context(0), parent, ); - table.register( + register( + &table, child_pid, "wasmvm", "child", @@ -1425,7 +1650,8 @@ fn zombie_reaper_reaps_exited_children_after_their_parent_exits() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register( + &table, parent_pid, "wasmvm", "parent", @@ -1433,7 +1659,8 @@ fn zombie_reaper_reaps_exited_children_after_their_parent_exits() { create_context(0), parent.clone(), ); - table.register( + register( + &table, child_pid, "wasmvm", "child", diff --git a/crates/kernel/tests/pty.rs b/crates/kernel/tests/pty.rs index 7998f38a27..7aa74c92dc 100644 --- a/crates/kernel/tests/pty.rs +++ b/crates/kernel/tests/pty.rs @@ -490,7 +490,7 @@ fn oversized_raw_write_fails_atomically() { vec![b'x'; MAX_PTY_BUFFER_BYTES + 1], ) .expect_err("oversized write should fail"); - assert_eq!(error.code(), "EAGAIN"); + assert_eq!(error.code(), "E2BIG"); manager .write(pty.master.description.id(), vec![b'a'; MAX_CANON.min(8)]) diff --git a/crates/kernel/tests/resource_accounting.rs b/crates/kernel/tests/resource_accounting.rs index b3121e0b74..36c3fb7faa 100644 --- a/crates/kernel/tests/resource_accounting.rs +++ b/crates/kernel/tests/resource_accounting.rs @@ -1,8 +1,10 @@ +use agentos_bridge::queue_tracker::{set_limit_warning_handler, LimitWarning, TrackedLimit}; use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::fd_table::O_RDWR; use agentos_kernel::kernel::{KernelVm, KernelVmConfig, SpawnOptions, SEEK_SET}; use agentos_kernel::mount_table::{MountOptions, MountTable}; use agentos_kernel::permissions::Permissions; +use agentos_kernel::process_table::SIGSTOP; use agentos_kernel::pty::LineDisciplineConfig; use agentos_kernel::resource_accounting::{ ResourceLimits, DEFAULT_BLOCKING_READ_TIMEOUT_MS, DEFAULT_MAX_CONNECTIONS, @@ -17,6 +19,7 @@ use agentos_kernel::root_fs::{ use agentos_kernel::socket_table::{InetSocketAddress, SocketSpec}; use agentos_kernel::vfs::{MemoryFileSystem, VirtualFileSystem}; use std::collections::BTreeMap; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; #[test] @@ -340,6 +343,62 @@ fn resource_limits_reject_extra_processes_pipes_and_ptys() { kernel.wait_and_reap(process.pid()).expect("reap process"); } +#[test] +fn stopped_processes_remain_visible_to_the_process_limit() { + let mut config = KernelVmConfig::new("vm-stopped-process-limit"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_processes: Some(1), + ..ResourceLimits::default() + }; + + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn initial process"); + let runtime_control = process + .attach_runtime_control(Arc::new(|| {})) + .expect("attach runtime control"); + + kernel + .signal_process("shell", process.pid() as i32, SIGSTOP) + .expect("stop process"); + let stop = runtime_control.pending(); + assert_eq!(stop.stopped, Some(true)); + runtime_control + .acknowledge(stop) + .expect("acknowledge stopped runtime state"); + let snapshot = kernel.resource_snapshot(); + assert_eq!(snapshot.running_processes, 0); + assert_eq!(snapshot.stopped_processes, 1); + assert_eq!(snapshot.exited_processes, 0); + + let error = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect_err("stopped process should still consume the process slot"); + assert_eq!(error.code(), "EAGAIN"); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap process"); +} + #[test] fn resource_limits_reject_global_fd_growth_with_enfile() { let mut config = KernelVmConfig::new("vm-open-fd-limit"); @@ -1448,6 +1507,16 @@ fn filesystem_limits_reject_overlay_rename_copy_up_in_nested_root_mount() { #[test] fn blocking_pipe_and_pty_reads_time_out_instead_of_hanging_forever() { + let captured: Arc>> = Arc::new(Mutex::new(Vec::new())); + let sink = Arc::clone(&captured); + set_limit_warning_handler(Box::new(move |warning| { + if warning.name == TrackedLimit::VmBlockingReadMs { + sink.lock() + .expect("blocking-read warning sink") + .push(warning.clone()); + } + })); + let mut config = KernelVmConfig::new("vm-read-timeouts"); config.permissions = Permissions::allow_all(); config.resources = ResourceLimits { @@ -1509,6 +1578,17 @@ fn blocking_pipe_and_pty_reads_time_out_instead_of_hanging_forever() { started.elapsed() ); + let warnings = captured.lock().expect("blocking-read warnings"); + assert_eq!(warnings.len(), 2, "pipe and PTY must each warn once"); + for warning in warnings.iter() { + assert_eq!(warning.capacity, 25); + assert!( + warning.observed >= 20 && warning.observed < 25, + "warning must precede the hard deadline: {warning:?}" + ); + assert!(warning.fill_percent >= 80 && warning.fill_percent < 100); + } + process.finish(0); kernel.wait_and_reap(process.pid()).expect("reap shell"); } @@ -1528,6 +1608,37 @@ fn resource_limits_reject_oversized_spawn_payloads() { .register_driver(CommandDriver::new("shell", ["sh"])) .expect("register shell"); + let exact_argv = kernel + .spawn_process( + "sh", + vec![String::from("123456789")], + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("exact argv byte boundary should be admitted"); + exact_argv.finish(0); + kernel + .wait_and_reap(exact_argv.pid()) + .expect("reap exact argv process"); + + let exact_env = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + env: BTreeMap::from([(String::from("LONG"), String::from("123456789"))]), + ..SpawnOptions::default() + }, + ) + .expect("exact environment byte boundary should be admitted"); + exact_env.finish(0); + kernel + .wait_and_reap(exact_env.pid()) + .expect("reap exact environment process"); + let argv_error = kernel .spawn_process( "sh", @@ -1539,6 +1650,14 @@ fn resource_limits_reject_oversized_spawn_payloads() { ) .expect_err("oversized argv should be rejected"); assert_eq!(argv_error.code(), "EINVAL"); + assert!(argv_error + .to_string() + .contains("limits.resources.maxProcessArgvBytes")); + assert_eq!( + kernel.resource_snapshot().running_processes, + 0, + "rejected argv must not create a process" + ); let env_error = kernel .spawn_process( @@ -1552,6 +1671,14 @@ fn resource_limits_reject_oversized_spawn_payloads() { ) .expect_err("oversized environment should be rejected"); assert_eq!(env_error.code(), "EINVAL"); + assert!(env_error + .to_string() + .contains("limits.resources.maxProcessEnvBytes")); + assert_eq!( + kernel.resource_snapshot().running_processes, + 0, + "rejected environment must not create a process" + ); } #[test] @@ -1583,18 +1710,40 @@ fn resource_limits_reject_oversized_pread_and_write_operations() { ) .expect("spawn shell"); let fd = kernel - .fd_open("shell", process.pid(), "/tmp/data.txt", 0, None) + .fd_open("shell", process.pid(), "/tmp/data.txt", O_RDWR, None) .expect("open file"); + assert_eq!( + kernel + .fd_pread("shell", process.pid(), fd, 4, 0) + .expect("exact pread byte boundary"), + b"hell" + ); + assert_eq!( + kernel + .fd_pwrite("shell", process.pid(), fd, b"xyz", 0) + .expect("exact write byte boundary"), + 3 + ); + kernel + .write_file("/tmp/data.txt", b"hello".to_vec()) + .expect("restore rollback sentinel"); + let pread_error = kernel .fd_pread("shell", process.pid(), fd, 5, 0) .expect_err("oversized pread should be rejected"); assert_eq!(pread_error.code(), "EINVAL"); + assert!(pread_error + .to_string() + .contains("limits.resources.maxPreadBytes")); let write_error = kernel .fd_write("shell", process.pid(), fd, b"four") .expect_err("oversized fd_write should be rejected"); assert_eq!(write_error.code(), "EINVAL"); + assert!(write_error + .to_string() + .contains("limits.resources.maxFdWriteBytes")); let pwrite_error = kernel .fd_pwrite("shell", process.pid(), fd, b"four", 0) @@ -1800,6 +1949,20 @@ fn resource_limits_reject_oversized_readdir_batches() { }; let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel.create_dir("/exact").expect("create exact directory"); + kernel + .write_file("/exact/a.txt", b"a".to_vec()) + .expect("write exact first entry"); + kernel + .write_file("/exact/b.txt", b"b".to_vec()) + .expect("write exact second entry"); + assert_eq!( + kernel + .read_dir("/exact") + .expect("exact readdir entry boundary") + .len(), + 2 + ); kernel.create_dir("/tmp").expect("create tmp"); kernel .write_file("/tmp/a.txt", b"a".to_vec()) @@ -1815,4 +1978,7 @@ fn resource_limits_reject_oversized_readdir_batches() { .read_dir("/tmp") .expect_err("oversized readdir batch should be rejected"); assert_eq!(error.code(), "ENOMEM"); + assert!(error + .to_string() + .contains("limits.resources.maxReaddirEntries")); } diff --git a/crates/kernel/tests/stdio_devices.rs b/crates/kernel/tests/stdio_devices.rs index 18a48f166c..16187ea677 100644 --- a/crates/kernel/tests/stdio_devices.rs +++ b/crates/kernel/tests/stdio_devices.rs @@ -51,3 +51,38 @@ fn default_process_stdout_and_stderr_accept_writes_without_pipe_rewiring() { "ENOENT" ); } + +#[test] +fn terminal_size_distinguishes_valid_non_tty_from_invalid_fd() { + let mut config = KernelVmConfig::new("vm-terminal-size-errno"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + + assert_eq!( + kernel + .pty_window_size("shell", process.pid(), 1) + .expect_err("valid stdout is not a terminal") + .code(), + "ENOTTY" + ); + assert_eq!( + kernel + .pty_window_size("shell", process.pid(), 999) + .expect_err("unknown descriptor remains EBADF") + .code(), + "EBADF" + ); +} diff --git a/crates/kernel/tests/user.rs b/crates/kernel/tests/user.rs index 63c8216254..dc3665fd1d 100644 --- a/crates/kernel/tests/user.rs +++ b/crates/kernel/tests/user.rs @@ -159,9 +159,64 @@ fn getgroups_and_getgrgid_use_kernel_managed_group_state() { user.getgrgid(456), Some(String::from("group456:x:456:deploy")) ); + assert_eq!( + user.getgrnam("group456"), + Some(String::from("group456:x:456:deploy")) + ); assert_eq!( user.getgrgid(789), Some(String::from("group789:x:789:deploy")) ); assert_eq!(user.getgrgid(999), None); + assert!(user + .group_entries() + .contains(&String::from("group789:x:789:deploy"))); +} + +#[test] +fn explicit_groups_win_over_synthesized_account_groups() { + let user = UserManager::from_config(UserConfig { + gid: Some(123), + username: Some(String::from("deploy")), + group_name: Some(String::from("default-name")), + supplementary_gids: vec![456], + groups: vec![agentos_kernel::user::GroupRecord { + gid: 123, + name: String::from("explicit"), + members: vec![String::from("configured-member")], + }], + ..UserConfig::default() + }); + + assert_eq!( + user.getgrgid(123), + Some(String::from("explicit:x:123:configured-member")) + ); + assert_eq!(user.getgrnam("default-name"), None); + assert_eq!( + user.getgrnam("group456"), + Some(String::from("group456:x:456:deploy")) + ); +} + +#[test] +fn supplementary_credentials_do_not_rewrite_explicit_group_membership() { + let user = UserManager::from_config(UserConfig { + gid: Some(123), + username: Some(String::from("deploy")), + supplementary_gids: vec![456], + groups: vec![agentos_kernel::user::GroupRecord { + gid: 456, + name: String::from("audited"), + members: vec![String::from("configured-member")], + }], + ..UserConfig::default() + }); + + assert_eq!(user.getgroups(), vec![123, 456]); + assert_eq!( + user.getgrgid(456), + Some(String::from("audited:x:456:configured-member")) + ); + assert_eq!(user.getgrnam("group456"), None); } diff --git a/crates/native-sidecar-core/src/bridge_bytes.rs b/crates/native-sidecar-core/src/bridge_bytes.rs index dd9a6a6ccf..16643bea01 100644 --- a/crates/native-sidecar-core/src/bridge_bytes.rs +++ b/crates/native-sidecar-core/src/bridge_bytes.rs @@ -9,15 +9,18 @@ pub fn encoded_bytes_value(bytes: &[u8]) -> Value { } pub fn decode_encoded_bytes_value(value: &Value) -> Result, String> { - let Some(base64_value) = value - .get("__agentOSType") - .and_then(Value::as_str) - .filter(|kind| *kind == "bytes") - .and_then(|_| value.get("base64")) - .and_then(Value::as_str) - else { - return Err(String::from("must be a string or encoded bytes payload")); - }; + let base64_value = if value.get("__agentOSType").and_then(Value::as_str) == Some("bytes") { + value.get("base64").and_then(Value::as_str) + } else if value.get("__type").and_then(Value::as_str) == Some("Buffer") { + // The V8 bridge serializes Uint8Array/Buffer arguments as a CBOR byte + // string. Its JSON compatibility projection is this exact tagged + // shape; accepting it here keeps every syscall adapter on one strict + // byte decoder without admitting arbitrary numeric arrays or objects. + value.get("data").and_then(Value::as_str) + } else { + None + } + .ok_or_else(|| String::from("must be a string or encoded bytes payload"))?; decode_base64(base64_value) } @@ -57,6 +60,13 @@ mod tests { assert_eq!(decode_encoded_bytes_value(&value), Ok(b"hello".to_vec())); } + #[test] + fn decodes_the_canonical_v8_cbor_byte_projection() { + let value = json!({ "__type": "Buffer", "data": "aGVsbG8=" }); + + assert_eq!(decode_encoded_bytes_value(&value), Ok(b"hello".to_vec())); + } + #[test] fn round_trips_bridge_buffer_payload() { let value = bridge_buffer_value(b"secret"); @@ -70,6 +80,14 @@ mod tests { decode_encoded_bytes_value(&json!({ "__agentOSType": "text", "base64": "aGk=" })), Err(String::from("must be a string or encoded bytes payload")) ); + assert_eq!( + decode_encoded_bytes_value(&json!({ "__type": "Buffer", "value": "aGk=" })), + Err(String::from("must be a string or encoded bytes payload")) + ); + assert_eq!( + decode_encoded_bytes_value(&json!([104, 105])), + Err(String::from("must be a string or encoded bytes payload")) + ); assert_eq!( decode_bridge_buffer_value(&json!({ "__type": "bytes", "value": "aGk=" })), Err(String::from("must be a serialized bridge buffer")) diff --git a/crates/native-sidecar-core/src/diagnostics.rs b/crates/native-sidecar-core/src/diagnostics.rs index 15ccc3c460..a9822e17eb 100644 --- a/crates/native-sidecar-core/src/diagnostics.rs +++ b/crates/native-sidecar-core/src/diagnostics.rs @@ -91,6 +91,9 @@ mod tests { command: "node".to_owned(), status, exit_code, + pending_termination: None, + termination: None, + runtime_fault: None, identity: ProcessIdentity::default(), } } diff --git a/crates/native-sidecar-core/src/guest_fs.rs b/crates/native-sidecar-core/src/guest_fs.rs index 2a21e9439d..5348f0a2b6 100644 --- a/crates/native-sidecar-core/src/guest_fs.rs +++ b/crates/native-sidecar-core/src/guest_fs.rs @@ -383,7 +383,7 @@ pub fn guest_filesystem_stat(stat: VirtualStat) -> GuestFilesystemStat { } fn kernel_error(error: agentos_kernel::kernel::KernelError) -> SidecarCoreError { - SidecarCoreError::new(error.to_string()) + SidecarCoreError::typed(error.code(), error.message()) } #[cfg(test)] @@ -467,6 +467,19 @@ mod tests { assert_eq!(error.to_string(), "guest filesystem pwrite requires offset"); } + #[test] + fn preserves_kernel_errno_separately_from_the_diagnostic() { + let mut kernel = test_kernel(); + let error = handle_guest_filesystem_call( + &mut kernel, + request(GuestFilesystemOperation::ReadFile, "/missing"), + ) + .unwrap_err(); + assert_eq!(error.code(), Some("ENOENT")); + assert!(!error.message().starts_with("ENOENT:")); + assert!(error.to_string().starts_with("ENOENT:")); + } + #[test] fn pwrite_overwrites_in_place_without_truncating_the_file() { let mut kernel = test_kernel(); diff --git a/crates/native-sidecar-core/src/guest_net.rs b/crates/native-sidecar-core/src/guest_net.rs index 946333e1e9..f7d92e6d77 100644 --- a/crates/native-sidecar-core/src/guest_net.rs +++ b/crates/native-sidecar-core/src/guest_net.rs @@ -107,7 +107,11 @@ where socket_id, InetSocketAddress::new(host, port), ) { - let _ = kernel.socket_close(driver, pid, socket_id); + if let Err(cleanup_error) = kernel.socket_close(driver, pid, socket_id) { + eprintln!( + "ERR_AGENTOS_SOCKET_ROLLBACK: failed to close socket {socket_id} after connect failure: {cleanup_error}" + ); + } return Err(kernel_error(error)); } let record = kernel.socket_get(socket_id); @@ -142,7 +146,11 @@ where .socket_bind_inet(driver, pid, socket_id, InetSocketAddress::new(host, port)) .and_then(|()| kernel.socket_listen(driver, pid, socket_id, backlog)); if let Err(error) = result { - let _ = kernel.socket_close(driver, pid, socket_id); + if let Err(cleanup_error) = kernel.socket_close(driver, pid, socket_id) { + eprintln!( + "ERR_AGENTOS_SOCKET_ROLLBACK: failed to close socket {socket_id} after listen setup failure: {cleanup_error}" + ); + } return Err(kernel_error(error)); } let record = kernel.socket_get(socket_id); @@ -273,7 +281,11 @@ where if let Err(error) = kernel.socket_bind_inet(driver, pid, socket_id, InetSocketAddress::new(host, port)) { - let _ = kernel.socket_close(driver, pid, socket_id); + if let Err(cleanup_error) = kernel.socket_close(driver, pid, socket_id) { + eprintln!( + "ERR_AGENTOS_SOCKET_ROLLBACK: failed to close socket {socket_id} after UDP bind failure: {cleanup_error}" + ); + } return Err(kernel_error(error)); } let record = kernel.socket_get(socket_id); @@ -364,9 +376,12 @@ where { let socket_id = require_socket_id(request)?; let host = optional_str(request, "host").unwrap_or(DEFAULT_LOOPBACK_HOST); - let port = optional_u64(request, "port") - .map(|value| u16::try_from(value).unwrap_or(0)) - .unwrap_or(0); + let port = match optional_u64(request, "port") { + Some(value) => u16::try_from(value).map_err(|_| { + SidecarCoreError::new("guest kernel call field `port` must be a valid port") + })?, + None => 0, + }; kernel .socket_bind_inet(driver, pid, socket_id, InetSocketAddress::new(host, port)) .map_err(kernel_error)?; @@ -582,7 +597,7 @@ fn would_block(error: &KernelError) -> bool { } fn kernel_error(error: KernelError) -> SidecarCoreError { - SidecarCoreError::new(error.to_string()) + SidecarCoreError::typed(error.code(), error.message()) } #[cfg(test)] @@ -623,6 +638,62 @@ mod tests { serde_json::from_slice(&bytes).expect("decode response") } + fn call_error( + kernel: &mut KernelVm, + pid: u32, + operation: &str, + request: Value, + ) -> SidecarCoreError { + let payload = serde_json::to_vec(&request).expect("encode request"); + handle_guest_kernel_call(kernel, pid, "shell", operation, &payload) + .expect_err("guest networking call must fail") + } + + #[test] + fn failed_socket_setup_rolls_back_allocated_kernel_sockets() { + let mut kernel = test_kernel(); + let pid = guest_pid(&mut kernel); + let baseline = kernel.resource_snapshot().sockets; + + let _connect_error = call_error( + &mut kernel, + pid, + "net.connect", + json!({ "host": "127.0.0.1", "port": 44991 }), + ); + assert_eq!(kernel.resource_snapshot().sockets, baseline); + + call( + &mut kernel, + pid, + "net.listen", + json!({ "host": "127.0.0.1", "port": 44992 }), + ); + let after_listener = kernel.resource_snapshot().sockets; + let _listen_error = call_error( + &mut kernel, + pid, + "net.listen", + json!({ "host": "127.0.0.1", "port": 44992 }), + ); + assert_eq!(kernel.resource_snapshot().sockets, after_listener); + + call( + &mut kernel, + pid, + "net.udp_bind", + json!({ "host": "127.0.0.1", "port": 44993 }), + ); + let after_udp = kernel.resource_snapshot().sockets; + let _udp_error = call_error( + &mut kernel, + pid, + "net.udp_bind", + json!({ "host": "127.0.0.1", "port": 44993 }), + ); + assert_eq!(kernel.resource_snapshot().sockets, after_udp); + } + #[test] fn loopback_tcp_round_trip_through_kernel() { let mut kernel = test_kernel(); diff --git a/crates/native-sidecar-core/src/guest_pty.rs b/crates/native-sidecar-core/src/guest_pty.rs index 7fdf20243c..600093472e 100644 --- a/crates/native-sidecar-core/src/guest_pty.rs +++ b/crates/native-sidecar-core/src/guest_pty.rs @@ -290,7 +290,7 @@ fn would_block(error: &KernelError) -> bool { } fn kernel_error(error: KernelError) -> SidecarCoreError { - SidecarCoreError::new(error.to_string()) + SidecarCoreError::typed(error.code(), error.message()) } #[cfg(test)] diff --git a/crates/native-sidecar-core/src/identity.rs b/crates/native-sidecar-core/src/identity.rs index 11f2509b16..880ac1387f 100644 --- a/crates/native-sidecar-core/src/identity.rs +++ b/crates/native-sidecar-core/src/identity.rs @@ -1,4 +1,5 @@ use agentos_kernel::resource_accounting::ResourceLimits; +use agentos_kernel::system::SystemIdentity; use agentos_kernel::user::UserManager; use crate::{virtual_os_cpu_count, virtual_os_freemem_bytes, virtual_os_totalmem_bytes}; @@ -30,6 +31,22 @@ pub fn shared_guest_runtime_identity( resource_limits: &ResourceLimits, virtual_pid: Option, virtual_ppid: Option, +) -> SharedGuestRuntimeIdentity { + shared_guest_runtime_identity_with_system( + user, + resource_limits, + &SystemIdentity::default(), + virtual_pid, + virtual_ppid, + ) +} + +pub fn shared_guest_runtime_identity_with_system( + user: &UserManager, + resource_limits: &ResourceLimits, + system: &SystemIdentity, + virtual_pid: Option, + virtual_ppid: Option, ) -> SharedGuestRuntimeIdentity { SharedGuestRuntimeIdentity { virtual_pid, @@ -42,14 +59,14 @@ pub fn shared_guest_runtime_identity( os_totalmem: virtual_os_totalmem_bytes(resource_limits), os_freemem: virtual_os_freemem_bytes(resource_limits), os_homedir: user.homedir.clone(), - os_hostname: String::from("secure-exec"), + os_hostname: system.hostname.clone(), os_shell: user.shell.clone(), os_user: user.username.clone(), os_tmpdir: String::from("/tmp"), - os_type: String::from("Linux"), - os_release: String::from("6.8.0-secure-exec"), - os_version: String::from("#1 SMP PREEMPT_DYNAMIC secure-exec"), - os_machine: String::from("x86_64"), + os_type: system.os_type.clone(), + os_release: system.os_release.clone(), + os_version: system.os_version.clone(), + os_machine: system.machine.clone(), } } @@ -95,4 +112,27 @@ mod tests { assert_eq!(identity.os_version, "#1 SMP PREEMPT_DYNAMIC secure-exec"); assert_eq!(identity.os_machine, "x86_64"); } + + #[test] + fn injected_guest_identity_reuses_kernel_system_identity() { + let user = UserManager::default(); + let limits = ResourceLimits::default(); + let system = SystemIdentity { + hostname: String::from("vm-host"), + os_type: String::from("TestOS"), + os_release: String::from("1.2.3"), + os_version: String::from("build-7"), + machine: String::from("vm64"), + domain_name: String::from("vm-domain"), + }; + + let identity = + shared_guest_runtime_identity_with_system(&user, &limits, &system, None, None); + + assert_eq!(identity.os_hostname, "vm-host"); + assert_eq!(identity.os_type, "TestOS"); + assert_eq!(identity.os_release, "1.2.3"); + assert_eq!(identity.os_version, "build-7"); + assert_eq!(identity.os_machine, "vm64"); + } } diff --git a/crates/native-sidecar-core/src/lib.rs b/crates/native-sidecar-core/src/lib.rs index 6c581349e9..b9c7c5b780 100644 --- a/crates/native-sidecar-core/src/lib.rs +++ b/crates/native-sidecar-core/src/lib.rs @@ -58,7 +58,10 @@ pub use guest_fs::{ targeted_guest_filesystem_response, }; pub use guest_net::handle_guest_kernel_call; -pub use identity::{shared_guest_runtime_identity, SharedGuestRuntimeIdentity}; +pub use identity::{ + shared_guest_runtime_identity, shared_guest_runtime_identity_with_system, + SharedGuestRuntimeIdentity, +}; pub use layers::{VmLayerStore, MAX_VM_LAYERS}; pub use limits::{ validate_vm_limits, virtual_os_cpu_count, virtual_os_freemem_bytes, virtual_os_totalmem_bytes, diff --git a/crates/native-sidecar-core/src/limits.rs b/crates/native-sidecar-core/src/limits.rs index 6ac5a65f67..e1d2703485 100644 --- a/crates/native-sidecar-core/src/limits.rs +++ b/crates/native-sidecar-core/src/limits.rs @@ -70,12 +70,14 @@ pub const DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024; pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024; pub const DEFAULT_WASM_PREWARM_TIMEOUT_MS: u64 = 30_000; pub const DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB: u32 = 2048; -pub const DEFAULT_WASM_RUNNER_CPU_TIME_LIMIT_MS: u32 = 30_000; +pub const DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS: u32 = 30_000; pub const DEFAULT_PROCESS_PENDING_STDIN_BYTES: usize = 64 * 1024 * 1024; pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS: usize = 4096; pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES: usize = 1024 * 1024; pub const DEFAULT_PROCESS_PENDING_EVENT_COUNT: usize = 10_000; pub const DEFAULT_PROCESS_PENDING_EVENT_BYTES: usize = 64 * 1024 * 1024; +pub const DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_COUNT: usize = 64; +pub const DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_BYTES: usize = 64 * 1024 * 1024; pub const DEFAULT_REACTOR_MAX_CAPABILITIES: usize = 4096; pub const DEFAULT_REACTOR_MAX_READY_HANDLES: usize = 4096; @@ -343,8 +345,14 @@ pub struct WasmLimits { pub prewarm_timeout_ms: u64, /// V8 heap cap for the trusted JS runner isolate that hosts WASI/WASM. pub runner_heap_limit_mb: u32, - /// Active-CPU cap for the trusted JS runner isolate that hosts WASI/WASM. - pub runner_cpu_time_limit_ms: u32, + /// Active CPU-time cap for standalone WASM execution. The V8 compatibility + /// backend applies this to its runner isolate's CPU watchdog. + pub active_cpu_time_limit_ms: u32, + /// Optional elapsed wall-clock backstop for standalone WASM execution. + pub wall_clock_limit_ms: Option, + /// Optional deterministic instruction budget. The V8 compatibility backend + /// rejects explicit values because V8 cannot meter deterministic fuel. + pub deterministic_fuel: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -359,6 +367,11 @@ pub struct ProcessLimits { pub pending_event_count: usize, /// Maximum aggregate payload bytes retained at each process-event stage. pub pending_event_bytes: usize, + /// Maximum concurrent synchronous child-process calls retained by one VM. + pub max_pending_child_sync_count: usize, + /// Maximum aggregate input and output capacity retained for synchronous + /// child-process calls by one VM. + pub max_pending_child_sync_bytes: usize, } impl Default for HttpLimits { @@ -498,7 +511,9 @@ impl Default for WasmLimits { sync_read_limit_bytes: DEFAULT_WASM_SYNC_READ_LIMIT_BYTES, prewarm_timeout_ms: DEFAULT_WASM_PREWARM_TIMEOUT_MS, runner_heap_limit_mb: DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB, - runner_cpu_time_limit_ms: DEFAULT_WASM_RUNNER_CPU_TIME_LIMIT_MS, + active_cpu_time_limit_ms: DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS, + wall_clock_limit_ms: None, + deterministic_fuel: None, } } } @@ -511,6 +526,8 @@ impl Default for ProcessLimits { pending_stdin_bytes: DEFAULT_PROCESS_PENDING_STDIN_BYTES, pending_event_count: DEFAULT_PROCESS_PENDING_EVENT_COUNT, pending_event_bytes: DEFAULT_PROCESS_PENDING_EVENT_BYTES, + max_pending_child_sync_count: DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_COUNT, + max_pending_child_sync_bytes: DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_BYTES, } } } @@ -796,10 +813,12 @@ pub fn vm_limits_from_config( limits.wasm.runner_heap_limit_mb = u32::try_from(value) .map_err(|_| integer_too_large("limits.wasm.runnerHeapLimitMb", value))?; } - if let Some(value) = wasm.runner_cpu_time_limit_ms { - limits.wasm.runner_cpu_time_limit_ms = u32::try_from(value) - .map_err(|_| integer_too_large("limits.wasm.runnerCpuTimeLimitMs", value))?; + if let Some(value) = wasm.active_cpu_time_limit_ms { + limits.wasm.active_cpu_time_limit_ms = u32::try_from(value) + .map_err(|_| integer_too_large("limits.wasm.activeCpuTimeLimitMs", value))?; } + limits.wasm.wall_clock_limit_ms = wasm.wall_clock_limit_ms; + limits.wasm.deterministic_fuel = wasm.deterministic_fuel; } if let Some(process) = config.process.as_ref() { set_usize( @@ -827,6 +846,16 @@ pub fn vm_limits_from_config( process.pending_event_bytes, "limits.process.pendingEventBytes", )?; + set_usize( + &mut limits.process.max_pending_child_sync_count, + process.max_pending_child_sync_count, + "limits.process.maxPendingChildSyncCount", + )?; + set_usize( + &mut limits.process.max_pending_child_sync_bytes, + process.max_pending_child_sync_bytes, + "limits.process.maxPendingChildSyncBytes", + )?; } validate_vm_limits(&limits, sidecar_max_frame_bytes)?; @@ -1127,7 +1156,6 @@ fn apply_resource_limits_config( config.max_recursive_fs_entries, "limits.resources.maxRecursiveFsEntries", )?; - set_optional_u64(&mut limits.max_wasm_fuel, config.max_wasm_fuel); set_optional_u64( &mut limits.max_wasm_memory_bytes, config.max_wasm_memory_bytes, @@ -1538,7 +1566,7 @@ pub fn validate_vm_limits( ))); } - let nonzero_usize: [(&str, usize); 36] = [ + let nonzero_usize: [(&str, usize); 38] = [ ( "limits.bindings.max_registered_collections", limits.bindings.max_registered_collections, @@ -1674,6 +1702,14 @@ pub fn validate_vm_limits( "limits.process.pending_event_bytes", limits.process.pending_event_bytes, ), + ( + "limits.process.max_pending_child_sync_count", + limits.process.max_pending_child_sync_count, + ), + ( + "limits.process.max_pending_child_sync_bytes", + limits.process.max_pending_child_sync_bytes, + ), ]; for (key, value) in nonzero_usize { if value == 0 { @@ -1738,7 +1774,7 @@ mod tests { use super::*; use agentos_vm_config::{ AcpLimitsConfig, Http2LimitsConfig, ProcessLimitsConfig, ReactorLimitsConfig, - TlsLimitsConfig, UdpLimitsConfig, + TlsLimitsConfig, UdpLimitsConfig, WasmLimitsConfig, }; const FRAME_CAP: usize = 16 * 1024 * 1024; @@ -1780,6 +1816,12 @@ mod tests { limits.http2.max_pending_events, DEFAULT_HTTP2_MAX_PENDING_EVENTS ); + assert_eq!( + limits.wasm.active_cpu_time_limit_ms, + DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS + ); + assert_eq!(limits.wasm.wall_clock_limit_ms, None); + assert_eq!(limits.wasm.deterministic_fuel, None); } #[test] @@ -1793,11 +1835,21 @@ mod tests { defaults.process.max_spawn_file_action_bytes, DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES ); + assert_eq!( + defaults.process.max_pending_child_sync_count, + DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_COUNT + ); + assert_eq!( + defaults.process.max_pending_child_sync_bytes, + DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_BYTES + ); let config = VmLimitsConfig { process: Some(ProcessLimitsConfig { max_spawn_file_actions: Some(7), max_spawn_file_action_bytes: Some(321), + max_pending_child_sync_count: Some(3), + max_pending_child_sync_bytes: Some(12_345), ..ProcessLimitsConfig::default() }), ..VmLimitsConfig::default() @@ -1805,6 +1857,8 @@ mod tests { let overridden = vm_limits_from_config(Some(&config), 64 * 1024 * 1024).expect("overrides"); assert_eq!(overridden.process.max_spawn_file_actions, 7); assert_eq!(overridden.process.max_spawn_file_action_bytes, 321); + assert_eq!(overridden.process.max_pending_child_sync_count, 3); + assert_eq!(overridden.process.max_pending_child_sync_bytes, 12_345); for (max_actions, max_bytes, field) in [ (Some(0), Some(321), "limits.process.max_spawn_file_actions"), @@ -1826,6 +1880,58 @@ mod tests { .expect_err("zero process spawn limit must be rejected"); assert!(error.to_string().contains(field), "{error}"); } + + for (count, bytes, field) in [ + ( + Some(0), + Some(1024), + "limits.process.max_pending_child_sync_count", + ), + ( + Some(1), + Some(0), + "limits.process.max_pending_child_sync_bytes", + ), + ] { + let config = VmLimitsConfig { + process: Some(ProcessLimitsConfig { + max_pending_child_sync_count: count, + max_pending_child_sync_bytes: bytes, + ..ProcessLimitsConfig::default() + }), + ..VmLimitsConfig::default() + }; + let error = vm_limits_from_config(Some(&config), 64 * 1024 * 1024) + .expect_err("zero pending child-sync limit must be rejected"); + assert!(error.to_string().contains(field), "{error}"); + } + } + + #[test] + fn process_queue_limits_reject_zero_before_runtime_construction() { + let cases: [(&str, fn(&mut ProcessLimitsConfig)); 3] = [ + ("limits.process.pending_stdin_bytes", |process| { + process.pending_stdin_bytes = Some(0) + }), + ("limits.process.pending_event_count", |process| { + process.pending_event_count = Some(0) + }), + ("limits.process.pending_event_bytes", |process| { + process.pending_event_bytes = Some(0) + }), + ]; + + for (field, set_zero) in cases { + let mut process = ProcessLimitsConfig::default(); + set_zero(&mut process); + let config = VmLimitsConfig { + process: Some(process), + ..VmLimitsConfig::default() + }; + let error = vm_limits_from_config(Some(&config), FRAME_CAP) + .expect_err("zero process queue limit must be rejected during VM admission"); + assert!(error.to_string().contains(field), "{error}"); + } } #[test] @@ -1879,6 +1985,12 @@ mod tests { max_permission_outcomes_per_vm: Some(45), ..AcpLimitsConfig::default() }), + wasm: Some(WasmLimitsConfig { + active_cpu_time_limit_ms: Some(45_000), + wall_clock_limit_ms: Some(90_000), + deterministic_fuel: Some(1_000_000), + ..WasmLimitsConfig::default() + }), ..VmLimitsConfig::default() }; @@ -1900,6 +2012,9 @@ mod tests { assert_eq!(limits.acp.max_pending_permissions_per_vm, 23); assert_eq!(limits.acp.max_permission_outcomes_per_session, 34); assert_eq!(limits.acp.max_permission_outcomes_per_vm, 45); + assert_eq!(limits.wasm.active_cpu_time_limit_ms, 45_000); + assert_eq!(limits.wasm.wall_clock_limit_ms, Some(90_000)); + assert_eq!(limits.wasm.deterministic_fuel, Some(1_000_000)); } #[test] diff --git a/crates/native-sidecar-core/src/root_fs.rs b/crates/native-sidecar-core/src/root_fs.rs index 03763d115d..96b6b676e8 100644 --- a/crates/native-sidecar-core/src/root_fs.rs +++ b/crates/native-sidecar-core/src/root_fs.rs @@ -26,20 +26,44 @@ use vfs::posix::usage::RootFilesystemResourceLimits; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SidecarCoreError { + code: Option, message: String, } impl SidecarCoreError { pub fn new(message: impl Into) -> Self { Self { + code: None, message: message.into(), } } + + /// Construct a stable typed error for an executor-facing operation. + /// + /// The code is transported independently from the diagnostic message so + /// adapters never need to recover errno by parsing `Display` output. + pub fn typed(code: impl Into, message: impl Into) -> Self { + Self { + code: Some(code.into()), + message: message.into(), + } + } + + pub fn code(&self) -> Option<&str> { + self.code.as_deref() + } + + pub fn message(&self) -> &str { + &self.message + } } impl fmt::Display for SidecarCoreError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.message) + match &self.code { + Some(code) => write!(f, "{code}: {}", self.message), + None => f.write_str(&self.message), + } } } diff --git a/crates/native-sidecar-core/src/signals.rs b/crates/native-sidecar-core/src/signals.rs index 3092ae12c3..0838995391 100644 --- a/crates/native-sidecar-core/src/signals.rs +++ b/crates/native-sidecar-core/src/signals.rs @@ -25,7 +25,7 @@ pub fn default_signal_exit_code(signal: i32) -> Option { } pub fn is_valid_posix_signal_number(signal: u32) -> bool { - signal <= 31 + signal <= 64 } pub fn parse_posix_signal(signal: &str) -> Option { @@ -35,7 +35,7 @@ pub fn parse_posix_signal(signal: &str) -> Option { } if let Ok(value) = trimmed.parse::() { - return (0..=31).contains(&value).then_some(value); + return (0..=64).contains(&value).then_some(value); } let upper = trimmed.to_ascii_uppercase(); @@ -127,9 +127,10 @@ pub fn parse_process_signal_state_request( let mask_json = signal_state_str_arg(args, 2, "process.signal_state mask")?; let flags = signal_state_u32_arg(args, 3, "process.signal_state flags")?; let mask: Vec = serde_json::from_str(mask_json).map_err(|error| { - crate::SidecarCoreError::new(format!( - "process.signal_state mask must be valid JSON: {error}" - )) + crate::SidecarCoreError::typed( + "EINVAL", + format!("process.signal_state mask must be valid JSON: {error}"), + ) })?; for signal in &mask { validate_process_signal_number(*signal, "process.signal_state mask entries")?; @@ -139,9 +140,10 @@ pub fn parse_process_signal_state_request( "ignore" => SignalDispositionAction::Ignore, "user" => SignalDispositionAction::User, other => { - return Err(crate::SidecarCoreError::new(format!( - "unsupported process.signal_state action {other}" - ))); + return Err(crate::SidecarCoreError::typed( + "EINVAL", + format!("unsupported process.signal_state action {other}"), + )); } }; @@ -188,9 +190,10 @@ fn validate_process_signal_number(signal: u32, label: &str) -> Result<(), crate: if is_valid_posix_signal_number(signal) { Ok(()) } else { - Err(crate::SidecarCoreError::new(format!( - "{label} must be a valid POSIX signal" - ))) + Err(crate::SidecarCoreError::typed( + "EINVAL", + format!("{label} must be a valid POSIX signal"), + )) } } @@ -201,23 +204,36 @@ fn signal_state_u32_arg( ) -> Result { let value = args .get(index) - .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} missing")))?; + .ok_or_else(|| crate::SidecarCoreError::typed("EINVAL", format!("{label} missing")))?; if let Some(value) = value.as_u64() { - return u32::try_from(value) - .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32"))); + return u32::try_from(value).map_err(|_| { + crate::SidecarCoreError::typed("EINVAL", format!("{label} must fit in u32")) + }); } if let Some(value) = value.as_i64() { - return u32::try_from(value) - .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32"))); + return u32::try_from(value).map_err(|_| { + crate::SidecarCoreError::typed("EINVAL", format!("{label} must fit in u32")) + }); + } + if let Some(value) = value.as_f64() { + if value.is_finite() && value.fract() == 0.0 && value >= 0.0 && value <= f64::from(u32::MAX) + { + return Ok(value as u32); + } + return Err(crate::SidecarCoreError::typed( + "EINVAL", + format!("{label} must fit in u32"), + )); } if let Some(value) = value.as_str() { - return value - .parse::() - .map_err(|error| crate::SidecarCoreError::new(format!("{label}: {error}"))); + return value.parse::().map_err(|error| { + crate::SidecarCoreError::typed("EINVAL", format!("{label}: {error}")) + }); } - Err(crate::SidecarCoreError::new(format!( - "{label} must be a u32" - ))) + Err(crate::SidecarCoreError::typed( + "EINVAL", + format!("{label} must be a u32"), + )) } fn signal_state_str_arg<'a>( @@ -225,9 +241,9 @@ fn signal_state_str_arg<'a>( index: usize, label: &str, ) -> Result<&'a str, crate::SidecarCoreError> { - args.get(index) - .and_then(Value::as_str) - .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} must be a string"))) + args.get(index).and_then(Value::as_str).ok_or_else(|| { + crate::SidecarCoreError::typed("EINVAL", format!("{label} must be a string")) + }) } #[cfg(test)] @@ -262,8 +278,8 @@ mod tests { #[test] fn validates_posix_signal_number_range() { assert!(is_valid_posix_signal_number(0)); - assert!(is_valid_posix_signal_number(31)); - assert!(!is_valid_posix_signal_number(32)); + assert!(is_valid_posix_signal_number(64)); + assert!(!is_valid_posix_signal_number(65)); } #[test] @@ -273,8 +289,9 @@ mod tests { assert_eq!(canonical_signal_name(16), Some("SIGSTKFLT")); assert_eq!(parse_posix_signal("9"), Some(9)); assert_eq!(parse_posix_signal("0"), Some(0)); + assert_eq!(parse_posix_signal("64"), Some(64)); assert_eq!(parse_posix_signal("SIGBOGUS"), None); - assert_eq!(parse_posix_signal("32"), None); + assert_eq!(parse_posix_signal("65"), None); } #[test] @@ -313,27 +330,57 @@ mod tests { #[test] fn rejects_unknown_process_signal_state_values() { let invalid_signal = parse_process_signal_state_request(&[ - Value::from(32), + Value::from(65), Value::from("user"), Value::from("[]"), Value::from(0), ]) .expect_err("unknown signal must fail"); + assert_eq!(invalid_signal.code(), Some("EINVAL")); assert_eq!( - invalid_signal.to_string(), + invalid_signal.message(), "process.signal_state signal must be a valid POSIX signal" ); let invalid_mask = parse_process_signal_state_request(&[ Value::from(15), Value::from("user"), - Value::from("[32]"), + Value::from("[65]"), Value::from(0), ]) .expect_err("unknown mask signal must fail"); + assert_eq!(invalid_mask.code(), Some("EINVAL")); assert_eq!( - invalid_mask.to_string(), + invalid_mask.message(), "process.signal_state mask entries must be a valid POSIX signal" ); } + + #[test] + fn accepts_only_bounded_integral_float_signal_state_flags() { + let parse_flags = |flags: f64| { + parse_process_signal_state_request(&[ + Value::from(15), + Value::from("user"), + Value::from("[]"), + Value::Number(serde_json::Number::from_f64(flags).expect("finite test value")), + ]) + }; + + assert_eq!( + parse_flags(f64::from(u32::MAX)) + .expect("u32 maximum") + .1 + .flags, + u32::MAX + ); + for invalid in [-1.0, 1.5, f64::from(u32::MAX) + 1.0] { + let error = parse_flags(invalid).expect_err("invalid float flags must fail"); + assert_eq!(error.code(), Some("EINVAL")); + assert_eq!( + error.message(), + "process.signal_state flags must fit in u32" + ); + } + } } diff --git a/crates/native-sidecar/Cargo.toml b/crates/native-sidecar/Cargo.toml index 9ac6aa003e..e6a7d568f1 100644 --- a/crates/native-sidecar/Cargo.toml +++ b/crates/native-sidecar/Cargo.toml @@ -33,6 +33,7 @@ base64 = "0.22" bytes = "1" ctr = "0.9" filetime = "0.2" +getrandom = "0.2" h2 = "0.4" http = "1" hmac = "0.12" @@ -74,6 +75,7 @@ uuid = { version = "1", features = ["v4"] } vfs = { workspace = true } [dev-dependencies] +agentos-wasm-abi-generator = { path = "../wasm-abi-generator" } command-fds = "0.3" tar = "0.4" tempfile = "3" diff --git a/crates/native-sidecar/assets/base-filesystem.json b/crates/native-sidecar/assets/base-filesystem.json index e641efddd3..3404d53032 100644 --- a/crates/native-sidecar/assets/base-filesystem.json +++ b/crates/native-sidecar/assets/base-filesystem.json @@ -6,6 +6,7 @@ "builtAt": "2026-06-23T03:47:12.644Z", "transforms": [ "Normalize HOSTNAME to secure-exec", + "Allow traversal into the AgentOS /root/node_modules compatibility projection", "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", "Restore Alpine 3.22's /etc/services database and add the VM's Docker-style /etc/hosts entries for libc lookup parity" @@ -336,7 +337,7 @@ { "path": "/root", "type": "directory", - "mode": "700", + "mode": "711", "uid": 0, "gid": 0 }, diff --git a/crates/native-sidecar/src/bindings.rs b/crates/native-sidecar/src/bindings.rs index 19ade95e46..e1b6089512 100644 --- a/crates/native-sidecar/src/bindings.rs +++ b/crates/native-sidecar/src/bindings.rs @@ -7,7 +7,6 @@ use crate::state::{BridgeError, VmState, BINDING_DRIVER_NAME}; use crate::{NativeSidecar, NativeSidecarBridge, SidecarError}; use agentos_kernel::command_registry::CommandDriver; use agentos_native_sidecar_core::bindings::{ - ensure_binding_registry_capacity as core_ensure_binding_registry_capacity, ensure_collection_name_available as core_ensure_collection_name_available, ensure_command_aliases_available as core_ensure_command_aliases_available, registered_binding_command_names, @@ -63,13 +62,9 @@ where validate_bindings_registration(&payload)?; let registered_name = payload.name.clone(); - let (original_permissions, original_bindings, original_command_guest_paths) = { + let (original_permissions, original_bindings) = { let vm = sidecar.vms.get(&vm_id).expect("owned VM should exist"); - ( - vm.configuration.permissions.clone(), - vm.bindings.clone(), - vm.command_guest_paths.clone(), - ) + (vm.configuration.permissions.clone(), vm.bindings.clone()) }; sidecar .bridge @@ -78,7 +73,7 @@ where let vm = sidecar.vms.get_mut(&vm_id).expect("owned VM should exist"); ensure_collection_name_available(&vm.bindings, ®istered_name)?; ensure_command_aliases_available(&vm.bindings, &payload)?; - ensure_binding_registry_capacity(&vm.bindings, &payload)?; + ensure_binding_registry_capacity(vm, &payload)?; vm.bindings.insert(registered_name.clone(), payload); refresh_binding_registry(vm)?; Ok::<_, SidecarError>(binding_command_names(vm).len() as u32) @@ -91,21 +86,43 @@ where result } Err(error) => { - let vm = sidecar.vms.get_mut(&vm_id).expect("owned VM should exist"); - vm.bindings = original_bindings; - vm.command_guest_paths = original_command_guest_paths; + let registry_rollback = { + let vm = sidecar.vms.get_mut(&vm_id).expect("owned VM should exist"); + vm.bindings = original_bindings; + refresh_binding_registry(vm) + }; match sidecar.bridge.restore_vm_permissions_fail_closed( &vm_id, &original_permissions, "binding collection registration rollback", &error, ) { - Ok(()) => return Err(error), + Ok(()) => {} Err(rollback_error) => { - vm.configuration.permissions = deny_all_policy(); + sidecar + .vms + .get_mut(&vm_id) + .expect("owned VM should exist") + .configuration + .permissions = deny_all_policy(); return Err(rollback_error); } } + if let Err(rollback_error) = registry_rollback { + sidecar + .vms + .get_mut(&vm_id) + .expect("owned VM should exist") + .configuration + .permissions = deny_all_policy(); + sidecar + .bridge + .set_vm_permissions(&vm_id, &deny_all_policy())?; + return Err(SidecarError::InvalidState(format!( + "binding registry rollback failed after {error}: {rollback_error}" + ))); + } + return Err(error); } }; @@ -124,16 +141,11 @@ where fn refresh_binding_registry(vm: &mut VmState) -> Result<(), SidecarError> { let commands = binding_command_names(vm); vm.kernel - .register_driver(CommandDriver::new( + .replace_driver(CommandDriver::new( BINDING_DRIVER_NAME, commands.iter().cloned(), )) .map_err(kernel_error)?; - - for command in commands { - vm.command_guest_paths - .insert(command.clone(), format!("/bin/{command}")); - } Ok(()) } @@ -600,10 +612,55 @@ fn ensure_command_aliases_available( } fn ensure_binding_registry_capacity( - bindings: &BTreeMap, + vm: &VmState, payload: &RegisterHostCallbacksRequest, ) -> Result<(), SidecarError> { - core_ensure_binding_registry_capacity(bindings, payload).map_err(binding_registration_error) + const COLLECTION_CONFIG_PATH: &str = "limits.bindings.maxRegisteredCollections"; + const BINDING_CONFIG_PATH: &str = "limits.bindings.maxRegisteredBindingsPerVm"; + + let collection_limit = vm.limits.bindings.max_registered_collections; + if vm.bindings.len() >= collection_limit { + let observed = vm.bindings.len().saturating_add(1); + return Err(SidecarError::host_resource_limit( + COLLECTION_CONFIG_PATH, + collection_limit, + observed, + format!( + "VM would have {observed} registered binding collections, limit is {collection_limit}; raise {COLLECTION_CONFIG_PATH}" + ), + )); + } + + let registered_bindings = vm + .bindings + .values() + .map(|collection| collection.callbacks.len()) + .sum::(); + let total_bindings = registered_bindings + .checked_add(payload.callbacks.len()) + .ok_or_else(|| { + SidecarError::host_resource_limit( + BINDING_CONFIG_PATH, + vm.limits.bindings.max_registered_bindings_per_vm, + usize::MAX, + format!( + "registered host callback count overflow; raise {BINDING_CONFIG_PATH} only after reducing the requested collection size" + ), + ) + })?; + let binding_limit = vm.limits.bindings.max_registered_bindings_per_vm; + if total_bindings > binding_limit { + return Err(SidecarError::host_resource_limit( + BINDING_CONFIG_PATH, + binding_limit, + total_bindings, + format!( + "VM would have {total_bindings} registered host callbacks, limit is {binding_limit}; raise {BINDING_CONFIG_PATH}" + ), + )); + } + + Ok(()) } fn binding_command_names(vm: &VmState) -> Vec { diff --git a/crates/native-sidecar/src/bootstrap.rs b/crates/native-sidecar/src/bootstrap.rs index 4d9ded5d1a..f0ae094a0f 100644 --- a/crates/native-sidecar/src/bootstrap.rs +++ b/crates/native-sidecar/src/bootstrap.rs @@ -6,7 +6,7 @@ use crate::SidecarError; use agentos_kernel::root_fs::{FilesystemEntry as KernelFilesystemEntry, RootFilesystemSnapshot}; use agentos_kernel::vfs::VirtualFileSystem; -use std::collections::BTreeMap; +use std::collections::BTreeSet; pub(crate) fn root_snapshot_entry(entry: &KernelFilesystemEntry) -> RootFilesystemEntry { agentos_native_sidecar_core::root_snapshot_entry(entry) @@ -34,10 +34,19 @@ where .map_err(|error| SidecarError::InvalidState(error.to_string())) } -pub(crate) fn discover_command_guest_paths(kernel: &mut SidecarKernel) -> BTreeMap { - let mut command_guest_paths = BTreeMap::new(); +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct KernelCommandInventory { + pub(crate) names: BTreeSet, + pub(crate) search_roots: Vec, +} + +/// Enumerate legacy command mounts from the live kernel VFS for one immediate +/// registration/PATH rebuild. Nothing returned here is retained as pathname +/// authority; launch resolution revalidates the selected file in the kernel. +pub(crate) fn discover_kernel_commands(kernel: &mut SidecarKernel) -> KernelCommandInventory { + let mut inventory = KernelCommandInventory::default(); let Ok(command_roots) = kernel.read_dir("/__secure_exec/commands") else { - return command_guest_paths; + return inventory; }; let mut ordered_roots = command_roots @@ -52,13 +61,82 @@ pub(crate) fn discover_command_guest_paths(kernel: &mut SidecarKernel) -> BTreeM continue; }; + let mut root_has_commands = false; for entry in entries { - if entry.starts_with('.') || command_guest_paths.contains_key(&entry) { + if entry.starts_with('.') || entry.contains('/') { + continue; + } + let candidate = format!("{guest_root}/{entry}"); + let Some(canonical) = kernel.realpath(&candidate).ok() else { + continue; + }; + let Some(stat) = kernel.lstat(&canonical).ok() else { + continue; + }; + if stat.is_directory || stat.is_symbolic_link { continue; } - command_guest_paths.insert(entry.clone(), format!("{guest_root}/{entry}")); + root_has_commands = true; + inventory.names.insert(entry); + } + if root_has_commands { + inventory.search_roots.push(guest_root); } } - command_guest_paths + inventory +} + +#[cfg(test)] +mod tests { + use super::*; + use agentos_kernel::kernel::KernelVmConfig; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::MemoryFileSystem; + + fn test_kernel() -> SidecarKernel { + let mut config = KernelVmConfig::new("vm-transient-command-discovery"); + config.permissions = Permissions::allow_all(); + SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config) + } + + #[test] + fn kernel_command_inventory_tracks_live_files_and_roots() { + let mut kernel = test_kernel(); + kernel + .mkdir("/__secure_exec/commands/001", true) + .expect("create first command root"); + kernel + .mkdir("/__secure_exec/commands/002/directory", true) + .expect("create non-command directory"); + kernel + .write_file( + "/__secure_exec/commands/001/alpha", + b"#!/usr/bin/env node\n".to_vec(), + ) + .expect("write command"); + kernel + .write_file( + "/__secure_exec/commands/001/.hidden", + b"#!/usr/bin/env node\n".to_vec(), + ) + .expect("write hidden entry"); + + let discovered = discover_kernel_commands(&mut kernel); + assert_eq!(discovered.names, BTreeSet::from([String::from("alpha")])); + assert_eq!( + discovered.search_roots, + vec![String::from("/__secure_exec/commands/001")] + ); + + kernel + .remove_file("/__secure_exec/commands/001/alpha") + .expect("remove command after initial discovery"); + assert_eq!( + discover_kernel_commands(&mut kernel), + KernelCommandInventory::default(), + "transient discovery must not retain deleted commands or roots" + ); + } } diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index a29ae4d45c..1283cc82f1 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -1,7 +1,58 @@ use super::*; +use crate::state::ManagedHostNetRoute; +use agentos_execution::host::{SocketDomain as HostSocketDomain, SocketKind as HostSocketKind}; const SYNTHETIC_V8_TERMINATION_STDERR: &[u8] = b"Error: Execution terminated\n"; +fn finish_kernel_child_from_runtime_exit( + kernel_handle: &KernelProcessHandle, + exit_code: i32, + exit_signal: Option, + core_dumped: bool, +) { + if let Some(signal) = exit_signal { + kernel_handle.finish_signaled(signal, core_dumped); + } else { + kernel_handle.finish(exit_code); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum InheritedOutputStream { + Stdout, + Stderr, +} + +fn classify_inherited_output_stream( + _child_description: u64, + child_description_path: &str, +) -> Option { + match child_description_path { + "/dev/stdout" => Some(InheritedOutputStream::Stdout), + "/dev/stderr" => Some(InheritedOutputStream::Stderr), + _ => None, + } +} + +fn cancel_host_call_completion( + completion: &crate::state::HostCallCompletion, + message: impl Into, +) -> Result<(), SidecarError> { + completion + .reply + .fail(HostServiceError::new("ECANCELED", message)) + .map_err(SidecarError::from) +} + +fn cancel_direct_host_reply( + reply: &DirectHostReplyHandle, + message: impl Into, +) -> Result<(), SidecarError> { + reply + .fail(HostServiceError::new("ECANCELED", message)) + .map_err(SidecarError::from) +} + #[derive(Debug)] pub(super) enum TransferredHostNetSocket { Tcp { @@ -27,9 +78,36 @@ pub(super) enum TransferredHostNetSocket { Pending { metadata: TransferredHostNetMetadata, description_handles: Arc<()>, + tcp_reservation: Option<(SocketFamily, u16)>, }, } +impl TransferredHostNetSocket { + pub(super) fn kernel_transfer_guard(&self) -> Option { + match self { + Self::Tcp { socket, .. } => socket.kernel_transfer_guard.clone(), + Self::TcpListener { listener, .. } => listener.kernel_transfer_guard.clone(), + Self::Udp { socket, .. } => socket.kernel_transfer_guard.clone(), + Self::Unix { .. } | Self::UnixListener { .. } | Self::Pending { .. } => None, + } + } +} + +#[derive(Debug)] +pub(super) struct ManagedTransferredHostNetSocket { + pub resource: TransferredHostNetSocket, + pub transfer: TransferredFd, +} + +impl ManagedTransferredHostNetSocket { + pub(super) fn clone_for_fd_transfer(&self) -> Result { + Ok(Self { + resource: self.resource.clone_for_fd_transfer()?, + transfer: self.transfer.clone(), + }) + } +} + #[cfg(test)] mod scm_rights_tests { use super::*; @@ -178,18 +256,44 @@ mod scm_rights_tests { .contains("EPROTONOSUPPORT")); } + #[test] + fn spawn_pending_socket_is_keyed_by_managed_description_identity() { + let pending = ProcessSpawnHostNetworkDescriptor { + guest_fd: 7, + description_id: Some(String::from("42")), + close_on_exec: false, + socket_id: None, + server_id: None, + udp_socket_id: None, + metadata: Value::Null, + }; + assert_eq!( + spawn_host_net_source(&pending).expect("managed pending spawn source"), + SpawnHostNetSource::Pending(42) + ); + + let legacy = ProcessSpawnHostNetworkDescriptor { + description_id: None, + ..pending + }; + assert!(spawn_host_net_source(&legacy).is_err()); + } + #[test] fn duplicate_rights_share_one_description_and_queue_lifecycle_is_counted() { let registry = Arc::new(Mutex::new(BTreeMap::new())); let resource = TransferredHostNetSocket::Pending { metadata: canonical_tcp_metadata(false), description_handles: Arc::new(()), + tcp_reservation: None, }; let duplicate = resource .clone_for_fd_transfer() .expect("duplicate one open-file description"); - register_host_net_transfer_description(®istry, &resource); - register_host_net_transfer_description(®istry, &duplicate); + register_host_net_transfer_description(®istry, &resource) + .expect("register source description"); + register_host_net_transfer_description(®istry, &duplicate) + .expect("register duplicate description"); let mut queued = BTreeMap::new(); add_live_host_net_transfer_descriptions(®istry, &mut queued); @@ -236,33 +340,107 @@ mod scm_rights_tests { #[cfg(test)] mod descendant_rpc_route_tests { #[test] - fn descendant_dispatch_routes_exec_and_fd_image_commit() { + fn descendant_dispatch_routes_context_owned_operations() { let source = include_str!("child_process.rs"); let start = source - .rfind("async fn poll_descendant_javascript_child_process") + .rfind("async fn poll_descendant_process") .expect("descendant pump must exist"); let end = source[start..] - .find("fn write_descendant_javascript_child_process_stdin") + .find("fn write_descendant_process_stdin") .map(|offset| start + offset) .expect("descendant pump end must exist"); let dispatcher = &source[start..end]; - for (method, handler) in [ - ("process.exec", "self.exec_javascript_process_image"), - ( - "process.exec_fd_image_commit", - "self.commit_wasm_fd_process_image", - ), + for method in ["process.exec", "process.exec_fd_image_commit"] { + assert!( + !dispatcher.contains(&format!("request.method == \"{method}\"")), + "typed exec operation must not fall back to the descendant legacy RPC dispatcher" + ); + } + let process_dispatch_start = source + .rfind("async fn dispatch_descendant_context_process_operation(") + .expect("typed descendant process dispatcher"); + let process_dispatch = &source[process_dispatch_start..start]; + for required in [ + "ProcessOperation::Exec(request)", + "validate_wasm_fd_image_commit_request(&request)", + "self.commit_wasm_fd_process_image(", + "self.exec_process_image(", + ] { + assert!( + process_dispatch.contains(required), + "typed descendant process dispatcher is missing {required}" + ); + } + + for operation in ["SendDescriptorRights", "ReceiveDescriptorRights"] { + assert!( + dispatcher.contains(&format!( + "agentos_execution::host::NetworkOperation::{operation}" + )), + "descendant dispatcher does not classify {operation}" + ); + } + for operation in [ + "Socket", "Bind", "Connect", "Listen", "Accept", "Receive", "Send", ] { assert!( - dispatcher.contains(&format!("request.method == \"{method}\"")), - "descendant dispatcher does not route {method}" + dispatcher.contains(&format!( + "agentos_execution::host::NetworkOperation::{operation}" + )), + "descendant dispatcher does not classify managed-fd {operation}" + ); + } + assert!(dispatcher.contains("self.dispatch_descendant_context_managed_network_operation(")); + assert!(dispatcher.contains("FilesystemOperation::StdinRead")); + assert!(dispatcher.contains("self.service_descendant_kernel_stdin_read(")); + for operation in ["Close", "CloseFrom", "Renumber", "DuplicateTo", "Move"] { + assert!( + dispatcher.contains(&format!("FilesystemOperation::{operation}")), + "descendant dispatcher does not classify {operation}" ); + } + assert!(dispatcher.contains("self.dispatch_descendant_context_descriptor_operation(")); + + let descriptor_start = source + .rfind("fn dispatch_descendant_context_descriptor_operation(") + .expect("descendant descriptor dispatcher"); + let descriptor_end = source[descriptor_start..] + .find("fn dispatch_descendant_context_dns_operation(") + .map(|offset| descriptor_start + offset) + .expect("descendant descriptor dispatcher end"); + let descriptor = &source[descriptor_start..descriptor_end]; + for shared_service in [ + "host_dispatch::close_with_managed_retirement(", + "host_dispatch::closefrom_with_managed_retirement(", + "host_dispatch::replace_descriptor_with_managed_retirement(", + ] { assert!( - dispatcher.contains(handler), - "descendant dispatcher does not call {handler}" + descriptor.contains(shared_service), + "descendant descriptor dispatcher bypasses {shared_service}" ); } + assert!(descriptor.contains("host_dispatch::authorize_host_operation(")); + + let managed_start = source + .rfind("fn dispatch_descendant_context_managed_network_operation(") + .expect("descendant managed-network dispatcher"); + let managed_end = source[managed_start..] + .find("async fn dispatch_descendant_context_process_operation(") + .map(|offset| managed_start + offset) + .expect("descendant managed-network dispatcher end"); + let managed = &source[managed_start..managed_end]; + assert!(managed.contains("HostNetworkOperation::SendDescriptorRights")); + assert!(managed.contains("HostNetworkOperation::ReceiveDescriptorRights")); + assert!(managed.contains("descriptor_rights_compat_request(")); + assert!(managed.contains("service_javascript_sync_rpc(")); + assert!(managed.contains("host_dispatch::authorize_host_operation(")); + assert!(managed.contains("host_dispatch::service_descendant_managed_fd_network_operation(")); + + assert!( + dispatcher.contains("settle_host_call_completion_for_process("), + "descendant deferred completions must share root connect finalization" + ); } } @@ -340,9 +518,11 @@ impl TransferredHostNetSocket { Self::Pending { metadata, description_handles, + tcp_reservation, } => Ok(Self::Pending { metadata: metadata.clone(), description_handles: Arc::clone(description_handles), + tcp_reservation: *tcp_reservation, }), } } @@ -351,15 +531,17 @@ impl TransferredHostNetSocket { pub(super) fn register_host_net_transfer_description( registry: &HostNetTransferDescriptionRegistry, resource: &TransferredHostNetSocket, -) { +) -> Result<(), SidecarError> { let (handles, connected, kernel_backed) = resource.description_identity(); // Adopted kernel sockets remain present in the kernel resource snapshot // while queued. Only sidecar-only descriptions need this weak queue lease. if kernel_backed { - return; + return Ok(()); } let description_id = Arc::as_ptr(handles) as usize; - let mut descriptions = registry.lock().unwrap_or_else(|error| error.into_inner()); + let mut descriptions = registry + .lock() + .map_err(|_| SidecarError::host("EIO", "host-network transfer registry lock poisoned"))?; descriptions.retain(|_, description| description.handles.upgrade().is_some()); descriptions .entry(description_id) @@ -368,6 +550,7 @@ pub(super) fn register_host_net_transfer_description( handles: Arc::downgrade(handles), connected, }); + Ok(()) } #[derive(Debug, Clone)] @@ -383,7 +566,7 @@ pub(super) struct TransferredHostNetMetadata { local_reservation: Option, remote_info: Option, remote_unix_address: Option, - listening: bool, + pub(super) listening: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -397,6 +580,7 @@ pub(super) enum SpawnHostNetSource { Tcp(String), TcpListener(String), Udp(String), + Pending(u64), } #[derive(Debug, Clone, Copy)] @@ -411,6 +595,7 @@ pub(super) enum ResolvedHostNetSourceClass { #[derive(Debug)] pub(super) struct PreparedSpawnHostNetDescription { guest_fds: Vec, + description_id: Option, resource: TransferredHostNetSocket, metadata: Value, } @@ -418,7 +603,7 @@ pub(super) struct PreparedSpawnHostNetDescription { #[derive(Debug, Default)] pub(super) struct PreparedSpawnHostNetFds { descriptions: Vec, - kernel_actions: Vec, + kernel_actions: Vec, } #[derive(Debug, Clone, Copy)] @@ -448,19 +633,17 @@ pub(super) fn validate_host_net_metadata_size( ) -> Result<(), SidecarError> { let encoded_len = serde_json::to_vec(value) .map_err(|error| { - SidecarError::InvalidState(format!("EINVAL: invalid {label} metadata: {error}")) + SidecarError::host("EINVAL", format!("invalid {label} metadata: {error}")) })? .len(); if encoded_len > HOST_NET_METADATA_MAX_BYTES { - return Err(SidecarError::InvalidState(format!( - "E2BIG: {label} metadata is {encoded_len} bytes, exceeding the {HOST_NET_METADATA_MAX_BYTES}-byte limit" + return Err(SidecarError::host("E2BIG", format!("{label} metadata is {encoded_len} bytes, exceeding the {HOST_NET_METADATA_MAX_BYTES}-byte limit" ))); } fn validate_strings(value: &Value, label: &str) -> Result<(), SidecarError> { match value { Value::String(value) if value.len() > HOST_NET_METADATA_MAX_STRING_BYTES => { - Err(SidecarError::InvalidState(format!( - "ENAMETOOLONG: {label} metadata string exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" + Err(SidecarError::host("ENAMETOOLONG", format!("{label} metadata string exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" ))) } Value::Array(values) => { @@ -472,8 +655,7 @@ pub(super) fn validate_host_net_metadata_size( Value::Object(values) => { for (key, value) in values { if key.len() > HOST_NET_METADATA_MAX_STRING_BYTES { - return Err(SidecarError::InvalidState(format!( - "ENAMETOOLONG: {label} metadata key exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" + return Err(SidecarError::host("ENAMETOOLONG", format!("{label} metadata key exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" ))); } validate_strings(value, label)?; @@ -492,31 +674,39 @@ pub(super) fn host_net_open_description_options( ) -> Result { validate_host_net_metadata_size(value, label)?; let object = value.as_object().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "EINVAL: host-network metadata must be an object", - )) + SidecarError::host( + "EINVAL", + String::from("host-network metadata must be an object"), + ) })?; let nonblocking = match object.get("nonblocking") { None => false, Some(Value::Bool(value)) => *value, Some(_) => { - return Err(SidecarError::InvalidState(format!( - "EINVAL: {label} metadata nonblocking must be boolean" - ))) + return Err(SidecarError::host( + "EINVAL", + format!("{label} metadata nonblocking must be boolean"), + )) } }; let recv_timeout_ms = match object.get("recvTimeoutMs") { None | Some(Value::Null) => None, Some(value) => { let timeout = value.as_u64().ok_or_else(|| { - SidecarError::InvalidState(format!( - "EINVAL: {label} metadata recvTimeoutMs must be a non-negative integer or null" - )) + SidecarError::host( + "EINVAL", + format!( + "{label} metadata recvTimeoutMs must be a non-negative integer or null" + ), + ) })?; if timeout > HOST_NET_RECV_TIMEOUT_MAX_MS { - return Err(SidecarError::InvalidState(format!( - "EINVAL: {label} metadata recvTimeoutMs exceeds {HOST_NET_RECV_TIMEOUT_MAX_MS}" - ))); + return Err(SidecarError::host( + "EINVAL", + format!( + "{label} metadata recvTimeoutMs exceeds {HOST_NET_RECV_TIMEOUT_MAX_MS}" + ), + )); } Some(timeout) } @@ -608,8 +798,8 @@ impl TransferredHostNetMetadata { fn udp_socket(socket: &ActiveUdpSocket, options: HostNetOpenDescriptionOptions) -> Self { let domain = match socket.family { - JavascriptUdpFamily::Ipv4 => HOST_NET_AF_INET, - JavascriptUdpFamily::Ipv6 => HOST_NET_AF_INET6, + UdpFamily::Ipv4 => HOST_NET_AF_INET, + UdpFamily::Ipv6 => HOST_NET_AF_INET6, }; Self { domain, @@ -693,24 +883,30 @@ impl TransferredHostNetMetadata { & !(HOST_NET_SOCKET_TYPE_MASK | HOST_NET_SOCK_NONBLOCK | HOST_NET_SOCK_CLOEXEC) != 0 { - return Err(SidecarError::InvalidState(format!( - "EINVAL: {label} metadata socketType contains unsupported flags" - ))); + return Err(SidecarError::host( + "EINVAL", + format!("{label} metadata socketType contains unsupported flags"), + )); } let socket_type = raw_socket_type & HOST_NET_SOCKET_TYPE_MASK; let requested_protocol = required_host_net_u32(object, "protocol", label)?; let protocol = match (domain, socket_type, requested_protocol) { - (HOST_NET_AF_INET | HOST_NET_AF_INET6, HOST_NET_SOCK_STREAM, 0 | HOST_NET_IPPROTO_TCP) => { - HOST_NET_IPPROTO_TCP - } - (HOST_NET_AF_INET | HOST_NET_AF_INET6, HOST_NET_SOCK_DGRAM, 0 | HOST_NET_IPPROTO_UDP) => { - HOST_NET_IPPROTO_UDP - } + ( + HOST_NET_AF_INET | HOST_NET_AF_INET6, + HOST_NET_SOCK_STREAM, + 0 | HOST_NET_IPPROTO_TCP, + ) => HOST_NET_IPPROTO_TCP, + ( + HOST_NET_AF_INET | HOST_NET_AF_INET6, + HOST_NET_SOCK_DGRAM, + 0 | HOST_NET_IPPROTO_UDP, + ) => HOST_NET_IPPROTO_UDP, (HOST_NET_AF_UNIX, HOST_NET_SOCK_STREAM, 0) => 0, _ => { - return Err(SidecarError::InvalidState(format!( - "EPROTONOSUPPORT: {label} metadata does not describe a supported unconnected socket" - ))) + return Err(SidecarError::host( + "EPROTONOSUPPORT", + format!("{label} metadata does not describe a supported unconnected socket"), + )) } }; let metadata = Self { @@ -759,7 +955,10 @@ pub(super) fn required_host_net_u32( .and_then(Value::as_u64) .and_then(|value| u32::try_from(value).ok()) .ok_or_else(|| { - SidecarError::InvalidState(format!("EINVAL: {label} metadata field {name} must be u32")) + SidecarError::host( + "EINVAL", + format!("{label} metadata field {name} must be u32"), + ) }) } @@ -776,7 +975,7 @@ pub(super) fn validate_host_net_metadata( return Err(host_net_metadata_mismatch(label, "open-description state")); } let object = value.as_object().ok_or_else(|| { - SidecarError::InvalidState(format!("EINVAL: {label} metadata must be an object")) + SidecarError::host("EINVAL", format!("{label} metadata must be an object")) })?; let domain = required_host_net_u32(object, "domain", label)?; if domain != expected.domain { @@ -824,13 +1023,14 @@ pub(super) fn validate_host_net_metadata( } pub(super) fn host_net_metadata_mismatch(label: &str, field: &str) -> SidecarError { - SidecarError::InvalidState(format!( - "EINVAL: {label} metadata {field} does not match the sidecar-owned socket" - )) + SidecarError::host( + "EINVAL", + format!("{label} metadata {field} does not match the sidecar-owned socket"), + ) } pub(super) fn spawn_host_net_source( - fd: &JavascriptSpawnHostNetFd, + fd: &ProcessSpawnHostNetworkDescriptor, ) -> Result { let mut sources = Vec::new(); if let Some(id) = fd.socket_id.as_deref().filter(|id| !id.is_empty()) { @@ -845,19 +1045,30 @@ pub(super) fn spawn_host_net_source( validate_host_net_resource_id(id, "inherited UDP socket id")?; sources.push(SpawnHostNetSource::Udp(id.to_owned())); } + if sources.is_empty() { + if let Some(description_id) = fd + .description_id + .as_deref() + .and_then(|value| value.parse::().ok()) + { + return Ok(SpawnHostNetSource::Pending(description_id)); + } + } if sources.len() != 1 { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: inherited host-network fd requires exactly one resource id", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("inherited host-network fd requires exactly one resource id"), + )); } Ok(sources.pop().expect("one source checked")) } pub(super) fn validate_host_net_resource_id(id: &str, label: &str) -> Result<(), SidecarError> { if id.len() > 256 { - return Err(SidecarError::InvalidState(format!( - "ENAMETOOLONG: {label} exceeds 256 bytes" - ))); + return Err(SidecarError::host( + "ENAMETOOLONG", + format!("{label} exceeds 256 bytes"), + )); } Ok(()) } @@ -867,9 +1078,10 @@ pub(super) fn scm_rights_host_net_source( ) -> Result, SidecarError> { validate_host_net_metadata_size(value, "SCM_RIGHTS host-network")?; let object = value.as_object().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "EINVAL: SCM_RIGHTS host-network entry must be an object", - )) + SidecarError::host( + "EINVAL", + String::from("SCM_RIGHTS host-network entry must be an object"), + ) })?; let mut sources = Vec::new(); for (name, source) in [("socketId", 0u8), ("serverId", 1u8), ("udpSocketId", 2u8)] { @@ -880,9 +1092,10 @@ pub(super) fn scm_rights_host_net_source( continue; } let id = value.as_str().filter(|id| !id.is_empty()).ok_or_else(|| { - SidecarError::InvalidState(format!( - "EINVAL: SCM_RIGHTS host-network {name} must be a non-empty string or null" - )) + SidecarError::host( + "EINVAL", + format!("SCM_RIGHTS host-network {name} must be a non-empty string or null"), + ) })?; validate_host_net_resource_id(id, name)?; sources.push(match source { @@ -893,33 +1106,40 @@ pub(super) fn scm_rights_host_net_source( }); } if sources.len() > 1 { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: SCM_RIGHTS host-network entry requires at most one resource id", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("SCM_RIGHTS host-network entry requires at most one resource id"), + )); } Ok(sources.pop()) } pub(super) fn posix_spawn_action_guest_fd( - action: &JavascriptPosixSpawnFileAction, + action: &ProcessSpawnFileAction, label: &str, ) -> Result { u32::try_from(action.guest_fd.unwrap_or(action.fd)).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn {label} fd {}", - action.guest_fd.unwrap_or(action.fd) - )) + SidecarError::host( + "EBADF", + format!( + "invalid posix_spawn {label} fd {}", + action.guest_fd.unwrap_or(action.fd) + ), + ) }) } pub(super) fn posix_spawn_action_guest_source_fd( - action: &JavascriptPosixSpawnFileAction, + action: &ProcessSpawnFileAction, ) -> Result { u32::try_from(action.guest_source_fd.unwrap_or(action.source_fd)).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn dup2 source {}", - action.guest_source_fd.unwrap_or(action.source_fd) - )) + SidecarError::host( + "EBADF", + format!( + "invalid posix_spawn dup2 source {}", + action.guest_source_fd.unwrap_or(action.source_fd) + ), + ) }) } @@ -939,6 +1159,7 @@ impl PreparedSpawnHostNetFds { .map(|(index, description)| { let mut value = json!({ "guestFds": description.guest_fds, + "descriptionId": description.description_id.map(|id| id.to_string()), "metadata": description.metadata, }); let object = value @@ -960,9 +1181,7 @@ impl PreparedSpawnHostNetFds { TransferredHostNetSocket::UnixListener { .. } => { ("serverId", format!("spawn-unix-listener-{index}")) } - TransferredHostNetSocket::Pending { .. } => unreachable!( - "pending host-network descriptions are rejected before spawn" - ), + TransferredHostNetSocket::Pending { .. } => return value, }; object.insert(key.to_owned(), Value::String(id)); value @@ -971,39 +1190,113 @@ impl PreparedSpawnHostNetFds { ) } - fn install(self, child: &mut ActiveProcess) { + fn validate_install( + &self, + managed_descriptions: &BTreeMap, + child_kernel_pid: u32, + ) -> Result<(), SidecarError> { + let pending_tcp_reservations = self + .descriptions + .iter() + .filter(|description| { + matches!( + &description.resource, + TransferredHostNetSocket::Pending { + tcp_reservation: Some(_), + .. + } + ) + }) + .count(); + 0usize + .checked_add(pending_tcp_reservations) + .ok_or_else(|| { + SidecarError::host( + "EOVERFLOW", + "inherited TCP reservation identifiers exceed the child identifier space", + ) + })?; + for description in &self.descriptions { + if let Some(description_id) = description.description_id { + let canonical = managed_descriptions.get(&description_id).ok_or_else(|| { + SidecarError::host( + "ESTALE", + "managed spawn description disappeared before child installation", + ) + })?; + if canonical.routes.contains_key(&child_kernel_pid) { + return Err(SidecarError::host( + "EEXIST", + format!( + "managed spawn description {description_id} already has a route for child PID {child_kernel_pid}" + ), + )); + } + } + } + Ok(()) + } + + fn install( + self, + child: &mut ActiveProcess, + managed_descriptions: &mut BTreeMap, + ) { for (index, description) in self.descriptions.into_iter().enumerate() { - match description.resource { + let route = match description.resource { TransferredHostNetSocket::Tcp { mut socket, .. } => { socket.listener_id = None; - child - .tcp_sockets - .insert(format!("spawn-tcp-{index}"), *socket); + let id = format!("spawn-tcp-{index}"); + child.tcp_sockets.insert(id.clone(), *socket); + ManagedHostNetRoute::TcpSocket(id) } TransferredHostNetSocket::TcpListener { listener, .. } => { - child - .tcp_listeners - .insert(format!("spawn-listener-{index}"), listener); + let id = format!("spawn-listener-{index}"); + child.tcp_listeners.insert(id.clone(), listener); + ManagedHostNetRoute::TcpListener(id) } TransferredHostNetSocket::Udp { socket, .. } => { - child - .udp_sockets - .insert(format!("spawn-udp-{index}"), socket); + let id = format!("spawn-udp-{index}"); + child.udp_sockets.insert(id.clone(), socket); + ManagedHostNetRoute::UdpSocket(id) } TransferredHostNetSocket::Unix { mut socket, .. } => { socket.listener_id = None; - child - .unix_sockets - .insert(format!("spawn-unix-{index}"), socket); + let id = format!("spawn-unix-{index}"); + child.unix_sockets.insert(id.clone(), socket); + ManagedHostNetRoute::UnixSocket(id) } - TransferredHostNetSocket::UnixListener { listener, .. } => { - child - .unix_listeners - .insert(format!("spawn-unix-listener-{index}"), listener); + TransferredHostNetSocket::UnixListener { listener, metadata } => { + let id = format!("spawn-unix-listener-{index}"); + let listening = metadata.listening; + child.unix_listeners.insert(id.clone(), listener); + if listening { + ManagedHostNetRoute::UnixListener(id) + } else { + ManagedHostNetRoute::UnixBound { listener_id: id } + } } - TransferredHostNetSocket::Pending { .. } => { - unreachable!("pending host-network descriptions are rejected before spawn") + TransferredHostNetSocket::Pending { + tcp_reservation, .. + } => { + // No reactor transport exists until the child binds or + // connects this canonical pending socket description. + if let Some(reservation) = tcp_reservation { + let reservation_id = child.allocate_tcp_port_reservation_id(); + child + .tcp_port_reservations + .insert(reservation_id.clone(), reservation); + ManagedHostNetRoute::TcpBound { reservation_id } + } else { + ManagedHostNetRoute::Unbound + } } + }; + if let Some(description_id) = description.description_id { + let canonical = managed_descriptions + .get_mut(&description_id) + .expect("managed spawn descriptions were prevalidated"); + canonical.routes.insert(child.kernel_pid, route); } } } @@ -1089,6 +1382,26 @@ pub(super) fn prepare_transferred_host_net_resource( value: &Value, label: &str, ) -> Result { + prepare_transferred_host_net_resource_with_options(kernel, process, source, value, label, None) +} + +fn prepare_transferred_host_net_resource_with_options( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + source: &SpawnHostNetSource, + value: &Value, + label: &str, + trusted_options: Option, +) -> Result { + if matches!(source, SpawnHostNetSource::Pending(_)) { + let options = trusted_options.unwrap_or(host_net_open_description_options(value, label)?); + let metadata = TransferredHostNetMetadata::pending(value, options, label)?; + return Ok(TransferredHostNetSocket::Pending { + metadata, + description_handles: Arc::new(()), + tcp_reservation: None, + }); + } // Resolve the sidecar-owned resource before reading any guest-controlled // metadata. Metadata may describe open-description flags, but it never // selects the resource class or lifecycle. @@ -1100,9 +1413,10 @@ pub(super) fn prepare_transferred_host_net_resource( ResolvedHostNetSourceClass::Unix } SpawnHostNetSource::Tcp(socket_id) => { - return Err(SidecarError::InvalidState(format!( - "EBADF: unknown transferable socket {socket_id}" - ))) + return Err(SidecarError::host( + "EBADF", + format!("unknown transferable socket {socket_id}"), + )) } SpawnHostNetSource::TcpListener(listener_id) if process.tcp_listeners.contains_key(listener_id) => @@ -1115,20 +1429,26 @@ pub(super) fn prepare_transferred_host_net_resource( ResolvedHostNetSourceClass::UnixListener } SpawnHostNetSource::TcpListener(listener_id) => { - return Err(SidecarError::InvalidState(format!( - "EBADF: unknown transferable listener {listener_id}" - ))) + return Err(SidecarError::host( + "EBADF", + format!("unknown transferable listener {listener_id}"), + )) } SpawnHostNetSource::Udp(socket_id) if process.udp_sockets.contains_key(socket_id) => { ResolvedHostNetSourceClass::Udp } SpawnHostNetSource::Udp(socket_id) => { - return Err(SidecarError::InvalidState(format!( - "EBADF: unknown transferable UDP socket {socket_id}" - ))) + return Err(SidecarError::host( + "EBADF", + format!("unknown transferable UDP socket {socket_id}"), + )) + } + SpawnHostNetSource::Pending(_) => { + unreachable!("pending resources return before sidecar lookup") } }; - let options = host_net_open_description_options(value, label)?; + let validate_metadata = trusted_options.is_none(); + let options = trusted_options.unwrap_or(host_net_open_description_options(value, label)?); let resource = match (source, resolved_class) { (SpawnHostNetSource::Tcp(socket_id), ResolvedHostNetSourceClass::Tcp) => { let socket = process @@ -1136,7 +1456,9 @@ pub(super) fn prepare_transferred_host_net_resource( .get_mut(socket_id) .expect("resolved TCP socket remains present"); let metadata = TransferredHostNetMetadata::tcp_socket(socket, options); - validate_host_net_metadata(value, &metadata, "tcp", label)?; + if validate_metadata { + validate_host_net_metadata(value, &metadata, "tcp", label)?; + } if socket.kernel_transfer_guard.is_none() { if let Some(kernel_socket_id) = socket.kernel_socket_id { socket.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( @@ -1158,7 +1480,9 @@ pub(super) fn prepare_transferred_host_net_resource( .get(socket_id) .expect("resolved Unix socket remains present"); let metadata = TransferredHostNetMetadata::unix_socket(socket, options); - validate_host_net_metadata(value, &metadata, "unix", label)?; + if validate_metadata { + validate_host_net_metadata(value, &metadata, "unix", label)?; + } TransferredHostNetSocket::Unix { socket: socket.clone_for_fd_transfer(), metadata, @@ -1170,7 +1494,9 @@ pub(super) fn prepare_transferred_host_net_resource( .get_mut(listener_id) .expect("resolved TCP listener remains present"); let metadata = TransferredHostNetMetadata::tcp_listener(listener, options); - validate_host_net_metadata(value, &metadata, "listener", label)?; + if validate_metadata { + validate_host_net_metadata(value, &metadata, "listener", label)?; + } if listener.kernel_transfer_guard.is_none() { if let Some(kernel_socket_id) = listener.kernel_socket_id { listener.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( @@ -1195,7 +1521,9 @@ pub(super) fn prepare_transferred_host_net_resource( .get(listener_id) .expect("resolved Unix listener remains present"); let metadata = TransferredHostNetMetadata::unix_listener(listener, options); - validate_host_net_metadata(value, &metadata, "unix-listener", label)?; + if validate_metadata { + validate_host_net_metadata(value, &metadata, "unix-listener", label)?; + } TransferredHostNetSocket::UnixListener { listener: listener.clone_for_fd_transfer()?, metadata, @@ -1203,12 +1531,15 @@ pub(super) fn prepare_transferred_host_net_resource( } (SpawnHostNetSource::Udp(socket_id), ResolvedHostNetSourceClass::Udp) => { let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { - SidecarError::InvalidState(format!( - "EBADF: unknown transferable UDP socket {socket_id}" - )) + SidecarError::host( + "EBADF", + format!("unknown transferable UDP socket {socket_id}"), + ) })?; let metadata = TransferredHostNetMetadata::udp_socket(socket, options); - validate_host_net_metadata(value, &metadata, "udp", label)?; + if validate_metadata { + validate_host_net_metadata(value, &metadata, "udp", label)?; + } if socket.kernel_transfer_guard.is_none() { if let Some(kernel_socket_id) = socket.kernel_socket_id { socket.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( @@ -1224,19 +1555,127 @@ pub(super) fn prepare_transferred_host_net_resource( metadata, } } + (SpawnHostNetSource::Pending(_), _) => { + unreachable!("pending resources return before sidecar lookup") + } _ => unreachable!("resource source and resolved class must agree"), }; Ok(resource) } +pub(super) fn prepare_managed_transferred_host_net_resource( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + description_id: u64, + kernel_fd: u32, + description: &crate::state::ManagedHostNetDescription, + label: &str, +) -> Result { + let mut tcp_reservation = None; + let source = match description.route_for(process.kernel_pid).ok_or_else(|| { + SidecarError::host( + "ESTALE", + "managed transfer description has no process route", + ) + })? { + ManagedHostNetRoute::TcpSocket(id) | ManagedHostNetRoute::UnixSocket(id) => { + SpawnHostNetSource::Tcp(id.clone()) + } + ManagedHostNetRoute::TcpListener(id) + | ManagedHostNetRoute::UnixListener(id) + | ManagedHostNetRoute::UnixBound { listener_id: id } => { + SpawnHostNetSource::TcpListener(id.clone()) + } + ManagedHostNetRoute::UdpSocket(id) => SpawnHostNetSource::Udp(id.clone()), + ManagedHostNetRoute::Unbound => SpawnHostNetSource::Pending(description_id), + ManagedHostNetRoute::TcpBound { reservation_id } => { + tcp_reservation = Some( + *process + .tcp_port_reservations + .get(reservation_id) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + "managed bound TCP reservation disappeared before transfer", + ) + })?, + ); + SpawnHostNetSource::Pending(description_id) + } + }; + let nonblocking = kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, kernel_fd) + .map_err(kernel_error)? + .flags + & agentos_kernel::fd_table::O_NONBLOCK + != 0; + let domain = match description.domain { + HostSocketDomain::Inet4 => HOST_NET_AF_INET, + HostSocketDomain::Inet6 => HOST_NET_AF_INET6, + HostSocketDomain::Unix => HOST_NET_AF_UNIX, + }; + let socket_type = match description.kind { + HostSocketKind::Stream => HOST_NET_SOCK_STREAM, + HostSocketKind::Datagram => HOST_NET_SOCK_DGRAM, + HostSocketKind::SeqPacket => { + return Err(SidecarError::host( + "EPROTONOSUPPORT", + "managed host-network transfer does not support SOCK_SEQPACKET", + )); + } + }; + let protocol = match (description.domain, description.kind) { + (HostSocketDomain::Unix, _) => 0, + (_, HostSocketKind::Stream) => HOST_NET_IPPROTO_TCP, + (_, HostSocketKind::Datagram) => HOST_NET_IPPROTO_UDP, + (_, HostSocketKind::SeqPacket) => unreachable!("SOCK_SEQPACKET rejected above"), + }; + let canonical = json!({ + "domain": domain, + "socketType": socket_type, + "protocol": protocol, + "nonblocking": nonblocking, + "recvTimeoutMs": description.receive_timeout_ms, + "bindOptions": Value::Null, + "localInfo": Value::Null, + "localUnixAddress": if description.domain == HostSocketDomain::Unix { json!("unix-unnamed") } else { Value::Null }, + "localReservation": Value::Null, + "remoteInfo": Value::Null, + "remoteUnixAddress": Value::Null, + "listening": false, + }); + let mut resource = prepare_transferred_host_net_resource_with_options( + kernel, + process, + &source, + &canonical, + label, + Some(HostNetOpenDescriptionOptions { + nonblocking, + recv_timeout_ms: description.receive_timeout_ms, + }), + )?; + if let TransferredHostNetSocket::Pending { + tcp_reservation: transferred_reservation, + .. + } = &mut resource + { + *transferred_reservation = tcp_reservation; + } + Ok(resource) +} + +pub(super) const POSIX_SPAWN_RESETIDS: u32 = 1 << 0; pub(super) const POSIX_SPAWN_SETPGROUP: u32 = 1 << 1; +pub(super) const POSIX_SPAWN_SETSIGDEF: u32 = 1 << 2; +pub(super) const POSIX_SPAWN_SETSIGMASK: u32 = 1 << 3; pub(super) const POSIX_SPAWN_SETSCHEDPARAM: u32 = 1 << 4; pub(super) const POSIX_SPAWN_SETSCHEDULER: u32 = 1 << 5; pub(super) const POSIX_SPAWN_SETSID: u32 = 1 << 7; -pub(super) const SUPPORTED_POSIX_SPAWN_FLAGS: u32 = (1 << 0) +pub(super) const SUPPORTED_POSIX_SPAWN_FLAGS: u32 = POSIX_SPAWN_RESETIDS | POSIX_SPAWN_SETPGROUP - | (1 << 2) - | (1 << 3) + | POSIX_SPAWN_SETSIGDEF + | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER | (1 << 6) @@ -1324,15 +1763,20 @@ mod direct_runtime_stdio_mapping_tests { use agentos_kernel::permissions::Permissions; use agentos_kernel::vfs::MemoryFileSystem; - #[test] - fn materializes_guest_fd_mappings_at_canonical_fds() { - let mut config = KernelVmConfig::new("vm-python-stdio-mappings"); + fn test_kernel(name: &str) -> SidecarKernel { + let mut config = KernelVmConfig::new(name); config.permissions = Permissions::allow_all(); let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); kernel .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) .unwrap(); - let process = kernel + kernel + } + + #[test] + fn inherited_output_authority_survives_parent_close_and_cross_dup() { + let mut kernel = test_kernel("vm-inherited-output-authority"); + let parent = kernel .spawn_process( WASM_COMMAND, Vec::new(), @@ -1341,68 +1785,555 @@ mod direct_runtime_stdio_mapping_tests { ..SpawnOptions::default() }, ) - .unwrap(); - let (read_fd, write_fd) = kernel - .open_pipe(EXECUTION_DRIVER_NAME, process.pid()) - .unwrap(); + .expect("spawn parent"); + kernel - .fd_write( - EXECUTION_DRIVER_NAME, - process.pid(), - write_fd, - b"python-stdin", + .fd_dup2(EXECUTION_DRIVER_NAME, parent.pid(), 2, 1) + .expect("redirect parent stdout to stderr"); + let stderr_child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, ) - .unwrap(); - - materialize_direct_runtime_stdio_mappings( - &mut kernel, - process.pid(), - &AppliedPosixSpawnFileActions { - fd_mappings: vec![[0, read_fd]], - closed_guest_fds: vec![1], - }, - ) - .unwrap(); - + .expect("spawn stderr-redirected child"); + let stderr_description = kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, stderr_child.pid(), 1) + .expect("child stdout description") + .0; + let stderr_path = kernel + .fd_path(EXECUTION_DRIVER_NAME, stderr_child.pid(), 1) + .expect("child stdout path"); assert_eq!( + classify_inherited_output_stream(stderr_description, stderr_path.as_str()), + Some(InheritedOutputStream::Stderr), + "1>&2 must route by the inherited open description" + ); + + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent.pid(), 1) + .expect("parent closes inherited stdout alias"); + kernel.write_file("/replacement", Vec::new()).unwrap(); + let replacement_source = kernel + .fd_open(EXECUTION_DRIVER_NAME, parent.pid(), "/replacement", 1, None) + .expect("parent reassigns fd 1"); + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, parent.pid(), replacement_source, 1) + .expect("install replacement at fd 1"); + assert_ne!( kernel - .fd_read(EXECUTION_DRIVER_NAME, process.pid(), 0, 64) - .unwrap(), - b"python-stdin" + .fd_description_identity(EXECUTION_DRIVER_NAME, parent.pid(), 1) + .unwrap() + .0, + stderr_description ); assert_eq!( kernel - .fd_path(EXECUTION_DRIVER_NAME, process.pid(), 1) - .expect_err("closed stdout must remain closed") - .code(), - "EBADF" + .fd_description_identity(EXECUTION_DRIVER_NAME, stderr_child.pid(), 1) + .unwrap() + .0, + stderr_description, + "the child description remains authoritative after parent reassignment" ); - } - fn assert_closed_stdin_canonicalization_is_idempotent(materialize_direct_runtime_first: bool) { - let mut config = KernelVmConfig::new("vm-closed-stdin-canonicalization"); - config.permissions = Permissions::allow_all(); - let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + // Build the reverse cross-dup from a fresh process so fd 1 still owns + // a /dev/stdout description rather than the prior stderr authority. + let reverse_parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn reverse parent"); kernel - .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) - .unwrap(); - let process = kernel + .fd_dup2(EXECUTION_DRIVER_NAME, reverse_parent.pid(), 1, 2) + .expect("redirect reverse parent stderr to stdout"); + let stdout_child = kernel .spawn_process( WASM_COMMAND, Vec::new(), SpawnOptions { requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(reverse_parent.pid()), ..SpawnOptions::default() }, ) + .expect("spawn stdout-redirected child"); + let stdout_description = kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, stdout_child.pid(), 2) + .unwrap() + .0; + let stdout_path = kernel + .fd_path(EXECUTION_DRIVER_NAME, stdout_child.pid(), 2) .unwrap(); - let close_stdin = JavascriptPosixSpawnFileAction { - command: 1, - guest_fd: Some(0), - fd: 0, - source_fd: -1, - guest_source_fd: None, - oflag: 0, + assert_eq!( + classify_inherited_output_stream(stdout_description, stdout_path.as_str()), + Some(InheritedOutputStream::Stdout), + "2>&1 must route by the inherited open description" + ); + assert_eq!(classify_inherited_output_stream(91, "/replacement"), None); + assert_eq!(classify_inherited_output_stream(92, "pipe:17"), None); + + stderr_child.finish(0); + stdout_child.finish(0); + kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + parent.pid(), + stderr_child.pid() as i32, + WaitPidFlags::empty(), + ) + .expect("wait stderr child") + .expect("reap stderr child"); + kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + reverse_parent.pid(), + stdout_child.pid() as i32, + WaitPidFlags::empty(), + ) + .expect("wait stdout child") + .expect("reap stdout child"); + parent.finish(0); + reverse_parent.finish(0); + kernel.waitpid(parent.pid()).expect("reap parent"); + kernel + .waitpid(reverse_parent.pid()) + .expect("reap reverse parent"); + } + + #[test] + fn wasm_exit_event_precedes_kernel_authoritative_wait_and_reap() { + let mut kernel = test_kernel("vm-wasm-exit-before-wait"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn WASM parent"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .expect("spawn WASM child"); + + child.finish(7); + let bridge_exit_event = json!({ "type": "exit", "exitCode": 7 }); + assert_eq!(bridge_exit_event["exitCode"], 7); + + let waited = kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + parent.pid(), + child.pid() as i32, + WaitPidFlags::WNOHANG, + ) + .expect("kernel wait must remain valid after bridge exit delivery") + .expect("exited WASM child must be waitable"); + assert_eq!(waited.pid, child.pid()); + assert_eq!(waited.status, 7); + assert_eq!(waited.event, agentos_kernel::kernel::WaitPidEvent::Exited); + assert_eq!( + kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + parent.pid(), + child.pid() as i32, + WaitPidFlags::WNOHANG, + ) + .expect_err("successful wait must reap the child") + .code(), + "ECHILD" + ); + } + + #[test] + fn runtime_signal_exit_stays_signaled_in_kernel_wait_status() { + let mut kernel = test_kernel("vm-runtime-signal-exit"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn WASM parent"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .expect("spawn WASM child"); + + finish_kernel_child_from_runtime_exit( + &child, + 128 + libc::SIGUSR1, + Some(libc::SIGUSR1), + false, + ); + + let waited = kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + parent.pid(), + child.pid() as i32, + WaitPidFlags::WNOHANG, + ) + .expect("kernel wait must succeed") + .expect("signaled child must be waitable"); + assert_eq!(waited.status, 128 + libc::SIGUSR1); + assert_eq!(waited.event, agentos_kernel::kernel::WaitPidEvent::Exited); + assert_eq!( + waited.termination, + Some(agentos_kernel::process_runtime::ProcessExit::Signaled { + signal: libc::SIGUSR1, + core_dumped: false, + }) + ); + } + + #[test] + fn posix_spawn_signal_attributes_are_committed_to_the_kernel_child() { + let mut kernel = test_kernel("vm-posix-spawn-signal-attributes"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + parent + .signal_action( + libc::SIGUSR1, + Some(agentos_kernel::process_table::SignalAction { + disposition: agentos_kernel::process_table::SignalDisposition::Ignore, + ..agentos_kernel::process_table::SignalAction::DEFAULT + }), + ) + .unwrap(); + parent + .sigprocmask( + SigmaskHow::SetMask, + SignalSet::from_signals([libc::SIGUSR2]).unwrap(), + ) + .unwrap(); + + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let options = ProcessLaunchOptions { + spawn_attr_flags: POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK, + spawn_signal_defaults: vec![ + libc::SIGUSR1 as u32, + libc::SIGKILL as u32, + libc::SIGSTOP as u32, + ], + spawn_signal_mask: vec![libc::SIGTERM as u32], + ..ProcessLaunchOptions::default() + }; + apply_spawn_process_attributes_or_rollback(&mut kernel, &child, &options).unwrap(); + + assert_eq!( + child.signal_action(libc::SIGUSR1, None).unwrap(), + agentos_kernel::process_table::SignalAction::DEFAULT + ); + let mask = child + .sigprocmask(SigmaskHow::Block, SignalSet::empty()) + .unwrap(); + assert!(mask.contains(libc::SIGTERM)); + assert!(!mask.contains(libc::SIGUSR2)); + } + + #[test] + fn materializes_guest_fd_mappings_at_canonical_fds() { + let mut kernel = test_kernel("vm-python-stdio-mappings"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let (read_fd, write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, process.pid()) + .unwrap(); + kernel + .fd_write( + EXECUTION_DRIVER_NAME, + process.pid(), + write_fd, + b"python-stdin", + ) + .unwrap(); + + materialize_direct_runtime_stdio_mappings( + &mut kernel, + process.pid(), + &AppliedPosixSpawnFileActions { + fd_mappings: vec![[0, read_fd]], + closed_guest_fds: vec![1], + }, + ) + .unwrap(); + + assert_eq!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, process.pid(), 0, 64) + .unwrap(), + b"python-stdin" + ); + assert_eq!( + kernel + .fd_path(EXECUTION_DRIVER_NAME, process.pid(), 1) + .expect_err("closed stdout must remain closed") + .code(), + "EBADF" + ); + } + + #[test] + fn posix_spawn_file_actions_preserve_hidden_wasi_preopens() { + let mut kernel = test_kernel("vm-posix-spawn-hidden-preopens"); + kernel.mkdir("/workspace", true).unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let parent_preopens = kernel + .initialize_wasi_preopens(EXECUTION_DRIVER_NAME, parent.pid()) + .expect("initialize parent preopens"); + assert!(!parent_preopens.is_empty()); + kernel.write_file("/redirect", Vec::new()).unwrap(); + let redirect_fd = kernel + .fd_open(EXECUTION_DRIVER_NAME, parent.pid(), "/redirect", 1, None) + .unwrap(); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let actions = [ + ProcessSpawnFileAction { + command: 2, + guest_fd: Some(3), + fd: 5, + source_fd: redirect_fd as i32, + guest_source_fd: Some(5), + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ProcessSpawnFileAction { + command: 1, + guest_fd: Some(5), + fd: redirect_fd as i32, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ProcessSpawnFileAction { + command: 6, + guest_fd: Some(3), + fd: 3, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: vec![3, 5], + }, + ]; + apply_posix_spawn_file_actions( + &mut kernel, + child.pid(), + "/", + &[[5, redirect_fd]], + &actions, + ) + .expect("apply guest redirection actions"); + + let child_preopens = kernel + .wasi_preopens(EXECUTION_DRIVER_NAME, child.pid()) + .expect("read inherited child preopens"); + assert_eq!( + child_preopens + .iter() + .map(|preopen| preopen.guest_path.as_str()) + .collect::>(), + parent_preopens + .iter() + .map(|preopen| preopen.guest_path.as_str()) + .collect::>() + ); + } + + #[test] + fn preapplied_posix_spawn_snapshot_preserves_hidden_wasi_preopens() { + let mut kernel = test_kernel("vm-preapplied-spawn-hidden-preopens"); + kernel.mkdir("/workspace", true).unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let parent_preopens = kernel + .initialize_wasi_preopens(EXECUTION_DRIVER_NAME, parent.pid()) + .expect("initialize parent preopens"); + kernel.write_file("/redirect", Vec::new()).unwrap(); + let redirect_fd = kernel + .fd_open(EXECUTION_DRIVER_NAME, parent.pid(), "/redirect", 1, None) + .unwrap(); + let actions = [ + ProcessSpawnFileAction { + command: 2, + guest_fd: Some(3), + fd: 5, + source_fd: redirect_fd as i32, + guest_source_fd: Some(5), + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ProcessSpawnFileAction { + command: 1, + guest_fd: Some(5), + fd: redirect_fd as i32, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ProcessSpawnFileAction { + command: 6, + guest_fd: Some(3), + fd: 3, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: vec![3, 5], + }, + ]; + let prepared = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[[5, redirect_fd]], + &actions, + ) + .expect("preapply spawn file actions"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap(); + install_preapplied_posix_spawn_file_actions(&mut kernel, &child, prepared) + .expect("install preapplied spawn snapshot"); + + let child_preopens = kernel + .wasi_preopens(EXECUTION_DRIVER_NAME, child.pid()) + .expect("read restored child preopens"); + assert_eq!( + child_preopens + .iter() + .map(|preopen| preopen.guest_path.as_str()) + .collect::>(), + parent_preopens + .iter() + .map(|preopen| preopen.guest_path.as_str()) + .collect::>() + ); + } + + fn assert_closed_stdin_canonicalization_is_idempotent(materialize_direct_runtime_first: bool) { + let mut kernel = test_kernel("vm-closed-stdin-canonicalization"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let close_stdin = ProcessSpawnFileAction { + command: 1, + guest_fd: Some(0), + fd: 0, + source_fd: -1, + guest_source_fd: None, + oflag: 0, mode: 0, path: String::new(), close_from_guest_fds: Vec::new(), @@ -1452,16 +2383,16 @@ pub(super) struct PreparedPosixSpawnFileActions { pub(super) fn prepare_spawn_host_net_fds( kernel: &mut SidecarKernel, parent: &mut ActiveProcess, + managed_descriptions: &crate::state::ManagedHostNetDescriptionRegistry, current_network_counts: NetworkResourceCounts, - inherited_fds: &[JavascriptSpawnHostNetFd], + inherited_fds: &[ProcessSpawnHostNetworkDescriptor], inherited_kernel_mappings: &[[u32; 2]], - actions: &[JavascriptPosixSpawnFileAction], + actions: &[ProcessSpawnFileAction], ) -> Result { const LINUX_GUEST_FD_LIMIT: u32 = 1 << 20; if let Some(limit) = kernel.resource_limits().max_open_fds { if inherited_fds.len() > limit { - return Err(SidecarError::InvalidState(format!( - "EMFILE: inherited host-network fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + return Err(SidecarError::host("EMFILE", format!("inherited host-network fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", inherited_fds.len() ))); } @@ -1474,53 +2405,212 @@ pub(super) fn prepare_spawn_host_net_fds( let mut fd_states = BTreeMap::::new(); let mut source_descriptions = BTreeMap::::new(); let mut description_metadata = Vec::::new(); + let mut description_ids = Vec::>::new(); let mut description_resources = Vec::>::new(); for inherited in inherited_fds { + let managed_description_id = inherited + .description_id + .as_deref() + .map(|value| { + value.parse::().map_err(|_| { + SidecarError::host( + "EINVAL", + "managed host-network descriptionId must be a u64 decimal string", + ) + }) + }) + .transpose()?; if inherited.guest_fd >= LINUX_GUEST_FD_LIMIT { - return Err(SidecarError::InvalidState(format!( - "EBADF: inherited host-network guest fd {} exceeds the Linux descriptor limit", - inherited.guest_fd - ))); + return Err(SidecarError::host( + "EBADF", + format!( + "inherited host-network guest fd {} exceeds the Linux descriptor limit", + inherited.guest_fd + ), + )); } - if inherited_kernel_guest_fds.contains(&inherited.guest_fd) + if (inherited_kernel_guest_fds.contains(&inherited.guest_fd) + && managed_description_id.is_none()) || fd_states.contains_key(&inherited.guest_fd) { - return Err(SidecarError::InvalidState(format!( - "EINVAL: duplicate inherited guest fd {}", - inherited.guest_fd - ))); + return Err(SidecarError::host( + "EINVAL", + format!("duplicate inherited guest fd {}", inherited.guest_fd), + )); } + let managed_kernel_fd = if let Some(description_id) = managed_description_id { + let kernel_fd = inherited_kernel_mappings + .iter() + .find(|mapping| mapping[0] == inherited.guest_fd) + .map(|mapping| mapping[1]) + .ok_or_else(|| { + SidecarError::host( + "EBADF", + format!( + "managed host-network guest fd {} has no canonical kernel mapping", + inherited.guest_fd + ), + ) + })?; + let (actual_description_id, _) = kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, parent.kernel_pid, kernel_fd) + .map_err(kernel_error)?; + if actual_description_id != description_id { + return Err(SidecarError::host( + "EINVAL", + format!( + "managed host-network guest fd {} description identity does not match kernel fd {}", + inherited.guest_fd, kernel_fd + ), + )); + } + Some(kernel_fd) + } else { + None + }; - let source = spawn_host_net_source(inherited)?; + let managed_description = managed_description_id + .map(|description_id| { + managed_descriptions + .lock() + .map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })? + .get(&description_id) + .cloned() + .ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed spawn description is unknown") + }) + }) + .transpose()?; + let source = if let Some(description) = managed_description.as_ref() { + match description.route_for(parent.kernel_pid).ok_or_else(|| { + SidecarError::host("ESTALE", "managed spawn description has no parent route") + })? { + ManagedHostNetRoute::TcpSocket(id) | ManagedHostNetRoute::UnixSocket(id) => { + SpawnHostNetSource::Tcp(id.clone()) + } + ManagedHostNetRoute::TcpListener(id) + | ManagedHostNetRoute::UnixListener(id) + | ManagedHostNetRoute::UnixBound { listener_id: id } => { + SpawnHostNetSource::TcpListener(id.clone()) + } + ManagedHostNetRoute::UdpSocket(id) => SpawnHostNetSource::Udp(id.clone()), + ManagedHostNetRoute::Unbound => { + SpawnHostNetSource::Pending(managed_description_id.expect("managed id exists")) + } + ManagedHostNetRoute::TcpBound { .. } => { + return Err(SidecarError::host( + "EOPNOTSUPP", + "forking a bound non-listening TCP socket is not yet supported", + )); + } + } + } else { + spawn_host_net_source(inherited)? + }; let description = if let Some(index) = source_descriptions.get(&source).copied() { let existing = description_resources[index] .as_ref() .expect("spawn host-network description resource exists"); - if validate_host_net_metadata( - &inherited.metadata, - existing.metadata(), - existing.class(), - "spawn host-network", - ) - .is_err() + if managed_description_id.is_none() + && validate_host_net_metadata( + &inherited.metadata, + existing.metadata(), + existing.class(), + "spawn host-network", + ) + .is_err() { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: aliases of one inherited host-network description disagree on metadata", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from( + "aliases of one inherited host-network description disagree on metadata", + ), + )); + } + if description_ids[index] != managed_description_id { + return Err(SidecarError::host( + "EINVAL", + String::from( + "aliases of one inherited host-network resource disagree on kernel description identity", + ), + )); } index } else { - let resource = prepare_transferred_host_net_resource( - kernel, - parent, - &source, - &inherited.metadata, - "spawn host-network", - )?; + let (metadata, resource) = if let Some(description) = managed_description.as_ref() { + let kernel_fd = managed_kernel_fd.expect("managed kernel fd exists"); + let nonblocking = kernel + .fd_stat(EXECUTION_DRIVER_NAME, parent.kernel_pid, kernel_fd) + .map_err(kernel_error)? + .flags + & agentos_kernel::fd_table::O_NONBLOCK + != 0; + let domain = match description.domain { + HostSocketDomain::Inet4 => HOST_NET_AF_INET, + HostSocketDomain::Inet6 => HOST_NET_AF_INET6, + HostSocketDomain::Unix => HOST_NET_AF_UNIX, + }; + let socket_type = match description.kind { + HostSocketKind::Stream => HOST_NET_SOCK_STREAM, + HostSocketKind::Datagram => HOST_NET_SOCK_DGRAM, + HostSocketKind::SeqPacket => { + return Err(SidecarError::host( + "EPROTONOSUPPORT", + "managed spawn does not support SOCK_SEQPACKET", + )); + } + }; + let protocol = match (description.domain, description.kind) { + (HostSocketDomain::Unix, _) => 0, + (_, HostSocketKind::Stream) => HOST_NET_IPPROTO_TCP, + (_, HostSocketKind::Datagram) => HOST_NET_IPPROTO_UDP, + (_, HostSocketKind::SeqPacket) => { + unreachable!("SOCK_SEQPACKET rejected above") + } + }; + let canonical = json!({ + "domain": domain, + "socketType": socket_type, + "protocol": protocol, + "nonblocking": nonblocking, + "recvTimeoutMs": description.receive_timeout_ms, + "bindOptions": Value::Null, + "localInfo": Value::Null, + "localUnixAddress": if description.domain == HostSocketDomain::Unix { json!("unix-unnamed") } else { Value::Null }, + "localReservation": Value::Null, + "remoteInfo": Value::Null, + "remoteUnixAddress": Value::Null, + "listening": false, + }); + let resource = prepare_transferred_host_net_resource_with_options( + kernel, + parent, + &source, + &canonical, + "managed spawn host-network", + Some(HostNetOpenDescriptionOptions { + nonblocking, + recv_timeout_ms: description.receive_timeout_ms, + }), + )?; + (resource.metadata().as_value(), resource) + } else { + let resource = prepare_transferred_host_net_resource( + kernel, + parent, + &source, + &inherited.metadata, + "spawn host-network", + )?; + (resource.metadata().as_value(), resource) + }; let index = description_resources.len(); source_descriptions.insert(source, index); - description_metadata.push(resource.metadata().as_value()); + description_metadata.push(metadata); + description_ids.push(managed_description_id); description_resources.push(Some(resource)); index }; @@ -1533,7 +2623,16 @@ pub(super) fn prepare_spawn_host_net_fds( ); } - let kernel_actions = apply_spawn_host_net_file_actions(&mut fd_states, actions)?; + let mut kernel_actions = apply_spawn_host_net_file_actions(&mut fd_states, actions)?; + if inherited_fds + .iter() + .any(|inherited| inherited.description_id.is_some()) + { + // The metadata simulation above determines which descriptions reach + // the child, but managed descriptors already live in the canonical + // table, so the kernel must execute every file action as well. + kernel_actions = actions.to_vec(); + } fd_states.retain(|_, state| !state.close_on_exec); // fork/exec inheritance installs new descriptor references to the same @@ -1567,6 +2666,7 @@ pub(super) fn prepare_spawn_host_net_fds( } descriptions.push(PreparedSpawnHostNetDescription { guest_fds, + description_id: description_ids[index], resource: description_resources[index] .take() .expect("spawn host-network description resource exists"), @@ -1601,8 +2701,8 @@ pub(super) fn check_spawn_host_net_resource_limit( pub(super) fn apply_spawn_host_net_file_actions( fd_states: &mut BTreeMap, - actions: &[JavascriptPosixSpawnFileAction], -) -> Result, SidecarError> { + actions: &[ProcessSpawnFileAction], +) -> Result, SidecarError> { let mut kernel_actions = Vec::with_capacity(actions.len()); for action in actions { match action.command { @@ -1628,9 +2728,10 @@ pub(super) fn apply_spawn_host_net_file_actions( let mut close_target = action.clone(); close_target.command = 1; close_target.guest_fd = Some(i32::try_from(guest_fd).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: posix_spawn dup2 target {guest_fd} exceeds i32" - )) + SidecarError::host( + "EBADF", + format!("posix_spawn dup2 target {guest_fd} exceeds i32"), + ) })?); close_target.source_fd = 0; close_target.guest_source_fd = None; @@ -1651,9 +2752,10 @@ pub(super) fn apply_spawn_host_net_file_actions( 5 => { let guest_fd = posix_spawn_action_guest_fd(action, "fchdir")?; if fd_states.contains_key(&guest_fd) { - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: posix_spawn fchdir fd {guest_fd} is a socket" - ))); + return Err(SidecarError::host( + "ENOTDIR", + format!("posix_spawn fchdir fd {guest_fd} is a socket"), + )); } kernel_actions.push(action.clone()); } @@ -1663,9 +2765,10 @@ pub(super) fn apply_spawn_host_net_file_actions( kernel_actions.push(action.clone()); } command => { - return Err(SidecarError::InvalidState(format!( - "EINVAL: unknown posix_spawn file action {command}" - ))); + return Err(SidecarError::host( + "EINVAL", + format!("unknown posix_spawn file action {command}"), + )); } } } @@ -1677,7 +2780,7 @@ pub(super) fn apply_posix_spawn_file_actions( pid: u32, initial_cwd: &str, inherited_mappings: &[[u32; 2]], - actions: &[JavascriptPosixSpawnFileAction], + actions: &[ProcessSpawnFileAction], ) -> Result<(AppliedPosixSpawnFileActions, String), SidecarError> { let inherited_kernel_fds = kernel .fd_snapshot(EXECUTION_DRIVER_NAME, pid) @@ -1685,6 +2788,16 @@ pub(super) fn apply_posix_spawn_file_actions( .into_iter() .map(|entry| entry.fd) .collect::>(); + // WASI preopens are kernel-owned capability roots used only by libc's + // tagged pathname resolver. Their kernel descriptor numbers are not + // Linux guest descriptor numbers, so guest closefrom actions must not + // close them merely because the internal number is above the cutoff. + let hidden_wasi_preopen_kernel_fds = kernel + .wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)? + .into_iter() + .map(|preopen| preopen.fd) + .collect::>(); let mut mappings = BTreeMap::new(); let mut mapped_kernel_fds = BTreeSet::new(); let mut closed_guest_fds = BTreeSet::new(); @@ -1701,18 +2814,22 @@ pub(super) fn apply_posix_spawn_file_actions( } if mappings.insert(*guest_fd, *kernel_fd).is_some() || !mapped_kernel_fds.insert(*kernel_fd) { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: duplicate posix_spawn guest/kernel fd mapping", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("duplicate posix_spawn guest/kernel fd mapping"), + )); } } - let action_guest_fd = |action: &JavascriptPosixSpawnFileAction| { + let action_guest_fd = |action: &ProcessSpawnFileAction| { u32::try_from(action.guest_fd.unwrap_or(action.fd)).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn guest fd {}", - action.guest_fd.unwrap_or(action.fd) - )) + SidecarError::host( + "EBADF", + format!( + "invalid posix_spawn guest fd {}", + action.guest_fd.unwrap_or(action.fd) + ), + ) }) }; for action in actions { @@ -1729,14 +2846,13 @@ pub(super) fn apply_posix_spawn_file_actions( None } else { let raw_fd = u32::try_from(action.fd).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn close fd {}", - action.fd - )) + SidecarError::host( + "EBADF", + format!("invalid posix_spawn close fd {}", action.fd), + ) })?; if mapped_kernel_fds.contains(&raw_fd) { - return Err(SidecarError::InvalidState(format!( - "EBADF: posix_spawn guest fd {guest_fd} collides with another mapped descriptor" + return Err(SidecarError::host("EBADF", format!("posix_spawn guest fd {guest_fd} collides with another mapped descriptor" ))); } Some(raw_fd) @@ -1754,24 +2870,26 @@ pub(super) fn apply_posix_spawn_file_actions( action.guest_source_fd.unwrap_or(action.source_fd), ) .map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn dup2 source {}", - action.guest_source_fd.unwrap_or(action.source_fd) - )) + SidecarError::host( + "EBADF", + format!( + "invalid posix_spawn dup2 source {}", + action.guest_source_fd.unwrap_or(action.source_fd) + ), + ) })?; if guest_source_fd == guest_fd { let fd = if let Some(fd) = mappings.get(&guest_source_fd).copied() { fd } else if action.guest_source_fd.is_some() && guest_source_fd > 2 { - return Err(SidecarError::InvalidState(format!( - "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" + return Err(SidecarError::host("EBADF", format!("posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" ))); } else { u32::try_from(action.source_fd).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn dup2 source {}", - action.source_fd - )) + SidecarError::host( + "EBADF", + format!("invalid posix_spawn dup2 source {}", action.source_fd), + ) })? }; // POSIX spawn dup2(fd, fd) clears FD_CLOEXEC. @@ -1790,19 +2908,17 @@ pub(super) fn apply_posix_spawn_file_actions( let source_fd = if let Some(fd) = mappings.get(&guest_source_fd).copied() { fd } else if action.guest_source_fd.is_some() && guest_source_fd > 2 { - return Err(SidecarError::InvalidState(format!( - "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" + return Err(SidecarError::host("EBADF", format!("posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" ))); } else { let raw_fd = u32::try_from(action.source_fd).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn dup2 source {}", - action.source_fd - )) + SidecarError::host( + "EBADF", + format!("invalid posix_spawn dup2 source {}", action.source_fd), + ) })?; if mapped_kernel_fds.contains(&raw_fd) { - return Err(SidecarError::InvalidState(format!( - "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} collides with another mapped descriptor" + return Err(SidecarError::host("EBADF", format!("posix_spawn dup2 source guest fd {guest_source_fd} collides with another mapped descriptor" ))); } raw_fd @@ -1847,10 +2963,10 @@ pub(super) fn apply_posix_spawn_file_actions( kernel .fd_close(EXECUTION_DRIVER_NAME, pid, opened_fd) .map_err(kernel_error)?; - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: posix_spawn open path is not a directory: {}", - action.path - ))); + return Err(SidecarError::host( + "ENOTDIR", + format!("posix_spawn open path is not a directory: {}", action.path), + )); } } mappings.insert(guest_fd, opened_fd); @@ -1863,10 +2979,10 @@ pub(super) fn apply_posix_spawn_file_actions( .stat_for_process(EXECUTION_DRIVER_NAME, pid, &action_path) .map_err(kernel_error)?; if !stat.is_directory { - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: posix_spawn chdir path is not a directory: {}", - action.path - ))); + return Err(SidecarError::host( + "ENOTDIR", + format!("posix_spawn chdir path is not a directory: {}", action.path), + )); } cwd = kernel .realpath_for_process(EXECUTION_DRIVER_NAME, pid, &action_path) @@ -1878,24 +2994,26 @@ pub(super) fn apply_posix_spawn_file_actions( let fd = if let Some(fd) = mappings.get(&guest_fd).copied() { fd } else if action.guest_fd.is_some() && guest_fd > 2 { - return Err(SidecarError::InvalidState(format!( - "EBADF: posix_spawn fchdir guest fd {guest_fd} is not kernel-backed" - ))); + return Err(SidecarError::host( + "EBADF", + format!("posix_spawn fchdir guest fd {guest_fd} is not kernel-backed"), + )); } else { u32::try_from(action.fd).map_err(|_| { - SidecarError::InvalidState(format!( - "EBADF: invalid posix_spawn fchdir fd {}", - action.fd - )) + SidecarError::host( + "EBADF", + format!("invalid posix_spawn fchdir fd {}", action.fd), + ) })? }; let stat = kernel .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) .map_err(kernel_error)?; if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: posix_spawn fchdir fd {guest_fd} is not a directory" - ))); + return Err(SidecarError::host( + "ENOTDIR", + format!("posix_spawn fchdir fd {guest_fd} is not a directory"), + )); } cwd = normalize_path( &kernel @@ -1907,16 +3025,14 @@ pub(super) fn apply_posix_spawn_file_actions( let low_fd = action_guest_fd(action)?; if let Some(limit) = kernel.resource_limits().max_open_fds { if action.close_from_guest_fds.len() > limit { - return Err(SidecarError::InvalidState(format!( - "EMFILE: posix_spawn closefrom guest fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + return Err(SidecarError::host("EMFILE", format!("posix_spawn closefrom guest fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", action.close_from_guest_fds.len() ))); } } for guest_fd in &action.close_from_guest_fds { if *guest_fd < low_fd { - return Err(SidecarError::InvalidState(format!( - "EINVAL: posix_spawn closefrom guest fd {guest_fd} is below cutoff {low_fd}" + return Err(SidecarError::host("EINVAL", format!("posix_spawn closefrom guest fd {guest_fd} is below cutoff {low_fd}" ))); } closed_guest_fds.insert(*guest_fd); @@ -1940,7 +3056,10 @@ pub(super) fn apply_posix_spawn_file_actions( } } for kernel_fd in open_kernel_fds { - if !mapped_kernel_fds.contains(&kernel_fd) && kernel_fd >= low_fd { + if !mapped_kernel_fds.contains(&kernel_fd) + && !hidden_wasi_preopen_kernel_fds.contains(&kernel_fd) + && kernel_fd >= low_fd + { to_close.insert(kernel_fd, kernel_fd); } } @@ -1956,9 +3075,10 @@ pub(super) fn apply_posix_spawn_file_actions( } } command => { - return Err(SidecarError::InvalidState(format!( - "EINVAL: unknown posix_spawn file action {command}" - ))); + return Err(SidecarError::host( + "EINVAL", + format!("unknown posix_spawn file action {command}"), + )); } } } @@ -1994,7 +3114,7 @@ pub(super) fn apply_posix_spawn_file_actions_or_rollback( process: &KernelProcessHandle, cwd: &str, inherited_mappings: &[[u32; 2]], - actions: &[JavascriptPosixSpawnFileAction], + actions: &[ProcessSpawnFileAction], ) -> Result { match apply_posix_spawn_file_actions(kernel, process.pid(), cwd, inherited_mappings, actions) { Ok((mappings, _)) => Ok(mappings), @@ -2019,9 +3139,7 @@ pub(super) fn rollback_unregistered_spawn_child( context: &str, ) { if let Some(execution) = execution { - if let ActiveExecution::Binding(binding) = execution { - binding.cancelled.store(true, Ordering::Relaxed); - } else if let Err(error) = execution.terminate() { + if let Err(error) = execution.terminate() { eprintln!( "[agentos] failed to terminate rejected {context} runtime for PID {}: {error}", process.pid() @@ -2059,6 +3177,70 @@ pub(super) fn apply_spawn_session_or_rollback( Ok(()) } +/// Apply the POSIX spawn attributes that belong to the newly allocated kernel +/// process before any executor can run guest instructions. +pub(super) fn apply_spawn_process_attributes_or_rollback( + kernel: &mut SidecarKernel, + process: &KernelProcessHandle, + options: &ProcessLaunchOptions, +) -> Result<(), SidecarError> { + let apply = (|| { + if options.spawn_attr_flags & POSIX_SPAWN_RESETIDS != 0 { + let uid = kernel + .getuid(EXECUTION_DRIVER_NAME, process.pid()) + .map_err(kernel_error)?; + let gid = kernel + .getgid(EXECUTION_DRIVER_NAME, process.pid()) + .map_err(kernel_error)?; + kernel + .seteuid(EXECUTION_DRIVER_NAME, process.pid(), uid) + .map_err(kernel_error)?; + kernel + .setegid(EXECUTION_DRIVER_NAME, process.pid(), gid) + .map_err(kernel_error)?; + } + + if options.spawn_attr_flags & POSIX_SPAWN_SETSIGDEF != 0 { + for signal in &options.spawn_signal_defaults { + if matches!(*signal as i32, libc::SIGKILL | libc::SIGSTOP) { + continue; + } + process + .signal_action( + *signal as i32, + Some(agentos_kernel::process_table::SignalAction::DEFAULT), + ) + .map_err(kernel_error)?; + } + } + + if options.spawn_attr_flags & POSIX_SPAWN_SETSIGMASK != 0 { + let mask = SignalSet::from_signals( + options + .spawn_signal_mask + .iter() + .map(|signal| *signal as i32), + ) + .map_err(|error| SidecarError::host("EINVAL", error.to_string()))?; + process + .sigprocmask(SigmaskHow::SetMask, mask) + .map_err(kernel_error)?; + } + Ok(()) + })(); + + if let Err(error) = apply { + rollback_unregistered_spawn_child( + kernel, + process, + None, + "POSIX spawn attribute application", + ); + return Err(error); + } + Ok(()) +} + fn canonicalize_host_runtime_posix_stdin( kernel: &mut SidecarKernel, pid: u32, @@ -2094,7 +3276,7 @@ pub(super) fn preapply_posix_spawn_file_actions( cwd: &str, requested_pgid: Option, inherited_mappings: &[[u32; 2]], - actions: &[JavascriptPosixSpawnFileAction], + actions: &[ProcessSpawnFileAction], ) -> Result { let process = kernel .spawn_process_with_process_group_preserving_cloexec( @@ -2105,6 +3287,7 @@ pub(super) fn preapply_posix_spawn_file_actions( parent_pid: Some(parent_pid), env: BTreeMap::new(), cwd: Some(cwd.to_owned()), + ..SpawnOptions::default() }, requested_pgid, ) @@ -2178,7 +3361,7 @@ pub(super) fn install_preapplied_posix_spawn_file_actions( } for entry in &prepared.fds { kernel - .fd_install_transfer_at( + .fd_install_spawn_transfer_at( EXECUTION_DRIVER_NAME, process.pid(), entry.fd, @@ -2209,7 +3392,7 @@ pub(super) struct JavascriptSpawnAttributes { } pub(super) fn javascript_spawn_attributes( - options: &JavascriptChildProcessSpawnOptions, + options: &ProcessLaunchOptions, ) -> Result { if options.spawn_attr_flags & !SUPPORTED_POSIX_SPAWN_FLAGS != 0 { return Err(SidecarError::InvalidState(format!( @@ -2231,28 +3414,32 @@ pub(super) fn javascript_spawn_attributes( let new_session = options.spawn_attr_flags & POSIX_SPAWN_SETSID != 0; if new_session && options.spawn_attr_flags & POSIX_SPAWN_SETPGROUP != 0 { - return Err(SidecarError::InvalidState(String::from( - "EPERM: POSIX_SPAWN_SETSID cannot be combined with POSIX_SPAWN_SETPGROUP", - ))); + return Err(SidecarError::host( + "EPERM", + String::from("POSIX_SPAWN_SETSID cannot be combined with POSIX_SPAWN_SETPGROUP"), + )); } if new_session && options.detached { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: POSIX_SPAWN_SETSID cannot be combined with detached child-process mode", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("POSIX_SPAWN_SETSID cannot be combined with detached child-process mode"), + )); } if options.spawn_attr_flags & (POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER) != 0 && options.spawn_sched_priority.unwrap_or_default() != 0 { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: SCHED_OTHER requires scheduling priority zero", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("SCHED_OTHER requires scheduling priority zero"), + )); } if options.spawn_attr_flags & POSIX_SPAWN_SETSCHEDULER != 0 && options.spawn_sched_policy.unwrap_or_default() != 0 { - return Err(SidecarError::InvalidState(String::from( - "EPERM: requested POSIX spawn scheduler policy requires host privilege", - ))); + return Err(SidecarError::host( + "EPERM", + String::from("requested POSIX spawn scheduler policy requires host privilege"), + )); } if options.spawn_attr_flags & POSIX_SPAWN_SETPGROUP == 0 { @@ -2295,230 +3482,830 @@ pub(super) fn apply_child_process_argv0( } } -impl NativeSidecar -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - pub(crate) async fn pump_child_process_events( - &mut self, - vm_id: &str, - ) -> Result { - let root_process_ids = self - .vms - .get(vm_id) - .map(|vm| vm.active_processes.keys().cloned().collect::>()) - .unwrap_or_default(); - let mut child_candidates = Vec::new(); - - for process_id in root_process_ids { - if self - .vms - .get(vm_id) - .is_some_and(|vm| vm.detached_child_processes.contains(&process_id)) - { - continue; - } - let mut child_paths = Vec::new(); - if let Some(root) = self - .vms - .get(vm_id) - .and_then(|vm| vm.active_processes.get(&process_id)) - { - Self::collect_attached_child_paths(root, &mut Vec::new(), &mut child_paths); - } - child_candidates.extend( - child_paths - .into_iter() - .map(|child_path| (process_id.clone(), child_path)), - ); - } - - if child_candidates.is_empty() { - return Ok(false); +pub(super) fn validate_process_launch_request( + request: &ProcessLaunchRequest, + exec_replacement: bool, +) -> Result<(), SidecarError> { + if request.command.is_empty() { + return Err(SidecarError::host( + "ENOENT", + "process launch executable path is empty", + )); + } + javascript_spawn_attributes(&request.options)?; + if exec_replacement { + if request.options.executable_fd.is_some() { + return Err(SidecarError::host( + "EINVAL", + "executableFd is only valid for process.exec_fd_image_commit", + )); } - let start = self - .vms - .get(vm_id) - .map(|vm| vm.attached_child_event_cursor % child_candidates.len()) - .unwrap_or_default(); - child_candidates.rotate_left(start); - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.attached_child_event_cursor = (start + 1) % child_candidates.len(); + if request.options.shell || request.options.detached { + return Err(SidecarError::host( + "EINVAL", + "execve does not accept shell or detached process options", + )); } + } + Ok(()) +} - let vm_work_limit = self.config.runtime.fairness.vm_quantum_operations; - let child_work_limit = self.config.runtime.fairness.capability_quantum_operations; - let mut emitted_any = false; - let mut work = 0usize; - let mut child_work = vec![0usize; child_candidates.len()]; - let mut yielded = false; - let mut delivery_backpressured = false; - - loop { - let mut emitted_this_round = false; - for (candidate_index, (process_id, child_path)) in child_candidates.iter().enumerate() { - if work >= vm_work_limit { - yielded = true; - break; - } - if child_work[candidate_index] >= child_work_limit { - yielded = true; - continue; - } +pub(super) fn validate_wasm_fd_image_commit_request( + request: &ProcessLaunchRequest, +) -> Result<(), SidecarError> { + if !request.options.local_replacement || request.options.executable_fd.is_none() { + return Err(SidecarError::host( + "EINVAL", + String::from("fd-image exec commit requires localReplacement and executableFd"), + )); + } + if request.options.shell || request.options.detached || request.options.cwd.is_some() { + return Err(SidecarError::host( + "EINVAL", + String::from("fexecve does not accept shell, detached, or cwd options"), + )); + } + Ok(()) +} - let Some(child_process_id) = child_path.last().cloned() else { - continue; - }; - let parent_path = child_path[..child_path.len() - 1] - .iter() - .map(String::as_str) - .collect::>(); +fn reserve_child_process_sync_budget( + count_budget: &Arc, + bytes_budget: &Arc, + max_buffer: usize, + input_bytes: usize, +) -> Result<(VmPendingBudgetReservation, VmPendingBudgetReservation), SidecarError> { + let count_reservation = VmPendingBudgetReservation::try_new(Arc::clone(count_budget), 1) + .ok_or_else(|| { + let limit = count_budget.limit(); + let observed = count_budget.used().saturating_add(1); + SidecarError::host_resource_limit( + "limits.process.maxPendingChildSyncCount", + limit, + observed, + format!( + "pending child-process sync calls ({observed}) exceed limits.process.maxPendingChildSyncCount ({limit}); raise limits.process.maxPendingChildSyncCount" + ), + ) + })?; - // Deadline and capacity wakes must service the child's parked - // synchronous RPC even when a standalone WASM parent owns - // output delivery through child_process.poll. - self.recheck_child_deferred_kernel_wait_rpc( - vm_id, - process_id, - &parent_path, - &child_process_id, - )?; + // stdout and stderr each retain up to maxBuffer plus one overflow byte, + // while request input remains live until the child has been spawned and + // its stdin has been written. Reserve the conservative combined envelope + // before starting the child so rejection has no process side effects. + let retained_bytes = max_buffer + .saturating_add(1) + .saturating_mul(2) + .saturating_add(input_bytes); + let bytes_reservation = + VmPendingBudgetReservation::try_new(Arc::clone(bytes_budget), retained_bytes).ok_or_else( + || { + let limit = bytes_budget.limit(); + let observed = bytes_budget.used().saturating_add(retained_bytes); + SidecarError::host_resource_limit( + "limits.process.maxPendingChildSyncBytes", + limit, + observed, + format!( + "pending child-process sync retained bytes ({observed}) exceed limits.process.maxPendingChildSyncBytes ({limit}); raise limits.process.maxPendingChildSyncBytes" + ), + ) + }, + )?; + Ok((count_reservation, bytes_reservation)) +} - // The standalone WASM runner pulls descendant output through - // child_process.poll while implementing waitpid. Keep stream - // and exit delivery single-owner; the parked kernel wait was - // already rechecked above without leasing either event lane. - let parent_is_pull_driven_wasm = self - .vms - .get(vm_id) - .and_then(|vm| vm.active_processes.get(process_id)) - .and_then(|root| Self::active_process_by_path(root, &parent_path)) - .is_some_and(|parent| parent.runtime == GuestRuntimeKind::WebAssembly); - if parent_is_pull_driven_wasm { - continue; - } - self.expire_child_process_sync_if_needed( - vm_id, - process_id, - &parent_path, - &child_process_id, - )?; +#[derive(Debug, Clone, PartialEq, Eq)] +struct SpawnedChildIdentity { + child_process_id: String, + pid: u32, +} - let event = match self - .poll_descendant_javascript_child_process( - vm_id, - process_id, - &parent_path, - &child_process_id, - 0, - false, - ) - .await - { - Ok(event) => event, - Err(error) if is_javascript_child_process_gone_error(&error) => continue, - Err(error) => return Err(error), - }; - if event.is_null() { - continue; - } - if !self.route_child_process_bridge_event( - vm_id, - process_id, - &parent_path, - &child_process_id, - event, - )? { - yielded = true; - delivery_backpressured = true; - break; - } - emitted_any = true; - emitted_this_round = true; - work += 1; - child_work[candidate_index] += 1; - } - if yielded || !emitted_this_round { - break; - } +fn checked_child_process_sync_deadline( + timeout_ms: Option, +) -> Result, SidecarError> { + let Some(timeout_ms) = timeout_ms else { + return Ok(None); + }; + Instant::now() + .checked_add(Duration::from_millis(timeout_ms)) + .map(Some) + .ok_or_else(|| { + SidecarError::Host( + HostServiceError::new( + "EINVAL", + format!( + "child process timeout {timeout_ms}ms cannot be represented by the host monotonic clock" + ), + ) + .with_details(json!({ + "field": "timeout", + "timeoutMs": timeout_ms, + })), + ) + }) +} + +fn parse_spawned_child_identity(spawned: &Value) -> Result { + let child_process_id = spawned + .get("childId") + .and_then(Value::as_str) + .filter(|child_process_id| !child_process_id.is_empty()) + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "child_process.spawn_sync response is missing childId", + )) + })? + .to_owned(); + let pid = spawned + .get("pid") + .and_then(Value::as_u64) + .and_then(|pid| u32::try_from(pid).ok()) + .filter(|pid| *pid != 0) + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "child_process.spawn_sync response is missing a valid pid", + )) + })?; + Ok(SpawnedChildIdentity { + child_process_id, + pid, + }) +} + +#[cfg(test)] +#[derive(Debug)] +struct ChildSyncTimerAdmissionFailureHook { + vm_id: String, + root_process_id: String, + parent_path: Vec, + consumed: bool, + rolled_back_child: Option, +} + +#[cfg(test)] +static CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK: Mutex> = + Mutex::new(None); + +#[cfg(test)] +fn child_sync_test_hook_matches( + hook: &ChildSyncTimerAdmissionFailureHook, + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], +) -> bool { + hook.vm_id == vm_id + && hook.root_process_id == root_process_id + && hook + .parent_path + .iter() + .map(String::as_str) + .eq(parent_path.iter().copied()) +} + +#[cfg(test)] +fn force_child_sync_timer_admission_failure_for_test( + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], +) -> bool { + let mut hook = CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK + .lock() + .unwrap_or_else(|poisoned| { + eprintln!("ERR_AGENTOS_CHILD_SYNC_TEST_HOOK_POISONED: recovering timer-admission hook"); + poisoned.into_inner() + }); + let Some(hook) = hook.as_mut() else { + return false; + }; + if hook.consumed || !child_sync_test_hook_matches(hook, vm_id, root_process_id, parent_path) { + return false; + } + hook.consumed = true; + true +} + +#[cfg(test)] +fn record_child_sync_rollback_for_test( + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], + identity: &SpawnedChildIdentity, +) { + let mut hook = CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK + .lock() + .unwrap_or_else(|poisoned| { + eprintln!("ERR_AGENTOS_CHILD_SYNC_TEST_HOOK_POISONED: recovering rollback hook"); + poisoned.into_inner() + }); + let Some(hook) = hook.as_mut() else { + return; + }; + if hook.consumed && child_sync_test_hook_matches(hook, vm_id, root_process_id, parent_path) { + hook.rolled_back_child = Some(identity.clone()); + } +} + +fn admit_child_process_sync_timer( + runtime: &agentos_runtime::RuntimeContext, + notify: Arc, + deadline: Instant, + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], +) -> Result<(), SidecarError> { + #[cfg(not(test))] + let _ = (vm_id, root_process_id, parent_path); + #[cfg(test)] + { + if force_child_sync_timer_admission_failure_for_test(vm_id, root_process_id, parent_path) { + return Err(SidecarError::Execution(format!( + "ERR_AGENTOS_TASK_ADMISSION_CLOSED: forced test failure for vm={vm_id} root={root_process_id}" + ))); } + } + let delay = deadline.saturating_duration_since(Instant::now()); + runtime + .spawn(agentos_runtime::TaskClass::Timer, async move { + tokio::time::sleep(delay).await; + notify.notify_one(); + }) + .map(|_| ()) + .map_err(SidecarError::from) +} - if delivery_backpressured { - // The parent V8 lane is bounded and deliberately nonblocking: a - // blocking send here can deadlock when that isolate is waiting on - // a synchronous sidecar RPC. Retry after a short gap so the - // isolate can drain its lane and the stdio loop can continue - // servicing control requests. An immediate self-notification - // hot-loops this pump and can starve session/new for seconds. - let notify = Arc::clone(&self.process_event_notify); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(2)).await; - notify.notify_one(); - }); - } else if yielded { - self.process_event_notify.notify_one(); +#[cfg(test)] +mod child_process_sync_budget_tests { + use super::*; + + fn budget( + limit: usize, + tracked: agentos_bridge::queue_tracker::TrackedLimit, + ) -> Arc { + VmPendingByteBudget::new(limit, tracked) + } + + fn assert_limit(error: SidecarError, name: &str, limit: u64, observed: u64) { + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + let SidecarError::Host(host) = error else { + panic!("expected typed host limit error"); + }; + let details = host.details.expect("typed limit details"); + assert_eq!(details["limitName"], name); + assert_eq!(details["limit"], limit); + assert_eq!(details["observed"], observed); + } + + #[test] + fn pending_child_sync_count_and_bytes_admit_at_limit_reject_over_and_reclaim() { + let count = budget( + 1, + agentos_bridge::queue_tracker::TrackedLimit::PendingChildProcessSyncCount, + ); + let bytes = budget( + 12, + agentos_bridge::queue_tracker::TrackedLimit::PendingChildProcessSyncBytes, + ); + + // maxBuffer=4 reserves five bytes for each output stream, plus two + // request-input bytes: exactly the configured twelve-byte envelope. + let admitted = reserve_child_process_sync_budget(&count, &bytes, 4, 2) + .expect("the exact count and byte limits must be admitted"); + assert_eq!(count.used(), 1); + assert_eq!(bytes.used(), 12); + + let count_error = reserve_child_process_sync_budget(&count, &bytes, 0, 0) + .expect_err("one more pending call must fail before spawning a child"); + assert_limit(count_error, "limits.process.maxPendingChildSyncCount", 1, 2); + assert_eq!(count.used(), 1, "rejection must not change count usage"); + assert_eq!(bytes.used(), 12, "rejection must not change byte usage"); + + drop(admitted); + assert_eq!(count.used(), 0, "completion/removal must reclaim count"); + assert_eq!(bytes.used(), 0, "completion/removal must reclaim bytes"); + + let too_small_bytes = budget( + 11, + agentos_bridge::queue_tracker::TrackedLimit::PendingChildProcessSyncBytes, + ); + let byte_error = reserve_child_process_sync_budget(&count, &too_small_bytes, 4, 2) + .expect_err("an over-limit byte envelope must fail before spawning a child"); + assert_limit( + byte_error, + "limits.process.maxPendingChildSyncBytes", + 11, + 12, + ); + assert_eq!( + count.used(), + 0, + "byte rejection must roll back its count reservation" + ); + assert_eq!(too_small_bytes.used(), 0); + } + + #[test] + fn child_sync_deadline_handles_u64_max_without_panicking() { + // Linux can represent this deadline even though some host Instant + // implementations cannot. Either result is valid; the contract is + // that checked conversion never panics and any rejection is typed. + if let Err(error) = checked_child_process_sync_deadline(Some(u64::MAX)) { + assert_eq!(error.code(), Some("EINVAL")); + let SidecarError::Host(host) = error else { + panic!("unrepresentable timeout must be a typed host error"); + }; + assert_eq!( + host.details.expect("timeout details")["timeoutMs"], + u64::MAX + ); } - Ok(emitted_any) + assert!(checked_child_process_sync_deadline(None) + .expect("an absent timeout is valid") + .is_none()); + assert!(checked_child_process_sync_deadline(Some(0)) + .expect("a zero timeout is valid") + .is_some()); } - fn expire_child_process_sync_if_needed( + #[test] + fn spawned_child_identity_requires_nonempty_id_and_nonzero_u32_pid() { + assert!(parse_spawned_child_identity(&json!({ "pid": 1 })).is_err()); + assert!(parse_spawned_child_identity(&json!({ "childId": "", "pid": 1 })).is_err()); + assert!(parse_spawned_child_identity(&json!({ "childId": "child-1", "pid": 0 })).is_err()); + assert!(parse_spawned_child_identity(&json!({ + "childId": "child-1", + "pid": u64::from(u32::MAX) + 1, + })) + .is_err()); + assert_eq!( + parse_spawned_child_identity(&json!({ "childId": "child-1", "pid": 42 })) + .expect("valid child identity"), + SpawnedChildIdentity { + child_process_id: String::from("child-1"), + pid: 42, + } + ); + } +} + +impl NativeSidecar +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + fn child_process_ids_at_path( + &self, + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], + ) -> Result, SidecarError> { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let parent = Self::active_process_by_path(root, parent_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path during spawnSync admission: {}", + parent_path.join("/") + )) + })?; + Ok(parent.child_processes.keys().cloned().collect()) + } + + fn rollback_registered_child_process_sync( &mut self, vm_id: &str, - process_id: &str, + root_process_id: &str, parent_path: &[&str], - child_process_id: &str, - ) -> Result<(), SidecarError> { - let signal = { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); + prior_child_ids: &BTreeSet, + hinted_child_id: Option<&str>, + hinted_pid: Option, + context: &str, + ) { + let bridge = self.bridge.clone(); + let Some(vm) = self.vms.get_mut(vm_id) else { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK: {context}: VM {vm_id} disappeared before rollback" + ); + return; + }; + let identity = { + let Some(root) = vm.active_processes.get(root_process_id) else { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK: {context}: root process {root_process_id} disappeared before rollback" + ); + return; }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(()); + let Some(parent) = Self::active_process_by_path(root, parent_path) else { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK: {context}: parent path {} disappeared before rollback", + parent_path.join("/") + ); + return; }; - let Some(parent) = Self::active_process_by_path_mut(root, parent_path) else { - return Ok(()); + let new_children = parent + .child_processes + .iter() + .filter(|(child_id, _)| !prior_child_ids.contains(*child_id)) + .collect::>(); + let selected = if new_children.len() == 1 { + new_children.first().copied() + } else { + let matching = new_children + .iter() + .copied() + .filter(|(child_id, child)| { + hinted_child_id.is_none_or(|hint| hint == child_id.as_str()) + && hinted_pid.is_none_or(|hint| hint == child.kernel_pid) + }) + .collect::>(); + (matching.len() == 1).then(|| matching[0]) }; - let Some(pending) = parent.pending_child_process_sync.get_mut(child_process_id) else { - return Ok(()); + let Some((child_process_id, child)) = selected else { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK: {context}: could not identify exactly one newly registered child under {root_process_id}/{} (childId={hinted_child_id:?}, pid={hinted_pid:?}, candidates={})", + parent_path.join("/"), + new_children.len() + ); + return; }; - if pending.kill_sent - || pending - .deadline - .is_none_or(|deadline| Instant::now() < deadline) + SpawnedChildIdentity { + child_process_id: child_process_id.clone(), + pid: child.kernel_pid, + } + }; + + #[cfg(test)] + record_child_sync_rollback_for_test(vm_id, root_process_id, parent_path, &identity); + + let terminating_kernel_pids = { + let Some(root) = vm.active_processes.get(root_process_id) else { + return; + }; + let Some(parent) = Self::active_process_by_path(root, parent_path) else { + return; + }; + let Some(child) = parent.child_processes.get(&identity.child_process_id) else { + return; + }; + Self::terminating_process_tree_kernel_pids(child) + }; + for kernel_pid in terminating_kernel_pids { + if let Err(error) = retire_managed_process_routes(&bridge, vm_id, vm, kernel_pid) { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK_ROUTE: {context}: failed to retire managed routes for PID {kernel_pid}: {error}" + ); + } + } + + let mut child = { + let Some(root) = vm.active_processes.get_mut(root_process_id) else { + return; + }; + let Some(parent) = Self::active_process_by_path_mut(root, parent_path) else { + return; + }; + if parent + .pending_child_process_sync + .get(&identity.child_process_id) + .is_some_and(|pending| pending.pid == identity.pid) { - None - } else { - pending.kill_sent = true; - pending.timed_out = true; - Some(pending.timeout_signal.clone()) + parent + .pending_child_process_sync + .remove(&identity.child_process_id); } + let Some(child) = parent.child_processes.remove(&identity.child_process_id) else { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK: {context}: child {} disappeared during rollback", + identity.child_process_id + ); + return; + }; + child }; - if let Some(signal) = signal { - self.kill_descendant_javascript_child_process( - vm_id, - process_id, - parent_path, - child_process_id, - &signal, - )?; + + if let Err(error) = release_inherited_child_raw_mode(&mut vm.kernel, &child) { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK_TTY: {context}: failed to release child {} raw mode: {error}", + identity.child_process_id + ); + } + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let unix_address_registry = Arc::clone(&vm.unix_address_registry); + terminate_child_process_tree( + &mut vm.kernel, + &mut child, + &kernel_readiness, + &unix_address_registry, + ); + if let Err(error) = child.execution.terminate() { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK_EXECUTOR: {context}: failed to terminate child {} runtime: {error}", + identity.child_process_id + ); + } + child.kernel_handle.finish(127); + if let Err(error) = vm.kernel.wait_and_reap(child.kernel_pid) { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_ROLLBACK_REAP: {context}: failed to reap child {} PID {}: {error}", + identity.child_process_id, child.kernel_pid + ); } - Ok(()) } - fn route_child_process_bridge_event( - &mut self, + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn arm_child_sync_timer_admission_failure_for_test( vm_id: &str, - process_id: &str, + root_process_id: &str, parent_path: &[&str], - child_process_id: &str, - event: Value, + ) { + let mut hook = CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + assert!( + hook.is_none(), + "child sync timer-admission hook already armed" + ); + *hook = Some(ChildSyncTimerAdmissionFailureHook { + vm_id: vm_id.to_owned(), + root_process_id: root_process_id.to_owned(), + parent_path: parent_path.iter().map(|part| (*part).to_owned()).collect(), + consumed: false, + rolled_back_child: None, + }); + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn take_child_sync_rollback_for_test() -> Option<(String, u32)> { + CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_mut() + .and_then(|hook| hook.rolled_back_child.take()) + .map(|identity| (identity.child_process_id, identity.pid)) + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn clear_child_sync_timer_admission_failure_for_test() { + *CHILD_SYNC_TIMER_ADMISSION_FAILURE_HOOK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = None; + } + + pub(crate) async fn pump_child_process_events( + &mut self, + vm_id: &str, ) -> Result { - let event_type = event - .get("type") - .and_then(Value::as_str) + let mut emitted_any = false; + let root_process_ids = self + .vms + .get(vm_id) + .map(|vm| vm.active_processes.keys().cloned().collect::>()) .unwrap_or_default(); - let chunk = match event_type { + let mut child_candidates = Vec::new(); + + for process_id in root_process_ids { + if self + .vms + .get(vm_id) + .is_some_and(|vm| vm.detached_child_processes.contains(&process_id)) + { + continue; + } + let mut child_paths = Vec::new(); + if let Some(root) = self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(&process_id)) + { + Self::collect_attached_child_paths(root, &mut Vec::new(), &mut child_paths); + } + child_candidates.extend( + child_paths + .into_iter() + .map(|child_path| (process_id.clone(), child_path)), + ); + } + + if child_candidates.is_empty() { + return Ok(emitted_any); + } + let start = self + .vms + .get(vm_id) + .map(|vm| vm.attached_child_event_cursor % child_candidates.len()) + .unwrap_or_default(); + child_candidates.rotate_left(start); + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.attached_child_event_cursor = (start + 1) % child_candidates.len(); + } + + let vm_work_limit = self.config.runtime.fairness.vm_quantum_operations; + let child_work_limit = self.config.runtime.fairness.capability_quantum_operations; + let mut work = 0usize; + let mut child_work = vec![0usize; child_candidates.len()]; + let mut yielded = false; + let mut delivery_backpressured = false; + + loop { + let mut emitted_this_round = false; + for (candidate_index, (process_id, child_path)) in child_candidates.iter().enumerate() { + if work >= vm_work_limit { + yielded = true; + break; + } + if child_work[candidate_index] >= child_work_limit { + yielded = true; + continue; + } + + let Some(child_process_id) = child_path.last().cloned() else { + continue; + }; + let parent_path = child_path[..child_path.len() - 1] + .iter() + .map(String::as_str) + .collect::>(); + + // Deadline and capacity wakes must service the child's parked + // synchronous RPC even when a standalone WASM parent owns + // output delivery through child_process.poll. + self.recheck_child_deferred_kernel_wait_rpc( + vm_id, + process_id, + &parent_path, + &child_process_id, + )?; + self.service_descendant_guest_wait( + vm_id, + process_id, + child_path + .iter() + .map(String::as_str) + .collect::>() + .as_slice(), + None, + )?; + self.service_descendant_kernel_read( + vm_id, + process_id, + child_path + .iter() + .map(String::as_str) + .collect::>() + .as_slice(), + None, + )?; + + self.expire_child_process_sync_if_needed( + vm_id, + process_id, + &parent_path, + &child_process_id, + )?; + + let guest_owns_child_output = self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|root| Self::active_process_by_path(root, &parent_path)) + .is_some_and(|parent| { + parent.execution.descendant_output_ownership() + == DescendantOutputOwnership::GuestDescriptors + && parent + .child_processes + .get(&child_process_id) + .is_some_and(|child| child.child_process_bridge_owns_output) + && !parent + .pending_child_process_sync + .contains_key(&child_process_id) + }); + if guest_owns_child_output { + // Standalone WASM consumes this child's output and exit + // through child_process.poll. The proactive bridge pump + // still services controls and deferred kernel waits above, + // but must not steal or translate the pull-owned event. + continue; + } + + let event = match self + .poll_descendant_process(vm_id, process_id, &parent_path, &child_process_id, 0) + .await + { + Ok(event) => event, + Err(error) if is_javascript_child_process_gone_error(&error) => continue, + Err(error) => return Err(error), + }; + if event.is_null() { + continue; + } + if !self.route_child_process_bridge_event( + vm_id, + process_id, + &parent_path, + &child_process_id, + event, + )? { + yielded = true; + delivery_backpressured = true; + break; + } + emitted_any = true; + emitted_this_round = true; + work += 1; + child_work[candidate_index] += 1; + } + if yielded || !emitted_this_round { + break; + } + } + + if delivery_backpressured { + // The parent V8 lane is bounded and deliberately nonblocking: a + // blocking send here can deadlock when that isolate is waiting on + // a synchronous sidecar RPC. Retry after a short gap so the + // isolate can drain its lane and the stdio loop can continue + // servicing control requests. An immediate self-notification + // hot-loops this pump and can starve session/new for seconds. + let notify = Arc::clone(&self.process_event_notify); + let runtime = self + .vms + .get(vm_id) + .ok_or_else(|| SidecarError::InvalidState(format!("unknown VM {vm_id}")))? + .runtime_context + .clone(); + runtime + .spawn(agentos_runtime::TaskClass::Timer, async move { + tokio::time::sleep(Duration::from_millis(2)).await; + notify.notify_one(); + }) + .map_err(SidecarError::from)?; + } else if yielded { + self.process_event_notify.notify_one(); + } + Ok(emitted_any) + } + + fn expire_child_process_sync_if_needed( + &mut self, + vm_id: &str, + process_id: &str, + parent_path: &[&str], + child_process_id: &str, + ) -> Result<(), SidecarError> { + let signal = { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(parent) = Self::active_process_by_path_mut(root, parent_path) else { + return Ok(()); + }; + let Some(pending) = parent.pending_child_process_sync.get_mut(child_process_id) else { + return Ok(()); + }; + if pending.kill_sent + || pending + .deadline + .is_none_or(|deadline| Instant::now() < deadline) + { + None + } else { + pending.kill_sent = true; + pending.timed_out = true; + Some(pending.timeout_signal.clone()) + } + }; + if let Some(signal) = signal { + self.kill_descendant_javascript_child_process( + vm_id, + process_id, + parent_path, + child_process_id, + &signal, + )?; + } + Ok(()) + } + + fn route_child_process_bridge_event( + &mut self, + vm_id: &str, + process_id: &str, + parent_path: &[&str], + child_process_id: &str, + event: Value, + ) -> Result { + let event_type = event + .get("type") + .and_then(Value::as_str) + .unwrap_or_default(); + let chunk = match event_type { "stdout" | "stderr" => Some(javascript_sync_rpc_bytes_arg( &[event.get("data").cloned().unwrap_or(Value::Null)], 0, @@ -2674,19 +4461,21 @@ where ); } } - PendingChildProcessSyncCompletion::Python { request_id } => { - self.respond_python_rpc( - vm_id, - process_id, - request_id, - Ok(PythonVfsRpcResponsePayload::SubprocessRun { - exit_code, - stdout: String::from_utf8_lossy(&pending.stdout).into_owned(), - stderr: String::from_utf8_lossy(&pending.stderr).into_owned(), - max_buffer_exceeded: pending.max_buffer_exceeded, - }), - )?; - } + PendingChildProcessSyncCompletion::Direct(reply) => reply + .succeed_json(json!({ + "pid": pending.pid, + "stdout": String::from_utf8_lossy(&pending.stdout), + "stderr": String::from_utf8_lossy(&pending.stderr), + "code": exit_code, + "signal": if pending.timed_out { + Value::String(pending.timeout_signal) + } else { + Value::Null + }, + "timedOut": pending.timed_out, + "maxBufferExceeded": pending.max_buffer_exceeded, + })) + .map_err(SidecarError::from)?, } } Ok(true) @@ -2761,14 +4550,7 @@ where } else { match process.poll_execution_event(Duration::ZERO).await { Ok(event) => ProcessPollResult::Event(Box::new(event)), - Err(SidecarError::Execution(message)) - if (process.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { + Err(SidecarError::ExecutionEventChannelClosed { .. }) => { ProcessPollResult::RecoverClosedChannel } Err(error) => return Err(error), @@ -2802,6 +4584,48 @@ where }; let PolledExecutionEvent { event, reservation } = event; match event { + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { + operation, + reply, + }) => { + drop(reservation); + let Some((operation, reply)) = dispatch_context_host_operation( + self, + vm_id, + &root_process_id, + operation, + reply, + ) + .await? + else { + continue; + }; + let Some(vm) = self.vms.get_mut(vm_id) else { + break; + }; + let generation = vm.generation; + let (kernel, active_processes) = + (&mut vm.kernel, &mut vm.active_processes); + let Some(process) = active_processes.get_mut(&detached_process_id) + else { + break; + }; + let effects = dispatch_host_operation( + generation, kernel, process, operation, reply, + )?; + if effects.may_make_fd_readable { + Self::wake_ready_deferred_fd_reads(vm)?; + } + if effects.may_make_fd_writable { + Self::wake_ready_deferred_fd_writes(vm)?; + } + } + ActiveExecutionEvent::Common(other) => { + drop(reservation); + return Err(SidecarError::InvalidState(format!( + "unsupported common detached-child event: {other:?}" + ))); + } ActiveExecutionEvent::Stdout(chunk) => { let envelope = ProcessEventEnvelope { connection_id, @@ -2890,7 +4714,7 @@ where emitted_any = true; break; } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::HostRpcRequest(request) => { drop(reservation); self.handle_javascript_sync_rpc_request( vm_id, @@ -2899,37 +4723,41 @@ where ) .await?; } - ActiveExecutionEvent::JavascriptSyncRpcCompletion(completion) => { + ActiveExecutionEvent::HostCallCompletion(completion) => { drop(reservation); - self.handle_javascript_sync_rpc_completion( - vm_id, - &root_process_id, - completion, - )?; + self.handle_host_call_completion(vm_id, &root_process_id, completion)?; } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { + ActiveExecutionEvent::ManagedStreamReadRecheck(pending) => { drop(reservation); - self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request) - .await?; + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "stream read re-entry targeted detached child routing", + )) + .map_err(SidecarError::from)?; } - ActiveExecutionEvent::PythonSocketConnectCompletion(completion) => { + ActiveExecutionEvent::ManagedUdpPollRecheck(pending) => { drop(reservation); - self.handle_python_socket_connect_completion( - vm_id, - &root_process_id, - *completion, - )?; + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "UDP poll re-entry targeted detached child routing", + )) + .map_err(SidecarError::from)?; } ActiveExecutionEvent::SignalState { signal, registration, } => { drop(reservation); - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.signal_states - .entry(root_process_id.clone()) - .or_default() - .insert(signal, registration); + if let Some(process) = self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(&root_process_id)) + { + apply_kernel_signal_registration(process, signal, ®istration)?; } } } @@ -2949,27 +4777,17 @@ where break; } let event = match self - .poll_descendant_javascript_child_process( + .poll_descendant_process( vm_id, &root_process_id, &parent_path, child_process_id, 0, - false, ) .await { Ok(event) => event, - Err(SidecarError::InvalidState(message)) - if message.contains("unknown child process") - || message.contains("unknown child process path") => - { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - break; - } - Err(error) if is_javascript_child_process_gone_error(&error) => { + Err(error) if is_javascript_child_process_gone_error(&error) => { if let Some(vm) = self.vms.get_mut(vm_id) { vm.detached_child_processes.remove(&detached_process_id); } @@ -3165,13 +4983,13 @@ where #[allow(dead_code)] pub(crate) fn resolve_javascript_child_process_execution( &self, - vm: &VmState, + vm: &mut VmState, parent_env: &BTreeMap, parent_guest_cwd: &str, parent_host_cwd: &Path, - request: &JavascriptChildProcessSpawnRequest, + request: &ProcessLaunchRequest, ) -> Result { - self.resolve_javascript_child_process_execution_with_mode( + Self::resolve_javascript_child_process_execution_with_mode( vm, parent_env, parent_guest_cwd, @@ -3186,19 +5004,19 @@ where // are distinct security inputs, not interchangeable options. #[allow(clippy::too_many_arguments)] pub(crate) fn resolve_javascript_child_process_execution_with_mode( - &self, - vm: &VmState, + vm: &mut VmState, parent_env: &BTreeMap, parent_guest_cwd: &str, parent_host_cwd: &Path, - request: &JavascriptChildProcessSpawnRequest, + request: &ProcessLaunchRequest, exact_exec_path: bool, search_path_override: Option<&str>, ) -> Result { if exact_exec_path && search_path_override.is_some() { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: exact spawn path cannot also request PATH search", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("exact spawn path cannot also request PATH search"), + )); } let mut runtime_env = parent_env.clone(); runtime_env.extend(request.options.internal_bootstrap_env.clone()); @@ -3247,7 +5065,7 @@ where if guest_cwd == parent_guest_cwd { normalize_host_path(parent_host_cwd) } else if candidate.is_absolute() { - shadow_path_for_guest(vm, &guest_cwd) + runtime_asset_path_for_guest(vm, &guest_cwd) } else { vm.host_cwd.clone() } @@ -3267,7 +5085,14 @@ where is_posix_shell_builtin(command) || shell_first_token_requires_shell(command) }); if requires_shell { - if !vm.command_guest_paths.contains_key("sh") { + if resolve_guest_command_entrypoint( + vm, + &guest_cwd, + "sh", + env.get("PATH").map(String::as_str), + ) + .is_none() + { return Err(SidecarError::InvalidState(format!( "shell-mode child_process command requires /bin/sh, which is not \ installed in this VM (install a software package that provides sh, \ @@ -3306,6 +5131,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: true, + adapter_policy: ExecutionAdapterPolicy::BINDING, }); } @@ -3357,11 +5183,12 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } let resolves_to_registered_node_runtime = exact_exec_path - && registered_command_name_for_path(vm, &command) + && registered_command_name_for_path(&vm.kernel, &command) .is_some_and(|name| is_node_runtime_command(&name)); if (!exact_exec_path || resolves_to_registered_node_runtime) && is_node_runtime_command(&command) @@ -3395,6 +5222,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -3413,6 +5241,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -3434,6 +5263,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -3506,6 +5336,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -3530,8 +5361,8 @@ where search_path_override.or_else(|| env.get("PATH").map(String::as_str)), ) } - .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?; - let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); + .ok_or_else(|| SidecarError::host("ENOENT", format!("command not found: {command}")))?; + let host_entrypoint = runtime_launch_path_for_guest(vm, &guest_entrypoint); let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| { Path::new(&guest_entrypoint) .file_name() @@ -3562,6 +5393,7 @@ where host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } prepare_guest_runtime_env( @@ -3585,6 +5417,7 @@ where host_cwd, wasm_permission_tier, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::KERNEL_HOST_CALL_POSIX, }) } @@ -3594,13 +5427,16 @@ where parent_env: &BTreeMap, parent_guest_cwd: &str, parent_host_cwd: &Path, - request: &mut JavascriptChildProcessSpawnRequest, + request: &mut ProcessLaunchRequest, ) -> Result { const MAX_SHEBANG_REDIRECTS: usize = 4; let mut resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution_with_mode( + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Self::resolve_javascript_child_process_execution_with_mode( vm, parent_env, parent_guest_cwd, @@ -3623,13 +5459,17 @@ where return Ok(resolved); } if redirects == MAX_SHEBANG_REDIRECTS { - return Err(SidecarError::Execution(format!( - "ELOOP: exceeded {MAX_SHEBANG_REDIRECTS} shebang redirects" - ))); + return Err(SidecarError::host( + "ELOOP", + format!("exceeded {MAX_SHEBANG_REDIRECTS} shebang redirects"), + )); } resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution_with_mode( + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Self::resolve_javascript_child_process_execution_with_mode( vm, parent_env, parent_guest_cwd, @@ -3644,43 +5484,21 @@ where Ok(resolved) } - pub(crate) async fn spawn_javascript_child_process( + pub(crate) async fn spawn_child_process( &mut self, vm_id: &str, process_id: &str, - mut request: JavascriptChildProcessSpawnRequest, + mut request: ProcessLaunchRequest, ) -> Result { let spawn_attributes = javascript_spawn_attributes(&request.options)?; let requested_pgid = spawn_attributes.process_group; - let parent_sync_roots = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let parent = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - (parent.host_write_dirty_recursive() - || !parent.clean_host_writes_are_observable_recursive()) - .then(|| { - ( - parent.host_cwd.clone(), - parent.guest_cwd.clone(), - parent.runtime != GuestRuntimeKind::JavaScript, - ) - }) - }; - if let Some((host_cwd, guest_cwd, sync_root_shadow)) = parent_sync_roots { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - sync_process_host_roots_to_kernel(vm, &host_cwd, &guest_cwd, sync_root_shadow)?; - } let prepared_host_net_fds = { let vm = self .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; let current_network_counts = vm_spawn_host_net_resource_counts(vm); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); let parent = active_processes .get_mut(process_id) @@ -3688,6 +5506,7 @@ where prepare_spawn_host_net_fds( kernel, parent, + &managed_descriptions, current_network_counts, &request.options.spawn_host_net_fds, &request.options.spawn_fd_mappings, @@ -3696,7 +5515,10 @@ where }; let prepared_spawn_actions = if !prepared_host_net_fds.kernel_actions.is_empty() { let (parent_pid, parent_cwd) = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; let parent = vm .active_processes .get(process_id) @@ -3752,8 +5574,11 @@ where let total_start = Instant::now(); let process_event_capacity = self.config.runtime.protocol.max_process_events; let phase_start = Instant::now(); - let (parent_env, parent_guest_cwd, parent_host_cwd) = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let (parent_env, parent_guest_cwd, parent_host_cwd, parent_kernel_pid) = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; let parent = vm .active_processes .get(process_id) @@ -3762,6 +5587,7 @@ where parent.env.clone(), parent.guest_cwd.clone(), parent.host_cwd.clone(), + parent.kernel_pid, ) }; let mut resolved = @@ -3774,8 +5600,11 @@ where &mut request, )? } else { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution_with_mode( + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Self::resolve_javascript_child_process_execution_with_mode( vm, &parent_env, &parent_guest_cwd, @@ -3791,7 +5620,20 @@ where .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; - stage_agentos_package_command(vm, &mut resolved)?; + stage_agentos_package_command( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: parent_kernel_pid, + }, + )?; + stage_kernel_wasm_launch_asset( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: parent_kernel_pid, + }, + )?; } tracing::debug!( vm_id, @@ -3808,11 +5650,12 @@ where ); let resolved = resolved; if prepared_host_net_fds.inherited_fd_count() != 0 - && (resolved.runtime != GuestRuntimeKind::WebAssembly || resolved.binding_command) + && !resolved.adapter_policy.accepts_inherited_host_network_fds { - return Err(SidecarError::InvalidState(String::from( - "ENOTSUP: inherited host-network fds require a WebAssembly child runtime", - ))); + return Err(SidecarError::host( + "ENOTSUP", + String::from("inherited host-network fds require a WebAssembly child runtime"), + )); } record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); let (parent_kernel_pid, child_process_id) = { @@ -3831,14 +5674,15 @@ where .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; - enforce_resolved_wasm_execute_dac(vm, parent_kernel_pid, &resolved)?; let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); let phase_start = Instant::now(); let ( kernel_pid, kernel_handle, - execution, + mut execution, + runtime_control, + binding_event_request, kernel_stdin_writer_fd, kernel_stdin_reader_fd, direct_posix_stdin, @@ -3866,6 +5710,9 @@ where parent_pid: Some(parent_kernel_pid), env: resolved.env.clone(), cwd: Some(resolved.guest_cwd.clone()), + permission_tier: resolved + .wasm_permission_tier + .map(kernel_process_permission_tier), }, requested_pgid, ) @@ -3891,6 +5738,26 @@ where &kernel_handle, spawn_attributes.new_session || request.options.detached, )?; + apply_spawn_process_attributes_or_rollback( + &mut vm.kernel, + &kernel_handle, + &request.options, + )?; + let runtime_control = match ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + ) { + Ok(runtime_control) => runtime_control, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + None, + "child_process.spawn binding runtime-control attachment", + ); + return Err(error); + } + }; let binding_execution = BindingExecution::with_event_notify( Arc::clone(&self.process_event_notify), process_event_capacity, @@ -3905,7 +5772,7 @@ where let binding_vm_pending_event_bytes_budget = binding_execution.vm_pending_event_bytes_budget.clone(); let event_notify = binding_execution.event_notify.clone(); - spawn_binding_process_events(BindingProcessEventRequest { + let binding_event_request = BindingProcessEventRequest { runtime_context: vm.runtime_context.clone(), sidecar_requests: sidecar_requests.clone(), connection_id: vm.connection_id.clone(), @@ -3913,6 +5780,8 @@ where vm_id: vm_id.to_owned(), binding_resolution, cancelled, + paused: Arc::clone(&binding_execution.paused), + pause_notify: Arc::clone(&binding_execution.pause_notify), pending_events, event_overflow_reason, pending_event_bytes, @@ -3920,21 +5789,19 @@ where pending_event_bytes_limit, vm_pending_event_bytes_budget: binding_vm_pending_event_bytes_budget, event_notify, - }); + }; ( kernel_pid, kernel_handle, ActiveExecution::Binding(binding_execution), + runtime_control, + Some(binding_event_request), None, 0, false, ) } else { - let kernel_command = match resolved.runtime { - GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, - GuestRuntimeKind::WebAssembly => WASM_COMMAND, - GuestRuntimeKind::Python => PYTHON_COMMAND, - }; + let kernel_command = resolved.adapter_policy.kernel_driver_command; let kernel_handle = vm .kernel .spawn_process_with_process_group( @@ -3945,6 +5812,9 @@ where parent_pid: Some(parent_kernel_pid), env: resolved.env.clone(), cwd: Some(resolved.guest_cwd.clone()), + permission_tier: resolved + .wasm_permission_tier + .map(kernel_process_permission_tier), }, requested_pgid, ) @@ -3975,17 +5845,14 @@ where .descriptions .iter() .any(|description| description.guest_fds.contains(&0))); - if matches!( - resolved.runtime, - GuestRuntimeKind::JavaScript | GuestRuntimeKind::Python - ) { + if resolved.adapter_policy.materializes_direct_runtime_stdio { materialize_direct_runtime_stdio_mappings( &mut vm.kernel, kernel_pid, &applied_spawn_actions, )?; } - let kernel_stdin_reader_fd = if resolved.runtime != GuestRuntimeKind::WebAssembly { + let kernel_stdin_reader_fd = if resolved.adapter_policy.canonicalizes_runtime_stdin { canonicalize_host_runtime_posix_stdin( &mut vm.kernel, kernel_pid, @@ -3999,17 +5866,19 @@ where &kernel_handle, spawn_attributes.new_session || request.options.detached, )?; + apply_spawn_process_attributes_or_rollback( + &mut vm.kernel, + &kernel_handle, + &request.options, + )?; let mut execution_env = resolved.env.clone(); - if resolved.runtime == GuestRuntimeKind::JavaScript - && (posix_spawn_controls_stdin - || javascript_child_process_stdin_mode(&request) != "pipe") - { + if resolved.adapter_policy.forwards_kernel_stdin_rpc { execution_env.insert( String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), String::from("1"), ); } - if resolved.runtime == GuestRuntimeKind::WebAssembly { + if resolved.adapter_policy.encodes_inherited_fd_bootstrap { execution_env.insert( String::from("AGENTOS_WASM_INHERITED_FD_MAPPINGS"), serde_json::to_string(&applied_spawn_actions.fd_mappings).map_err(|error| { @@ -4041,10 +5910,32 @@ where } execution_env.insert( String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned(), ); - let execution = match resolved.runtime { + macro_rules! attach_child_runtime_control { + ($label:literal) => { + match ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + ) { + Ok(runtime_control) => runtime_control, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + None, + $label, + ); + return Err(error); + } + } + }; + } + + let (execution, runtime_control) = match resolved.runtime { GuestRuntimeKind::JavaScript => { execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( &request.options.internal_bootstrap_env, @@ -4053,24 +5944,26 @@ where .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint( vm, + kernel_pid, &mut execution_env, - ) + )? .unwrap_or_else(|| resolved.entrypoint.clone()); let inline_code = load_javascript_entrypoint_source( vm, - &resolved.host_cwd, + kernel_pid, + &resolved.guest_cwd, &launch_entrypoint, &execution_env, - ); - prepare_javascript_shadow(vm, &resolved, &execution_env)?; - - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = built_reader - .map(|reader| Box::new(reader) as Box); + )?; + prepare_javascript_launch_assets( + vm, + &resolved, + &execution_env, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: kernel_pid, + }, + inline_code.as_deref(), + )?; let context = self.javascript_engine .create_context(CreateJavascriptContextRequest { @@ -4081,6 +5974,9 @@ where ), }); let context_id = context.context_id; + let runtime_control = attach_child_runtime_control!( + "child_process.spawn JavaScript runtime-control attachment" + ); let execution_result = self .javascript_engine .start_execution_with_module_reader_and_runtime( @@ -4102,13 +5998,24 @@ where inline_code, wasm_module_bytes: None, }, - module_reader, - guest_reader, + None, + None, vm.runtime_context.clone(), ); self.javascript_engine.dispose_context(&context_id); - let execution = execution_result.map_err(javascript_error)?; - ActiveExecution::Javascript(execution) + let execution = match execution_result.map_err(javascript_error) { + Ok(execution) => execution, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + None, + "child_process.spawn JavaScript engine start", + ); + return Err(error); + } + }; + (ActiveExecution::Javascript(execution), runtime_control) } GuestRuntimeKind::WebAssembly => { // These values configure the trusted WASM runner, not @@ -4129,19 +6036,23 @@ where module_path: Some(resolved.entrypoint.clone()), }); let context_id = context.context_id; + let runtime_control = attach_child_runtime_control!( + "child_process.spawn WebAssembly runtime-control attachment" + ); let execution_result = self .wasm_engine .start_execution_with_runtime_async( StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), + managed_kernel_host: true, argv: resolved.process_args.clone(), env: execution_env, cwd: resolved.host_cwd.clone(), permission_tier: execution_wasm_permission_tier( - resolved - .wasm_permission_tier - .unwrap_or(WasmPermissionTier::Full), + vm.kernel + .process_permission_tier(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error)?, ), limits: wasm_limits, guest_runtime: wasm_guest_runtime, @@ -4150,8 +6061,19 @@ where ) .await; self.wasm_engine.dispose_context(&context_id); - let execution = execution_result.map_err(wasm_error)?; - ActiveExecution::Wasm(Box::new(execution)) + let execution = match execution_result.map_err(wasm_error) { + Ok(execution) => execution, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + None, + "child_process.spawn WebAssembly engine start", + ); + return Err(error); + } + }; + (ActiveExecution::Wasm(Box::new(execution)), runtime_control) } GuestRuntimeKind::Python => { // Nested `python` child_process: set up the Pyodide context the @@ -4206,6 +6128,9 @@ where pyodide_dist_path, }); let context_id = context.context_id; + let runtime_control = attach_child_runtime_control!( + "child_process.spawn Python runtime-control attachment" + ); let execution_result = self .python_engine .start_execution_with_runtime_async( @@ -4227,8 +6152,19 @@ where ) .await; self.python_engine.dispose_context(&context_id); - let execution = execution_result.map_err(python_error)?; - ActiveExecution::Python(execution) + let execution = match execution_result.map_err(python_error) { + Ok(execution) => execution, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + None, + "child_process.spawn Python engine start", + ); + return Err(error); + } + }; + (ActiveExecution::Python(execution), runtime_control) } }; let kernel_stdin_writer_fd = if posix_spawn_controls_stdin { @@ -4237,9 +6173,7 @@ where match javascript_child_process_stdin_mode(&request) { "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), "ignore" => { - vm.kernel - .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) - .map_err(kernel_error)?; + install_kernel_ignored_stdin(&mut vm.kernel, kernel_pid)?; None } "inherit" => None, @@ -4250,6 +6184,8 @@ where kernel_pid, kernel_handle, execution, + runtime_control, + None, kernel_stdin_writer_fd, kernel_stdin_reader_fd, posix_spawn_controls_stdin, @@ -4261,24 +6197,76 @@ where ); let phase_start = Instant::now(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let mut managed_description_guard = match managed_descriptions.lock() { + Ok(descriptions) => descriptions, + Err(_) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + Some(&mut execution), + "child_process.spawn managed-description preflight", + ); + return Err(SidecarError::host( + "EIO", + "managed description registry lock poisoned", + )); + } + }; + if let Err(error) = + prepared_host_net_fds.validate_install(&managed_description_guard, kernel_pid) + { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + Some(&mut execution), + "child_process.spawn managed-description preflight", + ); + return Err(error); + } // Shared-terminal detection: when the child's kernel fd 1 is a PTY (the // slave inherited from a TTY shell), record who owns the host-facing // master so the child's stdio writes surface through master drains // instead of child stdout events (see `tty_master_owner`). - let child_fd1_is_tty = vm - .kernel - .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) - .unwrap_or(false); - let child_process_group = vm - .kernel - .getpgid(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error)?; + let child_fd1_is_tty = match vm.kernel.isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) { + Ok(is_tty) => is_tty, + Err(error) if error.code() == "EBADF" => false, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + Some(&mut execution), + "child_process.spawn tty preflight", + ); + return Err(kernel_error(error)); + } + }; + let child_process_group = match vm.kernel.getpgid(EXECUTION_DRIVER_NAME, kernel_pid) { + Ok(process_group) => process_group, + Err(error) => { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + Some(&mut execution), + "child_process.spawn process-group preflight", + ); + return Err(kernel_error(error)); + } + }; let process_event_limits = vm.limits.process.clone(); - let shadow_root = normalize_host_path(&vm.cwd); - let process = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let process = match vm.active_processes.get_mut(process_id) { + Some(process) => process, + None => { + let error = missing_process_error(vm_id, process_id); + rollback_unregistered_spawn_child( + &mut vm.kernel, + &kernel_handle, + Some(&mut execution), + "child_process.spawn parent lookup", + ); + return Err(error); + } + }; let inherited_tty_master_owner = if child_fd1_is_tty { process .tty_master_fd @@ -4287,45 +6275,53 @@ where } else { None }; - process.child_processes.insert( - child_process_id.clone(), - ActiveProcess::new( - kernel_pid, - kernel_handle, - process.runtime_context.clone(), - process.limits.clone(), - process_event_capacity, - resolved.runtime, - execution, - ) - .with_event_notify(Arc::clone(&self.process_event_notify)) - .with_process_event_limits(&process_event_limits) - .with_vm_pending_byte_budgets( - Arc::clone(&vm_pending_stdin_bytes_budget), - Arc::clone(&vm_pending_event_bytes_budget), - ) - .with_detached(request.options.detached) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(resolved.env.clone()) - .with_shadow_root(shadow_root) - .with_host_cwd(resolved.host_cwd.clone()), - ); - { - let child = process - .child_processes - .get_mut(&child_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "child process {child_process_id} disappeared during spawn" - )) - })?; - child.tty_master_owner = inherited_tty_master_owner; - child.direct_posix_stdin = direct_posix_stdin; - child.kernel_stdin_reader_fd = kernel_stdin_reader_fd; - if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { - child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); - } - prepared_host_net_fds.install(child); + let child_process_bridge_owns_output = !request.options.stdio.is_empty() + && process.execution.descendant_output_ownership() + == DescendantOutputOwnership::SidecarBridge; + let mut child = ActiveProcess::new_with_attached_runtime_control( + kernel_pid, + kernel_handle, + process.runtime_context.clone(), + process.limits.clone(), + process_event_capacity, + resolved.runtime, + execution, + runtime_control, + Arc::clone(&self.process_event_notify), + ) + .with_adapter_policy(resolved.adapter_policy) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_detached(request.options.detached) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(resolved.env.clone()) + .with_host_cwd(resolved.host_cwd.clone()); + child.child_process_bridge_owns_output = child_process_bridge_owns_output; + child.tty_master_owner = inherited_tty_master_owner; + child.direct_posix_stdin = direct_posix_stdin; + child.kernel_stdin_reader_fd = kernel_stdin_reader_fd; + if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { + child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); + } + prepared_host_net_fds.install(&mut child, &mut managed_description_guard); + if let Err(error) = child.apply_runtime_controls() { + let rollback_handle = child.kernel_handle.clone(); + rollback_unregistered_spawn_child( + &mut vm.kernel, + &rollback_handle, + Some(&mut child.execution), + "child_process.spawn pending runtime control", + ); + return Err(error); + } + process + .child_processes + .insert(child_process_id.clone(), child); + if let Some(binding_event_request) = binding_event_request { + spawn_binding_process_events(binding_event_request); } record_execute_phase("child_process_register", phase_start.elapsed()); record_execute_phase("child_process_spawn_total", total_start.elapsed()); @@ -4343,24 +6339,11 @@ where process: &ActiveProcess, requested: Option, ) -> Result { - let (limit, setting) = match process.runtime { - GuestRuntimeKind::JavaScript => ( - process.limits.js_runtime.captured_output_limit_bytes, - "limits.jsRuntime.capturedOutputLimitBytes", - ), - GuestRuntimeKind::Python => ( - process.limits.python.output_buffer_max_bytes, - "limits.python.outputBufferMaxBytes", - ), - GuestRuntimeKind::WebAssembly => ( - process.limits.wasm.captured_output_limit_bytes, - "limits.wasm.capturedOutputLimitBytes", - ), - }; + let limit = (process.adapter_policy.captured_output_limit)(&process.limits); + let setting = process.adapter_policy.captured_output_limit_setting; let requested = requested.unwrap_or(1024 * 1024); if requested > limit { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_CHILD_PROCESS_BUFFER_LIMIT: child process maxBuffer {requested} exceeds {setting} ({limit}); raise {setting} for larger captured output" + return Err(SidecarError::host("ERR_AGENTOS_CHILD_PROCESS_BUFFER_LIMIT", format!("child process maxBuffer {requested} exceeds {setting} ({limit}); raise {setting} for larger captured output" ))); } Ok(requested) @@ -4370,7 +6353,7 @@ where &mut self, vm_id: &str, process_id: &str, - request: JavascriptChildProcessSpawnRequest, + request: ProcessLaunchRequest, max_buffer: Option, completion: PendingChildProcessSyncCompletion, ) -> Result<(), SidecarError> { @@ -4382,44 +6365,91 @@ where .ok_or_else(|| missing_process_error(vm_id, process_id))?; Self::child_process_sync_max_buffer(process, max_buffer)? }; + let deadline = checked_child_process_sync_deadline(request.options.timeout)?; let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; - let deadline = request - .options - .timeout - .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); + let (count_reservation, bytes_reservation) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + reserve_child_process_sync_budget( + &vm.pending_child_sync_count_budget, + &vm.pending_child_sync_bytes_budget, + max_buffer, + sync_input.as_deref().map_or(0, <[u8]>::len), + )? + }; let timeout_signal = request .options .kill_signal .clone() .unwrap_or_else(|| String::from("SIGTERM")); - let spawned = self - .spawn_javascript_child_process(vm_id, process_id, request) - .await?; - let child_process_id = spawned - .get("childId") - .and_then(Value::as_str) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing childId", - )) - })? - .to_owned(); - let pid = spawned - .get("pid") - .and_then(Value::as_u64) - .and_then(|pid| u32::try_from(pid).ok()) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing a valid pid", - )) - })?; + let prior_child_ids = self.child_process_ids_at_path(vm_id, process_id, &[])?; + let spawned = self.spawn_child_process(vm_id, process_id, request).await?; + let identity = match parse_spawned_child_identity(&spawned) { + Ok(identity) => identity, + Err(error) => { + let hinted_child_id = spawned.get("childId").and_then(Value::as_str); + let hinted_pid = spawned + .get("pid") + .and_then(Value::as_u64) + .and_then(|pid| u32::try_from(pid).ok()); + self.rollback_registered_child_process_sync( + vm_id, + process_id, + &[], + &prior_child_ids, + hinted_child_id, + hinted_pid, + "root spawnSync response parsing", + ); + return Err(error); + } + }; if let Some(input) = sync_input.as_deref() { - self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?; + if let Err(error) = + self.write_child_process_stdin(vm_id, process_id, &identity.child_process_id, input) + { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + &[], + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "root spawnSync stdin write", + ); + return Err(error); + } + } + if let Err(error) = + self.close_child_process_stdin(vm_id, process_id, &identity.child_process_id) + { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + &[], + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "root spawnSync stdin close", + ); + return Err(error); } - self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?; - let (runtime, notify) = { + let pending = PendingChildProcessSync { + pid: identity.pid, + stdout: Vec::new(), + stderr: Vec::new(), + max_buffer, + deadline, + timeout_signal, + kill_sent: false, + timed_out: false, + max_buffer_exceeded: false, + completion, + _count_reservation: count_reservation, + _bytes_reservation: bytes_reservation, + }; + let registration = (|| -> Result<_, SidecarError> { let vm = self .vms .get_mut(vm_id) @@ -4428,35 +6458,57 @@ where .active_processes .get_mut(process_id) .ok_or_else(|| missing_process_error(vm_id, process_id))?; - process.pending_child_process_sync.insert( - child_process_id, - PendingChildProcessSync { - pid, - stdout: Vec::new(), - stderr: Vec::new(), - max_buffer, - deadline, - timeout_signal, - kill_sent: false, - timed_out: false, - max_buffer_exceeded: false, - completion, - }, - ); - ( + if process + .pending_child_process_sync + .contains_key(&identity.child_process_id) + { + return Err(SidecarError::host( + "EEXIST", + format!( + "pending child-process sync entry {} already exists", + identity.child_process_id + ), + )); + } + process + .pending_child_process_sync + .insert(identity.child_process_id.clone(), pending); + Ok(( process.runtime_context.clone(), Arc::clone(&process.process_event_notify), - ) + )) + })(); + let (runtime, notify) = match registration { + Ok(registration) => registration, + Err(error) => { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + &[], + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "root spawnSync pending registration", + ); + return Err(error); + } }; if let Some(deadline) = deadline { - let delay = deadline.saturating_duration_since(Instant::now()); - runtime - .spawn(agentos_runtime::TaskClass::Timer, async move { - tokio::time::sleep(delay).await; - notify.notify_one(); - }) - .map_err(SidecarError::from)?; - } + if let Err(error) = + admit_child_process_sync_timer(&runtime, notify, deadline, vm_id, process_id, &[]) + { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + &[], + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "root spawnSync timer admission", + ); + return Err(error); + } + } Ok(()) } @@ -4464,9 +6516,9 @@ where &mut self, vm_id: &str, process_id: &str, - request: JavascriptChildProcessSpawnRequest, + request: ProcessLaunchRequest, max_buffer: Option, - ) -> Result { + ) -> Result { let (respond_to, receiver) = tokio::sync::oneshot::channel(); self.begin_javascript_child_process_sync( vm_id, @@ -4476,7 +6528,7 @@ where PendingChildProcessSyncCompletion::Javascript(respond_to), ) .await?; - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Vm, @@ -4488,32 +6540,27 @@ where /// environment: execve's supplied envp replaces the old environment rather /// than being overlaid on it. The existing PID, process tree, cwd, stdio, /// and non-CLOEXEC kernel descriptors remain attached to `ActiveProcess`. - pub(crate) fn exec_javascript_process_image( + pub(crate) fn exec_process_image( &mut self, vm_id: &str, root_process_id: &str, process_path: &[&str], - mut request: JavascriptChildProcessSpawnRequest, + mut request: ProcessLaunchRequest, ) -> Result<(), SidecarError> { if request.options.executable_fd.is_some() { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: executableFd is only valid for process.exec_fd_image_commit", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("executableFd is only valid for process.exec_fd_image_commit"), + )); } if request.options.shell || request.options.detached { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: execve does not accept shell or detached process options", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("execve does not accept shell or detached process options"), + )); } - let ( - guest_cwd, - host_cwd, - kernel_pid, - parent_kernel_pid, - current_runtime, - direct_posix_stdin, - ) = { + let (guest_cwd, host_cwd, kernel_pid, parent_kernel_pid, current_adapter_policy) = { let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; let root = vm .active_processes @@ -4536,15 +6583,15 @@ where process.host_cwd.clone(), process.kernel_pid, parent_kernel_pid, - process.runtime.clone(), - process.direct_posix_stdin, + process.adapter_policy, ) }; if request.command.is_empty() { - return Err(SidecarError::InvalidState(String::from( - "ENOENT: execve path is empty", - ))); + return Err(SidecarError::host( + "ENOENT", + String::from("execve path is empty"), + )); } // execve resolves a relative pathname from cwd; it never searches // PATH. Making the command explicitly path-like keeps the shared child @@ -4569,8 +6616,11 @@ where request.options.detached = false; let mut resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution_with_mode( + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Self::resolve_javascript_child_process_execution_with_mode( vm, &BTreeMap::new(), &guest_cwd, @@ -4582,14 +6632,14 @@ where }; apply_child_process_argv0(&mut resolved, request.options.argv0.as_deref()); if resolved.binding_command { - return Err(SidecarError::InvalidState(format!( - "ENOEXEC: exec format error: {}", - request.command - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("exec format error: {}", request.command), + )); } if request.options.local_replacement - && current_runtime == GuestRuntimeKind::WebAssembly - && resolved.runtime == GuestRuntimeKind::WebAssembly + && current_adapter_policy.supports_prepared_in_place_exec + && resolved.adapter_policy.supports_prepared_in_place_exec { let vm = self .vms @@ -4615,22 +6665,43 @@ where .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; - stage_agentos_package_command(vm, &mut resolved)?; + stage_agentos_package_command( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: kernel_pid, + }, + )?; + stage_kernel_wasm_launch_asset( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: kernel_pid, + }, + )?; } // Keep guest-visible envp separate from executor bootstrap variables // added during resolution. Both local and separate-runtime exec paths // must publish and inherit exactly the supplied environment. let replacement_guest_env = request.options.env.clone(); + let requested_exec_permission_tier = resolved + .wasm_permission_tier + .map(kernel_process_permission_tier) + .unwrap_or(ProcessPermissionTier::Full); if request.options.local_replacement { - if current_runtime != GuestRuntimeKind::WebAssembly - || resolved.runtime != GuestRuntimeKind::WebAssembly + if !current_adapter_policy.supports_prepared_in_place_exec + || !resolved.adapter_policy.supports_prepared_in_place_exec { - return Err(SidecarError::InvalidState(format!( - "ENOEXEC: in-place exec only supports WebAssembly images: {}", - literal_exec_path - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!( + "in-place exec only supports WebAssembly images: {}", + literal_exec_path + ), + )); } + let bridge = self.bridge.clone(); let vm = self .vms .get_mut(vm_id) @@ -4659,8 +6730,10 @@ where &retained_internal_fds, &request.options.cloexec_fds, Some(&literal_exec_path), + Some(requested_exec_permission_tier), ) .map_err(kernel_error)?; + prune_managed_process_routes_without_aliases(&bridge, vm_id, vm, kernel_pid)?; let root = vm .active_processes @@ -4679,19 +6752,14 @@ where process.exit_signal = None; process.exit_core_dumped = false; process.clear_deferred_kernel_wait_rpc(); + discard_exec_signal_state(process); process.module_resolution_cache = Default::default(); discard_replaced_image_pending_events(process); - // POSIX exec resets caught dispositions to default, preserves - // ignored dispositions, and preserves the signal mask/pending set. - reset_caught_signal_dispositions_after_exec( - &mut vm.signal_states, - root_process_id, - process_path, - ); return Ok(()); } + let bridge = self.bridge.clone(); let vm = self .vms .get_mut(vm_id) @@ -4699,9 +6767,11 @@ where let mut execution_env = resolved.env.clone(); execution_env.insert( String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned(), ); - let replacement = match resolved.runtime { + let mut replacement = match resolved.runtime { GuestRuntimeKind::JavaScript => { execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( &request.options.internal_bootstrap_env, @@ -4710,31 +6780,32 @@ where execution_env.remove("AGENTOS_EAGER_STDIN_HANDLE"); } execution_env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - if direct_posix_stdin { - execution_env.insert( - String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), - String::from("1"), - ); - } else { - execution_env.remove("AGENTOS_FORWARD_KERNEL_STDIN_RPC"); - } - let launch_entrypoint = - resolve_agentos_package_javascript_launch_entrypoint(vm, &mut execution_env) - .unwrap_or_else(|| resolved.entrypoint.clone()); + execution_env.insert( + String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), + String::from("1"), + ); + let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint( + vm, + kernel_pid, + &mut execution_env, + )? + .unwrap_or_else(|| resolved.entrypoint.clone()); let inline_code = load_javascript_entrypoint_source( vm, - &resolved.host_cwd, + kernel_pid, + &resolved.guest_cwd, &launch_entrypoint, &execution_env, - ); - prepare_javascript_shadow(vm, &resolved, &execution_env)?; - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = - built_reader.map(|reader| Box::new(reader) as Box); + )?; + prepare_javascript_launch_assets( + vm, + &resolved, + &execution_env, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: kernel_pid, + }, + inline_code.as_deref(), + )?; let context = self.javascript_engine .create_context(CreateJavascriptContextRequest { @@ -4764,8 +6835,8 @@ where inline_code, wasm_module_bytes: None, }, - module_reader, - guest_reader, + None, + None, vm.runtime_context.clone(), ); self.javascript_engine.dispose_context(&context_id); @@ -4787,13 +6858,18 @@ where .prepare_execution(StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), + managed_kernel_host: true, argv: resolved.process_args.clone(), env: execution_env, cwd: resolved.host_cwd.clone(), permission_tier: execution_wasm_permission_tier( - resolved - .wasm_permission_tier - .unwrap_or(WasmPermissionTier::Full), + vm.kernel + .effective_exec_permission_tier( + EXECUTION_DRIVER_NAME, + kernel_pid, + requested_exec_permission_tier, + ) + .map_err(kernel_error)?, ), limits: wasm_execution_limits(vm), guest_runtime: guest_runtime_identity( @@ -4881,16 +6957,13 @@ where // impossible to accidentally regress to the old start-before-commit // path without failing execve before kernel state is mutated. if !replacement.is_prepared_for_start() { - return Err(SidecarError::InvalidState(String::from( - "EIO: cross-runtime execve replacement started before kernel commit", - ))); + return Err(SidecarError::host( + "EIO", + String::from("cross-runtime execve replacement started before kernel commit"), + )); } - let kernel_command = match resolved.runtime { - GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, - GuestRuntimeKind::WebAssembly => WASM_COMMAND, - GuestRuntimeKind::Python => PYTHON_COMMAND, - }; + let kernel_command = resolved.adapter_policy.kernel_driver_command; let retained_internal_fds = Self::active_process_by_path( vm.active_processes .get(root_process_id) @@ -4910,6 +6983,7 @@ where &retained_internal_fds, &request.options.cloexec_fds, Some(&literal_exec_path), + Some(requested_exec_permission_tier), ) { let mut replacement = replacement; if let Err(terminate_error) = replacement.terminate() { @@ -4922,6 +6996,7 @@ where } return Err(kernel_error(error)); } + prune_managed_process_routes_without_aliases(&bridge, vm_id, vm, kernel_pid)?; let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let root = vm @@ -4934,25 +7009,27 @@ where Self::child_process_path_label(root_process_id, process_path) )) })?; + ExecutionBackend::configure_host_services( + &mut replacement, + process.host_capabilities.clone(), + ); let mut old_execution = std::mem::replace(&mut process.execution, replacement); process.runtime = resolved.runtime; + process.adapter_policy = resolved.adapter_policy; process.guest_cwd = resolved.guest_cwd; process.host_cwd = resolved.host_cwd; process.env = replacement_guest_env; process.exit_signal = None; process.exit_core_dumped = false; process.clear_deferred_kernel_wait_rpc(); + discard_exec_signal_state(process); process.module_resolution_cache = Default::default(); discard_replaced_image_pending_events(process); rebind_process_runtime_event_targets(process, &kernel_readiness); - // POSIX exec resets caught dispositions to default but preserves - // dispositions explicitly set to ignore. - let signal_key = reset_caught_signal_dispositions_after_exec( - &mut vm.signal_states, - root_process_id, - process_path, - ); + // The committed kernel exec operation already reset caught + // dispositions while preserving ignored dispositions. + let signal_key = Self::child_process_path_label(root_process_id, process_path); // The replacement isolate was registered and fully loaded before the // kernel commit, but no guest code was enqueued. Only start it now, // after both kernel-visible process state and sidecar-owned descriptors @@ -5028,18 +7105,9 @@ where vm_id: &str, root_process_id: &str, process_path: &[&str], - request: JavascriptChildProcessSpawnRequest, + request: ProcessLaunchRequest, ) -> Result<(), SidecarError> { - if !request.options.local_replacement || request.options.executable_fd.is_none() { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: fd-image exec commit requires localReplacement and executableFd", - ))); - } - if request.options.shell || request.options.detached || request.options.cwd.is_some() { - return Err(SidecarError::InvalidState(String::from( - "EINVAL: fexecve does not accept shell, detached, or cwd options", - ))); - } + validate_wasm_fd_image_commit_request(&request)?; let (kernel_pid, retained_internal_fds) = { let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; @@ -5053,10 +7121,13 @@ where Self::child_process_path_label(root_process_id, process_path) )) })?; - if process.runtime != GuestRuntimeKind::WebAssembly { - return Err(SidecarError::InvalidState(String::from( - "ENOEXEC: fd-image exec commit requires a WebAssembly process", - ))); + if !process.adapter_policy.supports_prepared_in_place_exec { + return Err(SidecarError::host( + "ENOEXEC", + String::from( + "fd-image exec commit requires an adapter with prepared in-place exec", + ), + )); } ( process.kernel_pid, @@ -5078,6 +7149,7 @@ where argv.extend(request.args); let replacement_guest_env = request.options.env; + let bridge = self.bridge.clone(); let vm = self .vms .get_mut(vm_id) @@ -5093,8 +7165,10 @@ where &retained_internal_fds, &request.options.cloexec_fds, None, + None, ) .map_err(kernel_error)?; + prune_managed_process_routes_without_aliases(&bridge, vm_id, vm, kernel_pid)?; let root = vm .active_processes @@ -5110,62 +7184,30 @@ where process.exit_signal = None; process.exit_core_dumped = false; process.clear_deferred_kernel_wait_rpc(); + discard_exec_signal_state(process); process.module_resolution_cache = Default::default(); discard_replaced_image_pending_events(process); - reset_caught_signal_dispositions_after_exec( - &mut vm.signal_states, - root_process_id, - process_path, - ); Ok(()) } - async fn spawn_descendant_javascript_child_process( + async fn spawn_descendant_process( &mut self, vm_id: &str, process_id: &str, current_process_path: &[&str], - mut request: JavascriptChildProcessSpawnRequest, + mut request: ProcessLaunchRequest, ) -> Result { let spawn_attributes = javascript_spawn_attributes(&request.options)?; let requested_pgid = spawn_attributes.process_group; let current_process_label = Self::child_process_path_label(process_id, current_process_path); - let parent_sync_roots = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let root = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )) - })?; - (parent.host_write_dirty_recursive() - || !parent.clean_host_writes_are_observable_recursive()) - .then(|| { - ( - parent.host_cwd.clone(), - parent.guest_cwd.clone(), - parent.runtime != GuestRuntimeKind::JavaScript, - ) - }) - }; - if let Some((host_cwd, guest_cwd, sync_root_shadow)) = parent_sync_roots { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - sync_process_host_roots_to_kernel(vm, &host_cwd, &guest_cwd, sync_root_shadow)?; - } let prepared_host_net_fds = { let vm = self .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; let current_network_counts = vm_spawn_host_net_resource_counts(vm); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); let root = active_processes .get_mut(process_id) @@ -5180,6 +7222,7 @@ where prepare_spawn_host_net_fds( kernel, parent, + &managed_descriptions, current_network_counts, &request.options.spawn_host_net_fds, &request.options.spawn_fd_mappings, @@ -5287,8 +7330,11 @@ where &mut request, )? } else { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution_with_mode( + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Self::resolve_javascript_child_process_execution_with_mode( vm, &parent_env, &parent_guest_cwd, @@ -5304,7 +7350,20 @@ where .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; - stage_agentos_package_command(vm, &mut resolved)?; + stage_agentos_package_command( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: parent_kernel_pid, + }, + )?; + stage_kernel_wasm_launch_asset( + vm, + &mut resolved, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: parent_kernel_pid, + }, + )?; } tracing::debug!( vm_id, @@ -5322,11 +7381,12 @@ where ); let resolved = resolved; if prepared_host_net_fds.inherited_fd_count() != 0 - && (resolved.runtime != GuestRuntimeKind::WebAssembly || resolved.binding_command) + && !resolved.adapter_policy.accepts_inherited_host_network_fds { - return Err(SidecarError::InvalidState(String::from( - "ENOTSUP: inherited host-network fds require a WebAssembly child runtime", - ))); + return Err(SidecarError::host( + "ENOTSUP", + String::from("inherited host-network fds require a WebAssembly child runtime"), + )); } record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); let sidecar_requests = self.sidecar_requests.clone(); @@ -5334,7 +7394,6 @@ where .vms .get_mut(vm_id) .ok_or_else(|| missing_vm_error(vm_id))?; - enforce_resolved_wasm_execute_dac(vm, parent_kernel_pid, &resolved)?; let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); let phase_start = Instant::now(); @@ -5379,6 +7438,9 @@ where parent_pid: Some(parent_kernel_pid), env: resolved.env.clone(), cwd: Some(resolved.guest_cwd.clone()), + permission_tier: resolved + .wasm_permission_tier + .map(kernel_process_permission_tier), }, requested_pgid, ) @@ -5404,7 +7466,16 @@ where &kernel_handle, spawn_attributes.new_session || request.options.detached, )?; + apply_spawn_process_attributes_or_rollback( + &mut vm.kernel, + &kernel_handle, + &request.options, + )?; pending_kernel_handle = Some(kernel_handle.clone()); + let runtime_control = ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + )?; let binding_execution = BindingExecution::with_event_notify( Arc::clone(&self.process_event_notify), process_event_capacity, @@ -5419,7 +7490,7 @@ where let binding_vm_pending_event_bytes_budget = binding_execution.vm_pending_event_bytes_budget.clone(); let event_notify = binding_execution.event_notify.clone(); - spawn_binding_process_events(BindingProcessEventRequest { + let binding_event_request = BindingProcessEventRequest { runtime_context: vm.runtime_context.clone(), sidecar_requests: sidecar_requests.clone(), connection_id: vm.connection_id.clone(), @@ -5427,6 +7498,8 @@ where vm_id: vm_id.to_owned(), binding_resolution, cancelled, + paused: Arc::clone(&binding_execution.paused), + pause_notify: Arc::clone(&binding_execution.pause_notify), pending_events, event_overflow_reason, pending_event_bytes, @@ -5434,21 +7507,19 @@ where pending_event_bytes_limit, vm_pending_event_bytes_budget: binding_vm_pending_event_bytes_budget, event_notify, - }); + }; ( kernel_pid, kernel_handle, ActiveExecution::Binding(binding_execution), + runtime_control, + Some(binding_event_request), None, 0, false, ) } else { - let kernel_command = match resolved.runtime { - GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, - GuestRuntimeKind::WebAssembly => WASM_COMMAND, - GuestRuntimeKind::Python => PYTHON_COMMAND, - }; + let kernel_command = resolved.adapter_policy.kernel_driver_command; let kernel_handle = vm .kernel .spawn_process_with_process_group( @@ -5459,6 +7530,9 @@ where parent_pid: Some(parent_kernel_pid), env: resolved.env.clone(), cwd: Some(resolved.guest_cwd.clone()), + permission_tier: resolved + .wasm_permission_tier + .map(kernel_process_permission_tier), }, requested_pgid, ) @@ -5489,17 +7563,15 @@ where .descriptions .iter() .any(|description| description.guest_fds.contains(&0))); - if matches!( - resolved.runtime, - GuestRuntimeKind::JavaScript | GuestRuntimeKind::Python - ) { + if resolved.adapter_policy.materializes_direct_runtime_stdio { materialize_direct_runtime_stdio_mappings( &mut vm.kernel, kernel_pid, &applied_spawn_actions, )?; } - let kernel_stdin_reader_fd = if resolved.runtime != GuestRuntimeKind::WebAssembly { + let kernel_stdin_reader_fd = if resolved.adapter_policy.canonicalizes_runtime_stdin + { canonicalize_host_runtime_posix_stdin( &mut vm.kernel, kernel_pid, @@ -5513,18 +7585,20 @@ where &kernel_handle, spawn_attributes.new_session || request.options.detached, )?; + apply_spawn_process_attributes_or_rollback( + &mut vm.kernel, + &kernel_handle, + &request.options, + )?; pending_kernel_handle = Some(kernel_handle.clone()); let mut execution_env = resolved.env.clone(); - if resolved.runtime == GuestRuntimeKind::JavaScript - && (posix_spawn_controls_stdin - || javascript_child_process_stdin_mode(&request) != "pipe") - { + if resolved.adapter_policy.forwards_kernel_stdin_rpc { execution_env.insert( String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), String::from("1"), ); } - if resolved.runtime == GuestRuntimeKind::WebAssembly { + if resolved.adapter_policy.encodes_inherited_fd_bootstrap { execution_env.insert( String::from("AGENTOS_WASM_INHERITED_FD_MAPPINGS"), serde_json::to_string(&applied_spawn_actions.fd_mappings).map_err( @@ -5558,9 +7632,11 @@ where } execution_env.insert( String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned(), ); - let execution = match resolved.runtime { + let (execution, runtime_control) = match resolved.runtime { GuestRuntimeKind::JavaScript => { execution_env.extend( sanitize_javascript_child_process_internal_bootstrap_env( @@ -5570,35 +7646,33 @@ where execution_env.remove("AGENTOS_EAGER_STDIN_HANDLE"); execution_env .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - if posix_spawn_controls_stdin { - execution_env.insert( - String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), - String::from("1"), - ); - } else { - execution_env.remove("AGENTOS_FORWARD_KERNEL_STDIN_RPC"); - } + execution_env.insert( + String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), + String::from("1"), + ); let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint( vm, + kernel_pid, &mut execution_env, - ) + )? .unwrap_or_else(|| resolved.entrypoint.clone()); let inline_code = load_javascript_entrypoint_source( vm, - &resolved.host_cwd, + kernel_pid, + &resolved.guest_cwd, &launch_entrypoint, &execution_env, - ); - prepare_javascript_shadow(vm, &resolved, &execution_env)?; - - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = built_reader - .map(|reader| Box::new(reader) as Box); + )?; + prepare_javascript_launch_assets( + vm, + &resolved, + &execution_env, + WasmLaunchAuthority::GuestProcessImage { + requester_pid: kernel_pid, + }, + inline_code.as_deref(), + )?; let context = self.javascript_engine .create_context(CreateJavascriptContextRequest { @@ -5609,6 +7683,10 @@ where ), }); let context_id = context.context_id; + let runtime_control = ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + )?; let execution_result = self .javascript_engine .start_execution_with_module_reader_and_runtime( @@ -5630,13 +7708,13 @@ where inline_code, wasm_module_bytes: None, }, - module_reader, - guest_reader, + None, + None, vm.runtime_context.clone(), ); self.javascript_engine.dispose_context(&context_id); let execution = execution_result.map_err(javascript_error)?; - ActiveExecution::Javascript(execution) + (ActiveExecution::Javascript(execution), runtime_control) } GuestRuntimeKind::WebAssembly => { execution_env.extend( @@ -5659,19 +7737,27 @@ where module_path: Some(resolved.entrypoint.clone()), }); let context_id = context.context_id; + let runtime_control = ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + )?; let execution_result = self .wasm_engine .start_execution_with_runtime_async( StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), + managed_kernel_host: true, argv: resolved.process_args.clone(), env: execution_env, cwd: resolved.host_cwd.clone(), permission_tier: execution_wasm_permission_tier( - resolved - .wasm_permission_tier - .unwrap_or(WasmPermissionTier::Full), + vm.kernel + .process_permission_tier( + EXECUTION_DRIVER_NAME, + kernel_pid, + ) + .map_err(kernel_error)?, ), limits: wasm_limits, guest_runtime: wasm_guest_runtime, @@ -5681,7 +7767,7 @@ where .await; self.wasm_engine.dispose_context(&context_id); let execution = execution_result.map_err(wasm_error)?; - ActiveExecution::Wasm(Box::new(execution)) + (ActiveExecution::Wasm(Box::new(execution)), runtime_control) } GuestRuntimeKind::Python => { // Nested `python` child_process: set up the Pyodide context the @@ -5737,6 +7823,10 @@ where pyodide_dist_path, }); let context_id = context.context_id; + let runtime_control = ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + )?; let execution_result = self .python_engine .start_execution_with_runtime_async( @@ -5759,7 +7849,7 @@ where .await; self.python_engine.dispose_context(&context_id); let execution = execution_result.map_err(python_error)?; - ActiveExecution::Python(execution) + (ActiveExecution::Python(execution), runtime_control) } }; let kernel_stdin_writer_fd = if posix_spawn_controls_stdin { @@ -5768,9 +7858,7 @@ where match javascript_child_process_stdin_mode(&request) { "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), "ignore" => { - vm.kernel - .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) - .map_err(kernel_error)?; + install_kernel_ignored_stdin(&mut vm.kernel, kernel_pid)?; None } "inherit" => None, @@ -5781,6 +7869,8 @@ where kernel_pid, kernel_handle, execution, + runtime_control, + None, kernel_stdin_writer_fd, kernel_stdin_reader_fd, posix_spawn_controls_stdin, @@ -5793,6 +7883,8 @@ where kernel_pid, kernel_handle, mut execution, + runtime_control, + binding_event_request, kernel_stdin_writer_fd, kernel_stdin_reader_fd, direct_posix_stdin, @@ -5816,10 +7908,52 @@ where ); let phase_start = Instant::now(); - let child_fd1_is_tty = vm - .kernel - .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) - .unwrap_or(false); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let mut managed_description_guard = match managed_descriptions.lock() { + Ok(descriptions) => descriptions, + Err(_) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + Some(&mut execution), + "nested child_process.spawn managed-description preflight", + ); + } + return Err(SidecarError::host( + "EIO", + "managed description registry lock poisoned", + )); + } + }; + if let Err(error) = + prepared_host_net_fds.validate_install(&managed_description_guard, kernel_pid) + { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + Some(&mut execution), + "nested child_process.spawn managed-description preflight", + ); + } + return Err(error); + } + let child_fd1_is_tty = match vm.kernel.isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) { + Ok(is_tty) => is_tty, + Err(error) if error.code() == "EBADF" => false, + Err(error) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + Some(&mut execution), + "nested child_process.spawn tty preflight", + ); + } + return Err(kernel_error(error)); + } + }; let child_process_group = match vm.kernel.getpgid(EXECUTION_DRIVER_NAME, kernel_pid) { Ok(process_group) => process_group, Err(error) => { @@ -5833,241 +7967,1125 @@ where } return Err(kernel_error(error)); } - }; - let process_event_limits = vm.limits.process.clone(); - let shadow_root = normalize_host_path(&vm.cwd); - let root = match vm.active_processes.get_mut(process_id) { - Some(root) => root, - None => { - let error = missing_process_error(vm_id, process_id); - if let Some(child) = pending_kernel_handle.take() { - rollback_unregistered_spawn_child( - &mut vm.kernel, - &child, - Some(&mut execution), - "nested child_process.spawn", - ); - } - return Err(error); + }; + let process_event_limits = vm.limits.process.clone(); + let root = match vm.active_processes.get_mut(process_id) { + Some(root) => root, + None => { + let error = missing_process_error(vm_id, process_id); + if let Some(child) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &child, + Some(&mut execution), + "nested child_process.spawn", + ); + } + return Err(error); + } + }; + let parent = match Self::active_process_by_path_mut(root, current_process_path) { + Some(parent) => parent, + None => { + let error = SidecarError::InvalidState(format!( + "unknown child process path {current_process_label} during nested spawn" + )); + if let Some(child) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &child, + Some(&mut execution), + "nested child_process.spawn", + ); + } + return Err(error); + } + }; + let inherited_tty_master_owner = if child_fd1_is_tty { + parent + .tty_master_fd + .map(|master_fd| (parent.kernel_pid, master_fd)) + .or(parent.tty_master_owner) + } else { + None + }; + let child_process_bridge_owns_output = !request.options.stdio.is_empty() + && parent.execution.descendant_output_ownership() + == DescendantOutputOwnership::SidecarBridge; + let mut child = ActiveProcess::new_with_attached_runtime_control( + kernel_pid, + kernel_handle, + parent.runtime_context.clone(), + parent.limits.clone(), + process_event_capacity, + resolved.runtime, + execution, + runtime_control, + Arc::clone(&self.process_event_notify), + ) + .with_adapter_policy(resolved.adapter_policy) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_detached(request.options.detached) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(resolved.env.clone()) + .with_host_cwd(resolved.host_cwd.clone()); + child.child_process_bridge_owns_output = child_process_bridge_owns_output; + child.tty_master_owner = inherited_tty_master_owner; + child.direct_posix_stdin = direct_posix_stdin; + child.kernel_stdin_reader_fd = kernel_stdin_reader_fd; + if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { + child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); + } + prepared_host_net_fds.install(&mut child, &mut managed_description_guard); + if let Err(error) = child.apply_runtime_controls() { + if let Some(rollback_handle) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &rollback_handle, + Some(&mut child.execution), + "nested child_process.spawn pending runtime control", + ); + } + return Err(error); + } + pending_kernel_handle.take(); + parent + .child_processes + .insert(child_process_id.clone(), child); + if let Some(binding_event_request) = binding_event_request { + spawn_binding_process_events(binding_event_request); + } + record_execute_phase("child_process_register", phase_start.elapsed()); + record_execute_phase("child_process_spawn_total", total_start.elapsed()); + Ok(json!({ + "childId": child_process_id, + "pid": kernel_pid, + "pgid": child_process_group, + "directPosixStdin": direct_posix_stdin, + "command": resolved.command, + "args": resolved.process_args, + })) + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) async fn spawn_descendant_process_for_test( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: ProcessLaunchRequest, + ) -> Result { + self.spawn_descendant_process(vm_id, process_id, current_process_path, request) + .await + } + + async fn begin_descendant_child_process_sync( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: ProcessLaunchRequest, + max_buffer: Option, + completion: PendingChildProcessSyncCompletion, + ) -> Result<(), SidecarError> { + let max_buffer = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(String::from( + "unknown child process path during nested spawnSync", + )) + })?; + Self::child_process_sync_max_buffer(parent, max_buffer)? + }; + let deadline = checked_child_process_sync_deadline(request.options.timeout)?; + let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; + let (count_reservation, bytes_reservation) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + reserve_child_process_sync_budget( + &vm.pending_child_sync_count_budget, + &vm.pending_child_sync_bytes_budget, + max_buffer, + sync_input.as_deref().map_or(0, <[u8]>::len), + )? + }; + let timeout_signal = request + .options + .kill_signal + .clone() + .unwrap_or_else(|| String::from("SIGTERM")); + let prior_child_ids = + self.child_process_ids_at_path(vm_id, process_id, current_process_path)?; + let spawned = self + .spawn_descendant_process(vm_id, process_id, current_process_path, request) + .await?; + let identity = match parse_spawned_child_identity(&spawned) { + Ok(identity) => identity, + Err(error) => { + let hinted_child_id = spawned.get("childId").and_then(Value::as_str); + let hinted_pid = spawned + .get("pid") + .and_then(Value::as_u64) + .and_then(|pid| u32::try_from(pid).ok()); + self.rollback_registered_child_process_sync( + vm_id, + process_id, + current_process_path, + &prior_child_ids, + hinted_child_id, + hinted_pid, + "nested spawnSync response parsing", + ); + return Err(error); + } + }; + + if let Some(input) = sync_input.as_deref() { + if let Err(error) = self.write_descendant_process_stdin( + vm_id, + process_id, + current_process_path, + &identity.child_process_id, + input, + ) { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + current_process_path, + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "nested spawnSync stdin write", + ); + return Err(error); + } + } + if let Err(error) = self.close_descendant_process_stdin( + vm_id, + process_id, + current_process_path, + &identity.child_process_id, + ) { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + current_process_path, + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "nested spawnSync stdin close", + ); + return Err(error); + } + + let pending = PendingChildProcessSync { + pid: identity.pid, + stdout: Vec::new(), + stderr: Vec::new(), + max_buffer, + deadline, + timeout_signal, + kill_sent: false, + timed_out: false, + max_buffer_exceeded: false, + completion, + _count_reservation: count_reservation, + _bytes_reservation: bytes_reservation, + }; + let registration = (|| -> Result<_, SidecarError> { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(String::from( + "unknown child process path during nested spawnSync", + )) + })?; + if parent + .pending_child_process_sync + .contains_key(&identity.child_process_id) + { + return Err(SidecarError::host( + "EEXIST", + format!( + "pending nested child-process sync entry {} already exists", + identity.child_process_id + ), + )); + } + parent + .pending_child_process_sync + .insert(identity.child_process_id.clone(), pending); + Ok(( + parent.runtime_context.clone(), + Arc::clone(&parent.process_event_notify), + )) + })(); + let (runtime, notify) = match registration { + Ok(registration) => registration, + Err(error) => { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + current_process_path, + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "nested spawnSync pending registration", + ); + return Err(error); + } + }; + if let Some(deadline) = deadline { + if let Err(error) = admit_child_process_sync_timer( + &runtime, + notify, + deadline, + vm_id, + process_id, + current_process_path, + ) { + self.rollback_registered_child_process_sync( + vm_id, + process_id, + current_process_path, + &prior_child_ids, + Some(&identity.child_process_id), + Some(identity.pid), + "nested spawnSync timer admission", + ); + return Err(error); + } + } + Ok(()) + } + + async fn defer_descendant_javascript_child_process_sync( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: ProcessLaunchRequest, + max_buffer: Option, + ) -> Result { + let (respond_to, receiver) = tokio::sync::oneshot::channel(); + self.begin_descendant_child_process_sync( + vm_id, + process_id, + current_process_path, + request, + max_buffer, + PendingChildProcessSyncCompletion::Javascript(respond_to), + ) + .await?; + Ok(HostServiceResponse::Deferred { + receiver, + timeout: None, + task_class: agentos_runtime::TaskClass::Vm, + }) + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) async fn defer_descendant_javascript_child_process_sync_for_test( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: ProcessLaunchRequest, + max_buffer: Option, + ) -> Result { + self.defer_descendant_javascript_child_process_sync( + vm_id, + process_id, + current_process_path, + request, + max_buffer, + ) + .await + } + + fn settle_descendant_managed_network_response( + &self, + vm_id: &str, + root_process_id: &str, + caller_process_path: &[&str], + runtime: agentos_runtime::RuntimeContext, + reply: DirectHostReplyHandle, + operation: &str, + response: Result, + ) -> Result<(), SidecarError> { + let response = match response { + Ok(HostServiceResponse::Deferred { + receiver, + timeout, + task_class, + }) => { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "managed-network descendant VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let connection_id = vm.connection_id.clone(); + let session_id = vm.session_id.clone(); + let sender = self.process_event_sender.clone(); + let event_notify = Arc::clone(&self.process_event_notify); + let envelope_vm_id = vm_id.to_owned(); + let envelope_process_id = + Self::child_process_path_label(root_process_id, caller_process_path); + let task_reply = reply.clone(); + let method = operation.to_owned(); + if let Err(error) = runtime.spawn(task_class, async move { + let receive = async { + receiver.await.unwrap_or_else(|_| { + Err(crate::state::DeferredRpcError { + code: String::from( + "ERR_AGENTOS_DEFERRED_RPC_RESPONSE_CHANNEL_CLOSED", + ), + message: format!( + "deferred managed-network response channel closed for {method}" + ), + details: None, + }) + }) + }; + let result = match timeout { + Some(timeout) => match crate::execution::operation_deadline_timeout( + &method, + timeout, + receive, + ) + .await + { + Ok(result) => result, + Err(_) => Err(crate::state::DeferredRpcError { + code: String::from("ETIMEDOUT"), + message: format!( + "{method} exceeded limits.reactor.operationDeadlineMs ({} ms)", + timeout.as_millis() + ), + details: None, + }), + }, + None => receive.await, + }; + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: envelope_vm_id, + process_id: envelope_process_id, + event: ActiveExecutionEvent::HostCallCompletion( + crate::state::HostCallCompletion { + reply: task_reply, + result, + }, + ), + }; + if let Err(error) = sender.send(envelope).await { + if let ActiveExecutionEvent::HostCallCompletion(completion) = + error.0.event + { + if let Err(settlement_error) = completion.reply.fail(HostServiceError::new( + "ECANCELED", + "descendant managed-network completion lane closed", + )) { + eprintln!( + "ERR_AGENTOS_DESCENDANT_COMPLETION_SETTLEMENT: {settlement_error}" + ); + } + } + eprintln!( + "ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: descendant managed-network completion could not be delivered" + ); + } else { + event_notify.notify_one(); + } + }) { + reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + return Ok(()); + } + other => other, + }; + settle_execution_host_call(&reply, response) + } + + fn dispatch_descendant_context_descriptor_operation( + &mut self, + vm_id: &str, + root_process_id: &str, + caller_process_path: &[&str], + operation: agentos_execution::host::FilesystemOperation, + reply: DirectHostReplyHandle, + ) -> Result<(), SidecarError> { + let (generation, caller_pid) = { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(root) = vm.active_processes.get(root_process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call root process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(caller) = Self::active_process_by_path(root, caller_process_path) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call descendant process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + (vm.generation, caller.kernel_pid) + }; + let identity = reply.identity(); + if identity.generation != generation || identity.pid != caller_pid { + reply + .fail( + HostServiceError::new( + "ESTALE", + "descriptor host call identity does not match the descendant process", + ) + .with_details(json!({ + "expectedGeneration": generation, + "expectedPid": caller_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) + .map_err(SidecarError::from)?; + return Ok(()); + } + + let host_operation = HostOperation::Filesystem(operation.clone()); + let vm = self + .vms + .get(vm_id) + .expect("validated descriptor descendant VM remains registered"); + if let Err(error) = + host_dispatch::authorize_host_operation(&vm.kernel, caller_pid, &host_operation) + { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + let socket_paths = build_socket_path_context(vm)?; + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + + let bridge = self.bridge.clone(); + let vm = self + .vms + .get_mut(vm_id) + .expect("validated descriptor descendant VM remains registered"); + let result = match operation { + agentos_execution::host::FilesystemOperation::Close { fd } => { + host_dispatch::close_with_managed_retirement( + &bridge, + vm_id, + &socket_paths, + vm, + caller_pid, + fd, + ) + } + agentos_execution::host::FilesystemOperation::CloseFrom { min_fd, exact_fds } => { + host_dispatch::closefrom_with_managed_retirement( + &bridge, + vm_id, + &socket_paths, + vm, + caller_pid, + min_fd, + exact_fds, + ) } - }; - let parent = match Self::active_process_by_path_mut(root, current_process_path) { - Some(parent) => parent, - None => { - let error = SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )); - if let Some(child) = pending_kernel_handle.take() { - rollback_unregistered_spawn_child( - &mut vm.kernel, - &child, - Some(&mut execution), - "nested child_process.spawn", - ); - } - return Err(error); + operation @ (agentos_execution::host::FilesystemOperation::Renumber { .. } + | agentos_execution::host::FilesystemOperation::DuplicateTo { .. } + | agentos_execution::host::FilesystemOperation::Move { .. }) => { + host_dispatch::replace_descriptor_with_managed_retirement( + &bridge, + vm_id, + &socket_paths, + vm, + caller_pid, + operation, + ) } + other => Err(SidecarError::host( + "EINVAL", + format!( + "descendant descriptor dispatcher received unsupported operation: {other:?}" + ), + )), }; - let inherited_tty_master_owner = if child_fd1_is_tty { - parent - .tty_master_fd - .map(|master_fd| (parent.kernel_pid, master_fd)) - .or(parent.tty_master_owner) - } else { - None - }; - pending_kernel_handle.take(); - parent.child_processes.insert( - child_process_id.clone(), - ActiveProcess::new( - kernel_pid, - kernel_handle, - parent.runtime_context.clone(), - parent.limits.clone(), - process_event_capacity, - resolved.runtime, - execution, - ) - .with_event_notify(Arc::clone(&self.process_event_notify)) - .with_process_event_limits(&process_event_limits) - .with_vm_pending_byte_budgets( - Arc::clone(&vm_pending_stdin_bytes_budget), - Arc::clone(&vm_pending_event_bytes_budget), - ) - .with_detached(request.options.detached) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(resolved.env.clone()) - .with_shadow_root(shadow_root) - .with_host_cwd(resolved.host_cwd.clone()), - ); - { - let child = parent - .child_processes - .get_mut(&child_process_id) - .expect("inserted nested child exists during spawn registration"); - child.tty_master_owner = inherited_tty_master_owner; - child.direct_posix_stdin = direct_posix_stdin; - child.kernel_stdin_reader_fd = kernel_stdin_reader_fd; - if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { - child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); - } - prepared_host_net_fds.install(child); + match result { + Ok(response) => reply.succeed(response), + Err(error) => reply.fail(host_service_error(&error)), } - record_execute_phase("child_process_register", phase_start.elapsed()); - record_execute_phase("child_process_spawn_total", total_start.elapsed()); - Ok(json!({ - "childId": child_process_id, - "pid": kernel_pid, - "pgid": child_process_group, - "directPosixStdin": direct_posix_stdin, - "command": resolved.command, - "args": resolved.process_args, - })) + .map_err(SidecarError::from) } - #[cfg(test)] - #[allow(dead_code)] - pub(crate) async fn spawn_descendant_javascript_child_process_for_test( - &mut self, + fn dispatch_descendant_context_dns_operation( + &self, vm_id: &str, - process_id: &str, - current_process_path: &[&str], - request: JavascriptChildProcessSpawnRequest, - ) -> Result { - self.spawn_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - request, - ) - .await + root_process_id: &str, + caller_process_path: &[&str], + operation: agentos_execution::host::NetworkOperation, + reply: DirectHostReplyHandle, + ) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(root) = vm.active_processes.get(root_process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call root process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(caller) = Self::active_process_by_path(root, caller_process_path) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call descendant process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let identity = reply.identity(); + if identity.generation != vm.generation || identity.pid != caller.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "DNS host call identity does not match the descendant process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let runtime = vm.runtime_context.clone(); + let response = service_host_dns_operation( + self.bridge.clone(), + &vm.kernel, + vm_id.to_owned(), + vm.dns.clone(), + operation, + ); + let task_reply = reply.clone(); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Dns, async move { + let settled = match response.await { + Ok(response) => task_reply.succeed(response), + Err(error) => task_reply.fail(error), + }; + if let Err(error) = settled { + eprintln!("ERR_AGENTOS_DESCENDANT_DNS_DIRECT_REPLY: {error}"); + } + }) { + reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + Ok(()) } - async fn defer_descendant_javascript_child_process_sync( + async fn dispatch_descendant_context_managed_network_operation( &mut self, vm_id: &str, - process_id: &str, - current_process_path: &[&str], - request: JavascriptChildProcessSpawnRequest, - max_buffer: Option, - ) -> Result { - let max_buffer = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let root = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "unknown child process path during nested spawnSync", + root_process_id: &str, + caller_process_path: &[&str], + operation: agentos_execution::host::NetworkOperation, + reply: DirectHostReplyHandle, + ) -> Result<(), SidecarError> { + let (generation, caller_pid) = { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", )) - })?; - Self::child_process_sync_max_buffer(parent, max_buffer)? + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(root) = vm.active_processes.get(root_process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call root process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(caller) = Self::active_process_by_path(root, caller_process_path) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call descendant process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + (vm.generation, caller.kernel_pid) }; - let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; - let deadline = request - .options - .timeout - .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); - let timeout_signal = request - .options - .kill_signal - .clone() - .unwrap_or_else(|| String::from("SIGTERM")); - let spawned = self - .spawn_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - request, - ) - .await?; - let child_process_id = spawned - .get("childId") - .and_then(Value::as_str) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing childId", - )) - })? - .to_owned(); - let pid = spawned - .get("pid") - .and_then(Value::as_u64) - .and_then(|pid| u32::try_from(pid).ok()) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing a valid pid", - )) - })?; + let identity = reply.identity(); + if identity.generation != generation || identity.pid != caller_pid { + reply + .fail( + HostServiceError::new( + "ESTALE", + "managed-network host call identity does not match the descendant process", + ) + .with_details(json!({ + "expectedGeneration": generation, + "expectedPid": caller_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) + .map_err(SidecarError::from)?; + return Ok(()); + } - if let Some(input) = sync_input.as_deref() { - self.write_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - &child_process_id, - input, - )?; + let host_operation = HostOperation::Network(operation.clone()); + let vm = self + .vms + .get(vm_id) + .expect("validated managed-network descendant VM remains registered"); + if let Err(error) = + host_dispatch::authorize_host_operation(&vm.kernel, caller_pid, &host_operation) + { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); } - self.close_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - &child_process_id, - )?; - let (respond_to, receiver) = tokio::sync::oneshot::channel(); - let (runtime, notify) = { + let bridge = self.bridge.clone(); + let socket_paths = build_socket_path_context( + self.vms + .get(vm_id) + .expect("validated managed-network descendant VM remains registered"), + )?; + let (runtime, response, label) = { let vm = self .vms .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; + .expect("validated managed-network descendant VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let dns = vm.dns.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); let root = vm .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "unknown child process path during nested spawnSync", - )) - })?; - parent.pending_child_process_sync.insert( - child_process_id, - PendingChildProcessSync { - pid, - stdout: Vec::new(), - stderr: Vec::new(), - max_buffer, - deadline, - timeout_signal, - kill_sent: false, - timed_out: false, - max_buffer_exceeded: false, - completion: PendingChildProcessSyncCompletion::Javascript(respond_to), - }, - ); - ( - parent.runtime_context.clone(), - Arc::clone(&parent.process_event_notify), - ) + .get_mut(root_process_id) + .expect("validated managed-network root remains registered"); + let process = Self::active_process_by_path_mut(root, caller_process_path) + .expect("validated managed-network descendant remains registered"); + use agentos_execution::host::NetworkOperation as HostNetworkOperation; + let (response, label) = match operation { + operation @ (HostNetworkOperation::Socket { .. } + | HostNetworkOperation::Bind { .. } + | HostNetworkOperation::Connect { .. } + | HostNetworkOperation::Listen { .. } + | HostNetworkOperation::Accept { .. } + | HostNetworkOperation::Validate { .. } + | HostNetworkOperation::Receive { .. } + | HostNetworkOperation::Send { .. } + | HostNetworkOperation::LocalAddress { .. } + | HostNetworkOperation::PeerAddress { .. } + | HostNetworkOperation::GetOption { .. } + | HostNetworkOperation::SetOption { .. } + | HostNetworkOperation::Poll { .. } + | HostNetworkOperation::TlsConnect { .. }) => ( + host_dispatch::service_descendant_managed_fd_network_operation( + &bridge, + vm_id, + &dns, + &socket_paths, + &mut vm.kernel, + kernel_readiness, + process, + capabilities, + managed_descriptions, + reply.identity().call_id, + operation, + ), + "managed fd network", + ), + operation @ (HostNetworkOperation::ManagedUdpCreate { .. } + | HostNetworkOperation::ManagedUdpBind { .. } + | HostNetworkOperation::ManagedUdpSend { .. } + | HostNetworkOperation::ManagedUdpClose { .. }) => ( + service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge: &bridge, + kernel: &mut vm.kernel, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + process, + kernel_readiness, + capabilities, + }, + operation, + ), + "managed UDP", + ), + operation @ (HostNetworkOperation::ManagedPoll { .. } + | HostNetworkOperation::ManagedWaitConnect { .. } + | HostNetworkOperation::ManagedRead { .. } + | HostNetworkOperation::ManagedWrite { .. } + | HostNetworkOperation::ManagedDestroy { .. } + | HostNetworkOperation::ManagedAccept { .. } + | HostNetworkOperation::ManagedCloseListener { .. } + | HostNetworkOperation::ManagedTlsUpgrade { .. }) => ( + service_managed_network_operation( + ManagedNetworkServiceContext { + vm_id, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + capabilities, + }, + operation, + ), + "managed network", + ), + operation @ (HostNetworkOperation::ManagedBindUnix { .. } + | HostNetworkOperation::ManagedBindConnectedUnix { .. } + | HostNetworkOperation::ManagedReserveTcpPort { .. } + | HostNetworkOperation::ManagedReleaseTcpPort { .. } + | HostNetworkOperation::ManagedConnect { .. } + | HostNetworkOperation::ManagedListen { .. }) => ( + service_managed_endpoint_operation( + ManagedEndpointServiceContext { + bridge: &bridge, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + capabilities, + call_id: reply.identity().call_id, + }, + operation, + ), + "managed endpoint", + ), + operation @ (HostNetworkOperation::SendDescriptorRights { .. } + | HostNetworkOperation::ReceiveDescriptorRights { .. }) => { + let request = descriptor_rights_compat_request( + reply.identity().call_id, + operation, + )?; + ( + service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { + bridge: &bridge, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + sync_request: &request, + capabilities, + managed_descriptions: Some(Arc::clone( + &vm.managed_host_net_descriptions, + )), + }) + .await, + "descriptor rights", + ) + } + other => ( + Err(SidecarError::host( + "EINVAL", + format!( + "descendant managed-network dispatcher received unsupported operation: {other:?}" + ), + )), + "managed network", + ), + }; + (runtime, response, label) }; - if let Some(deadline) = deadline { - let delay = deadline.saturating_duration_since(Instant::now()); - runtime - .spawn(agentos_runtime::TaskClass::Timer, async move { - tokio::time::sleep(delay).await; - notify.notify_one(); - }) + self.settle_descendant_managed_network_response( + vm_id, + root_process_id, + caller_process_path, + runtime, + reply, + label, + response, + ) + } + + async fn dispatch_descendant_context_process_operation( + &mut self, + vm_id: &str, + root_process_id: &str, + caller_process_path: &[&str], + operation: HostOperation, + reply: DirectHostReplyHandle, + ) -> Result<(), SidecarError> { + let (generation, caller_pid) = { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(root) = vm.active_processes.get(root_process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call root process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(caller) = Self::active_process_by_path(root, caller_process_path) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call descendant process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + (vm.generation, caller.kernel_pid) + }; + let identity = reply.identity(); + if identity.generation != generation || identity.pid != caller_pid { + reply + .fail( + HostServiceError::new( + "ESTALE", + "host call identity does not match the descendant kernel process", + ) + .with_details(json!({ + "expectedGeneration": generation, + "expectedPid": caller_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) .map_err(SidecarError::from)?; + return Ok(()); } - Ok(JavascriptSyncRpcServiceResponse::Deferred { - receiver, - timeout: None, - task_class: agentos_runtime::TaskClass::Vm, - }) + + let HostOperation::Process(operation) = operation else { + reply + .fail(HostServiceError::new( + "EINVAL", + "descendant context dispatcher requires a process operation", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + + let result = match operation { + ProcessOperation::Spawn(request) => { + let mut request = request.into_request(); + if let Err(error) = merge_process_internal_bootstrap_env(self, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, false)) + { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + self.spawn_descendant_process(vm_id, root_process_id, caller_process_path, request) + .await + .map(HostCallReply::Json) + } + ProcessOperation::RunCaptured { + request, + max_buffer, + } => { + let mut request = request.into_request(); + if let Err(error) = merge_process_internal_bootstrap_env(self, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, false)) + { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + if let Err(error) = self + .begin_descendant_child_process_sync( + vm_id, + root_process_id, + caller_process_path, + request, + Some(max_buffer.get()), + PendingChildProcessSyncCompletion::Direct(reply.clone()), + ) + .await + { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + } + return Ok(()); + } + ProcessOperation::PollChild { child_id, wait_ms } => { + if let Err(error) = self.validate_child_poll_target( + vm_id, + root_process_id, + caller_process_path, + child_id.as_str(), + ) { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + // Keep descendant polling nonblocking and reply in the same + // event turn. Boxing is required because a descendant can in + // turn poll one of its own descendants through this path. + Box::pin(self.poll_descendant_process( + vm_id, + root_process_id, + caller_process_path, + child_id.as_str(), + wait_ms, + )) + .await + .map(HostCallReply::Json) + } + ProcessOperation::WriteChildStdin { child_id, chunk } => { + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + self.write_descendant_process_stdin( + vm_id, + root_process_id, + caller_process_path, + child_id.as_str(), + chunk.as_slice(), + ) + .map(|()| HostCallReply::Json(Value::Null)) + } + ProcessOperation::CloseChildStdin { child_id } => { + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + self.close_descendant_process_stdin( + vm_id, + root_process_id, + caller_process_path, + child_id.as_str(), + ) + .map(|()| HostCallReply::Json(Value::Null)) + } + ProcessOperation::Exec(request) => { + let mut request = request.into_request(); + let fd_image_commit = request.options.executable_fd.is_some(); + let preflight = if fd_image_commit { + validate_wasm_fd_image_commit_request(&request) + } else { + merge_process_internal_bootstrap_env(self, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, true)) + }; + if let Err(error) = preflight { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let local_replacement = request.options.local_replacement; + let result = if fd_image_commit { + self.commit_wasm_fd_process_image( + vm_id, + root_process_id, + caller_process_path, + request, + ) + } else { + self.exec_process_image(vm_id, root_process_id, caller_process_path, request) + }; + match result { + Ok(()) if local_replacement => { + reply + .succeed_json(json!({ "committed": true })) + .map_err(SidecarError::from)?; + return Ok(()); + } + Ok(()) => { + reply.dismiss_claimed().map_err(SidecarError::from)?; + return Ok(()); + } + Err(error) => Err(error), + } + } + other => Err(SidecarError::host( + "ENOSYS", + format!("unsupported descendant process operation: {other:?}"), + )), + }; + match result { + Ok(response) => reply.succeed(response), + Err(error) => reply.fail(host_service_error(&error)), + } + .map_err(SidecarError::from) } async fn handle_descendant_javascript_child_process_rpc( @@ -6075,22 +9093,17 @@ where vm_id: &str, process_id: &str, current_process_path: &[&str], - request: &JavascriptSyncRpcRequest, - ) -> Result { + request: &ExecutionHostCall, + ) -> Result { match request.method.as_str() { "child_process.spawn" => { let Some(vm) = self.vms.get(vm_id) else { return Ok(Value::Null.into()); }; let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.spawn_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - payload, - ) - .await - .map(Into::into) + self.spawn_descendant_process(vm_id, process_id, current_process_path, payload) + .await + .map(Into::into) } "child_process.spawn_sync" => { let Some(vm) = self.vms.get(vm_id) else { @@ -6116,13 +9129,12 @@ where "child_process.poll wait ms", )? .unwrap_or_default(); - Box::pin(self.poll_descendant_javascript_child_process( + Box::pin(self.poll_descendant_process( vm_id, process_id, current_process_path, child_process_id, wait_ms, - false, )) .await .map(Into::into) @@ -6138,7 +9150,7 @@ where 1, "child_process.write_stdin chunk", )?; - self.write_descendant_javascript_child_process_stdin( + self.write_descendant_process_stdin( vm_id, process_id, current_process_path, @@ -6153,7 +9165,7 @@ where 0, "child_process.close_stdin child id", )?; - self.close_descendant_javascript_child_process_stdin( + self.close_descendant_process_stdin( vm_id, process_id, current_process_path, @@ -6194,7 +9206,7 @@ where process_id: &str, current_process_path: &[&str], child_process_id: &str, - request: &JavascriptSyncRpcRequest, + request: &ExecutionHostCall, ) -> Result { let event_notify = Arc::clone(&self.process_event_notify); let Some(vm) = self.vms.get_mut(vm_id) else { @@ -6212,6 +9224,18 @@ where let Some(child) = parent.child_processes.get_mut(child_process_id) else { return Ok(true); }; + let pending_call_id = child + .deferred_kernel_wait_rpc + .as_ref() + .map(|(pending, _)| pending.id); + if let Err(error) = admit_one_slot_rpc(pending_call_id, request.id, "deferredKernelWaitRpc") + { + request + .reply + .fail(error) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + return Ok(true); + } if request.method == "__kernel_stdio_write" || request.method == "process.fd_write" { if request.method == "__kernel_stdio_write" && child.tty_master_owner.is_some() { return Ok(false); @@ -6232,33 +9256,39 @@ where match response { Ok(response) => { child.clear_deferred_kernel_wait_rpc(); - child - .execution - .respond_javascript_sync_rpc_response(request.id, response.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + settle_execution_host_call(&request.reply, Ok(response.into()))?; } - Err(error) - if javascript_sync_rpc_error_code(&error) == "EAGAIN" && now >= deadline => - { + Err(error) if host_service_error_code(&error) == "EAGAIN" && now >= deadline => { child.clear_deferred_kernel_wait_rpc(); - child - .execution - .respond_javascript_sync_rpc_error( - request.id, + request + .reply + .fail(HostServiceError::new( "ETIMEDOUT", format!( "pipe write exceeded limits.reactor.operationDeadlineMs ({operation_deadline_ms} ms); raise that limit for slower readers" ), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + )) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } - Err(error) if javascript_sync_rpc_error_code(&error) == "EAGAIN" => { + Err(error) if host_service_error_code(&error) == "EAGAIN" => { if arm_deadline_wake { - let delay = deadline.saturating_duration_since(now); + let limit = Duration::from_millis(operation_deadline_ms); + let operation = if request.method == "__kernel_stdio_write" { + "deferred child stdio write" + } else { + "deferred child fd write" + }; child.clear_deferred_kernel_wait_rpc(); child.deferred_kernel_wait_rpc = Some((request.clone(), Some(deadline))); let timer = runtime.spawn(agentos_runtime::TaskClass::Timer, async move { - tokio::time::sleep(delay).await; + let mut deadline = + crate::execution::OperationDeadlineTracker::from_deadline( + deadline, limit, false, + ); + tokio::time::sleep(deadline.remaining_until_next_edge()).await; + deadline.observe(operation); + event_notify.notify_one(); + tokio::time::sleep(deadline.remaining_until_deadline()).await; event_notify.notify_one(); }); match timer { @@ -6267,27 +9297,23 @@ where } Err(agentos_runtime::TaskSpawnError::ResourceLimit(limit)) => { child.clear_deferred_kernel_wait_rpc(); - child - .execution - .respond_javascript_sync_rpc_error( - request.id, - "ERR_AGENTOS_RESOURCE_LIMIT", - crate::state::guest_limit_message(&limit), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + let error = SidecarError::ResourceLimit(limit); + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } Err( error @ agentos_runtime::TaskSpawnError::AdmissionClosed { .. }, ) => { child.clear_deferred_kernel_wait_rpc(); - child - .execution - .respond_javascript_sync_rpc_error( - request.id, + request + .reply + .fail(HostServiceError::new( "ERR_AGENTOS_TASK_ADMISSION_CLOSED", error.to_string(), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + )) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } } else { @@ -6301,26 +9327,14 @@ where error = %error, "child JavaScript sync RPC failed" ); - child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } return Ok(true); } - if request.method == "__kernel_stdin_read" - && matches!(child.execution, ActiveExecution::Javascript(_)) - && child.tty_master_fd.is_none() - && !child.direct_posix_stdin - && child.kernel_stdin_writer_fd.is_some() - { - return Ok(false); - } if request.method == "process.fd_read" { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?; let stat = kernel @@ -6356,7 +9370,20 @@ where }; let deadline = match &child.deferred_kernel_wait_rpc { Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline, - _ => requested_timeout_ms.map(|timeout_ms| now + Duration::from_millis(timeout_ms)), + _ => match requested_timeout_ms { + Some(timeout_ms) => match checked_deferred_guest_wait_deadline(timeout_ms) { + Ok(deadline) => Some(deadline), + Err(error) => { + child.clear_deferred_kernel_wait_rpc(); + request + .reply + .fail(error) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + return Ok(true); + } + }, + None => None, + }, }; let kernel_pid = child.kernel_pid; let mut fd_read = None; @@ -6401,14 +9428,10 @@ where Ok(probe) => probe, Err(error) => { child.clear_deferred_kernel_wait_rpc(); - child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; return Ok(true); } }; @@ -6426,9 +9449,10 @@ where if let Some((fd, length)) = fd_read { // Claim before the destructive read. A stale reply token must // not consume bytes intended for a later read on this fd. - let claimed = child - .execution - .claim_javascript_sync_rpc_response(request.id)?; + let claimed = request + .reply + .claim() + .map_err(|error| SidecarError::Execution(error.to_string()))?; if !claimed { return Ok(true); } @@ -6440,33 +9464,25 @@ where Some(Duration::ZERO), ); match read_result { - Ok(Some(bytes)) => child - .execution - .respond_claimed_javascript_sync_rpc_success( - request.id, - javascript_sync_rpc_bytes_value(&bytes), - )?, - Ok(None) => child - .execution - .respond_claimed_javascript_sync_rpc_success( - request.id, - javascript_sync_rpc_bytes_value(&[]), - )?, + Ok(Some(bytes)) => request + .reply + .succeed(HostCallReply::Json(host_bytes_value(&bytes))) + .map_err(|error| SidecarError::Execution(error.to_string()))?, + Ok(None) => request + .reply + .succeed(HostCallReply::Json(host_bytes_value(&[]))) + .map_err(|error| SidecarError::Execution(error.to_string()))?, Err(error) => { let error = kernel_error(error); - child.execution.respond_claimed_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - )?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } return Ok(true); } - child - .execution - .respond_javascript_sync_rpc_response(request.id, probe.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + settle_execution_host_call(&request.reply, Ok(probe.into()))?; return Ok(true); } child.deferred_kernel_wait_rpc = Some((request.clone(), deadline)); @@ -6484,27 +9500,18 @@ where vm_id: &str, writer_kernel_pid: u32, owner: (u32, u32), - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?; let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?; - if fd != 1 && fd != 2 { - return Err(SidecarError::InvalidState(format!( - "__kernel_stdio_write only supports fd 1/2, got {fd}" - ))); - } let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(json!(chunk.len())); }; - let written = if fd == 1 { - vm.kernel - .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) - .map_err(kernel_error)? - } else { - vm.kernel - .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) - .map_err(kernel_error)? - }; + kernel_stdio_output_is_stdout(&vm.kernel, writer_kernel_pid, fd)?; + let written = vm + .kernel + .fd_write(EXECUTION_DRIVER_NAME, writer_kernel_pid, fd, &chunk) + .map_err(kernel_error)?; let (owner_pid, master_fd) = owner; let mut drained: Vec = Vec::new(); loop { @@ -6531,56 +9538,391 @@ where .queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?; } } - Ok(json!(written)) + Ok(json!(written)) + } + + /// Re-check a child's parked kernel-wait RPC (see + /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration. + fn recheck_child_deferred_kernel_wait_rpc( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + ) -> Result<(), SidecarError> { + let parked = { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Ok(()); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(()); + }; + child + .deferred_kernel_wait_rpc + .as_ref() + .map(|(request, _)| request.clone()) + }; + if let Some(request) = parked { + let handled = self.service_child_kernel_wait_rpc( + vm_id, + process_id, + current_process_path, + child_process_id, + &request, + )?; + if !handled { + return Err(SidecarError::InvalidState(format!( + "parked child kernel-wait RPC {} no longer belongs to the deferred path", + request.id + ))); + } + } + Ok(()) + } + + fn service_descendant_guest_wait( + &mut self, + vm_id: &str, + process_id: &str, + child_path: &[&str], + incoming: Option<( + DeferredGuestWaitKind, + Option, + DirectHostReplyHandle, + )>, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.process_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(process) = Self::active_process_by_path_mut(root, child_path) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if incoming.is_none() && process.deferred_guest_wait.is_none() { + return Ok(()); + } + service_deferred_guest_wait( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + incoming, + ) + } + + fn service_descendant_kernel_poll( + &mut self, + vm_id: &str, + process_id: &str, + child_path: &[&str], + incoming: Option<( + agentos_execution::host::BoundedVec, + Option, + DirectHostReplyHandle, + )>, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let socket_paths = self + .vms + .get(vm_id) + .map(build_socket_path_context) + .transpose()?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(process) = Self::active_process_by_path_mut(root, child_path) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if incoming.is_none() && process.deferred_kernel_poll.is_none() { + return Ok(()); + } + if incoming.is_none() + && process + .deferred_kernel_poll + .as_ref() + .is_some_and(|poll| poll.combined) + { + return host_dispatch::service_deferred_posix_poll( + generation, + &runtime, + wait_handle, + notify, + socket_paths + .as_ref() + .expect("registered VM has a socket path context"), + kernel_readiness, + capabilities, + managed_descriptions, + kernel, + process, + None, + ); + } + host_dispatch::service_deferred_kernel_poll( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + incoming, + ) + } + + fn service_descendant_posix_poll( + &mut self, + vm_id: &str, + process_id: &str, + child_path: &[&str], + incoming: ( + agentos_execution::host::BoundedVec, + Option, + Option, + DirectHostReplyHandle, + ), + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let socket_paths = self + .vms + .get(vm_id) + .map(build_socket_path_context) + .transpose()?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(process) = Self::active_process_by_path_mut(root, child_path) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + host_dispatch::service_deferred_posix_poll( + generation, + &runtime, + wait_handle, + notify, + socket_paths + .as_ref() + .expect("registered VM has a socket path context"), + kernel_readiness, + capabilities, + managed_descriptions, + kernel, + process, + Some(incoming), + ) + } + + fn service_descendant_kernel_read( + &mut self, + vm_id: &str, + process_id: &str, + child_path: &[&str], + incoming: Option<( + u32, + agentos_execution::host::BoundedUsize, + Instant, + DirectHostReplyHandle, + )>, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(process) = Self::active_process_by_path_mut(root, child_path) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if incoming.is_none() && process.deferred_kernel_read.is_none() { + return Ok(()); + } + host_dispatch::service_deferred_kernel_read( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + incoming, + ) + } + + fn service_descendant_kernel_stdin_read( + &mut self, + vm_id: &str, + process_id: &str, + child_path: &[&str], + incoming: Option<( + agentos_execution::host::BoundedUsize, + Instant, + DirectHostReplyHandle, + )>, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(process) = Self::active_process_by_path_mut(root, child_path) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if incoming.is_none() && process.deferred_kernel_read.is_none() { + return Ok(()); + } + host_dispatch::service_deferred_kernel_stdin_read( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + incoming, + ) } - /// Re-check a child's parked kernel-wait RPC (see - /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration. - fn recheck_child_deferred_kernel_wait_rpc( + fn descendant_output_stream( + &self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + emitted_stream: InheritedOutputStream, + ) -> Result, SidecarError> { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(None); + }; + let Some(root) = vm.active_processes.get(process_id) else { + return Ok(None); + }; + let Some(parent) = Self::active_process_by_path(root, current_process_path) else { + return Ok(None); + }; + let Some(child) = parent.child_processes.get(child_process_id) else { + return Ok(None); + }; + if child.child_process_bridge_owns_output + || parent + .pending_child_process_sync + .contains_key(child_process_id) + { + return Ok(None); + } + let source_fd = match emitted_stream { + InheritedOutputStream::Stdout => 1, + InheritedOutputStream::Stderr => 2, + }; + let description = vm + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, child.kernel_pid, source_fd) + .map_err(kernel_error)? + .0; + let path = vm + .kernel + .fd_path(EXECUTION_DRIVER_NAME, child.kernel_pid, source_fd) + .map_err(kernel_error)?; + Ok(classify_inherited_output_stream(description, path.as_str())) + } + + fn route_descendant_output_to_parent( &mut self, vm_id: &str, process_id: &str, current_process_path: &[&str], child_process_id: &str, + polled: PolledExecutionEvent, ) -> Result<(), SidecarError> { - let parked = { + let queued = { let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(()); }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Ok(()); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { return Ok(()); }; - child - .deferred_kernel_wait_rpc - .as_ref() - .map(|(request, _)| request.clone()) + parent.try_queue_pending_polled_execution_event(polled) }; - if let Some(request) = parked { - let _ = self.service_child_kernel_wait_rpc( - vm_id, - process_id, - current_process_path, - child_process_id, - &request, - )?; + let Err((error, polled)) = queued else { + return Ok(()); + }; + + // Parent output admission is backpressure, not permission to discard + // an already-accounted child event. Return the same leased envelope to + // its pull-owned queue without releasing/reacquiring VM byte budget. + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { + return Ok(()); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(()); + }; + child.queue_pull_owned_polled_execution_event(polled)?; + if error.code() == Some("ERR_AGENTOS_RESOURCE_LIMIT") { + return Ok(()); } - Ok(()) + Err(error) } - async fn poll_descendant_javascript_child_process( + async fn poll_descendant_process( &mut self, vm_id: &str, process_id: &str, current_process_path: &[&str], child_process_id: &str, wait_ms: u64, - preserve_pull_owned_events: bool, ) -> Result { let mut child_path = current_process_path.to_vec(); child_path.push(child_process_id); @@ -6589,8 +9931,22 @@ where // callers, but the sidecar never parks while servicing it. Runtime // event producers wake the shared process pump instead. let _ = wait_ms; + let internal_work_limit = self + .config + .runtime + .fairness + .capability_quantum_operations + .max(1); + let mut internal_work = 0usize; loop { + if internal_work >= internal_work_limit { + // The child event remains durable. Rearm the coalesced broker + // before yielding so one descendant cannot monopolize a + // sidecar turn with an unbounded stream of internal HostCalls. + self.process_event_notify.notify_one(); + return Ok(Value::Null); + } self.drain_queued_descendant_javascript_child_process_events( vm_id, process_id, @@ -6602,6 +9958,9 @@ where current_process_path, child_process_id, )?; + self.service_descendant_guest_wait(vm_id, process_id, &child_path, None)?; + self.service_descendant_kernel_poll(vm_id, process_id, &child_path, None)?; + self.service_descendant_kernel_read(vm_id, process_id, &child_path, None)?; enum ChildPollResult { Event(Box>), RecoverRuntimeExit, @@ -6625,14 +9984,7 @@ where match child.try_poll_execution_event() { Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))), Ok(None) => ChildPollResult::Timeout, - Err(SidecarError::Execution(message)) - if (child.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (child.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (child.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { + Err(SidecarError::ExecutionEventChannelClosed { .. }) => { ChildPollResult::RecoverRuntimeExit } Err(error) => return Err(error), @@ -6671,44 +10023,459 @@ where if synthetic_signal_termination { // The following exit event carries the authoritative signal status. drop(reservation); + internal_work += 1; continue; } - if preserve_pull_owned_events - && matches!( - &event, - ActiveExecutionEvent::Stdout(_) - | ActiveExecutionEvent::Stderr(_) - | ActiveExecutionEvent::Exited(_) - ) - { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - child.queue_pending_polled_execution_event(PolledExecutionEvent { - event, - reservation, - })?; - return Ok(Value::Null); + if matches!( + &event, + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { .. }) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) + | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + ) { + internal_work += 1; } match event { + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { operation, reply }) => { + drop(reservation); + let deferred_kernel_read = match &operation { + HostOperation::Filesystem( + agentos_execution::host::FilesystemOperation::Read { + fd, + max_bytes, + offset: None, + deadline_ms: Some(timeout_ms), + }, + ) => Some((Some(*fd), max_bytes.clone(), *timeout_ms)), + HostOperation::Filesystem( + agentos_execution::host::FilesystemOperation::StdinRead { + max_bytes, + timeout_ms, + }, + ) => Some((None, max_bytes.clone(), *timeout_ms)), + _ => None, + }; + if let Some((fd, max_bytes, timeout_ms)) = deferred_kernel_read { + let deadline = + match host_dispatch::checked_deferred_guest_wait_deadline(timeout_ms) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + continue; + } + }; + if let Some(fd) = fd { + self.service_descendant_kernel_read( + vm_id, + process_id, + &child_path, + Some((fd, max_bytes, deadline, reply)), + )?; + } else { + self.service_descendant_kernel_stdin_read( + vm_id, + process_id, + &child_path, + Some((max_bytes, deadline, reply)), + )?; + } + continue; + } + let descriptor_operation = match &operation { + HostOperation::Filesystem( + operation @ (agentos_execution::host::FilesystemOperation::Close { + .. + } + | agentos_execution::host::FilesystemOperation::CloseFrom { .. } + | agentos_execution::host::FilesystemOperation::Renumber { .. } + | agentos_execution::host::FilesystemOperation::DuplicateTo { .. } + | agentos_execution::host::FilesystemOperation::Move { .. }), + ) => Some(operation.clone()), + _ => None, + }; + if let Some(operation) = descriptor_operation { + self.dispatch_descendant_context_descriptor_operation( + vm_id, + process_id, + &child_path, + operation, + reply, + )?; + continue; + } + let deferred_posix_poll = match &operation { + HostOperation::Network( + agentos_execution::host::NetworkOperation::PosixPoll { + interests, + timeout_ms, + signal_mask, + }, + ) => Some((interests.clone(), *timeout_ms, *signal_mask)), + _ => None, + }; + if let Some((interests, timeout_ms, signal_mask)) = deferred_posix_poll { + let deadline = match timeout_ms + .map(host_dispatch::checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + continue; + } + }; + self.service_descendant_posix_poll( + vm_id, + process_id, + &child_path, + (interests, deadline, signal_mask, reply), + )?; + continue; + } + let deferred_kernel_poll = match &operation { + HostOperation::Network( + agentos_execution::host::NetworkOperation::KernelPoll { + interests, + timeout_ms, + }, + ) => Some((interests.clone(), *timeout_ms)), + _ => None, + }; + if let Some((interests, timeout_ms)) = deferred_kernel_poll { + let deadline = match timeout_ms + .map(host_dispatch::checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + continue; + } + }; + self.service_descendant_kernel_poll( + vm_id, + process_id, + &child_path, + Some((interests, deadline, reply)), + )?; + continue; + } + let dns_operation = match &operation { + HostOperation::Network(operation) + if matches!( + operation, + agentos_execution::host::NetworkOperation::ResolveDns { .. } + | agentos_execution::host::NetworkOperation::ResolveDnsRecord { .. } + ) => + { + Some(operation.clone()) + } + _ => None, + }; + if let Some(operation) = dns_operation { + self.dispatch_descendant_context_dns_operation( + vm_id, + process_id, + &child_path, + operation, + reply, + )?; + continue; + } + let descendant_udp_poll = match &operation { + HostOperation::Network( + operation @ agentos_execution::host::NetworkOperation::ManagedUdpPoll { + .. + }, + ) => Some(operation.clone()), + _ => None, + }; + if let Some(operation) = descendant_udp_poll { + dispatch_descendant_context_udp_poll( + self, + vm_id, + process_id, + &child_path, + operation, + reply, + )?; + continue; + } + let descendant_stream_read = match &operation { + HostOperation::Network( + agentos_execution::host::NetworkOperation::ManagedRead { + socket_id, + max_bytes, + peek, + wait_ms, + }, + ) => Some((socket_id.as_str().to_owned(), *max_bytes, *peek, *wait_ms)), + _ => None, + }; + if let Some((socket_id, max_bytes, peek, wait_ms)) = descendant_stream_read { + dispatch_descendant_context_stream_read( + self, + vm_id, + process_id, + &child_path, + socket_id, + max_bytes, + peek, + wait_ms, + reply, + )?; + continue; + } + let managed_network = match &operation { + HostOperation::Network(operation) + if matches!( + operation, + agentos_execution::host::NetworkOperation::Socket { .. } + | agentos_execution::host::NetworkOperation::Bind { .. } + | agentos_execution::host::NetworkOperation::Connect { .. } + | agentos_execution::host::NetworkOperation::Listen { .. } + | agentos_execution::host::NetworkOperation::Accept { .. } + | agentos_execution::host::NetworkOperation::Validate { .. } + | agentos_execution::host::NetworkOperation::Receive { .. } + | agentos_execution::host::NetworkOperation::Send { .. } + | agentos_execution::host::NetworkOperation::LocalAddress { .. } + | agentos_execution::host::NetworkOperation::PeerAddress { .. } + | agentos_execution::host::NetworkOperation::GetOption { .. } + | agentos_execution::host::NetworkOperation::SetOption { .. } + | agentos_execution::host::NetworkOperation::Poll { .. } + | agentos_execution::host::NetworkOperation::TlsConnect { .. } + | agentos_execution::host::NetworkOperation::ManagedBindUnix { .. } + | agentos_execution::host::NetworkOperation::ManagedBindConnectedUnix { .. } + | agentos_execution::host::NetworkOperation::ManagedReserveTcpPort { .. } + | agentos_execution::host::NetworkOperation::ManagedReleaseTcpPort { .. } + | agentos_execution::host::NetworkOperation::ManagedConnect { .. } + | agentos_execution::host::NetworkOperation::ManagedListen { .. } + | agentos_execution::host::NetworkOperation::ManagedPoll { .. } + | agentos_execution::host::NetworkOperation::ManagedWaitConnect { .. } + | agentos_execution::host::NetworkOperation::ManagedRead { .. } + | agentos_execution::host::NetworkOperation::ManagedWrite { .. } + | agentos_execution::host::NetworkOperation::ManagedDestroy { .. } + | agentos_execution::host::NetworkOperation::ManagedAccept { .. } + | agentos_execution::host::NetworkOperation::ManagedCloseListener { .. } + | agentos_execution::host::NetworkOperation::ManagedTlsUpgrade { .. } + | agentos_execution::host::NetworkOperation::ManagedUdpCreate { .. } + | agentos_execution::host::NetworkOperation::ManagedUdpBind { .. } + | agentos_execution::host::NetworkOperation::ManagedUdpSend { .. } + | agentos_execution::host::NetworkOperation::ManagedUdpClose { .. } + | agentos_execution::host::NetworkOperation::SendDescriptorRights { .. } + | agentos_execution::host::NetworkOperation::ReceiveDescriptorRights { .. } + ) => Some(operation.clone()), + _ => None, + }; + if let Some(operation) = managed_network { + self.dispatch_descendant_context_managed_network_operation( + vm_id, + process_id, + &child_path, + operation, + reply, + ) + .await?; + continue; + } + let deferred_guest_wait = match &operation { + HostOperation::Process(ProcessOperation::Wait { + target, + options, + deadline_ms, + temporary_mask, + }) => { + if temporary_mask.is_some() { + reply + .fail(HostServiceError::new( + "EINVAL", + "waitpid does not accept a temporary signal mask", + )) + .map_err(SidecarError::from)?; + continue; + } + let deadline = match deadline_ms + .map(host_dispatch::checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + continue; + } + }; + Some(( + DeferredGuestWaitKind::Process { + target: *target, + options: *options, + }, + deadline, + )) + } + HostOperation::Clock(ClockOperation::Sleep { duration_ms }) => { + let deadline = match host_dispatch::checked_deferred_guest_wait_deadline( + *duration_ms, + ) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + continue; + } + }; + Some((DeferredGuestWaitKind::Sleep, Some(deadline))) + } + _ => None, + }; + if let Some((kind, deadline)) = deferred_guest_wait { + self.service_descendant_guest_wait( + vm_id, + process_id, + &child_path, + Some((kind, deadline, reply)), + )?; + continue; + } + if matches!( + operation, + HostOperation::Process( + ProcessOperation::Spawn(_) + | ProcessOperation::RunCaptured { .. } + | ProcessOperation::Exec(_) + | ProcessOperation::PollChild { .. } + | ProcessOperation::WriteChildStdin { .. } + | ProcessOperation::CloseChildStdin { .. } + ) + ) { + self.dispatch_descendant_context_process_operation( + vm_id, + process_id, + &child_path, + operation, + reply, + ) + .await?; + continue; + } + let Some(vm) = self.vms.get_mut(vm_id) else { + cancel_direct_host_reply( + &reply, + "descendant host-call target VM no longer exists", + )?; + return Ok(Value::Null); + }; + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + cancel_direct_host_reply( + &reply, + "descendant host-call root process no longer exists", + )?; + return Ok(Value::Null); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) + else { + cancel_direct_host_reply( + &reply, + "descendant host-call parent process no longer exists", + )?; + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + cancel_direct_host_reply( + &reply, + "descendant host-call child process no longer exists", + )?; + return Ok(Value::Null); + }; + let effects = + dispatch_host_operation(generation, kernel, child, operation, reply)?; + if effects.may_make_fd_readable { + Self::wake_ready_deferred_fd_reads(vm)?; + } + if effects.may_make_fd_writable { + Self::wake_ready_deferred_fd_writes(vm)?; + } + continue; + } + ActiveExecutionEvent::Common(other) => { + drop(reservation); + return Err(SidecarError::InvalidState(format!( + "unsupported common child event: {other:?}" + ))); + } + ActiveExecutionEvent::ManagedStreamReadRecheck(pending) => { + drop(reservation); + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "stream read re-entry targeted descendant child routing", + )) + .map_err(SidecarError::from)?; + continue; + } + ActiveExecutionEvent::ManagedUdpPollRecheck(pending) => { + drop(reservation); + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "UDP poll re-entry targeted descendant child routing", + )) + .map_err(SidecarError::from)?; + continue; + } ActiveExecutionEvent::Stdout(chunk) => { + if let Some(sink) = self.descendant_output_stream( + vm_id, + process_id, + current_process_path, + child_process_id, + InheritedOutputStream::Stdout, + )? { + let event = match sink { + InheritedOutputStream::Stdout => ActiveExecutionEvent::Stdout(chunk), + InheritedOutputStream::Stderr => ActiveExecutionEvent::Stderr(chunk), + }; + self.route_descendant_output_to_parent( + vm_id, + process_id, + current_process_path, + child_process_id, + PolledExecutionEvent { event, reservation }, + )?; + return Ok(Value::Null); + } return Ok(json!({ "type": "stdout", - "data": javascript_sync_rpc_bytes_value(&chunk), + "data": host_bytes_value(&chunk), })); } ActiveExecutionEvent::Stderr(chunk) => { + if let Some(sink) = self.descendant_output_stream( + vm_id, + process_id, + current_process_path, + child_process_id, + InheritedOutputStream::Stderr, + )? { + let event = match sink { + InheritedOutputStream::Stdout => ActiveExecutionEvent::Stdout(chunk), + InheritedOutputStream::Stderr => ActiveExecutionEvent::Stderr(chunk), + }; + self.route_descendant_output_to_parent( + vm_id, + process_id, + current_process_path, + child_process_id, + PolledExecutionEvent { event, reservation }, + )?; + return Ok(Value::Null); + } return Ok(json!({ "type": "stderr", - "data": javascript_sync_rpc_bytes_value(&chunk), + "data": host_bytes_value(&chunk), })); } ActiveExecutionEvent::Exited(mut exit_code) => { @@ -6727,6 +10494,7 @@ where let Some(child) = parent.child_processes.get_mut(child_process_id) else { return Ok(Value::Null); }; + child.discard_pending_exit_events(); loop { let next = poll_child_execution_after_exit(child)?; let Some(next) = next else { @@ -6753,6 +10521,7 @@ where } }; if had_trailing_events { + internal_work += 1; continue; } @@ -6774,8 +10543,7 @@ where let Some(child) = parent.child_processes.get_mut(child_process_id) else { return Ok(Value::Null); }; - let runtime_pid = child.execution.child_pid(); - if runtime_pid != 0 && !child.execution.uses_shared_v8_runtime() { + if let Some(runtime_pid) = child.execution.native_process_id() { if let RuntimeChildStatusObservation::Exited(status) = runtime_child_exit_status(runtime_pid)? { @@ -6786,12 +10554,11 @@ where } } - let parent_signal_key = - Self::child_process_signal_key(process_id, current_process_path); + let bridge = self.bridge.clone(); let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(Value::Null); }; - let (signal_name, core_dumped) = { + let (exit_signal, signal_name, core_dumped) = { let Some(parent) = Self::descendant_parent_process_mut( vm, process_id, @@ -6803,48 +10570,37 @@ where return Ok(Value::Null); }; let actual_signal = child.exit_signal.take(); - child.pending_self_signal_exit = None; ( + actual_signal, actual_signal .and_then(canonical_signal_name) .map(str::to_owned), child.exit_core_dumped, ) }; - let ( - parent_runtime_pid, - parent_v8_signal_session, - parent_is_wasm, - should_signal_parent, - ) = { - let Some(parent) = - Self::descendant_parent_process(vm, process_id, current_process_path) - else { + let terminating_kernel_pids = { + let Some(parent) = Self::descendant_parent_process_mut( + vm, + process_id, + current_process_path, + ) else { return Ok(Value::Null); }; - ( - parent.execution.child_pid(), - parent.execution.javascript_v8_session_handle().filter(|_| { - matches!( - &parent.execution, - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() - ) - }), - matches!(&parent.execution, ActiveExecution::Wasm(_)), - vm.signal_states - .get(parent_signal_key) - .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32))) - .is_some_and(|registration| { - registration.action != SignalDispositionAction::Default - }), - ) + let Some(child) = parent.child_processes.get(child_process_id) else { + return Ok(Value::Null); + }; + Self::terminating_process_tree_kernel_pids(child) }; + for kernel_pid in terminating_kernel_pids { + retire_managed_process_routes(&bridge, vm_id, vm, kernel_pid)?; + } let Some(parent) = Self::descendant_parent_process_mut(vm, process_id, current_process_path) else { return Ok(Value::Null); }; + let guest_owns_kernel_wait = parent.execution.descendant_wait_ownership() + == DescendantWaitOwnership::Guest; let Some(mut child) = parent.child_processes.remove(child_process_id) else { return Ok(Value::Null); }; @@ -6852,17 +10608,6 @@ where Self::child_process_path_label(process_id, &child_path); let detached_children = Self::adopt_detached_child_processes(&child_process_label, &mut child); - // A WASM child writes directly to the kernel VFS. Importing - // its unchanged host shadow here would overwrite those - // writes with the pre-spawn snapshot (for example, undoing - // an append to an existing file). Host-backed runtimes still - // need their dirty or otherwise non-observable writes - // reconciled before teardown, matching root-process exit. - if child.host_write_dirty_recursive() - || !child.clean_host_writes_are_observable_recursive() - { - sync_process_host_writes_to_kernel(vm, &child)?; - } release_inherited_child_raw_mode(&mut vm.kernel, &child)?; let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let unix_address_registry = Arc::clone(&vm.unix_address_registry); @@ -6872,31 +10617,23 @@ where &kernel_readiness, &unix_address_registry, ); - child.kernel_handle.finish(exit_code); - let _ = vm.kernel.wait_and_reap(child.kernel_pid); - vm.signal_states.remove(child_process_id); + finish_kernel_child_from_runtime_exit( + &child.kernel_handle, + exit_code, + exit_signal, + core_dumped, + ); + if !guest_owns_kernel_wait { + vm.kernel + .wait_and_reap(child.kernel_pid) + .map_err(kernel_error)?; + } for (detached_process_id, detached_child) in detached_children { vm.detached_child_processes .insert(detached_process_id.clone()); vm.active_processes .insert(detached_process_id, detached_child); } - if should_signal_parent { - if parent_is_wasm { - let Some(parent) = Self::descendant_parent_process_mut( - vm, - process_id, - current_process_path, - ) else { - return Ok(Value::Null); - }; - parent.queue_pending_wasm_signal(libc::SIGCHLD)?; - } else if let Some(session) = parent_v8_signal_session { - dispatch_v8_session_signal(session, libc::SIGCHLD); - } else { - signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?; - } - } let mut payload = Map::new(); payload.insert(String::from("type"), Value::String(String::from("exit"))); payload.insert(String::from("exitCode"), Value::from(exit_code)); @@ -6907,7 +10644,7 @@ where record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed()); return Ok(Value::Object(payload)); } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::HostRpcRequest(request) => { drop(reservation); let mut current_child_path = current_process_path.to_vec(); current_child_path.push(child_process_id); @@ -6928,14 +10665,18 @@ where deferred_kernel_wait_request_for_process(&request, &vm.kernel, child)? }; if let Some(kernel_wait_request) = kernel_wait_request { + let kernel_wait_call = ExecutionHostCall { + request: kernel_wait_request, + reply: request.reply.clone(), + }; if self.service_child_kernel_wait_rpc( vm_id, process_id, current_process_path, child_process_id, - &kernel_wait_request, + &kernel_wait_call, )? { - if javascript_sync_rpc_may_make_fd_writable(&kernel_wait_request) { + if javascript_sync_rpc_may_make_fd_writable(&kernel_wait_call) { let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(Value::Null); }; @@ -6950,7 +10691,7 @@ where }) .and_then(|parent| parent.child_processes.get(child_process_id)) .and_then(|child| child.deferred_kernel_wait_rpc.as_ref()) - .is_some_and(|(parked, _)| parked.id == kernel_wait_request.id); + .is_some_and(|(parked, _)| parked.id == kernel_wait_call.id); if parked { // The execution keeps exposing the unresolved // sync request until it receives a reply. Yield @@ -6992,88 +10733,25 @@ where owner, &request, ); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::active_process_by_path_mut(root, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) - else { - return Ok(Value::Null); - }; - match response { - Ok(result) => child - .execution - .respond_javascript_sync_rpc_response(request.id, result.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - Err(error) => child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - } + settle_execution_host_call(&request.reply, response.map(Into::into))?; continue; } } - let response = if request.method == "process.exec_fd_image_commit" { - let payload = { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(Value::Null); - }; - parse_javascript_child_process_spawn_request(vm, &request.args)?.0 - }; - self.commit_wasm_fd_process_image( - vm_id, - process_id, - ¤t_child_path, - payload, - )?; - Ok(json!({ "committed": true }).into()) - } else if request.method == "process.exec" { - let payload = { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(Value::Null); - }; - parse_javascript_child_process_spawn_request(vm, &request.args)?.0 - }; - let local_replacement = payload.options.local_replacement; - match self.exec_javascript_process_image( - vm_id, - process_id, - ¤t_child_path, - payload, - ) { - Ok(()) if local_replacement => Ok(json!({ "committed": true }).into()), - // Separate-runtime exec destroys the old image, so - // no response may resume its blocked RPC call. - Ok(()) => return Ok(Value::Null), - Err(error) => Err(error), - } - } else if request.method == "process.signal_state" { + let response = if request.method == "process.signal_state" { let (signal, registration) = parse_process_signal_state_request(&request.args) - .map_err(|error| SidecarError::InvalidState(error.to_string()))?; + .map_err(SidecarError::from)?; let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(Value::Null); }; - let signal_key = - Self::child_process_signal_key(process_id, ¤t_child_path) - .to_owned(); - apply_process_signal_state_update( - &mut vm.signal_states, - &signal_key, - signal, - registration, - ); + let Some(root) = vm.active_processes.get(process_id) else { + return Ok(Value::Null); + }; + let Some(process) = Self::active_process_by_path(root, ¤t_child_path) + else { + return Ok(Value::Null); + }; + apply_kernel_signal_registration(process, signal, ®istration)?; Ok(Value::Null.into()) } else if request.method == "process.kill" { self.handle_descendant_process_kill_rpc( @@ -7096,7 +10774,7 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(Value::Null); }; - let socket_paths = build_javascript_socket_path_context(vm)?; + let socket_paths = build_socket_path_context(vm)?; let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let capabilities = vm.capabilities.clone(); let Some(root) = vm.active_processes.get_mut(process_id) else { @@ -7120,12 +10798,15 @@ where process: child, sync_request: &request, capabilities, + managed_descriptions: Some(Arc::clone( + &vm.managed_host_net_descriptions, + )), }) .await }; let response = match response { - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout, task_class, @@ -7141,7 +10822,7 @@ where let envelope_vm_id = vm_id.to_owned(); let envelope_process_id = Self::child_process_path_label(process_id, ¤t_child_path); - let request_id = request.id; + let reply = request.reply.clone(); let method = request.method.clone(); runtime .spawn(task_class, async move { @@ -7154,12 +10835,19 @@ where message: format!( "deferred sync RPC response channel closed for {method}" ), + details: None, }) }) }; let result = match timeout { Some(timeout) => { - match tokio::time::timeout(timeout, receive).await { + match crate::execution::operation_deadline_timeout( + &method, + timeout, + receive, + ) + .await + { Ok(result) => result, Err(_) => Err(crate::state::DeferredRpcError { code: String::from( @@ -7169,27 +10857,40 @@ where "deferred sync RPC {method} timed out after {} ms", timeout.as_millis() ), + details: None, }), } } None => receive.await, }; - if sender - .send(ProcessEventEnvelope { - connection_id, - session_id, - vm_id: envelope_vm_id, - process_id: envelope_process_id, - event: ActiveExecutionEvent::JavascriptSyncRpcCompletion( - crate::state::JavascriptSyncRpcCompletion { - request_id, - result, - }, - ), - }) - .await - .is_err() - { + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: envelope_vm_id, + process_id: envelope_process_id, + event: ActiveExecutionEvent::HostCallCompletion( + crate::state::HostCallCompletion { + reply, + result, + }, + ), + }; + if let Err(error) = sender.send(envelope).await { + if let ActiveExecutionEvent::HostCallCompletion( + completion, + ) = error.0.event + { + if let Err(settlement_error) = + cancel_host_call_completion( + &completion, + "nested deferred sync RPC completion lane closed", + ) + { + eprintln!( + "ERR_AGENTOS_NESTED_COMPLETION_SETTLEMENT: {settlement_error}" + ); + } + } eprintln!( "ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: nested deferred sync RPC completion could not be delivered" ); @@ -7216,21 +10917,10 @@ where Self::wake_ready_deferred_fd_writes(vm)?; } - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; let parent_signal_event = response .as_ref() .ok() - .and_then(JavascriptSyncRpcServiceResponse::as_json) + .and_then(HostServiceResponse::as_json) .and_then(|result| { let target_path_label = Self::child_process_path_label(process_id, current_process_path); @@ -7247,114 +10937,54 @@ where "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(), })) }); - match response { - Ok(result) => child - .execution - .respond_javascript_sync_rpc_response(request.id, result) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - Err(error) => child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - } - if preserve_pull_owned_events { - // The WASM parent owns stream delivery, but the global - // process pump may service one child RPC per turn so a - // blocked foreground child cannot starve a background - // sibling that supplies its readiness transition. - return Ok(Value::Null); - } + settle_execution_host_call(&request.reply, response)?; if let Some(event) = parent_signal_event { return Ok(event); } } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { + ActiveExecutionEvent::HostCallCompletion(completion) => { drop(reservation); - // The kernel-VFS bridge is wired for top-level Python - // executions; a nested Python child (spawned by a JS/Python - // parent) cannot service VFS RPCs through this child-event - // path. Respond with a recoverable error instead of aborting - // the child, so its runner falls back to the in-isolate FS - // for the nested process — top-level Python keeps the full - // VFS root. let Some(vm) = self.vms.get_mut(vm_id) else { + cancel_host_call_completion( + &completion, + "nested deferred sync RPC target VM no longer exists", + )?; return Ok(Value::Null); }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - // Best-effort: deliver the "unavailable" error so the child's - // pending VFS RPC resolves and its runner falls back to the - // in-isolate FS. A stale child is recoverable, but the failed - // settlement remains host-visible for lifecycle diagnosis. - if let Err(error) = child.execution.respond_python_vfs_rpc_error( - request.id, - "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE", - "python VFS is not available for nested child processes", - ) { - eprintln!( - "ERR_AGENTOS_PYTHON_VFS_RESPONSE: nested child response {} failed: {error}", - request.id - ); - } - } - ActiveExecutionEvent::PythonSocketConnectCompletion(_) => { - drop(reservation); - eprintln!( - "ERR_AGENTOS_PYTHON_SOCKET_COMPLETION_ROUTE: nested Python TCP completion reached a child execution queue" - ); - } - ActiveExecutionEvent::JavascriptSyncRpcCompletion(completion) => { - drop(reservation); - let Some(vm) = self.vms.get_mut(vm_id) else { + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let unix_addresses = Arc::clone(&vm.unix_address_registry); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(root) = active_processes.get_mut(process_id) else { + cancel_host_call_completion( + &completion, + "nested deferred sync RPC root process no longer exists", + )?; return Ok(Value::Null); }; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + cancel_host_call_completion( + &completion, + "nested deferred sync RPC parent process no longer exists", + )?; return Ok(Value::Null); }; let Some(child) = parent.child_processes.get_mut(child_process_id) else { + cancel_host_call_completion( + &completion, + "nested deferred sync RPC child process no longer exists", + )?; return Ok(Value::Null); }; - let connected = child - .pending_javascript_net_connects - .remove(&completion.request_id); - let completion_result = match (completion.result, connected) { - (Ok(_), Some(connected)) => { - finalize_javascript_net_connect(child, &kernel_readiness, connected) - .map_err(|error| crate::state::DeferredRpcError { - code: javascript_sync_rpc_error_code(&error), - message: javascript_sync_rpc_error_message(&error), - }) - } - (result @ Err(_), Some(connected)) => { - restore_pending_bound_unix_connect(child, &connected)?; - result - } - (result, None) => result, - }; - let result = match completion_result { - Ok(value) => child - .execution - .respond_javascript_sync_rpc_success(completion.request_id, value), - Err(error) => child.execution.respond_javascript_sync_rpc_error( - completion.request_id, - error.code, - error.message, - ), - }; - result.or_else(ignore_stale_javascript_sync_rpc_response)?; + settle_host_call_completion_for_process( + kernel, + &kernel_readiness, + &unix_addresses, + &managed_descriptions, + child, + completion, + )?; } ActiveExecutionEvent::SignalState { signal, @@ -7364,14 +10994,13 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(Value::Null); }; - let signal_key = - Self::child_process_signal_key(process_id, &child_path).to_owned(); - apply_process_signal_state_update( - &mut vm.signal_states, - &signal_key, - signal, - registration.clone(), - ); + let Some(root) = vm.active_processes.get(process_id) else { + return Ok(Value::Null); + }; + let Some(process) = Self::active_process_by_path(root, &child_path) else { + return Ok(Value::Null); + }; + apply_kernel_signal_registration(process, signal, ®istration)?; return Ok(json!({ "type": "signal_state", "signal": signal, @@ -7389,13 +11018,7 @@ where current_process_path: &[&str], child_process_id: &str, ) -> Result, SidecarError> { - let ( - parent_kernel_pid, - child_kernel_pid, - child_runtime_pid, - child_runtime, - child_shared_runtime, - ) = { + let (parent_kernel_pid, child_kernel_pid, child_runtime_pid) = { let mut child_path = current_process_path.to_vec(); child_path.push(child_process_id); let Some(vm) = self.vms.get_mut(vm_id) else { @@ -7412,17 +11035,9 @@ where ( parent.kernel_pid, child.kernel_pid, - child.execution.child_pid(), - child.runtime.clone(), - child.execution.uses_shared_v8_runtime(), + child.execution.native_process_id(), ) }; - if child_runtime != GuestRuntimeKind::JavaScript - && child_runtime != GuestRuntimeKind::Python - && child_runtime != GuestRuntimeKind::WebAssembly - { - return Ok(None); - } let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(None); }; @@ -7446,7 +11061,7 @@ where return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status))); } - if !child_shared_runtime && child_runtime_pid != 0 { + if let Some(child_runtime_pid) = child_runtime_pid { match runtime_child_exit_status(child_runtime_pid)? { RuntimeChildStatusObservation::Exited(status) => { let Some(root) = vm.active_processes.get_mut(process_id) else { @@ -7465,8 +11080,7 @@ where } RuntimeChildStatusObservation::Running => {} RuntimeChildStatusObservation::NotWaitable => { - return Err(SidecarError::Execution(format!( - "ECHILD: guest runtime process {child_runtime_pid} exited without an observable wait status" + return Err(SidecarError::host("ECHILD", format!("guest runtime process {child_runtime_pid} exited without an observable wait status" ))); } } @@ -7474,7 +11088,7 @@ where Ok(None) } - fn write_descendant_javascript_child_process_stdin( + fn write_descendant_process_stdin( &mut self, vm_id: &str, process_id: &str, @@ -7496,16 +11110,12 @@ where let Some(child) = parent.child_processes.get_mut(child_process_id) else { return Err(javascript_child_process_gone_error(process_id, &child_path)); }; - if let Err(error) = child.execution.write_stdin(chunk) { - if is_broken_pipe_error(&error) { - return Ok(()); - } - return Err(error); - } - write_kernel_process_stdin(&mut vm.kernel, child, chunk) + write_kernel_process_stdin(&mut vm.kernel, child, chunk)?; + self.process_event_notify.notify_one(); + Ok(()) } - fn close_descendant_javascript_child_process_stdin( + fn close_descendant_process_stdin( &mut self, vm_id: &str, process_id: &str, @@ -7530,8 +11140,9 @@ where "stdin close", ); }; - child.execution.close_stdin()?; - close_kernel_process_stdin(&mut vm.kernel, child) + close_kernel_process_stdin(&mut vm.kernel, child)?; + self.process_event_notify.notify_one(); + Ok(()) } fn kill_descendant_javascript_child_process( @@ -7547,11 +11158,6 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(()); }; - let registration = vm - .signal_states - .get(child_process_id) - .and_then(|handlers| handlers.get(&(signal as u32))) - .cloned(); let Some(root) = vm.active_processes.get_mut(process_id) else { return Ok(()); }; @@ -7562,12 +11168,7 @@ where let Some(child) = parent.child_processes.get_mut(child_process_id) else { return Ok(()); }; - terminate_tracked_child_process_for_signal( - &mut vm.kernel, - child, - signal, - registration.as_ref(), - )?; + terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal, None)?; let child_process_label = if current_process_path.is_empty() { child_process_id.to_owned() } else { @@ -7596,7 +11197,7 @@ where process_id: &str, current_process_path: &[&str], child_process_id: &str, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; @@ -7609,19 +11210,22 @@ where let pgid = target_pid.unsigned_abs(); let caller_kernel_pid = { let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + String::from("unknown VM during process.kill"), + )); }; let Some(root) = vm.active_processes.get(process_id) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process {process_id} during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + format!("unknown process {process_id} during process.kill",), + )); }; let Some(source) = Self::active_process_by_path(root, &source_path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown child process {child_process_id} during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + format!("unknown child process {child_process_id} during process.kill",), + )); }; source.kernel_pid }; @@ -7639,22 +11243,29 @@ where let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { return Ok(Value::Null); }; - if !matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) { - apply_active_process_default_signal(&mut vm.kernel, source, signal)?; - } + let action = protocol_signal_registration( + source + .kernel_handle + .signal_action(signal, None) + .map_err(kernel_error)?, + ) + .action; + terminate_tracked_child_process_for_signal(&mut vm.kernel, source, signal, None)?; return Ok(json!({ "self": true, - "action": "default", + "action": match action { + SignalDispositionAction::Default => "default", + SignalDispositionAction::Ignore => "ignore", + SignalDispositionAction::User => "user", + }, })); } let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + String::from("unknown VM during process.kill"), + )); }; if signal == 0 { @@ -7665,18 +11276,20 @@ where } let target_kernel_pid = u32::try_from(target_pid).map_err(|_| { - SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}")) + SidecarError::host("EINVAL", format!("invalid process pid {target_pid}")) })?; let (source_pid, located_target_path) = { let Some(root) = vm.active_processes.get(process_id) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process {process_id} during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + format!("unknown process {process_id} during process.kill",), + )); }; let Some(source) = Self::active_process_by_path(root, &source_path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown child process {child_process_id} during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + format!("unknown child process {child_process_id} during process.kill",), + )); }; vm.kernel .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0) @@ -7694,87 +11307,53 @@ where return Ok(Value::Null); }; let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + String::from("unknown VM during process.kill"), + )); }; - if source_pid == target_kernel_pid { - let Some(root) = vm.active_processes.get_mut(process_id) else { + let self_signal = source_pid == target_kernel_pid; + let action = { + let Some(root) = vm.active_processes.get(process_id) else { return Ok(Value::Null); }; - let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { - return Ok(Value::Null); + let target_path_refs = target_path.iter().map(String::as_str).collect::>(); + let Some(target) = Self::active_process_by_path(root, &target_path_refs) else { + return Err(SidecarError::host( + "ESRCH", + format!("unknown process pid {target_pid}"), + )); }; - if !matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) { - apply_active_process_default_signal(&mut vm.kernel, source, signal)?; - } - return Ok(json!({ - "self": true, - "action": "default", - })); - } + let action = if signal == 0 { + SignalDispositionAction::Default + } else { + protocol_signal_registration( + target + .kernel_handle + .signal_action(signal, None) + .map_err(kernel_error)?, + ) + .action + }; + action + }; - let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id); - let registration = vm - .signal_states - .get(signal_key) - .and_then(|handlers| handlers.get(&(signal as u32))) - .cloned(); + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) else { + return Err(SidecarError::host( + "ESRCH", + format!("unknown process pid {target_pid}"), + )); + }; + terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal, None)?; - let action = match registration - .as_ref() - .map(|registration| ®istration.action) - { - Some(SignalDispositionAction::Ignore) => "ignore", - Some(SignalDispositionAction::User) => { - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) - else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process pid {target_pid}" - ))); - }; - if matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) - { - target.queue_pending_wasm_signal(signal)?; - } else if let Some(session) = target.execution.javascript_v8_session_handle().filter( - |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime()), - ) { - dispatch_v8_session_signal(session, signal); - } else if !dispatch_v8_process_signal(target, signal)? { - return Err(SidecarError::InvalidState(format!( - "unsupported guest signal delivery for pid {target_pid}" - ))); - } - "user" - } - Some(SignalDispositionAction::Default) | None - if matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGURG") - ) => - { - "ignore" - } - Some(SignalDispositionAction::Default) | None => { - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) - else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process pid {target_pid}" - ))); - }; - apply_active_process_default_signal(&mut vm.kernel, target, signal)?; - "default" - } + let action = match action { + SignalDispositionAction::Default => "default", + SignalDispositionAction::Ignore => "ignore", + SignalDispositionAction::User => "user", }; let target_path_label = Self::child_process_path_label( @@ -7799,7 +11378,7 @@ where ); Ok(json!({ - "self": false, + "self": self_signal, "action": action, "signal": signal_name, "number": signal, @@ -7807,25 +11386,42 @@ where })) } - pub(crate) async fn poll_javascript_child_process( + pub(crate) async fn poll_child_process( &mut self, vm_id: &str, process_id: &str, child_process_id: &str, wait_ms: u64, ) -> Result { - self.poll_descendant_javascript_child_process( - vm_id, - process_id, - &[], - child_process_id, - wait_ms, - false, - ) - .await + self.poll_descendant_process(vm_id, process_id, &[], child_process_id, wait_ms) + .await + } + + pub(crate) fn validate_child_poll_target( + &self, + vm_id: &str, + root_process_id: &str, + caller_process_path: &[&str], + child_process_id: &str, + ) -> Result<(), SidecarError> { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let caller = Self::active_process_by_path(root, caller_process_path).ok_or_else(|| { + javascript_child_process_gone_error(root_process_id, caller_process_path) + })?; + if !caller.child_processes.contains_key(child_process_id) { + return Err(javascript_child_process_gone_error( + root_process_id, + &[child_process_id], + )); + } + Ok(()) } - pub(crate) fn write_javascript_child_process_stdin( + pub(crate) fn write_child_process_stdin( &mut self, vm_id: &str, process_id: &str, @@ -7850,16 +11446,12 @@ where &[child_process_id], )); }; - if let Err(error) = child.execution.write_stdin(chunk) { - if is_broken_pipe_error(&error) { - return Ok(()); - } - return Err(error); - } - write_kernel_process_stdin(&mut vm.kernel, child, chunk) + write_kernel_process_stdin(&mut vm.kernel, child, chunk)?; + self.process_event_notify.notify_one(); + Ok(()) } - pub(crate) fn close_javascript_child_process_stdin( + pub(crate) fn close_child_process_stdin( &mut self, vm_id: &str, process_id: &str, @@ -7871,19 +11463,21 @@ where &[child_process_id], )); }; - let process = vm + let Some(child) = vm .active_processes .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let Some(child) = process.child_processes.get_mut(child_process_id) else { - return missing_javascript_child_cleanup_result( - process.next_child_process_id, - child_process_id, - "stdin close", - ); + .ok_or_else(|| missing_process_error(vm_id, process_id))? + .child_processes + .get_mut(child_process_id) + else { + return Err(javascript_child_process_gone_error( + process_id, + &[child_process_id], + )); }; - child.execution.close_stdin()?; - close_kernel_process_stdin(&mut vm.kernel, child) + close_kernel_process_stdin(&mut vm.kernel, child)?; + self.process_event_notify.notify_one(); + Ok(()) } pub(crate) fn kill_javascript_child_process( @@ -7898,11 +11492,6 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(()); }; - let registration = vm - .signal_states - .get(child_process_id) - .and_then(|handlers| handlers.get(&(signal as u32))) - .cloned(); let process = vm .active_processes .get_mut(process_id) @@ -7918,12 +11507,7 @@ where "kill", ); }; - terminate_tracked_child_process_for_signal( - &mut vm.kernel, - child, - signal, - registration.as_ref(), - )?; + terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal, None)?; emit_security_audit_event( &self.bridge, vm_id, diff --git a/crates/native-sidecar/src/execution/coordinator.rs b/crates/native-sidecar/src/execution/coordinator.rs index 4ffd887014..6cb16f3656 100644 --- a/crates/native-sidecar/src/execution/coordinator.rs +++ b/crates/native-sidecar/src/execution/coordinator.rs @@ -16,7 +16,7 @@ impl DeferredResponseSettlement for tokio::sync::oneshot::Sender { pub(super) fn validate_guest_network_capability_alias( process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result<(), SidecarError> { if !(request.method.starts_with("net.") || request.method.starts_with("dgram.") @@ -72,9 +72,10 @@ pub(super) fn validate_guest_network_capability_alias( .lock() .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?; let generation = process.runtime_context.vm_generation().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_CAPABILITY_SESSION: process runtime is not VM-generation scoped", - )) + SidecarError::host( + "ERR_AGENTOS_CAPABILITY_SESSION", + String::from("process runtime is not VM-generation scoped"), + ) })?; for (key, kind) in [ ( @@ -99,18 +100,6 @@ pub(super) fn validate_guest_network_capability_alias( Ok(()) } -pub(super) fn closed_javascript_event_channel(message: &str) -> bool { - message == "guest JavaScript event channel closed unexpectedly" -} - -pub(super) fn closed_python_event_channel(message: &str) -> bool { - message == "guest Python event channel closed unexpectedly" -} - -pub(super) fn closed_wasm_event_channel(message: &str) -> bool { - message == WasmExecutionError::EventChannelClosed.to_string() -} - pub(super) fn missing_vm_error(vm_id: &str) -> SidecarError { SidecarError::InvalidState(format!("VM {vm_id} is no longer active")) } @@ -121,29 +110,15 @@ pub(super) fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarErr )) } -/// Map a shared guest-kernel-call dispatcher error into a sidecar error, -/// preserving POSIX errno codes (`ECODE: message`) as kernel errors so guest -/// callers observe Linux-faithful failures, mirroring the filesystem path. +/// Map a shared guest-kernel-call dispatcher error without reconstructing an +/// errno from its human-readable diagnostic. fn guest_kernel_core_error(error: agentos_native_sidecar_core::SidecarCoreError) -> SidecarError { - let message = error.to_string(); - let is_errno = message.split_once(':').is_some_and(|(code, _)| { - code.len() >= 2 - && code.starts_with('E') - && code[1..] - .bytes() - .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') - }); - if is_errno { - SidecarError::Kernel(message) - } else { - SidecarError::InvalidState(message) + match error.code() { + Some(code) => SidecarError::Host(HostServiceError::new(code, error.message())), + None => SidecarError::InvalidState(error.to_string()), } } -pub(super) fn is_broken_pipe_error(error: &SidecarError) -> bool { - matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE")) -} - pub(super) fn javascript_child_process_gone_error( process_id: &str, child_path: &[&str], @@ -153,16 +128,14 @@ pub(super) fn javascript_child_process_gone_error( } else { format!("{process_id}/{}", child_path.join("/")) }; - SidecarError::Execution(format!( - "ECHILD: child_process {child_label} is no longer available" + SidecarError::Host(HostServiceError::new( + "ECHILD", + format!("child_process {child_label} is no longer available"), )) } pub(super) fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool { - matches!( - error, - SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD") - ) + guest_error_code(error) == Some("ECHILD") } pub(super) fn missing_javascript_child_cleanup_result( @@ -304,16 +277,7 @@ where } else { match process.try_poll_execution_event() { Ok(event) => event, - Err(SidecarError::Execution(message)) - if (process.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { - None - } + Err(SidecarError::ExecutionEventChannelClosed { .. }) => None, Err(error) => return Err(error), } } @@ -329,13 +293,12 @@ where let signal = *signal; let registration = registration.clone(); drop(event); - if let Some(vm) = self.vms.get_mut(vm_id) { - apply_process_signal_state_update( - &mut vm.signal_states, - process_id, - signal, - registration, - ); + if let Some(process) = self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + { + apply_kernel_signal_registration(process, signal, ®istration)?; } } _ => deferred.push_back(event), @@ -375,16 +338,10 @@ where payload.process_id )) })?; - // For a TTY JavaScript process, host stdin must go ONLY to the kernel PTY - // master (so line discipline + echo apply); feeding the in-process local - // stdin bridge as well would double-deliver the input. Non-TTY JS (piped - // stdin) still uses the local bridge; wasm/python always take the - // streaming/no-op `write_stdin` path plus the kernel master write below. - let tty_js = - process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some(); - if !tty_js { - process.execution.write_stdin(&payload.chunk)?; - } + // Managed processes consume stdin exclusively through their kernel fd + // table. Executor-local stdin remains available to standalone execution + // users, but feeding it here would replicate state and can double-deliver + // bytes when the guest also reads fd 0 through the host bridge. write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?; Ok(DispatchResult { @@ -418,7 +375,6 @@ where payload.process_id )) })?; - process.execution.close_stdin()?; close_kernel_process_stdin(&mut vm.kernel, process)?; Ok(DispatchResult { @@ -558,6 +514,7 @@ where request, ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse { running_processes: snapshot.running_processes as u64, + stopped_processes: snapshot.stopped_processes as u64, exited_processes: snapshot.exited_processes as u64, fd_tables: snapshot.fd_tables as u64, open_fds: snapshot.open_fds as u64, @@ -784,9 +741,7 @@ where "binary vm.fetch bodies require a kernel-backed HTTP listener", ))); } - let socket_paths = build_javascript_socket_path_context(vm)?; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let capabilities = vm.capabilities.clone(); + let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?; let process = vm .active_processes .get_mut(&target_process_id) @@ -795,20 +750,75 @@ where "vm.fetch target process disappeared: {target_process_id}" )) })?; - let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?; - let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest { - bridge: &self.bridge, - vm_id: &vm_id, - dns: &vm.dns, - socket_paths: &socket_paths, - kernel: &mut vm.kernel, - kernel_readiness, - process, - server_id, - request_json: &request_json, - capabilities, - }) - .await?; + let request_key = begin_loopback_http_request(process, server_id, &request_json, || { + PendingHttpRequest::Buffered(None) + })?; + + // A loopback HTTP server is still an ordinary guest process. Drive it + // through the same VM-scoped event pump as every other execution so + // filesystem, network, process, signal, and deferred host operations + // retain their normal context. The old inline poll loop dispatched + // common HostCalls through the kernel-only fallback and could strand + // operations such as managed connect or UDP poll. + let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id); + let process_event_notify = Arc::clone(&self.process_event_notify); + let deadline = Instant::now() + http_loopback_request_timeout(); + let response_json = loop { + // Register before inspecting durable state so completion racing + // the probe cannot lose its only wake edge. + let notified = process_event_notify.notified(); + let response = { + let vm = self.vms.get_mut(&vm_id).ok_or_else(|| { + SidecarError::InvalidState(format!("VM {vm_id} is no longer active")) + })?; + let process = vm + .active_processes + .get_mut(&target_process_id) + .ok_or_else(|| { + SidecarError::Execution(format!( + "vm.fetch target process disappeared: {target_process_id}" + )) + })?; + take_loopback_http_response(process, request_key) + }; + if let Some(response) = response { + break response; + } + + if Instant::now() >= deadline { + if let Some(process) = self + .vms + .get_mut(&vm_id) + .and_then(|vm| vm.active_processes.get_mut(&target_process_id)) + { + process.pending_http_requests.remove(&request_key); + } + return Err(SidecarError::Execution(String::from( + "HTTP loopback request timed out waiting for net.http_respond", + ))); + } + + match self.pump_process_events(&ownership).await { + Ok(true) => continue, + Ok(false) => {} + Err(error) => { + if let Some(process) = self + .vms + .get_mut(&vm_id) + .and_then(|vm| vm.active_processes.get_mut(&target_process_id)) + { + process.pending_http_requests.remove(&request_key); + } + return Err(error); + } + } + + let remaining = deadline.saturating_duration_since(Instant::now()); + tokio::select! { + _ = notified => {} + _ = tokio::time::sleep(remaining) => {} + } + }; let response = self.respond( request, @@ -832,12 +842,22 @@ where self.drain_root_signal_state_events(&vm_id, &payload.process_id)?; - let handlers = self + let mut handlers = BTreeMap::new(); + if let Some(process) = self .vms .get(&vm_id) - .and_then(|vm| vm.signal_states.get(&payload.process_id)) - .cloned() - .unwrap_or_default(); + .and_then(|vm| vm.active_processes.get(&payload.process_id)) + { + for signal in 1..=64 { + let action = process + .kernel_handle + .signal_action(signal, None) + .map_err(kernel_error)?; + if action.disposition != agentos_kernel::process_table::SignalDisposition::Default { + handlers.insert(signal as u32, protocol_signal_registration(action)); + } + } + } Ok(DispatchResult { response: signal_state_response(request, payload.process_id, handlers), diff --git a/crates/native-sidecar/src/execution/host_dispatch/clock.rs b/crates/native-sidecar/src/execution/host_dispatch/clock.rs new file mode 100644 index 0000000000..bfb0a0d344 --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/clock.rs @@ -0,0 +1,62 @@ +use super::*; +use agentos_kernel::system::KernelClockId; + +pub(super) struct ClockCapability; + +impl SidecarHostCapability for ClockCapability { + fn requires_claim(operation: &ClockOperation) -> bool { + matches!( + operation, + ClockOperation::Sleep { .. } | ClockOperation::RealIntervalSet { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: ClockOperation, + ) -> Result { + let value = match operation { + ClockOperation::Time { + clock, + precision_ns: _, + deterministic_realtime_ns, + } => kernel + .clock_time_ns(kernel_clock(clock), deterministic_realtime_ns) + .map(|nanoseconds| json!(nanoseconds.to_string())) + .map_err(kernel_host_error)?, + ClockOperation::Resolution { clock } => kernel + .clock_resolution_ns(kernel_clock(clock)) + .map(|nanoseconds| json!(nanoseconds.to_string())) + .map_err(kernel_host_error)?, + ClockOperation::Sleep { .. } => { + return Err(HostServiceError::new( + "EINVAL", + "sleep requires sidecar timer context", + )); + } + ClockOperation::RealIntervalGet => { + let values = process.real_interval_timer.get(); + json!({ "remainingUs": values.0, "intervalUs": values.1 }) + } + ClockOperation::RealIntervalSet { + initial_us, + interval_us, + } => { + let values = process.real_interval_timer.set(initial_us, interval_us); + json!({ "remainingUs": values.0, "intervalUs": values.1 }) + } + other => return Err(unsupported("clock", other)), + }; + Ok(HostCallReply::Json(value)) + } +} + +fn kernel_clock(clock: GuestClockId) -> KernelClockId { + match clock { + GuestClockId::Realtime => KernelClockId::Realtime, + GuestClockId::Monotonic => KernelClockId::Monotonic, + GuestClockId::ProcessCpu => KernelClockId::ProcessCpu, + GuestClockId::ThreadCpu => KernelClockId::ThreadCpu, + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/entropy.rs b/crates/native-sidecar/src/execution/host_dispatch/entropy.rs new file mode 100644 index 0000000000..8f7c052540 --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/entropy.rs @@ -0,0 +1,24 @@ +use super::*; +use agentos_execution::host::EntropyOperation; + +pub(super) struct EntropyCapability; + +impl SidecarHostCapability for EntropyCapability { + fn requires_claim(_: &EntropyOperation) -> bool { + // Entropy is an observable host-side effect. Claiming first prevents a + // stale execution from consuming randomness after its reply capability + // has already been superseded. + true + } + + fn execute( + _: &mut SidecarKernel, + _: &mut ActiveProcess, + operation: EntropyOperation, + ) -> Result { + let mut bytes = vec![0_u8; operation.length.get()]; + getrandom::getrandom(&mut bytes) + .map_err(|error| HostServiceError::new("EIO", error.to_string()))?; + Ok(HostCallReply::Raw(bytes)) + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs new file mode 100644 index 0000000000..b4b1eee45a --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs @@ -0,0 +1,3281 @@ +use super::*; + +const NODE_CWD_FD: u32 = u32::MAX; +const MAX_PATH_BYTES: usize = 4096; +const MAX_XATTR_NAME_BYTES: usize = 255; +const MAX_READDIR_ENTRIES: usize = 4096; +const MAX_CLOSEFROM_TARGETS: usize = 1 << 20; + +fn complete_timed_fd_read(data: Option>) -> Result, HostServiceError> { + Ok(data.unwrap_or_default()) +} + +pub(super) fn dispatch_context_deferred_kernel_read( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: FilesystemOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let (fd, max_bytes, timeout_ms, response) = match operation { + FilesystemOperation::Read { + fd, + max_bytes, + offset: None, + deadline_ms: Some(timeout_ms), + } => ( + Some(fd), + max_bytes, + timeout_ms, + DeferredKernelReadResponse::DescriptorBytes, + ), + FilesystemOperation::StdinRead { + max_bytes, + timeout_ms, + } => ( + None, + max_bytes, + timeout_ms, + DeferredKernelReadResponse::KernelStdin, + ), + _ => { + return Err(SidecarError::host( + "EINVAL", + "deferred descriptor-read dispatcher received a non-timed read", + )); + } + }; + let deadline = match checked_deferred_guest_wait_deadline(timeout_ms) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + }; + let notify = Arc::clone(&sidecar.process_event_notify); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated descriptor-read VM remains registered"); + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes + .get_mut(process_id) + .expect("validated descriptor-read process remains registered"); + let fd = fd.unwrap_or(process.kernel_stdin_reader_fd); + service_deferred_kernel_read_with_response( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + Some((fd, max_bytes, response, deadline, reply)), + ) +} + +/// Park one typed descriptor read without blocking the sidecar actor. Every +/// destructive read is a zero-time owner-thread probe after the direct reply +/// has been claimed; the task retains only notifier/deadline state. +pub(in crate::execution) fn service_deferred_kernel_read( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::poll::PollWaitHandle, + notify: Arc, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<(u32, BoundedUsize, Instant, DirectHostReplyHandle)>, +) -> Result<(), SidecarError> { + service_deferred_kernel_read_with_response( + generation, + runtime, + wait_handle, + notify, + kernel, + process, + incoming.map(|(fd, max_bytes, deadline, reply)| { + ( + fd, + max_bytes, + DeferredKernelReadResponse::DescriptorBytes, + deadline, + reply, + ) + }), + ) +} + +/// Park a legacy `__kernel_stdin_read` for any active process in the VM tree. +/// Descendant event pumps use this wrapper so kernel stdin retains the same +/// response shape and bounded owner-side refill semantics as a root process. +pub(in crate::execution) fn service_deferred_kernel_stdin_read( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::poll::PollWaitHandle, + notify: Arc, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<(BoundedUsize, Instant, DirectHostReplyHandle)>, +) -> Result<(), SidecarError> { + let fd = process.kernel_stdin_reader_fd; + service_deferred_kernel_read_with_response( + generation, + runtime, + wait_handle, + notify, + kernel, + process, + incoming.map(|(max_bytes, deadline, reply)| { + ( + fd, + max_bytes, + DeferredKernelReadResponse::KernelStdin, + deadline, + reply, + ) + }), + ) +} + +fn service_deferred_kernel_read_with_response( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::poll::PollWaitHandle, + notify: Arc, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<( + u32, + BoundedUsize, + DeferredKernelReadResponse, + Instant, + DirectHostReplyHandle, + )>, +) -> Result<(), SidecarError> { + let newly_admitted = incoming.is_some(); + if let Some((fd, max_bytes, response, deadline, reply)) = incoming { + if process.deferred_kernel_read.is_some() { + reply + .fail(HostServiceError::new( + "EBUSY", + "process already owns a deferred descriptor read", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let identity = reply.identity(); + if identity.generation != generation || identity.pid != process.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "deferred descriptor-read identity does not match the active kernel process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if max_bytes.get() > process.limits.wasm.sync_read_limit_bytes { + reply + .fail(HostServiceError::limit( + "E2BIG", + "limits.wasm.syncReadLimitBytes", + process.limits.wasm.sync_read_limit_bytes as u64, + max_bytes.get() as u64, + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + process.deferred_kernel_read = Some(DeferredKernelRead { + fd, + max_bytes, + response, + reply, + deadline, + wake_task: None, + }); + } + + let now = Instant::now(); + let should_probe = process.deferred_kernel_read.as_ref().is_some_and(|read| { + newly_admitted + || now >= read.deadline + || read + .wake_task + .as_ref() + .is_none_or(tokio::task::JoinHandle::is_finished) + }); + if !should_probe { + return Ok(()); + } + + let mut read = process + .deferred_kernel_read + .take() + .expect("deferred descriptor read checked above"); + if let Some(task) = read.wake_task.take() { + task.abort(); + } + if read.fd == 0 || read.response == DeferredKernelReadResponse::KernelStdin { + if let Err(error) = flush_pending_kernel_stdin(kernel, process) { + return read + .reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from); + } + } + + // Snapshot before the destructive zero-time probe. A readiness edge that + // races the probe changes this generation, so the waiter immediately + // schedules another owner-thread probe instead of absorbing the wake. + let observed = wait_handle.snapshot(); + match kernel.fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + read.fd, + read.max_bytes.get(), + Some(Duration::ZERO), + ) { + Ok(Some(bytes)) => { + let value = match read.response { + DeferredKernelReadResponse::DescriptorBytes => host_bytes_value(&bytes), + DeferredKernelReadResponse::KernelStdin => json!({ + "dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes), + }), + }; + return read + .reply + .succeed(HostCallReply::Json(value)) + .map_err(SidecarError::from); + } + Ok(None) => { + let value = match read.response { + DeferredKernelReadResponse::DescriptorBytes => host_bytes_value(&[]), + DeferredKernelReadResponse::KernelStdin => json!({ "done": true }), + }; + return read + .reply + .succeed(HostCallReply::Json(value)) + .map_err(SidecarError::from); + } + Err(error) if matches!(error.code(), "EAGAIN" | "EWOULDBLOCK") && now >= read.deadline => { + return match read.response { + DeferredKernelReadResponse::DescriptorBytes => read + .reply + .fail(HostServiceError::new( + "EAGAIN", + "timed fd read is not ready", + )) + .map_err(SidecarError::from), + DeferredKernelReadResponse::KernelStdin => read + .reply + .succeed(HostCallReply::Json(Value::Null)) + .map_err(SidecarError::from), + }; + } + Err(error) if matches!(error.code(), "EAGAIN" | "EWOULDBLOCK") => {} + Err(error) => { + return read + .reply + .fail(kernel_host_error(error)) + .map_err(SidecarError::from); + } + } + + let deadline = read.deadline; + let wake_task = runtime.spawn(agentos_runtime::TaskClass::Vm, async move { + let delay = deadline.saturating_duration_since(Instant::now()); + tokio::select! { + _ = wait_handle.wait_for_change_async(observed) => {} + _ = tokio::time::sleep(delay) => {} + } + notify.notify_one(); + }); + match wake_task { + Ok(task) => { + read.wake_task = Some(task); + process.deferred_kernel_read = Some(read); + Ok(()) + } + Err(error) => read + .reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from), + } +} + +pub(super) fn decode( + request: &HostRpcRequest, + nonblocking_unpositioned_writes: bool, + max_reply_bytes: usize, +) -> Result, SidecarError> { + let path_limit = payload_limit("runtime.filesystem.maxPathBytes", MAX_PATH_BYTES)?; + let name_limit = payload_limit("runtime.filesystem.maxXattrNameBytes", MAX_XATTR_NAME_BYTES)?; + let response_limit = payload_limit("limits.reactor.maxBridgeResponseBytes", max_reply_bytes)?; + let request_limit = payload_limit("limits.reactor.maxBridgeRequestBytes", max_reply_bytes)?; + let readdir_limit = payload_limit("runtime.filesystem.maxReaddirEntries", MAX_READDIR_ENTRIES)?; + + let path = |index: usize, label: &str| { + BoundedString::try_new( + javascript_sync_rpc_arg_str(&request.args, index, label)?.to_owned(), + &path_limit, + ) + .map_err(SidecarError::Host) + }; + let name = |index: usize, label: &str| { + BoundedString::try_new( + javascript_sync_rpc_arg_str(&request.args, index, label)?.to_owned(), + &name_limit, + ) + .map_err(SidecarError::Host) + }; + let bytes = |index: usize, label: &str| { + BoundedBytes::try_new( + javascript_sync_rpc_request_bytes_arg(request, index, label)?, + &request_limit, + ) + .map_err(SidecarError::Host) + }; + let output_count = |value: u64, label: &str| { + let value = usize::try_from(value) + .map_err(|_| SidecarError::host("E2BIG", format!("{label} exceeds usize")))?; + response_limit + .admit(encoded_bytes_reply_size(value).ok_or_else(|| { + SidecarError::host( + "E2BIG", + format!("{label} encoded reply size overflows usize"), + ) + })?) + .map_err(SidecarError::Host)?; + BoundedUsize::try_new(value, &response_limit).map_err(SidecarError::Host) + }; + + let operation = match request.method.as_str() { + "__kernel_stdin_read" => { + let requested = javascript_sync_rpc_arg_u64_optional( + &request.args, + 0, + "__kernel_stdin_read max bytes", + )? + .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) + .clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64); + let timeout_ms = if request.args.get(1).is_some_and(Value::is_null) { + return Err(SidecarError::host( + "EINVAL", + "an indefinite __kernel_stdin_read must use deferred readiness", + )); + } else { + javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "__kernel_stdin_read timeout ms", + )? + .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS) + }; + FilesystemOperation::StdinRead { + max_bytes: output_count(requested, "stdin read length")?, + timeout_ms, + } + } + "__kernel_stdio_write" => FilesystemOperation::StdioWrite { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?, + bytes: bytes(1, "__kernel_stdio_write chunk")?, + }, + "fs.accessSync" => FilesystemOperation::AccessAt { + dir_fd: NODE_CWD_FD, + path: path(0, "filesystem access path")?, + mode: javascript_sync_rpc_arg_u32_optional(&request.args, 1, "filesystem access mode")? + .unwrap_or_default(), + effective_ids: javascript_sync_rpc_option_bool(&request.args, 2, "effective IDs") + .unwrap_or(false), + }, + "fs.chmodForProcessSync" => FilesystemOperation::SetMode { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: true, + }, + path: Some(path(0, "filesystem chmod path")?), + mode: javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")?, + }, + "fs.chownSync" | "fs.lchownSync" => FilesystemOperation::SetOwner { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: request.method == "fs.chownSync", + }, + path: Some(path(0, "filesystem chown path")?), + uid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 1, + "filesystem chown uid", + )?), + gid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "filesystem chown gid", + )?), + }, + "fs.truncateForProcessSync" => FilesystemOperation::SetPathLength { + dir_fd: NODE_CWD_FD, + path: path(0, "filesystem truncate path")?, + length: javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "filesystem truncate length", + )? + .unwrap_or_default(), + }, + "fs.namedFifoPeerReadySync" => FilesystemOperation::NamedPipePeerReady { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "named FIFO fd")?, + }, + "fs.openTmpfileSync" => FilesystemOperation::OpenTmpfileAt { + dir_fd: NODE_CWD_FD, + path: path(0, "unnamed-file directory")?, + options: GuestOpenSpec { + flags: javascript_sync_rpc_arg_u32(&request.args, 1, "unnamed-file flags")?, + mode: Some(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "unnamed-file mode", + )?), + rights: GuestOpenRights::Synthesized, + }, + linkable: javascript_sync_rpc_option_bool(&request.args, 3, "linkable").unwrap_or(true), + }, + "fs.linkFdSync" => FilesystemOperation::LinkDescriptorAt { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "unnamed-file fd")?, + dir_fd: NODE_CWD_FD, + path: path(1, "unnamed-file link destination")?, + }, + "fs.punchHoleSync" => FilesystemOperation::Range { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "punch-hole fd")?, + operation: FileRangeOperation::PunchHole, + offset: javascript_sync_rpc_arg_u64(&request.args, 1, "punch-hole offset")?, + length: javascript_sync_rpc_arg_u64(&request.args, 2, "punch-hole length")?, + keep_size: true, + }, + "fs.fallocateSync" | "fs.zeroRangeSync" | "fs.insertRangeSync" | "fs.collapseRangeSync" => { + FilesystemOperation::Range { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "file-range fd")?, + operation: match request.method.as_str() { + "fs.fallocateSync" => FileRangeOperation::Allocate, + "fs.zeroRangeSync" => FileRangeOperation::Zero, + "fs.insertRangeSync" => FileRangeOperation::Insert, + _ => FileRangeOperation::Collapse, + }, + offset: javascript_sync_rpc_arg_u64(&request.args, 1, "file-range offset")?, + length: javascript_sync_rpc_arg_u64(&request.args, 2, "file-range length")?, + keep_size: request.method == "fs.zeroRangeSync" + && javascript_sync_rpc_arg_u32(&request.args, 3, "zero-range keep-size")? != 0, + } + } + "fs.fiemapSync" => FilesystemOperation::Extents { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fiemap fd")?, + max_entries: BoundedUsize::try_new(MAX_READDIR_ENTRIES, &readdir_limit) + .map_err(SidecarError::Host)?, + }, + "fs.fiemapAtSync" => FilesystemOperation::ExtentAt { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fiemap fd")?, + index: javascript_sync_rpc_arg_u32(&request.args, 1, "fiemap extent index")?, + }, + "fs.statfsSync" => FilesystemOperation::FilesystemStatsAt { + dir_fd: NODE_CWD_FD, + path: path(0, "filesystem statfs path")?, + }, + "fs.statSync" => FilesystemOperation::NodeStatAt { + dir_fd: NODE_CWD_FD, + path: path(0, "filesystem stat path")?, + }, + "fs.mknodSync" => FilesystemOperation::MakeNodeAt { + dir_fd: NODE_CWD_FD, + path: path(0, "filesystem mknod path")?, + mode: javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem mknod mode")?, + device: javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem mknod device")?, + }, + "fs.remountSync" => FilesystemOperation::Remount { + path: path(0, "filesystem remount path")?, + options: BoundedString::try_new( + javascript_sync_rpc_arg_str(&request.args, 1, "filesystem remount options")? + .to_owned(), + &request_limit, + ) + .map_err(SidecarError::Host)?, + }, + "fs.renameAt2Sync" => FilesystemOperation::RenameAt { + old_dir_fd: NODE_CWD_FD, + old_path: path(0, "filesystem renameat2 source")?, + new_dir_fd: NODE_CWD_FD, + new_path: path(1, "filesystem renameat2 destination")?, + flags: javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem renameat2 flags")?, + }, + "fs.getxattrSync" | "fs.listxattrSync" | "fs.setxattrSync" | "fs.removexattrSync" => { + let (operation, name_value, value, follow_index) = match request.method.as_str() { + "fs.getxattrSync" => (XattrOperation::Get, Some(name(1, "xattr name")?), None, 2), + "fs.listxattrSync" => (XattrOperation::List, None, None, 1), + "fs.setxattrSync" => ( + XattrOperation::Set { + flags: javascript_sync_rpc_arg_u32(&request.args, 3, "xattr flags")?, + }, + Some(name(1, "xattr name")?), + Some(bytes(2, "xattr value")?), + 4, + ), + _ => ( + XattrOperation::Remove, + Some(name(1, "xattr name")?), + None, + 2, + ), + }; + FilesystemOperation::Xattr { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: javascript_sync_rpc_option_bool( + &request.args, + follow_index, + "follow symlinks", + ) + .unwrap_or(true), + }, + path: Some(path(0, "xattr path")?), + name: name_value, + value, + operation, + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + .map_err(SidecarError::Host)?, + } + } + "fs.fgetxattrSync" | "fs.flistxattrSync" | "fs.fsetxattrSync" | "fs.fremovexattrSync" => { + let (operation, name_value, value) = match request.method.as_str() { + "fs.fgetxattrSync" => (XattrOperation::Get, Some(name(1, "xattr name")?), None), + "fs.flistxattrSync" => (XattrOperation::List, None, None), + "fs.fsetxattrSync" => ( + XattrOperation::Set { + flags: javascript_sync_rpc_arg_u32(&request.args, 3, "xattr flags")?, + }, + Some(name(1, "xattr name")?), + Some(bytes(2, "xattr value")?), + ), + _ => (XattrOperation::Remove, Some(name(1, "xattr name")?), None), + }; + FilesystemOperation::Xattr { + target: MetadataTarget::Descriptor(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "xattr fd", + )?), + path: None, + name: name_value, + value, + operation, + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + .map_err(SidecarError::Host)?, + } + } + "process.fd_pipe" => FilesystemOperation::Pipe, + "process.fd_open" => FilesystemOperation::OpenAt { + dir_fd: NODE_CWD_FD, + path: path(0, "fd_open path")?, + options: open_spec(request, 1, 2, 3, 4)?, + }, + "process.fd_preopens" => FilesystemOperation::Preopens, + "process.fd_preopen" => FilesystemOperation::Preopen { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_preopen fd")?, + }, + "process.fd_read" | "process.fd_pread" => { + let offset = if request.method == "process.fd_pread" { + Some(parse_u64_string(request, 2, "fd_pread offset")?) + } else { + None + }; + FilesystemOperation::Read { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?, + max_bytes: output_count( + javascript_sync_rpc_arg_u64(&request.args, 1, "fd_read length")?, + "fd_read length", + )?, + offset, + deadline_ms: if offset.is_none() { + javascript_sync_rpc_arg_u64_optional(&request.args, 2, "fd_read timeout")? + } else { + None + }, + } + } + "process.fd_write" | "process.fd_pwrite" => FilesystemOperation::Write { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_write fd")?, + bytes: bytes(1, "fd_write data")?, + offset: if request.method == "process.fd_pwrite" { + Some(parse_u64_string(request, 2, "fd_pwrite offset")?) + } else { + None + }, + deadline_ms: None, + nonblocking: nonblocking_unpositioned_writes && request.method == "process.fd_write", + }, + "process.fd_sync" | "process.fd_datasync" => FilesystemOperation::Sync { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_sync fd")?, + kind: if request.method == "process.fd_datasync" { + DescriptorSyncKind::Data + } else { + DescriptorSyncKind::All + }, + }, + "process.fd_readdir" => FilesystemOperation::ReadDirectory { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_readdir fd")?, + cookie: parse_u64_string(request, 1, "fd_readdir cookie")?, + max_entries: BoundedUsize::try_new( + usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 2, + "fd_readdir max entries", + )?) + .unwrap_or(usize::MAX) + .min(MAX_READDIR_ENTRIES), + &readdir_limit, + ) + .map_err(SidecarError::Host)?, + max_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + .map_err(SidecarError::Host)?, + }, + "process.fd_close" => FilesystemOperation::Close { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_close fd")?, + }, + "process.fd_closefrom" => FilesystemOperation::CloseFrom { + min_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_closefrom minimum fd")?, + exact_fds: request + .args + .get(1) + .filter(|value| !value.is_null()) + .map(|value| { + let values = value.as_array().ok_or_else(|| { + SidecarError::host( + "EINVAL", + "fd_closefrom canonical targets must be an array", + ) + })?; + let fds = values + .iter() + .map(|value| { + value + .as_u64() + .and_then(|fd| u32::try_from(fd).ok()) + .ok_or_else(|| { + SidecarError::host( + "EINVAL", + "fd_closefrom canonical target must be u32", + ) + }) + }) + .collect::, _>>()?; + BoundedVec::try_new( + fds, + &PayloadLimit::new("limits.resources.maxOpenFds", MAX_CLOSEFROM_TARGETS) + .expect("static closefrom target limit"), + ) + .map_err(SidecarError::from) + }) + .transpose()?, + }, + "process.fd_stat" => FilesystemOperation::DescriptorStatus { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_stat fd")?, + }, + "process.fd_filestat" => FilesystemOperation::DescriptorFileStat { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_filestat fd")?, + }, + "process.fd_chown" => FilesystemOperation::SetOwner { + target: MetadataTarget::Descriptor(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "fd_chown fd", + )?), + path: None, + uid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 1, + "fd_chown uid", + )?), + gid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "fd_chown gid", + )?), + }, + "process.fd_chmod" => FilesystemOperation::SetMode { + target: MetadataTarget::Descriptor(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "fd_chmod fd", + )?), + path: None, + mode: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_chmod mode")?, + }, + "process.fd_truncate" => FilesystemOperation::SetLength { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_truncate fd")?, + length: parse_u64_string(request, 1, "fd_truncate length")?, + }, + "process.fd_set_flags" => FilesystemOperation::SetDescriptorFlags { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_set_flags fd")?, + flags: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_set_flags flags")?, + }, + "process.fd_getfd" => FilesystemOperation::DescriptorFdFlags { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_getfd fd")?, + }, + "process.fd_setfd" => FilesystemOperation::SetDescriptorFdFlags { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_setfd fd")?, + flags: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_setfd flags")?, + }, + "process.fd_flock" => FilesystemOperation::AdvisoryLock { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_flock fd")?, + operation: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_flock operation")?, + }, + "process.fd_record_lock" => FilesystemOperation::RecordLock { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_record_lock fd")?, + command: match javascript_sync_rpc_arg_u32(&request.args, 1, "fd_record_lock command")? + { + 12 => RecordLockCommand::Query, + 13 => RecordLockCommand::Set, + 14 => RecordLockCommand::Wait, + command => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported fd_record_lock command {command}"), + )) + } + }, + kind: match javascript_sync_rpc_arg_u32(&request.args, 2, "fd_record_lock type")? { + 0 => FilesystemRecordLockKind::Read, + 1 => FilesystemRecordLockKind::Write, + 2 => FilesystemRecordLockKind::Unlock, + _ => return Err(SidecarError::host("EINVAL", "invalid fd_record_lock type")), + }, + start: parse_u64_string(request, 3, "fd_record_lock start")?, + length: parse_u64_string(request, 4, "fd_record_lock length")?, + }, + "process.fd_record_lock_cancel" => FilesystemOperation::CancelRecordLocks, + "process.fd_dup" => FilesystemOperation::Duplicate { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup fd")?, + }, + "process.fd_dup2" => FilesystemOperation::DuplicateTo { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup2 source fd")?, + target_fd: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_dup2 target fd")?, + }, + "process.fd_dup_min" => FilesystemOperation::DuplicateMin { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup_min fd")?, + min_fd: javascript_sync_rpc_arg_u32(&request.args, 1, "fd_dup_min minimum")?, + }, + "process.fd_move" => FilesystemOperation::Move { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_move fd")?, + replaced_fd: javascript_sync_rpc_arg_u32_optional( + &request.args, + 1, + "fd_move replaced fd", + )?, + }, + "process.fd_seek" => { + let whence = match javascript_sync_rpc_arg_u32(&request.args, 2, "fd_seek whence")? { + 0 => DescriptorWhence::Set, + 1 => DescriptorWhence::Current, + 2 => DescriptorWhence::End, + value => { + return Err(SidecarError::host( + "EINVAL", + format!("invalid fd_seek whence {value}"), + )) + } + }; + FilesystemOperation::Seek { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_seek fd")?, + offset: javascript_sync_rpc_arg_str(&request.args, 1, "fd_seek offset")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "fd_seek offset must be i64"))?, + whence, + } + } + "process.fd_chdir_path" => FilesystemOperation::DescriptorPath { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fchdir fd")?, + require_directory: true, + }, + "process.fd_path" => FilesystemOperation::DescriptorPath { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_path fd")?, + require_directory: false, + }, + method if method.starts_with("process.path_") => decode_path_operation(request, &path)?, + _ => return Ok(None), + }; + Ok(Some(operation)) +} + +fn payload_limit( + name: &'static str, + maximum: usize, +) -> Result { + agentos_execution::backend::PayloadLimit::new(name, maximum).map_err(SidecarError::Host) +} + +fn encoded_bytes_reply_size(byte_length: usize) -> Option { + // `host_bytes_value` serializes the base64 payload in this + // fixed JSON object. Admit the encoded shape before any kernel read can + // consume a pipe or advance a file description. + const EMPTY_ENCODED_BYTES_JSON_LEN: usize = 37; + byte_length + .checked_add(2)? + .checked_div(3)? + .checked_mul(4)? + .checked_add(EMPTY_ENCODED_BYTES_JSON_LEN) +} + +fn open_spec( + request: &HostRpcRequest, + flags_index: usize, + mode_index: usize, + rights_base_index: usize, + rights_inheriting_index: usize, +) -> Result { + Ok(GuestOpenSpec { + flags: javascript_sync_rpc_arg_u32(&request.args, flags_index, "open flags")?, + mode: javascript_sync_rpc_arg_u32_optional(&request.args, mode_index, "open mode")?, + rights: GuestOpenRights::Explicit { + base: parse_u64_string(request, rights_base_index, "open base rights")?, + inheriting: parse_u64_string( + request, + rights_inheriting_index, + "open inheriting rights", + )?, + }, + }) +} + +fn parse_u64_string( + request: &HostRpcRequest, + index: usize, + label: &str, +) -> Result { + javascript_sync_rpc_arg_str(&request.args, index, label)? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", format!("{label} must be u64"))) +} + +fn decode_path_operation( + request: &HostRpcRequest, + path: &impl Fn(usize, &str) -> Result, +) -> Result { + Ok(match request.method.as_str() { + "process.path_open_at" => FilesystemOperation::OpenAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_open_at dir fd")?, + path: path(1, "path_open_at path")?, + options: open_spec(request, 2, 3, 4, 5)?, + }, + "process.path_mkdir_at" => FilesystemOperation::CreateDirectoryAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_mkdir_at dir fd")?, + path: path(1, "path_mkdir_at path")?, + mode: 0o777, + }, + "process.path_stat_at" => FilesystemOperation::Stat { + target: MetadataTarget::Path { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_stat_at dir fd")?, + follow_symlinks: javascript_sync_rpc_arg_bool( + &request.args, + 2, + "path_stat_at follow", + )?, + }, + path: Some(path(1, "path_stat_at path")?), + }, + "process.path_chmod_at" => FilesystemOperation::SetMode { + target: MetadataTarget::Path { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_chmod_at dir fd")?, + follow_symlinks: true, + }, + path: Some(path(1, "path_chmod_at path")?), + mode: javascript_sync_rpc_arg_u32(&request.args, 2, "path_chmod_at mode")?, + }, + "process.path_chown_at" => FilesystemOperation::SetOwner { + target: MetadataTarget::Path { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_chown_at dir fd")?, + follow_symlinks: javascript_sync_rpc_arg_bool( + &request.args, + 4, + "path_chown_at follow", + )?, + }, + path: Some(path(1, "path_chown_at path")?), + uid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "path_chown_at uid", + )?), + gid: Some(javascript_sync_rpc_arg_u32( + &request.args, + 3, + "path_chown_at gid", + )?), + }, + "process.path_utimes_at" => { + let flags = javascript_sync_rpc_arg_u32(&request.args, 5, "path_utimes_at flags")?; + let parse_time = |index: usize, + explicit: bool, + now: bool, + label: &str| + -> Result, SidecarError> { + if now || !explicit { + return Ok(None); + } + Ok(Some(parse_u64_string(request, index, label)?)) + }; + FilesystemOperation::SetTimes { + target: MetadataTarget::Path { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_utimes_at dir fd")?, + follow_symlinks: javascript_sync_rpc_arg_bool( + &request.args, + 2, + "path_utimes_at follow", + )?, + }, + path: Some(path(1, "path_utimes_at path")?), + update: FileTimeUpdate { + atime_ns: parse_time(3, flags & 1 != 0, flags & 2 != 0, "atime")?, + mtime_ns: parse_time(4, flags & 4 != 0, flags & 8 != 0, "mtime")?, + atime_now: flags & 2 != 0, + mtime_now: flags & 8 != 0, + }, + } + } + "process.path_link_at" => FilesystemOperation::LinkAt { + old_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_link_at old fd")?, + old_path: path(1, "path_link_at old path")?, + new_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 2, "path_link_at new fd")?, + new_path: path(3, "path_link_at new path")?, + follow_old: javascript_sync_rpc_arg_bool(&request.args, 4, "path_link_at follow")?, + }, + "process.path_readlink_at" => FilesystemOperation::ReadLinkAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_readlink_at dir fd")?, + path: path(1, "path_readlink_at path")?, + max_bytes: BoundedUsize::try_new( + MAX_PATH_BYTES, + &payload_limit("runtime.filesystem.maxPathBytes", MAX_PATH_BYTES)?, + ) + .map_err(SidecarError::Host)?, + }, + "process.path_remove_dir_at" => FilesystemOperation::UnlinkAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_remove_dir_at dir fd")?, + path: path(1, "path_remove_dir_at path")?, + remove_directory: true, + }, + "process.path_rename_at" => FilesystemOperation::RenameAt { + old_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_rename_at old fd")?, + old_path: path(1, "path_rename_at old path")?, + new_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 2, "path_rename_at new fd")?, + new_path: path(3, "path_rename_at new path")?, + flags: 0, + }, + "process.path_symlink_at" => FilesystemOperation::SymlinkAt { + target: path(0, "path_symlink_at target")?, + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 1, "path_symlink_at dir fd")?, + path: path(2, "path_symlink_at path")?, + }, + "process.path_unlink_at" => FilesystemOperation::UnlinkAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_unlink_at dir fd")?, + path: path(1, "path_unlink_at path")?, + remove_directory: false, + }, + _ => return Err(SidecarError::host("ENOSYS", "unknown path operation")), + }) +} + +pub(super) struct FilesystemCapability; + +impl SidecarHostCapability for FilesystemCapability { + fn requires_claim(operation: &FilesystemOperation) -> bool { + matches!( + operation, + FilesystemOperation::ReadFileAt { .. } + | FilesystemOperation::WriteFileAt { .. } + | FilesystemOperation::OpenAt { .. } + | FilesystemOperation::OpenTmpfileAt { .. } + | FilesystemOperation::Pipe + | FilesystemOperation::Preopen { .. } + | FilesystemOperation::Preopens + | FilesystemOperation::Close { .. } + | FilesystemOperation::CloseFrom { .. } + | FilesystemOperation::Renumber { .. } + | FilesystemOperation::Duplicate { .. } + | FilesystemOperation::DuplicateTo { .. } + | FilesystemOperation::DuplicateMin { .. } + | FilesystemOperation::Move { .. } + | FilesystemOperation::Read { .. } + | FilesystemOperation::Write { .. } + | FilesystemOperation::Seek { .. } + | FilesystemOperation::Sync { .. } + | FilesystemOperation::SetDescriptorFdFlags { .. } + | FilesystemOperation::SetDescriptorFlags { .. } + | FilesystemOperation::SetLength { .. } + | FilesystemOperation::SetPathLength { .. } + | FilesystemOperation::AdvisoryLock { .. } + | FilesystemOperation::RecordLock { .. } + | FilesystemOperation::CancelRecordLocks + | FilesystemOperation::SetTimes { .. } + | FilesystemOperation::SetMode { .. } + | FilesystemOperation::SetOwner { .. } + | FilesystemOperation::SetAttributesAt { .. } + | FilesystemOperation::CreateDirectoryAt { .. } + | FilesystemOperation::CreateDirectoriesAt { .. } + | FilesystemOperation::MakeNodeAt { .. } + | FilesystemOperation::LinkAt { .. } + | FilesystemOperation::LinkDescriptorAt { .. } + | FilesystemOperation::RenameAt { .. } + | FilesystemOperation::SymlinkAt { .. } + | FilesystemOperation::UnlinkAt { .. } + | FilesystemOperation::Range { .. } + | FilesystemOperation::Xattr { + operation: XattrOperation::Set { .. } | XattrOperation::Remove, + .. + } + | FilesystemOperation::Remount { .. } + | FilesystemOperation::StdinRead { .. } + | FilesystemOperation::StdioWrite { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: FilesystemOperation, + ) -> Result { + let pid = process.kernel_pid; + let value = match operation { + FilesystemOperation::ReadFileAt { + dir_fd, + path, + max_bytes, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + let expected = kernel + .stat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?; + let expected = usize::try_from(expected.size).map_err(|_| { + HostServiceError::new("EOVERFLOW", "file size exceeds host address space") + })?; + if expected > max_bytes.get() { + return Err(HostServiceError::limit( + "E2BIG", + "limits.reactor.maxBridgeResponseBytes", + max_bytes.get() as u64, + expected as u64, + )); + } + let bytes = kernel + .read_file_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?; + if bytes.len() > max_bytes.get() { + return Err(HostServiceError::limit( + "E2BIG", + "limits.reactor.maxBridgeResponseBytes", + max_bytes.get() as u64, + bytes.len() as u64, + )); + } + return Ok(HostCallReply::Raw(bytes)); + } + FilesystemOperation::WriteFileAt { + dir_fd, + path, + bytes, + mode, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .write_file_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + bytes.into_vec(), + mode, + ) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::OpenAt { + dir_fd, + path, + options, + } => { + let parent_fd = (dir_fd != NODE_CWD_FD).then_some(dir_fd); + let requested_rights = match options.rights { + GuestOpenRights::Explicit { base, inheriting } => Some((base, inheriting)), + GuestOpenRights::Synthesized => None, + }; + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + Value::from( + kernel + .fd_open_with_rights( + EXECUTION_DRIVER_NAME, + pid, + parent_fd, + &path, + options.flags, + options.mode, + requested_rights, + ) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::OpenTmpfileAt { + dir_fd, + path, + options, + linkable, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + Value::from( + kernel + .fd_open_tmpfile( + EXECUTION_DRIVER_NAME, + pid, + &path, + options.flags, + options.mode.unwrap_or_default(), + linkable, + ) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::Pipe => { + let (read_fd, write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!({ "readFd": read_fd, "writeFd": write_fd }) + } + FilesystemOperation::Preopen { fd } => kernel + .wasi_preopen(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)? + .map(preopen_value) + .unwrap_or(Value::Null), + FilesystemOperation::Preopens => { + let preopens = kernel + .initialize_wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + Value::Array(preopens.into_iter().map(preopen_value).collect()) + } + FilesystemOperation::Close { fd } => { + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::CloseFrom { min_fd, exact_fds } => { + let closed = if let Some(fds) = exact_fds { + kernel.fd_close_exact(EXECUTION_DRIVER_NAME, pid, fds.into_vec()) + } else { + kernel.fd_close_from(EXECUTION_DRIVER_NAME, pid, min_fd) + } + .map_err(kernel_host_error)?; + json!({ "closedFds": closed }) + } + FilesystemOperation::Renumber { from, to } => { + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, from, to) + .map_err(kernel_host_error)?; + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, from) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::Duplicate { fd } => Value::from( + kernel + .fd_dup(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::DuplicateTo { fd, target_fd } => { + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, fd, target_fd) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::DuplicateMin { fd, min_fd } => Value::from( + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + fd, + agentos_kernel::fd_table::F_DUPFD, + min_fd, + ) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::Move { fd, replaced_fd } => Value::from( + kernel + .fd_renumber_projection(EXECUTION_DRIVER_NAME, pid, fd, replaced_fd) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::Read { + fd, + max_bytes, + offset, + deadline_ms, + } => { + if max_bytes.get() > process.limits.wasm.sync_read_limit_bytes { + return Err(HostServiceError::limit( + "E2BIG", + "limits.wasm.syncReadLimitBytes", + process.limits.wasm.sync_read_limit_bytes as u64, + max_bytes.get() as u64, + )); + } + if fd == 0 && offset.is_none() { + flush_pending_kernel_stdin(kernel, process).map_err(sidecar_host_error)?; + } + let data = match offset { + Some(offset) => kernel + .fd_pread(EXECUTION_DRIVER_NAME, pid, fd, max_bytes.get(), offset) + .map_err(kernel_host_error)?, + None => match deadline_ms { + Some(timeout) => complete_timed_fd_read( + kernel + .fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + pid, + fd, + max_bytes.get(), + Some(Duration::from_millis(timeout)), + ) + .map_err(kernel_host_error)?, + )?, + None => kernel + .fd_read(EXECUTION_DRIVER_NAME, pid, fd, max_bytes.get()) + .map_err(kernel_host_error)?, + }, + }; + host_bytes_value(&data) + } + FilesystemOperation::Write { + fd, + bytes, + offset, + nonblocking, + .. + } => { + let written = match offset { + Some(offset) => { + kernel.fd_pwrite(EXECUTION_DRIVER_NAME, pid, fd, bytes.as_slice(), offset) + } + None if nonblocking => kernel.fd_write_nonblocking( + EXECUTION_DRIVER_NAME, + pid, + fd, + bytes.as_slice(), + ), + None => kernel.fd_write(EXECUTION_DRIVER_NAME, pid, fd, bytes.as_slice()), + } + .map_err(kernel_host_error)?; + Value::from(written) + } + FilesystemOperation::Seek { fd, offset, whence } => { + let whence = match whence { + DescriptorWhence::Set => agentos_kernel::kernel::SEEK_SET, + DescriptorWhence::Current => agentos_kernel::kernel::SEEK_CUR, + DescriptorWhence::End => agentos_kernel::kernel::SEEK_END, + }; + Value::String( + kernel + .fd_seek(EXECUTION_DRIVER_NAME, pid, fd, offset, whence) + .map_err(kernel_host_error)? + .to_string(), + ) + } + FilesystemOperation::Sync { fd, .. } => { + kernel + .fd_sync(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::DescriptorStatus { fd } => { + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + json!({ + "filetype": stat.filetype, + "flags": stat.flags, + "rightsBase": stat.rights, + "rightsInheriting": stat.rights_inheriting, + "preopenPath": stat.wasi_preopen_path, + }) + } + FilesystemOperation::DescriptorFileStat { fd } => { + let fd_stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + let stat = kernel + .dev_fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + wasi_stat_value(stat, fd_stat.filetype) + } + FilesystemOperation::DescriptorPath { + fd, + require_directory, + } => { + if require_directory { + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { + return Err(HostServiceError::new( + "ENOTDIR", + format!("file descriptor {fd} is not a directory"), + )); + } + } + Value::String( + kernel + .fd_path(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::DescriptorFdFlags { fd } => Value::from( + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + fd, + agentos_kernel::fd_table::F_GETFD, + 0, + ) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::SetDescriptorFdFlags { fd, flags } => Value::from( + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + fd, + agentos_kernel::fd_table::F_SETFD, + flags, + ) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::SetDescriptorFlags { fd, flags } => Value::from( + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + fd, + agentos_kernel::fd_table::F_SETFL, + flags, + ) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::SetLength { fd, length } => { + kernel + .fd_truncate(EXECUTION_DRIVER_NAME, pid, fd, length) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::SetPathLength { + dir_fd, + path, + length, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .truncate_for_process(EXECUTION_DRIVER_NAME, pid, &path, length) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::AdvisoryLock { fd, operation } => { + kernel + .fd_flock(EXECUTION_DRIVER_NAME, pid, fd, operation) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::RecordLock { + fd, + command, + kind, + start, + length, + } => { + let kind = match kind { + FilesystemRecordLockKind::Read => { + agentos_kernel::fd_table::RecordLockType::Read + } + FilesystemRecordLockKind::Write => { + agentos_kernel::fd_table::RecordLockType::Write + } + FilesystemRecordLockKind::Unlock => { + agentos_kernel::fd_table::RecordLockType::Unlock + } + }; + let conflict = match command { + RecordLockCommand::Query => kernel.fd_record_lock( + EXECUTION_DRIVER_NAME, + pid, + fd, + kind, + start, + length, + true, + ), + RecordLockCommand::Set => kernel.fd_record_lock( + EXECUTION_DRIVER_NAME, + pid, + fd, + kind, + start, + length, + false, + ), + RecordLockCommand::Wait => kernel + .fd_record_lock_wait(EXECUTION_DRIVER_NAME, pid, fd, kind, start, length) + .map(|()| None), + } + .map_err(kernel_host_error)?; + conflict.map_or_else( + || json!({ "type": 2, "pid": 0, "start": start.to_string(), "length": length.to_string() }), + |lock| json!({ + "type": match lock.lock_type { + agentos_kernel::fd_table::RecordLockType::Read => 0, + agentos_kernel::fd_table::RecordLockType::Write => 1, + agentos_kernel::fd_table::RecordLockType::Unlock => 2, + }, + "pid": lock.pid, + "start": lock.start.to_string(), + "length": lock.length().to_string(), + }), + ) + } + FilesystemOperation::CancelRecordLocks => { + kernel + .fd_record_lock_cancel(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::NamedPipePeerReady { fd } => Value::Bool( + kernel + .fd_named_pipe_peer_ready(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?, + ), + FilesystemOperation::ReadDirectory { + fd, + cookie, + max_entries, + .. + } => { + let cookie = usize::try_from(cookie).map_err(|_| { + HostServiceError::new("EINVAL", "fd_readdir cookie exceeds usize") + })?; + let entries = kernel + .fd_read_dir_with_types(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + Value::Array( + entries + .into_iter() + .enumerate() + .skip(cookie) + .take(max_entries.get()) + .map(|(index, entry)| { + json!({ + "name": entry.name, + "ino": entry.ino.to_string(), + "filetype": if entry.is_directory { + agentos_kernel::fd_table::FILETYPE_DIRECTORY + } else if entry.is_symbolic_link { + agentos_kernel::fd_table::FILETYPE_SYMBOLIC_LINK + } else { + agentos_kernel::fd_table::FILETYPE_REGULAR_FILE + }, + "next": index.saturating_add(1).to_string(), + }) + }) + .collect(), + ) + } + FilesystemOperation::ReadDirectoryAt { + dir_fd, + path, + max_entries, + max_reply_bytes, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + let entries = kernel + .read_dir_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?; + if entries.len() > max_entries.get() { + return Err(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "runtime.filesystem.maxReaddirEntries", + max_entries.get() as u64, + entries.len() as u64, + )); + } + PayloadLimit::new( + "limits.reactor.maxBridgeResponseBytes", + max_reply_bytes.get(), + )? + .admit_json(&entries)?; + json!(entries) + } + FilesystemOperation::Stat { target, path } => { + let MetadataTarget::Path { + dir_fd, + follow_symlinks, + } = target + else { + return Err(unsupported("filesystem stat target", target)); + }; + let path = resolve_path(kernel, process, dir_fd, required_path(path.as_ref())?)?; + let stat = if follow_symlinks { + kernel.stat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } else { + kernel.lstat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } + .map_err(kernel_host_error)?; + wasi_path_stat_value(stat) + } + FilesystemOperation::NodeStatAt { dir_fd, path } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + node_stat_value( + kernel + .stat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::NodeLstatAt { dir_fd, path } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + node_stat_value( + kernel + .lstat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::SetAttributesAt { + dir_fd, + path, + update, + follow_symlinks, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + if let Some(mode) = update.mode { + kernel + .chmod_for_process(EXECUTION_DRIVER_NAME, pid, &path, mode) + .map_err(kernel_host_error)?; + } + if update.uid.is_some() || update.gid.is_some() { + let current = if follow_symlinks { + kernel.stat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } else { + kernel.lstat_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } + .map_err(kernel_host_error)?; + kernel + .chown_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + update.uid.unwrap_or(current.uid), + update.gid.unwrap_or(current.gid), + follow_symlinks, + ) + .map_err(kernel_host_error)?; + } + if let (Some(atime_ms), Some(mtime_ms)) = (update.atime_ms, update.mtime_ms) { + kernel + .utimes_spec_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + agentos_kernel::vfs::VirtualUtimeSpec::Set( + agentos_kernel::vfs::VirtualTimeSpec::from_millis(atime_ms), + ), + agentos_kernel::vfs::VirtualUtimeSpec::Set( + agentos_kernel::vfs::VirtualTimeSpec::from_millis(mtime_ms), + ), + follow_symlinks, + ) + .map_err(kernel_host_error)?; + } + Value::Null + } + FilesystemOperation::SetTimes { + target, + path, + update, + } => { + let MetadataTarget::Path { + dir_fd, + follow_symlinks, + } = target + else { + return Err(unsupported("filesystem set-times target", target)); + }; + let path = resolve_path(kernel, process, dir_fd, required_path(path.as_ref())?)?; + kernel + .utimes_spec_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + time_spec(update.atime_ns, update.atime_now)?, + time_spec(update.mtime_ns, update.mtime_now)?, + follow_symlinks, + ) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::SetMode { target, path, mode } => { + match target { + MetadataTarget::Descriptor(fd) => { + kernel.fd_chmod_for_process(EXECUTION_DRIVER_NAME, pid, fd, mode) + } + MetadataTarget::Path { dir_fd, .. } => { + let path = + resolve_path(kernel, process, dir_fd, required_path(path.as_ref())?)?; + kernel.chmod_for_process(EXECUTION_DRIVER_NAME, pid, &path, mode) + } + } + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::SetOwner { + target, + path, + uid, + gid, + } => { + let uid = uid.ok_or_else(|| HostServiceError::new("EINVAL", "uid is required"))?; + let gid = gid.ok_or_else(|| HostServiceError::new("EINVAL", "gid is required"))?; + match target { + MetadataTarget::Descriptor(fd) => { + kernel.fd_chown_for_process(EXECUTION_DRIVER_NAME, pid, fd, uid, gid) + } + MetadataTarget::Path { + dir_fd, + follow_symlinks, + } => { + let path = + resolve_path(kernel, process, dir_fd, required_path(path.as_ref())?)?; + kernel.chown_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + uid, + gid, + follow_symlinks, + ) + } + } + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::AccessAt { + dir_fd, + path, + mode, + effective_ids, + } => { + let valid = libc::R_OK as u32 | libc::W_OK as u32 | libc::X_OK as u32; + if mode & !valid != 0 { + return Err(HostServiceError::new( + "EINVAL", + format!("invalid filesystem access mode {mode:o}"), + )); + } + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .access_for_process(EXECUTION_DRIVER_NAME, pid, &path, mode, effective_ids) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::CreateDirectoryAt { dir_fd, path, mode } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .mkdir_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + false, + (mode != 0o777).then_some(mode), + ) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::CreateDirectoriesAt { dir_fd, path, mode } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .mkdir_for_process(EXECUTION_DRIVER_NAME, pid, &path, true, mode) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::MakeNodeAt { + dir_fd, + path, + mode, + device, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .mknod_for_process(EXECUTION_DRIVER_NAME, pid, &path, mode, device) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::LinkAt { + old_dir_fd, + old_path, + follow_old, + new_dir_fd, + new_path, + } => { + let mut old_path = resolve_path(kernel, process, old_dir_fd, old_path.as_str())?; + let new_path = resolve_path(kernel, process, new_dir_fd, new_path.as_str())?; + if follow_old { + old_path = kernel + .realpath_for_process(EXECUTION_DRIVER_NAME, pid, &old_path) + .map_err(kernel_host_error)?; + } + kernel + .link_for_process(EXECUTION_DRIVER_NAME, pid, &old_path, &new_path) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::LinkDescriptorAt { fd, dir_fd, path } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .fd_link_tmpfile_for_process(EXECUTION_DRIVER_NAME, pid, fd, &path) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::RenameAt { + old_dir_fd, + old_path, + new_dir_fd, + new_path, + flags, + } => { + let old_path = resolve_path(kernel, process, old_dir_fd, old_path.as_str())?; + let new_path = resolve_path(kernel, process, new_dir_fd, new_path.as_str())?; + if flags == 0 { + kernel.rename_for_process(EXECUTION_DRIVER_NAME, pid, &old_path, &new_path) + } else { + kernel.rename_at2_for_process( + EXECUTION_DRIVER_NAME, + pid, + &old_path, + &new_path, + flags, + ) + } + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::SymlinkAt { + target, + dir_fd, + path, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + kernel + .symlink_for_process(EXECUTION_DRIVER_NAME, pid, target.as_str(), &path) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::ReadLinkAt { dir_fd, path, .. } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + Value::String( + kernel + .read_link_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?, + ) + } + FilesystemOperation::UnlinkAt { + dir_fd, + path, + remove_directory, + } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + if remove_directory { + kernel.remove_dir_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } else { + kernel.remove_file_for_process(EXECUTION_DRIVER_NAME, pid, &path) + } + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::Range { + fd, + operation, + offset, + length, + keep_size, + } => { + match operation { + FileRangeOperation::Allocate => { + kernel.fd_allocate(EXECUTION_DRIVER_NAME, pid, fd, offset, length) + } + FileRangeOperation::PunchHole => { + kernel.fd_punch_hole(EXECUTION_DRIVER_NAME, pid, fd, offset, length) + } + FileRangeOperation::Zero => kernel.fd_zero_range( + EXECUTION_DRIVER_NAME, + pid, + fd, + offset, + length, + keep_size, + ), + FileRangeOperation::Insert => { + kernel.fd_insert_range(EXECUTION_DRIVER_NAME, pid, fd, offset, length) + } + FileRangeOperation::Collapse => { + kernel.fd_collapse_range(EXECUTION_DRIVER_NAME, pid, fd, offset, length) + } + } + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::Extents { fd, max_entries } => { + let allocated = kernel + .fd_allocated_ranges(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + let unwritten = kernel + .fd_unwritten_ranges(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + Value::Array(classify_extents(allocated, &unwritten).into_iter().take(max_entries.get()).map( + |(start, end, unwritten)| json!({ "start": start, "end": end, "unwritten": unwritten }) + ).collect()) + } + FilesystemOperation::ExtentAt { fd, index } => kernel + .fd_extent_at(EXECUTION_DRIVER_NAME, pid, fd, index) + .map_err(kernel_host_error)? + .map(|extent| { + json!({ + "start": extent.start, + "end": extent.end, + "unwritten": extent.unwritten, + }) + }) + .unwrap_or(Value::Null), + FilesystemOperation::Xattr { + target, + path, + name, + value, + operation, + max_result_bytes, + } => execute_xattr( + kernel, + process, + target, + path.as_ref(), + name.as_ref(), + value.as_ref(), + operation, + max_result_bytes, + )?, + FilesystemOperation::FilesystemStatsAt { dir_fd, path } => { + let path = resolve_path(kernel, process, dir_fd, path.as_str())?; + let stats = kernel + .filesystem_stats_for_process(EXECUTION_DRIVER_NAME, pid, &path) + .map_err(kernel_host_error)?; + json!({ + "totalBytes": stats.total_bytes, + "usedBytes": stats.used_bytes, + "availableBytes": stats.available_bytes, + "totalInodes": stats.total_inodes, + "freeInodes": stats.free_inodes, + }) + } + FilesystemOperation::Remount { path, options } => { + let path = resolve_path(kernel, process, NODE_CWD_FD, path.as_str())?; + kernel + .remount_filesystem_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + options.as_str(), + ) + .map_err(kernel_host_error)?; + Value::Null + } + FilesystemOperation::StdinRead { + max_bytes, + timeout_ms, + } => { + if max_bytes.get() > process.limits.wasm.sync_read_limit_bytes { + return Err(HostServiceError::limit( + "E2BIG", + "limits.wasm.syncReadLimitBytes", + process.limits.wasm.sync_read_limit_bytes as u64, + max_bytes.get() as u64, + )); + } + typed_kernel_stdin_read(kernel, process, max_bytes.get(), timeout_ms) + .map_err(sidecar_host_error)? + } + FilesystemOperation::StdioWrite { fd, bytes } => { + typed_kernel_stdio_write(kernel, process, fd, bytes.into_vec()) + .map_err(sidecar_host_error)? + } + other => return Err(unsupported("filesystem", other)), + }; + Ok(HostCallReply::Json(value)) + } +} + +fn sidecar_host_error(error: SidecarError) -> HostServiceError { + match error { + SidecarError::Host(error) => error, + other => HostServiceError::new("EIO", other.to_string()), + } +} + +fn required_path(path: Option<&BoundedString>) -> Result<&str, HostServiceError> { + path.map(BoundedString::as_str) + .ok_or_else(|| HostServiceError::new("EINVAL", "filesystem path is required")) +} + +fn resolve_path( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + dir_fd: u32, + path: &str, +) -> Result { + if dir_fd == NODE_CWD_FD { + let path = if path.starts_with('/') { + normalize_path(path) + } else { + normalize_path(&format!( + "{}/{}", + process.guest_cwd.trim_end_matches('/'), + path + )) + }; + if path + .split('/') + .any(agentos_kernel::kernel::is_internal_unnamed_file_name) + { + return Err(HostServiceError::new( + "ENOENT", + format!("no such file or directory: {path}"), + )); + } + return Ok(path); + } + if path.starts_with('/') { + return Err(HostServiceError::new( + "EACCES", + format!("absolute path '{path}' cannot bypass directory fd {dir_fd}"), + )); + } + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, dir_fd) + .map_err(kernel_host_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { + return Err(HostServiceError::new( + "ENOTDIR", + format!("file descriptor {dir_fd} is not a directory"), + )); + } + let base = kernel + .fd_path(EXECUTION_DRIVER_NAME, process.kernel_pid, dir_fd) + .map_err(kernel_host_error)?; + Ok(normalize_path(&format!("{base}/{path}"))) +} + +fn preopen_value(preopen: agentos_kernel::kernel::ProcessWasiPreopen) -> Value { + json!({ + "fd": preopen.fd, + "guestPath": preopen.guest_path, + "rightsBase": preopen.rights_base, + "rightsInheriting": preopen.rights_inheriting, + }) +} + +fn node_stat_value(stat: agentos_kernel::vfs::VirtualStat) -> Value { + json!({ + "mode": stat.mode, + "size": stat.size, + "blocks": stat.blocks, + "dev": stat.dev, + "rdev": stat.rdev, + "isDirectory": stat.is_directory, + "isSymbolicLink": stat.is_symbolic_link, + "atimeMs": stat.atime_ms, + "atimeNsec": stat.atime_nsec, + "mtimeMs": stat.mtime_ms, + "mtimeNsec": stat.mtime_nsec, + "ctimeMs": stat.ctime_ms, + "ctimeNsec": stat.ctime_nsec, + "birthtimeMs": stat.birthtime_ms, + "ino": stat.ino, + "nlink": stat.nlink, + "uid": stat.uid, + "gid": stat.gid, + }) +} + +fn wasi_path_stat_value(stat: agentos_kernel::vfs::VirtualStat) -> Value { + let filetype = if stat.is_directory { + agentos_kernel::fd_table::FILETYPE_DIRECTORY + } else if stat.is_symbolic_link { + agentos_kernel::fd_table::FILETYPE_SYMBOLIC_LINK + } else { + agentos_kernel::fd_table::FILETYPE_REGULAR_FILE + }; + wasi_stat_value(stat, filetype) +} + +fn wasi_stat_value(stat: agentos_kernel::vfs::VirtualStat, filetype: u8) -> Value { + json!({ + "dev": stat.dev, + "ino": stat.ino, + "filetype": filetype, + "nlink": stat.nlink, + "mode": stat.mode, + "uid": stat.uid, + "gid": stat.gid, + "size": stat.size, + "blocks": stat.blocks, + "rdev": stat.rdev, + "atimeMs": stat.atime_ms, + "mtimeMs": stat.mtime_ms, + "ctimeMs": stat.ctime_ms, + }) +} + +fn time_spec( + nanoseconds: Option, + now: bool, +) -> Result { + use agentos_kernel::vfs::{VirtualTimeSpec, VirtualUtimeSpec}; + if now { + return Ok(VirtualUtimeSpec::Now); + } + let Some(nanoseconds) = nanoseconds else { + return Ok(VirtualUtimeSpec::Omit); + }; + let seconds = i64::try_from(nanoseconds / 1_000_000_000) + .map_err(|_| HostServiceError::new("EINVAL", "timestamp exceeds i64 seconds"))?; + VirtualTimeSpec::new(seconds, (nanoseconds % 1_000_000_000) as u32) + .map(VirtualUtimeSpec::Set) + .map_err(|error| HostServiceError::new("EINVAL", error.to_string())) +} + +fn classify_extents(allocated: Vec<(u64, u64)>, unwritten: &[(u64, u64)]) -> Vec<(u64, u64, bool)> { + let mut classified = Vec::new(); + for (start, end) in allocated { + let mut cursor = start; + for &(unwritten_start, unwritten_end) in unwritten { + if unwritten_end <= cursor || unwritten_start >= end { + continue; + } + if cursor < unwritten_start { + classified.push((cursor, unwritten_start.min(end), false)); + } + let overlap_start = cursor.max(unwritten_start); + let overlap_end = end.min(unwritten_end); + if overlap_start < overlap_end { + classified.push((overlap_start, overlap_end, true)); + cursor = overlap_end; + } + if cursor == end { + break; + } + } + if cursor < end { + classified.push((cursor, end, false)); + } + } + classified +} + +#[allow(clippy::too_many_arguments)] +fn execute_xattr( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + target: MetadataTarget, + path: Option<&BoundedString>, + name: Option<&BoundedString>, + value: Option<&BoundedBytes>, + operation: XattrOperation, + max_result_bytes: BoundedUsize, +) -> Result { + let pid = process.kernel_pid; + let name = || { + name.map(BoundedString::as_str) + .ok_or_else(|| HostServiceError::new("EINVAL", "xattr name is required")) + }; + let mut path_target = |dir_fd, follow| -> Result<(String, bool), HostServiceError> { + Ok(( + resolve_path(kernel, process, dir_fd, required_path(path)?)?, + follow, + )) + }; + match (target, operation) { + (MetadataTarget::Descriptor(fd), XattrOperation::Get) => { + let bytes = kernel + .fd_get_xattr_for_process(EXECUTION_DRIVER_NAME, pid, fd, name()?) + .map_err(kernel_host_error)?; + if bytes.len() > max_result_bytes.get() { + return Err(HostServiceError::limit( + "E2BIG", + "limits.reactor.maxBridgeResponseBytes", + max_result_bytes.get() as u64, + bytes.len() as u64, + )); + } + Ok(host_bytes_value(&bytes)) + } + (MetadataTarget::Descriptor(fd), XattrOperation::List) => Ok(json!(kernel + .fd_list_xattrs_for_process(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?)), + (MetadataTarget::Descriptor(fd), XattrOperation::Set { flags }) => { + kernel + .fd_set_xattr_for_process( + EXECUTION_DRIVER_NAME, + pid, + fd, + name()?, + value + .ok_or_else(|| HostServiceError::new("EINVAL", "xattr value is required"))? + .as_slice() + .to_vec(), + flags, + ) + .map_err(kernel_host_error)?; + Ok(Value::Null) + } + (MetadataTarget::Descriptor(fd), XattrOperation::Remove) => { + kernel + .fd_remove_xattr_for_process(EXECUTION_DRIVER_NAME, pid, fd, name()?) + .map_err(kernel_host_error)?; + Ok(Value::Null) + } + ( + MetadataTarget::Path { + dir_fd, + follow_symlinks, + }, + operation, + ) => { + let (path, follow) = path_target(dir_fd, follow_symlinks)?; + match operation { + XattrOperation::Get => { + let bytes = kernel + .get_xattr_for_process(EXECUTION_DRIVER_NAME, pid, &path, name()?, follow) + .map_err(kernel_host_error)?; + if bytes.len() > max_result_bytes.get() { + return Err(HostServiceError::limit( + "E2BIG", + "limits.reactor.maxBridgeResponseBytes", + max_result_bytes.get() as u64, + bytes.len() as u64, + )); + } + Ok(host_bytes_value(&bytes)) + } + XattrOperation::List => Ok(json!(kernel + .list_xattrs_for_process(EXECUTION_DRIVER_NAME, pid, &path, follow,) + .map_err(kernel_host_error)?)), + XattrOperation::Set { flags } => { + kernel + .set_xattr_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + name()?, + value + .ok_or_else(|| { + HostServiceError::new("EINVAL", "xattr value is required") + })? + .as_slice() + .to_vec(), + flags, + follow, + ) + .map_err(kernel_host_error)?; + Ok(Value::Null) + } + XattrOperation::Remove => { + kernel + .remove_xattr_for_process( + EXECUTION_DRIVER_NAME, + pid, + &path, + name()?, + follow, + ) + .map_err(kernel_host_error)?; + Ok(Value::Null) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::execution::host_dispatch::inventory::{ + capability_family, semantic_rpc_inventory, HostCapabilityFamily, + }; + use agentos_execution::backend::{ + DirectHostReplyTarget, HostCallIdentity, HostCallReply, PayloadLimit, + }; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::socket_table::SocketType; + use agentos_kernel::vfs::MemoryFileSystem; + use base64::Engine as _; + use std::collections::{BTreeSet, HashMap}; + use std::sync::{Arc, Mutex}; + + #[derive(Default)] + struct RecordingTarget { + replies: Mutex>>, + } + + impl DirectHostReplyTarget for RecordingTarget { + fn claim(&self, _call_id: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _call_id: u64, + _claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + self.replies.lock().expect("reply lock").push(result); + Ok(()) + } + } + + fn test_runtime_context() -> agentos_runtime::RuntimeContext { + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create test runtime") + .context() + } + + fn direct_reply( + target: Arc, + generation: u64, + pid: u32, + call_id: u64, + ) -> DirectHostReplyHandle { + DirectHostReplyHandle::new( + HostCallIdentity { + generation, + pid, + call_id, + }, + target, + 64 * 1024, + ) + .expect("direct reply") + } + + fn bounded_string(value: &str) -> BoundedString { + BoundedString::try_new( + value.to_owned(), + &PayloadLimit::new("testStringBytes", 4096).expect("string limit"), + ) + .expect("bounded string") + } + + fn kernel_process_at_tier(tier: ProcessPermissionTier) -> (SidecarKernel, KernelProcessHandle) { + let mut config = KernelVmConfig::new(format!("vm-tier-{tier:?}")); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register WASM driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + permission_tier: Some(tier), + ..SpawnOptions::default() + }, + ) + .expect("spawn tier process"); + (kernel, handle) + } + + #[test] + fn centralized_tier_matrix_separates_preview1_and_host_process_operations() { + let (isolated_kernel, isolated) = kernel_process_at_tier(ProcessPermissionTier::Isolated); + let isolated_pid = isolated.pid(); + for operation in [ + HostOperation::Filesystem(FilesystemOperation::Pipe), + HostOperation::Filesystem(FilesystemOperation::Duplicate { fd: 0 }), + HostOperation::Filesystem(FilesystemOperation::DuplicateTo { + fd: 0, + target_fd: 3, + }), + HostOperation::Filesystem(FilesystemOperation::DuplicateMin { fd: 0, min_fd: 3 }), + HostOperation::Filesystem(FilesystemOperation::DescriptorFdFlags { fd: 0 }), + HostOperation::Clock(ClockOperation::Sleep { duration_ms: 1 }), + HostOperation::Terminal(TerminalOperation::OpenPty), + HostOperation::Signal(SignalOperation::Pending), + ] { + assert_eq!( + super::authorize_host_operation(&isolated_kernel, isolated_pid, &operation) + .expect_err("isolated host-process operation must fail") + .code, + "EACCES" + ); + } + for operation in [ + HostOperation::Filesystem(FilesystemOperation::Preopens), + HostOperation::Filesystem(FilesystemOperation::Preopen { fd: 3 }), + HostOperation::Filesystem(FilesystemOperation::Move { + fd: 0, + replaced_fd: None, + }), + HostOperation::Process(ProcessOperation::GetResourceLimit { + kind: ResourceLimitKind::OpenFiles, + }), + HostOperation::Clock(ClockOperation::Resolution { + clock: GuestClockId::Monotonic, + }), + HostOperation::Signal(SignalOperation::UpdateMask { + how: SignalMaskHow::Block, + set: SignalSetValue(0), + }), + HostOperation::Signal(SignalOperation::BeginDelivery), + ] { + super::authorize_host_operation(&isolated_kernel, isolated_pid, &operation) + .expect("Preview1/internal operation remains available"); + } + + for tier in [ + ProcessPermissionTier::ReadOnly, + ProcessPermissionTier::ReadWrite, + ] { + let (kernel, handle) = kernel_process_at_tier(tier); + let pid = handle.pid(); + super::authorize_host_operation( + &kernel, + pid, + &HostOperation::Filesystem(FilesystemOperation::DuplicateMin { fd: 0, min_fd: 3 }), + ) + .expect("limited fd_dup_min remains available"); + for operation in [ + HostOperation::Filesystem(FilesystemOperation::Duplicate { fd: 0 }), + HostOperation::Filesystem(FilesystemOperation::DuplicateTo { + fd: 0, + target_fd: 3, + }), + HostOperation::Filesystem(FilesystemOperation::Pipe), + HostOperation::Clock(ClockOperation::RealIntervalGet), + HostOperation::Terminal(TerminalOperation::OpenPty), + ] { + assert_eq!( + super::authorize_host_operation(&kernel, pid, &operation) + .expect_err("full-only operation must fail") + .code, + "EACCES" + ); + } + } + } + + #[test] + fn real_wasi_dirfd_and_explicit_zero_rights_cannot_be_waived_by_absolute_paths() { + let (mut kernel, handle) = kernel_process_at_tier(ProcessPermissionTier::Full); + let pid = handle.pid(); + kernel + .mkdir("/workspace", true) + .expect("create default workspace preopen"); + kernel + .mkdir("/cap", true) + .expect("create capability directory"); + kernel + .mkdir("/outside", true) + .expect("create outside directory"); + let root = kernel + .initialize_wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .expect("initialize preopens") + .into_iter() + .find(|entry| entry.guest_path == "/") + .expect("root preopen"); + let zero_dir = kernel + .fd_open_with_rights( + EXECUTION_DRIVER_NAME, + pid, + Some(root.fd), + "/cap", + agentos_kernel::fd_table::O_DIRECTORY, + None, + Some((0, 0)), + ) + .expect("open explicit-zero directory"); + let open = HostOperation::Filesystem(FilesystemOperation::OpenAt { + dir_fd: zero_dir, + path: bounded_string("/outside"), + options: GuestOpenSpec { + flags: agentos_kernel::fd_table::O_DIRECTORY, + mode: None, + rights: GuestOpenRights::Explicit { + base: 0, + inheriting: 0, + }, + }, + }); + assert_eq!( + super::authorize_host_operation(&kernel, pid, &open) + .expect_err("absolute spelling cannot waive dirfd rights") + .code, + "EACCES" + ); + + let metadata = HostOperation::Filesystem(FilesystemOperation::SetMode { + target: MetadataTarget::Descriptor(zero_dir), + path: None, + mode: 0o700, + }); + assert_eq!( + super::authorize_host_operation(&kernel, pid, &metadata) + .expect_err("explicit-zero fd cannot mutate metadata") + .code, + "EACCES" + ); + } + + #[test] + fn pipe_and_socket_metadata_rights_survive_dup_and_process_inheritance() { + let (mut kernel, parent) = kernel_process_at_tier(ProcessPermissionTier::Full); + let parent_pid = parent.pid(); + let (pipe_read, _pipe_write) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent_pid) + .expect("create pipe"); + let (socket_left, _socket_right) = kernel + .fd_socketpair( + EXECUTION_DRIVER_NAME, + parent_pid, + SocketType::Stream, + false, + false, + ) + .expect("create socket pair"); + let pipe_alias = kernel + .fd_dup(EXECUTION_DRIVER_NAME, parent_pid, pipe_read) + .expect("duplicate pipe"); + let socket_alias = kernel + .fd_dup(EXECUTION_DRIVER_NAME, parent_pid, socket_left) + .expect("duplicate socket"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent_pid), + ..SpawnOptions::default() + }, + ) + .expect("spawn child with inherited descriptors"); + + for (pid, fd, resource) in [ + (parent_pid, pipe_read, "pipe"), + (parent_pid, pipe_alias, "duplicated pipe"), + (parent_pid, socket_left, "socket"), + (parent_pid, socket_alias, "duplicated socket"), + (child.pid(), pipe_read, "inherited pipe"), + (child.pid(), socket_left, "inherited socket"), + ] { + for operation in [ + HostOperation::Filesystem(FilesystemOperation::SetMode { + target: MetadataTarget::Descriptor(fd), + path: None, + mode: 0o640, + }), + HostOperation::Filesystem(FilesystemOperation::SetOwner { + target: MetadataTarget::Descriptor(fd), + path: None, + uid: None, + gid: None, + }), + ] { + super::authorize_host_operation(&kernel, pid, &operation) + .unwrap_or_else(|error| panic!("authorize {resource} metadata: {error}")); + } + + let stat = kernel + .dev_fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .unwrap_or_else(|error| panic!("stat {resource}: {error}")); + kernel + .fd_chmod_for_process(EXECUTION_DRIVER_NAME, pid, fd, 0o640) + .unwrap_or_else(|error| panic!("fchmod {resource}: {error}")); + kernel + .fd_chown_for_process(EXECUTION_DRIVER_NAME, pid, fd, stat.uid, stat.gid) + .unwrap_or_else(|error| panic!("fchown {resource}: {error}")); + } + } + + #[test] + fn limited_tiers_cannot_use_synthesized_pipe_or_socket_metadata_rights() { + for tier in [ + ProcessPermissionTier::ReadOnly, + ProcessPermissionTier::Isolated, + ] { + let (mut kernel, process) = kernel_process_at_tier(tier); + let pid = process.pid(); + let (pipe_read, _pipe_write) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, pid) + .expect("create pipe directly for authorization test"); + let (socket_left, _socket_right) = kernel + .fd_socketpair(EXECUTION_DRIVER_NAME, pid, SocketType::Stream, false, false) + .expect("create socket pair directly for authorization test"); + + for fd in [pipe_read, socket_left] { + for operation in [ + HostOperation::Filesystem(FilesystemOperation::SetMode { + target: MetadataTarget::Descriptor(fd), + path: None, + mode: 0o640, + }), + HostOperation::Filesystem(FilesystemOperation::SetOwner { + target: MetadataTarget::Descriptor(fd), + path: None, + uid: None, + gid: None, + }), + ] { + assert_eq!( + super::authorize_host_operation(&kernel, pid, &operation) + .expect_err("limited tier must deny descriptor metadata mutation") + .code, + "EROFS" + ); + } + } + } + } + + #[test] + fn sufficient_wasi_dirfd_rights_still_confine_absolute_paths() { + let (mut kernel, handle) = kernel_process_at_tier(ProcessPermissionTier::Full); + let pid = handle.pid(); + kernel + .mkdir("/cap", true) + .expect("create capability directory"); + kernel + .mkdir("/outside", true) + .expect("create outside directory"); + let cap_fd = kernel + .fd_open( + EXECUTION_DRIVER_NAME, + pid, + "/cap", + agentos_kernel::fd_table::O_DIRECTORY, + None, + ) + .expect("open directory capability"); + let process = ActiveProcess::new( + pid, + handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ); + let operation = HostOperation::Filesystem(FilesystemOperation::OpenAt { + dir_fd: cap_fd, + path: bounded_string("/outside"), + options: GuestOpenSpec { + flags: agentos_kernel::fd_table::O_DIRECTORY, + mode: None, + rights: GuestOpenRights::Explicit { + base: 0, + inheriting: 0, + }, + }, + }); + super::authorize_host_operation(&kernel, pid, &operation) + .expect("directory has sufficient PATH_OPEN rights"); + assert_eq!( + resolve_path(&mut kernel, &process, cap_fd, "/outside") + .expect_err("absolute path cannot escape real WASI dirfd") + .code, + "EACCES" + ); + assert_eq!( + resolve_path(&mut kernel, &process, cap_fd, "child") + .expect("relative path remains confined"), + "/cap/child" + ); + } + + fn request(method: &str) -> HostRpcRequest { + let args = match method { + "__kernel_stdin_read" => vec![json!(1), json!(0)], + "__kernel_stdio_write" => vec![json!(1), json!("x")], + "fs.accessSync" => vec![json!("/"), json!(0), json!(false)], + "fs.chmodForProcessSync" => vec![json!("/x"), json!(0o644)], + "fs.chownSync" | "fs.lchownSync" => { + vec![json!("/x"), json!(1000), json!(1000)] + } + "fs.truncateForProcessSync" => vec![json!("/x"), json!(0)], + "fs.fallocateSync" + | "fs.insertRangeSync" + | "fs.collapseRangeSync" + | "fs.punchHoleSync" => vec![json!(3), json!(0), json!(1)], + "fs.zeroRangeSync" => vec![json!(3), json!(0), json!(1), json!(1)], + "fs.fgetxattrSync" | "fs.fremovexattrSync" => vec![json!(3), json!("user.x")], + "fs.flistxattrSync" | "fs.fiemapSync" | "fs.namedFifoPeerReadySync" => vec![json!(3)], + "fs.fiemapAtSync" => vec![json!(3), json!(0)], + "fs.fsetxattrSync" => vec![json!(3), json!("user.x"), json!("x"), json!(0)], + "fs.getxattrSync" | "fs.removexattrSync" => { + vec![json!("/x"), json!("user.x"), json!(true)] + } + "fs.listxattrSync" => vec![json!("/x"), json!(true)], + "fs.setxattrSync" => vec![ + json!("/x"), + json!("user.x"), + json!("x"), + json!(0), + json!(true), + ], + "fs.linkFdSync" => vec![json!(3), json!("/x")], + "fs.mknodSync" => vec![json!("/x"), json!(0o644), json!(0)], + "fs.openTmpfileSync" => { + vec![json!("/tmp"), json!(0), json!(0o600), json!(true)] + } + "fs.remountSync" => vec![json!("/"), json!("rw")], + "fs.renameAt2Sync" => vec![json!("/a"), json!("/b"), json!(0)], + "fs.statSync" | "fs.statfsSync" => vec![json!("/")], + "process.fd_chdir_path" + | "process.fd_close" + | "process.fd_closefrom" + | "process.fd_dup" + | "process.fd_filestat" + | "process.fd_getfd" + | "process.fd_move" + | "process.fd_path" + | "process.fd_preopen" + | "process.fd_stat" => vec![json!(3)], + "process.fd_dup_min" => vec![json!(3), json!(4)], + "process.fd_dup2" => vec![json!(3), json!(4)], + "process.fd_chmod" + | "process.fd_flock" + | "process.fd_set_flags" + | "process.fd_setfd" => vec![json!(3), json!(0)], + "process.fd_chown" => vec![json!(3), json!(1), json!(1)], + "process.fd_datasync" | "process.fd_sync" => vec![json!(3)], + "process.fd_open" => vec![json!("/x"), json!(0), Value::Null, json!("0"), json!("0")], + "process.fd_pipe" | "process.fd_preopens" | "process.fd_record_lock_cancel" => vec![], + "process.fd_pread" => vec![json!(3), json!(1), json!("0")], + "process.fd_pwrite" => vec![json!(3), json!("x"), json!("0")], + "process.fd_read" => vec![json!(3), json!(1), json!(0)], + "process.fd_readdir" => vec![json!(3), json!("0"), json!(1)], + "process.fd_record_lock" => { + vec![json!(3), json!(12), json!(0), json!("0"), json!("1")] + } + "process.fd_seek" => vec![json!(3), json!("0"), json!(0)], + "process.fd_truncate" => vec![json!(3), json!("0")], + "process.fd_write" => vec![json!(3), json!("x")], + "process.path_chmod_at" => vec![json!(3), json!("x"), json!(0o644)], + "process.path_chown_at" => { + vec![json!(3), json!("x"), json!(1), json!(1), json!(true)] + } + "process.path_link_at" => { + vec![json!(3), json!("a"), json!(3), json!("b"), json!(false)] + } + "process.path_mkdir_at" + | "process.path_readlink_at" + | "process.path_remove_dir_at" + | "process.path_unlink_at" => vec![json!(3), json!("x")], + "process.path_open_at" => vec![ + json!(3), + json!("x"), + json!(0), + Value::Null, + json!("0"), + json!("0"), + ], + "process.path_rename_at" => { + vec![json!(3), json!("a"), json!(3), json!("b")] + } + "process.path_stat_at" => vec![json!(3), json!("x"), json!(true)], + "process.path_symlink_at" => vec![json!("a"), json!(3), json!("b")], + "process.path_utimes_at" => vec![ + json!(3), + json!("x"), + json!(true), + json!("0"), + json!("0"), + json!(5), + ], + other => panic!("missing filesystem fixture for {other}"), + }; + HostRpcRequest { + id: 1, + method: method.to_owned(), + args, + raw_bytes_args: HashMap::new(), + } + } + + #[test] + fn every_frozen_filesystem_rpc_decodes_to_a_typed_filesystem_operation() { + for method in semantic_rpc_inventory() + .into_iter() + .filter(|method| capability_family(method) == Some(HostCapabilityFamily::Filesystem)) + { + let decoded = super::super::decode_host_operation(&request(method), true, 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {method}: {error}")); + assert!( + matches!(decoded, Some(HostOperation::Filesystem(_))), + "{method} must not fall through to the legacy bridge" + ); + } + } + + #[test] + fn closefrom_decoder_preserves_exact_canonical_targets() { + let mut request = request("process.fd_closefrom"); + request.args[0] = json!(64); + request.args.push(json!([3, 7, 3])); + assert!(matches!( + super::super::decode_host_operation(&request, true, 1024) + .expect("decode closefrom") + .expect("typed closefrom"), + HostOperation::Filesystem(FilesystemOperation::CloseFrom { + min_fd: 64, + exact_fds: Some(fds), + }) if fds.as_slice() == [3, 7, 3] + )); + } + + #[test] + fn wrapped_only_filesystem_aliases_preserve_linux_semantics() { + let decode = |method| { + super::super::decode_host_operation(&request(method), true, 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {method}: {error}")) + .unwrap_or_else(|| panic!("{method} fell through to the legacy bridge")) + }; + + assert!(matches!( + decode("fs.chmodForProcessSync"), + HostOperation::Filesystem(FilesystemOperation::SetMode { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: true, + }, + mode: 0o644, + .. + }) + )); + assert!(matches!( + decode("fs.chownSync"), + HostOperation::Filesystem(FilesystemOperation::SetOwner { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: true, + }, + uid: Some(1000), + gid: Some(1000), + .. + }) + )); + assert!(matches!( + decode("fs.lchownSync"), + HostOperation::Filesystem(FilesystemOperation::SetOwner { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: false, + }, + uid: Some(1000), + gid: Some(1000), + .. + }) + )); + assert!(matches!( + decode("fs.truncateForProcessSync"), + HostOperation::Filesystem(FilesystemOperation::SetPathLength { + dir_fd: NODE_CWD_FD, + length: 0, + .. + }) + )); + assert!(matches!( + decode("process.fd_dup2"), + HostOperation::Filesystem(FilesystemOperation::DuplicateTo { + fd: 3, + target_fd: 4, + }) + )); + } + + #[test] + fn decoder_rejects_paths_and_payloads_before_constructing_operations() { + let mut overlong_path = request("fs.statSync"); + overlong_path.args[0] = json!("x".repeat(MAX_PATH_BYTES + 1)); + assert_eq!( + super::super::decode_host_operation(&overlong_path, true, 1024) + .expect_err("overlong path") + .code(), + Some("ENAMETOOLONG") + ); + + let oversized_read = HostRpcRequest { + args: vec![json!(3), json!(9), json!(0)], + ..request("process.fd_read") + }; + assert_eq!( + super::super::decode_host_operation(&oversized_read, true, 8) + .expect_err("oversized read") + .code(), + Some("E2BIG") + ); + + let oversized_write = HostRpcRequest { + args: vec![json!(3), json!("123456789")], + ..request("process.fd_write") + }; + assert_eq!( + super::super::decode_host_operation(&oversized_write, true, 8) + .expect_err("oversized write") + .code(), + Some("E2BIG") + ); + } + + #[test] + fn fd_write_decodes_the_v8_cbor_buffer_projection() { + let request = HostRpcRequest { + args: vec![json!(7), json!({ "__type": "Buffer", "data": "aGVsbG8=" })], + ..request("process.fd_write") + }; + let Some(HostOperation::Filesystem(FilesystemOperation::Write { + fd, bytes, offset, .. + })) = super::super::decode_host_operation(&request, true, 1024) + .expect("canonical V8 buffer must decode") + else { + panic!("fd_write did not decode as a filesystem write") + }; + + assert_eq!(fd, 7); + assert_eq!(bytes.as_slice(), b"hello"); + assert_eq!(offset, None); + } + + #[test] + fn fd_write_rejects_malformed_v8_cbor_buffer_base64_with_typed_einval() { + let request = HostRpcRequest { + args: vec![ + json!(7), + json!({ "__type": "Buffer", "data": "not base64!" }), + ], + ..request("process.fd_write") + }; + + assert_eq!( + super::super::decode_host_operation(&request, true, 1024) + .expect_err("malformed canonical V8 buffer") + .code(), + Some("EINVAL") + ); + } + + #[test] + fn completed_timed_fd_read_preserves_kernel_eof() { + assert_eq!( + complete_timed_fd_read(None).expect("kernel EOF must remain successful"), + Vec::::new() + ); + } + + #[test] + fn typed_stdin_read_refills_accepted_input_beyond_pipe_capacity() { + let mut config = KernelVmConfig::new("vm-typed-stdin-refill"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register WASM driver"); + let kernel_handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn typed-stdin process"); + let pid = kernel_handle.pid(); + let identity = kernel_handle.runtime_identity(); + let writer_fd = install_kernel_stdin_pipe(&mut kernel, pid).expect("install stdin pipe"); + let mut process = ActiveProcess::new( + pid, + kernel_handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ) + .with_kernel_stdin_writer_fd(writer_fd); + + // Three pipe capacities ensure that correctness depends on multiple + // typed read admissions refilling the bounded owner-side backlog. + let payload = (0..(3 * 64 * 1024 + 37)) + .map(|index| (index % 251) as u8) + .collect::>(); + write_kernel_process_stdin(&mut kernel, &mut process, &payload) + .expect("accept oversized stdin payload"); + assert!( + process.pending_kernel_stdin.total > 0, + "payload must exceed the live kernel pipe and exercise owner backlog refill" + ); + close_kernel_process_stdin(&mut kernel, &mut process) + .expect("defer stdin close until accepted bytes drain"); + assert!(process.pending_kernel_stdin.close_requested); + + let runtime = process.runtime_context.clone(); + let notify = Arc::new(tokio::sync::Notify::new()); + let target = Arc::new(RecordingTarget::default()); + let response_limit = + PayloadLimit::new("testStdinReadBytes", 64 * 1024).expect("stdin response limit"); + let maximum = + BoundedUsize::try_new(16 * 1024, &response_limit).expect("bounded stdin read"); + let stdin_reader_fd = process.kernel_stdin_reader_fd; + let mut observed = Vec::with_capacity(payload.len()); + let mut call_id = 1_u64; + + loop { + let reply_count = target.replies.lock().expect("stdin reply lock").len(); + service_deferred_kernel_read_with_response( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + Arc::clone(¬ify), + &mut kernel, + &mut process, + Some(( + stdin_reader_fd, + maximum.clone(), + DeferredKernelReadResponse::KernelStdin, + Instant::now(), + direct_reply(Arc::clone(&target), identity.generation, pid, call_id), + )), + ) + .expect("service typed stdin read"); + + let replies = target.replies.lock().expect("stdin reply lock"); + assert_eq!( + replies.len(), + reply_count + 1, + "each ready HostCall must be pumped and settled exactly once" + ); + let HostCallReply::Json(value) = replies + .last() + .expect("every ready stdin admission replies") + .as_ref() + .expect("stdin read succeeds") + else { + panic!("stdin reply must be JSON") + }; + if value.get("done").and_then(Value::as_bool) == Some(true) { + break; + } + let encoded = value + .get("dataBase64") + .and_then(Value::as_str) + .expect("accepted input must be readable before EOF"); + observed.extend( + base64::engine::general_purpose::STANDARD + .decode(encoded) + .expect("decode typed stdin response"), + ); + drop(replies); + call_id += 1; + assert!( + call_id < 64, + "bounded refill state machine must reach EOF without spinning" + ); + } + + assert!(call_id > 2, "the regression must cross multiple HostCalls"); + assert_eq!(observed, payload); + assert_eq!(process.pending_kernel_stdin.total, 0); + assert!(!process.pending_kernel_stdin.close_requested); + assert!(process.kernel_stdin_writer_fd.is_none()); + process.kernel_handle.finish(0); + } + + #[test] + fn managed_parent_read_parks_until_inherited_child_writer_progresses() { + let mut config = KernelVmConfig::new("vm-deferred-managed-parent-read"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register WASM driver"); + let parent_handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn managed parent"); + let parent_pid = parent_handle.pid(); + let identity = parent_handle.runtime_identity(); + let mut parent = ActiveProcess::new( + parent_pid, + parent_handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ); + let (read_fd, write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent_pid) + .expect("open parent pipe"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent_pid), + ..SpawnOptions::default() + }, + ) + .expect("spawn child inheriting pipe descriptions"); + let child_pid = child.pid(); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, write_fd) + .expect("parent closes writer before reading"); + + let runtime = parent.runtime_context.clone(); + let notify = Arc::new(tokio::sync::Notify::new()); + let target = Arc::new(RecordingTarget::default()); + let read_limit = PayloadLimit::new("testReadBytes", 64).expect("read limit"); + let maximum = BoundedUsize::try_new(64, &read_limit).expect("bounded read"); + service_deferred_kernel_read( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + Arc::clone(¬ify), + &mut kernel, + &mut parent, + Some(( + read_fd, + maximum.clone(), + Instant::now() + Duration::from_secs(1), + direct_reply(Arc::clone(&target), identity.generation, parent_pid, 1), + )), + ) + .expect("park parent read"); + assert!(parent.deferred_kernel_read.is_some()); + assert!(target.replies.lock().expect("reply lock").is_empty()); + + kernel + .fd_write(EXECUTION_DRIVER_NAME, child_pid, write_fd, b"child-payload") + .expect("child writes inherited pipe"); + kernel + .fd_close(EXECUTION_DRIVER_NAME, child_pid, write_fd) + .expect("child closes inherited writer"); + if let Some(task) = parent + .deferred_kernel_read + .as_mut() + .and_then(|read| read.wake_task.take()) + { + task.abort(); + } + service_deferred_kernel_read( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + Arc::clone(¬ify), + &mut kernel, + &mut parent, + None, + ) + .expect("complete parent read after child progress"); + + let replies = target.replies.lock().expect("reply lock"); + assert_eq!(replies.len(), 1); + let HostCallReply::Json(payload) = replies[0].as_ref().expect("successful read reply") + else { + panic!("read reply must be JSON") + }; + assert_eq!( + javascript_sync_rpc_bytes_arg(&[payload.clone()], 0, "read reply") + .expect("decode read reply"), + b"child-payload" + ); + drop(replies); + + let eof_target = Arc::new(RecordingTarget::default()); + service_deferred_kernel_read( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + notify, + &mut kernel, + &mut parent, + Some(( + read_fd, + maximum, + Instant::now() + Duration::from_secs(1), + direct_reply(Arc::clone(&eof_target), identity.generation, parent_pid, 2), + )), + ) + .expect("observe EOF after child close"); + let replies = eof_target.replies.lock().expect("EOF reply lock"); + let HostCallReply::Json(payload) = replies[0].as_ref().expect("successful EOF reply") + else { + panic!("EOF reply must be JSON") + }; + assert!( + javascript_sync_rpc_bytes_arg(&[payload.clone()], 0, "EOF reply") + .expect("decode EOF reply") + .is_empty() + ); + drop(replies); + + let (timeout_read_fd, timeout_write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent_pid) + .expect("open timeout pipe"); + let timeout_target = Arc::new(RecordingTarget::default()); + service_deferred_kernel_read( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + Arc::new(tokio::sync::Notify::new()), + &mut kernel, + &mut parent, + Some(( + timeout_read_fd, + BoundedUsize::try_new(64, &read_limit).expect("bounded timeout read"), + Instant::now(), + direct_reply( + Arc::clone(&timeout_target), + identity.generation, + parent_pid, + 3, + ), + )), + ) + .expect("settle expired not-ready read"); + let replies = timeout_target.replies.lock().expect("timeout reply lock"); + assert_eq!(replies.len(), 1); + assert_eq!( + replies[0] + .as_ref() + .expect_err("expired not-ready read must fail") + .code, + "EAGAIN" + ); + drop(replies); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, timeout_read_fd) + .expect("close timeout reader"); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, timeout_write_fd) + .expect("close timeout writer"); + + child.finish(0); + parent.kernel_handle.finish(0); + } + + #[test] + fn every_side_effecting_filesystem_rpc_requires_reply_claim_first() { + let side_effecting = [ + "__kernel_stdin_read", + "__kernel_stdio_write", + "fs.collapseRangeSync", + "fs.chmodForProcessSync", + "fs.chownSync", + "fs.fallocateSync", + "fs.fremovexattrSync", + "fs.fsetxattrSync", + "fs.insertRangeSync", + "fs.lchownSync", + "fs.linkFdSync", + "fs.mknodSync", + "fs.openTmpfileSync", + "fs.punchHoleSync", + "fs.remountSync", + "fs.removexattrSync", + "fs.renameAt2Sync", + "fs.setxattrSync", + "fs.truncateForProcessSync", + "fs.zeroRangeSync", + "process.fd_chmod", + "process.fd_chown", + "process.fd_close", + "process.fd_closefrom", + "process.fd_datasync", + "process.fd_dup", + "process.fd_dup2", + "process.fd_dup_min", + "process.fd_flock", + "process.fd_move", + "process.fd_open", + "process.fd_pipe", + "process.fd_preopen", + "process.fd_preopens", + "process.fd_pwrite", + "process.fd_read", + "process.fd_pread", + "process.fd_record_lock", + "process.fd_record_lock_cancel", + "process.fd_seek", + "process.fd_set_flags", + "process.fd_setfd", + "process.fd_sync", + "process.fd_truncate", + "process.fd_write", + "process.path_chmod_at", + "process.path_chown_at", + "process.path_link_at", + "process.path_mkdir_at", + "process.path_open_at", + "process.path_remove_dir_at", + "process.path_rename_at", + "process.path_symlink_at", + "process.path_unlink_at", + "process.path_utimes_at", + ] + .into_iter() + .collect::>(); + + for method in semantic_rpc_inventory() + .into_iter() + .filter(|method| capability_family(method) == Some(HostCapabilityFamily::Filesystem)) + { + let Some(HostOperation::Filesystem(operation)) = + super::super::decode_host_operation(&request(method), true, 1024 * 1024) + .expect("decode") + else { + panic!("{method} did not decode as filesystem") + }; + assert_eq!( + FilesystemCapability::requires_claim(&operation), + side_effecting.contains(method), + "claim classification drift for {method}" + ); + } + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/identity.rs b/crates/native-sidecar/src/execution/host_dispatch/identity.rs new file mode 100644 index 0000000000..423e23cf3a --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/identity.rs @@ -0,0 +1,250 @@ +use super::*; + +pub(super) struct IdentityCapability; + +impl SidecarHostCapability for IdentityCapability { + fn requires_claim(operation: &IdentityOperation) -> bool { + !matches!( + operation, + IdentityOperation::GetId { .. } + | IdentityOperation::GetUserIds + | IdentityOperation::GetGroupIds + | IdentityOperation::Get + | IdentityOperation::GetSupplementaryGroups + | IdentityOperation::PasswdById { .. } + | IdentityOperation::PasswdByName { .. } + | IdentityOperation::NextPasswd { .. } + | IdentityOperation::GroupById { .. } + | IdentityOperation::GroupByName { .. } + | IdentityOperation::NextGroup { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: IdentityOperation, + ) -> Result { + let pid = process.kernel_pid; + let value = match operation { + IdentityOperation::GetId { kind } => match kind { + IdentityIdKind::RealUser => json!(kernel + .getuid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?), + IdentityIdKind::EffectiveUser => json!(kernel + .geteuid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?), + IdentityIdKind::SavedUser => { + let (_, _, saved) = kernel + .getresuid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!(saved) + } + IdentityIdKind::RealGroup => json!(kernel + .getgid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?), + IdentityIdKind::EffectiveGroup => json!(kernel + .getegid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?), + IdentityIdKind::SavedGroup => { + let (_, _, saved) = kernel + .getresgid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!(saved) + } + }, + IdentityOperation::GetUserIds => { + let (real, effective, saved) = kernel + .getresuid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!([real, effective, saved]) + } + IdentityOperation::GetGroupIds => { + let (real, effective, saved) = kernel + .getresgid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!([real, effective, saved]) + } + IdentityOperation::GetSupplementaryGroups => json!(kernel + .getgroups(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?), + IdentityOperation::SetId { kind, value } => { + let value = value + .ok_or_else(|| HostServiceError::new("EINVAL", "identity value is required"))?; + match kind { + IdentityIdKind::RealUser => kernel.setuid(EXECUTION_DRIVER_NAME, pid, value), + IdentityIdKind::EffectiveUser => { + kernel.seteuid(EXECUTION_DRIVER_NAME, pid, value) + } + IdentityIdKind::RealGroup => kernel.setgid(EXECUTION_DRIVER_NAME, pid, value), + IdentityIdKind::EffectiveGroup => { + kernel.setegid(EXECUTION_DRIVER_NAME, pid, value) + } + IdentityIdKind::SavedUser | IdentityIdKind::SavedGroup => { + return Err(HostServiceError::new( + "EINVAL", + "saved IDs require a setres operation", + )); + } + } + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::SetRealEffectiveUserIds { real, effective } => { + kernel + .setreuid(EXECUTION_DRIVER_NAME, pid, real, effective) + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::SetUserIds { + real, + effective, + saved, + } => { + kernel + .setresuid(EXECUTION_DRIVER_NAME, pid, real, effective, saved) + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::SetRealEffectiveGroupIds { real, effective } => { + kernel + .setregid(EXECUTION_DRIVER_NAME, pid, real, effective) + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::SetGroupIds { + real, + effective, + saved, + } => { + kernel + .setresgid(EXECUTION_DRIVER_NAME, pid, real, effective, saved) + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::SetSupplementaryGroups { groups } => { + kernel + .setgroups(EXECUTION_DRIVER_NAME, pid, groups.into_vec()) + .map_err(kernel_host_error)?; + Value::Null + } + IdentityOperation::PasswdById { + uid, + max_record_bytes, + } => account_record( + kernel + .getpwuid_for_process(EXECUTION_DRIVER_NAME, pid, uid) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::PasswdByName { + name, + max_record_bytes, + } => account_record( + kernel + .getpwnam_for_process(EXECUTION_DRIVER_NAME, pid, name.as_str()) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::NextPasswd { + index, + max_record_bytes, + } => account_record( + kernel + .getpwent_for_process(EXECUTION_DRIVER_NAME, pid, index) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::GroupById { + gid, + max_record_bytes, + } => account_record( + kernel + .getgrgid_for_process(EXECUTION_DRIVER_NAME, pid, gid) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::GroupByName { + name, + max_record_bytes, + } => account_record( + kernel + .getgrnam_for_process(EXECUTION_DRIVER_NAME, pid, name.as_str()) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::NextGroup { + index, + max_record_bytes, + } => account_record( + kernel + .getgrent_for_process(EXECUTION_DRIVER_NAME, pid, index) + .map_err(kernel_host_error)?, + max_record_bytes, + )?, + IdentityOperation::Get => { + let (uid, euid, suid) = kernel + .getresuid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + let (gid, egid, sgid) = kernel + .getresgid(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + let groups = kernel + .getgroups(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + json!({ + "uid": uid, + "euid": euid, + "suid": suid, + "gid": gid, + "egid": egid, + "sgid": sgid, + "groups": groups, + }) + } + other => return Err(unsupported("identity", other)), + }; + Ok(HostCallReply::Json(value)) + } +} + +fn account_record(record: String, maximum: BoundedUsize) -> Result { + if record.len() > maximum.get() { + return Err(HostServiceError::new( + "E2BIG", + format!( + "account record is {} bytes, exceeding maxAccountRecordBytes ({})", + record.len(), + maximum.get() + ), + ) + .with_details(json!({ + "limitName": "maxAccountRecordBytes", + "limit": maximum.get(), + "requested": record.len(), + }))); + } + Ok(Value::String(record)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn account_record_accepts_exact_limit_and_rejects_limit_plus_one() { + let payload_limit = PayloadLimit::new("maxAccountRecordBytes", 4_096).unwrap(); + let maximum = BoundedUsize::try_new(4_096, &payload_limit).unwrap(); + assert_eq!( + account_record("x".repeat(4_096), maximum).unwrap(), + Value::String("x".repeat(4_096)) + ); + + let error = account_record("x".repeat(4_097), maximum).unwrap_err(); + assert_eq!(error.code, "E2BIG"); + let details = error.details.expect("typed limit details"); + assert_eq!(details["limitName"], "maxAccountRecordBytes"); + assert_eq!(details["limit"], 4_096); + assert_eq!(details["requested"], 4_097); + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/inventory.rs b/crates/native-sidecar/src/execution/host_dispatch/inventory.rs new file mode 100644 index 0000000000..6cd93d410f --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/inventory.rs @@ -0,0 +1,365 @@ +//! Reviewed compatibility-WASM RPC inventory. +//! +//! The architecture guard compares these exact lists with static `callSyncRpc` +//! literals in `wasm-runner.mjs` and methods hidden behind the generic +//! `process.wasm_sync_rpc` bootstrap. A new runner method must be assigned to +//! one capability family or explicitly reviewed as adapter-only. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum HostCapabilityFamily { + Filesystem, + Network, + Process, + Terminal, + Signal, + Identity, + Clock, + Entropy, +} + +pub(super) const WASM_RUNNER_RPC_INVENTORY: &[&str] = &[ + "__kernel_isatty", + "__kernel_poll", + "__kernel_stdin_read", + "__kernel_stdio_write", + "__kernel_tcgetattr", + "__kernel_tcgetpgrp", + "__kernel_tcgetsid", + "__kernel_tcsetattr", + "__kernel_tcsetpgrp", + "__kernel_tty_set_size", + "__kernel_tty_size", + "__pty_set_raw_mode", + "child_process.close_stdin", + "child_process.poll", + "child_process.spawn", + "child_process.write_stdin", + "dgram.bind", + "dgram.close", + "dgram.createSocket", + "dgram.poll", + "dgram.send", + "dns.lookup", + "dns.resolveRawRr", + "fs.accessSync", + "fs.blockingIoTimeoutMsSync", + "fs.collapseRangeSync", + "fs.fallocateSync", + "fs.fgetxattrSync", + "fs.fiemapAtSync", + "fs.flistxattrSync", + "fs.fremovexattrSync", + "fs.fsetxattrSync", + "fs.getxattrSync", + "fs.insertRangeSync", + "fs.linkFdSync", + "fs.listxattrSync", + "fs.mknodSync", + "fs.namedFifoPeerReadySync", + "fs.openTmpfileSync", + "fs.punchHoleSync", + "fs.remountSync", + "fs.removexattrSync", + "fs.renameAt2Sync", + "fs.setxattrSync", + "fs.statSync", + "fs.statfsSync", + "fs.zeroRangeSync", + "net.bind_connected_unix", + "net.bind_unix", + "net.connect", + "net.destroy", + "net.listen", + "net.poll", + "net.release_tcp_port", + "net.reserve_tcp_port", + "net.server_accept", + "net.server_close", + "net.socket_read", + "net.socket_upgrade_tls", + "net.socket_wait_connect", + "net.write", + "process.clock_resolution", + "process.clock_time", + "process.exec", + "process.exec_image_close", + "process.exec_image_open", + "process.exec_image_open_fd", + "process.exec_image_read", + "process.exec_fd_image_commit", + "process.fd_chdir_path", + "process.fd_chmod", + "process.fd_chown", + "process.fd_close", + "process.fd_closefrom", + "process.fd_datasync", + "process.fd_description_identity", + "process.fd_dup", + "process.fd_dup_min", + "process.fd_filestat", + "process.fd_flock", + "process.fd_getfd", + "process.fd_move", + "process.fd_path", + "process.fd_pipe", + "process.fd_pread", + "process.fd_preopen", + "process.fd_preopens", + "process.fd_pwrite", + "process.fd_read", + "process.fd_readdir", + "process.fd_record_lock", + "process.fd_record_lock_cancel", + "process.fd_recvmsg_rights", + "process.fd_seek", + "process.fd_sendmsg_rights", + "process.fd_set_flags", + "process.fd_setfd", + "process.fd_snapshot", + "process.fd_socket_shutdown", + "process.fd_socketpair", + "process.fd_stat", + "process.fd_sync", + "process.fd_truncate", + "process.fd_write", + "process.getegid", + "process.geteuid", + "process.getgid", + "process.getgrent", + "process.getgrgid", + "process.getgrnam", + "process.getgroups", + "process.getpgid", + "process.getpwent", + "process.getpwnam", + "process.getpwuid", + "process.getresgid", + "process.getresuid", + "process.getrlimit", + "process.getuid", + "process.hostnet_accept", + "process.hostnet_bind", + "process.hostnet_connect", + "process.hostnet_fd_open", + "process.hostnet_get_option", + "process.hostnet_listen", + "process.hostnet_local_address", + "process.hostnet_peer_address", + "process.hostnet_poll", + "process.hostnet_recv", + "process.hostnet_send", + "process.hostnet_set_option", + "process.hostnet_tls_connect", + "process.hostnet_validate", + "process.itimer_real", + "process.kill", + "process.path_chmod_at", + "process.path_chown_at", + "process.path_link_at", + "process.path_mkdir_at", + "process.path_open_at", + "process.path_readlink_at", + "process.path_remove_dir_at", + "process.path_rename_at", + "process.path_stat_at", + "process.path_symlink_at", + "process.path_unlink_at", + "process.path_utimes_at", + "process.posix_poll", + "process.pty_open", + "process.random_get", + "process.setegid", + "process.seteuid", + "process.setgid", + "process.setgroups", + "process.setpgid", + "process.setregid", + "process.setresgid", + "process.setresuid", + "process.setreuid", + "process.setrlimit", + "process.setuid", + "process.signal_end", + "process.signal_mask", + "process.signal_state", + "process.sleep", + "process.system_identity", + "process.take_signal", + "process.umask", + "process.waitpid", + "process.waitpid_transition", +]; + +/// Semantic RPCs emitted only through the generic `process.wasm_sync_rpc` +/// wrapper in the V8 compatibility bootstrap. They do not appear as literal +/// `callSyncRpc(...)` targets in `wasm-runner.mjs`, so keeping this reviewed +/// delta frozen prevents Linux operations from silently bypassing typed host +/// dispatch behind the wrapper. +pub(super) const WASM_WRAPPED_ONLY_RPC_INVENTORY: &[&str] = &[ + "fs.chmodForProcessSync", + "fs.chownSync", + "fs.fiemapSync", + "fs.lchownSync", + "fs.truncateForProcessSync", + "process.fd_description_alias_count", + "process.fd_dup2", + "process.fd_open", + "process.image", + "process.signal_begin", + "process.signal_mask_scope_begin", + "process.signal_mask_scope_end", +]; + +/// These calls coordinate the current V8 compatibility adapter rather than +/// representing a guest Linux operation. They may remain on the legacy bridge +/// until the corresponding runner projection is deleted. +pub(super) const WASM_ADAPTER_ONLY_RPCS: &[&str] = &[ + "fs.blockingIoTimeoutMsSync", + "process.fd_description_alias_count", + "process.fd_description_identity", + "process.fd_snapshot", +]; + +pub(super) fn capability_family(method: &str) -> Option { + if WASM_ADAPTER_ONLY_RPCS.contains(&method) { + return None; + } + if method.starts_with("child_process.") + || matches!( + method, + "process.exec" + | "process.exec_fd_image_commit" + | "process.exec_image_close" + | "process.exec_image_open" + | "process.exec_image_open_fd" + | "process.exec_image_read" + | "process.getpgid" + | "process.image" + | "process.getrlimit" + | "process.kill" + | "process.setpgid" + | "process.setrlimit" + | "process.system_identity" + | "process.umask" + | "process.waitpid" + | "process.waitpid_transition" + ) + { + return Some(HostCapabilityFamily::Process); + } + if matches!( + method, + "process.getuid" + | "process.getgid" + | "process.geteuid" + | "process.getegid" + | "process.getresuid" + | "process.getresgid" + | "process.getgroups" + | "process.getpwuid" + | "process.getpwnam" + | "process.getpwent" + | "process.getgrgid" + | "process.getgrnam" + | "process.getgrent" + | "process.setuid" + | "process.seteuid" + | "process.setreuid" + | "process.setresuid" + | "process.setgid" + | "process.setegid" + | "process.setregid" + | "process.setresgid" + | "process.setgroups" + ) { + return Some(HostCapabilityFamily::Identity); + } + if method.starts_with("process.signal_") || method == "process.take_signal" { + return Some(HostCapabilityFamily::Signal); + } + if matches!( + method, + "process.clock_time" | "process.clock_resolution" | "process.itimer_real" | "process.sleep" + ) { + return Some(HostCapabilityFamily::Clock); + } + if method == "process.random_get" { + return Some(HostCapabilityFamily::Entropy); + } + if method.starts_with("__kernel_tc") + || method.starts_with("__kernel_tty") + || method == "__kernel_isatty" + || method == "__pty_set_raw_mode" + || method == "process.pty_open" + { + return Some(HostCapabilityFamily::Terminal); + } + if method.starts_with("net.") + || method.starts_with("dgram.") + || method.starts_with("dns.") + || method.starts_with("process.hostnet_") + || matches!( + method, + "__kernel_poll" + | "process.posix_poll" + | "process.fd_recvmsg_rights" + | "process.fd_sendmsg_rights" + | "process.fd_socket_shutdown" + | "process.fd_socketpair" + ) + { + return Some(HostCapabilityFamily::Network); + } + if method.starts_with("fs.") + || method.starts_with("process.fd_") + || method.starts_with("process.path_") + || matches!(method, "__kernel_stdin_read" | "__kernel_stdio_write") + { + return Some(HostCapabilityFamily::Filesystem); + } + None +} + +#[cfg(test)] +pub(super) fn semantic_rpc_inventory() -> std::collections::BTreeSet<&'static str> { + WASM_RUNNER_RPC_INVENTORY + .iter() + .chain(WASM_WRAPPED_ONLY_RPC_INVENTORY) + .copied() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + #[test] + fn every_frozen_semantic_rpc_is_typed_or_explicitly_adapter_only() { + let direct = WASM_RUNNER_RPC_INVENTORY + .iter() + .copied() + .collect::>(); + assert_eq!(direct.len(), WASM_RUNNER_RPC_INVENTORY.len()); + let wrapped_only = WASM_WRAPPED_ONLY_RPC_INVENTORY + .iter() + .copied() + .collect::>(); + assert_eq!(wrapped_only.len(), WASM_WRAPPED_ONLY_RPC_INVENTORY.len()); + assert!(direct.is_disjoint(&wrapped_only)); + let inventory = semantic_rpc_inventory(); + let adapter_only = WASM_ADAPTER_ONLY_RPCS + .iter() + .copied() + .collect::>(); + assert_eq!(adapter_only.len(), WASM_ADAPTER_ONLY_RPCS.len()); + assert!(adapter_only.is_subset(&inventory)); + for method in inventory { + assert_eq!( + capability_family(method).is_some(), + !adapter_only.contains(method), + "{method} must have exactly one reviewed route", + ); + } + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/mod.rs b/crates/native-sidecar/src/execution/host_dispatch/mod.rs new file mode 100644 index 0000000000..557da2e3be --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/mod.rs @@ -0,0 +1,4012 @@ +//! Production runtime-neutral host-operation dispatch. +//! +//! Execution adapters decode only calls whose wire contract is already an +//! exact match for a [`HostOperation`]. Everything below this router is split +//! by capability family and invokes the existing kernel semantic operation; +//! no Linux state is mirrored here. + +mod clock; +mod entropy; +mod filesystem; +pub(super) use filesystem::{service_deferred_kernel_read, service_deferred_kernel_stdin_read}; +mod identity; +#[allow(dead_code)] +mod inventory; + +#[cfg(test)] +pub(crate) fn is_wasm_adapter_only_rpc(method: &str) -> bool { + inventory::WASM_ADAPTER_ONLY_RPCS.contains(&method) +} +mod network; +pub(super) use network::service_deferred_kernel_poll; +mod network_compat; +pub(in crate::execution) use network_compat::managed_socket_address_from_info; +pub(in crate::execution) use network_compat::{ + close_with_managed_retirement, closefrom_with_managed_retirement, + dispatch_claimed_context_stream_read, dispatch_claimed_context_udp_poll, + dispatch_descendant_context_stream_read, dispatch_descendant_context_udp_poll, + prune_managed_process_routes_without_aliases, replace_descriptor_with_managed_retirement, + retire_managed_process_routes, retire_orphaned_managed_descriptions, + service_deferred_posix_poll, service_descendant_managed_fd_network_operation, +}; +mod process; +mod signal; +mod terminal; + +use super::*; +use agentos_execution::backend::{ExecutionEvent, HostCallReply, PayloadLimit}; +use agentos_execution::host::{ + BoundedBytes, BoundedProcessLaunchRequest, BoundedString, BoundedUsize, BoundedVec, + ClockOperation, CommittedProcessImage, DescriptorSyncKind, DescriptorWhence, DnsAddressFamily, + EntropyOperation, ExecutableImageSource, FileRangeOperation, FileTimeUpdate, + FilesystemOperation, FilesystemRecordLockKind, GuestClockId, GuestOpenRights, GuestOpenSpec, + HostOperation, IdentityIdKind, IdentityOperation, KernelPollInterest, ManagedTcpEndpoint, + ManagedUdpFamily, ManagedUnixAddress, MetadataTarget, NetworkOperation, PollInterest, + ProcessLaunchOptions, ProcessLaunchRequest, ProcessOperation, RecordLockCommand, + ResourceLimitKind, ResourceLimitValue, SignalActionValue, SignalDispositionValue, + SignalMaskHow, SignalOperation, SignalSetValue, SocketAddress, + SocketDomain as HostSocketDomain, SocketKind, SocketOptionName, SocketOptionValue, + SocketShutdown, SocketValidationRequirement, TerminalAttributes, TerminalOperation, + TerminalWindowSize, WaitTarget, XattrOperation, +}; +use agentos_kernel::fd_table::{ + WASI_RIGHT_FD_ALLOCATE, WASI_RIGHT_FD_DATASYNC, WASI_RIGHT_FD_FDSTAT_SET_FLAGS, + WASI_RIGHT_FD_FILESTAT_GET, WASI_RIGHT_FD_FILESTAT_SET_SIZE, WASI_RIGHT_FD_FILESTAT_SET_TIMES, + WASI_RIGHT_FD_READ, WASI_RIGHT_FD_READDIR, WASI_RIGHT_FD_SEEK, WASI_RIGHT_FD_SYNC, + WASI_RIGHT_FD_WRITE, WASI_RIGHT_PATH_CREATE_DIRECTORY, WASI_RIGHT_PATH_FILESTAT_GET, + WASI_RIGHT_PATH_FILESTAT_SET_SIZE, WASI_RIGHT_PATH_FILESTAT_SET_TIMES, + WASI_RIGHT_PATH_LINK_SOURCE, WASI_RIGHT_PATH_LINK_TARGET, WASI_RIGHT_PATH_OPEN, + WASI_RIGHT_PATH_READLINK, WASI_RIGHT_PATH_REMOVE_DIRECTORY, WASI_RIGHT_PATH_RENAME_SOURCE, + WASI_RIGHT_PATH_RENAME_TARGET, WASI_RIGHT_PATH_SYMLINK, WASI_RIGHT_PATH_UNLINK_FILE, +}; +use agentos_kernel::kernel::KernelError; + +const MAX_ACCOUNT_NAME_BYTES: usize = 4 * 1024; +const MAX_ACCOUNT_RECORD_BYTES: usize = 4 * 1024; +const MAX_CHILD_PROCESS_ID_BYTES: usize = 4 * 1024; +const MAX_SUPPLEMENTARY_GROUPS: usize = 64; +const MAX_SIGNAL_SET_ENTRIES: usize = 64; +const MAX_SIGNAL_STATE_MASK_JSON_BYTES: usize = 4 * 1024; +const MAX_ENTROPY_CHUNK_BYTES: usize = 64 * 1024; +const MAX_DEFERRED_GUEST_WAIT_MS: u64 = u32::MAX as u64; + +pub(crate) fn checked_deferred_guest_wait_deadline( + delay_ms: u64, +) -> Result { + if delay_ms > MAX_DEFERRED_GUEST_WAIT_MS { + return Err(HostServiceError::new( + "EINVAL", + format!( + "guest wait duration {delay_ms} ms exceeds guestWaitDurationMs ({MAX_DEFERRED_GUEST_WAIT_MS} ms)" + ), + ) + .with_details(json!({ + "limitName": "guestWaitDurationMs", + "limit": MAX_DEFERRED_GUEST_WAIT_MS, + "observed": delay_ms, + }))); + } + Instant::now() + .checked_add(Duration::from_millis(delay_ms)) + .ok_or_else(|| { + HostServiceError::new("EINVAL", "guest wait deadline exceeds the host clock range") + .with_details(json!({ + "limitName": "guestWaitDurationMs", + "limit": MAX_DEFERRED_GUEST_WAIT_MS, + "observed": delay_ms, + })) + }) +} + +trait SidecarHostCapability { + fn requires_claim(operation: &Operation) -> bool; + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: Operation, + ) -> Result; +} + +fn permission_tier_label(tier: ProcessPermissionTier) -> &'static str { + match tier { + ProcessPermissionTier::Isolated => "isolated", + ProcessPermissionTier::ReadOnly => "read-only", + ProcessPermissionTier::ReadWrite => "read-write", + ProcessPermissionTier::Full => "full", + } +} + +fn tier_denied( + code: &'static str, + tier: ProcessPermissionTier, + family: &'static str, + operation: &impl fmt::Debug, +) -> HostServiceError { + HostServiceError::new( + code, + format!( + "{} process tier denies {family} operation {operation:?}", + permission_tier_label(tier) + ), + ) + .with_details(json!({ + "permissionTier": permission_tier_label(tier), + "capabilityFamily": family, + "operation": format!("{operation:?}"), + })) +} + +fn filesystem_operation_is_path_based(operation: &FilesystemOperation) -> bool { + matches!( + operation, + FilesystemOperation::ReadFileAt { .. } + | FilesystemOperation::WriteFileAt { .. } + | FilesystemOperation::OpenAt { .. } + | FilesystemOperation::OpenTmpfileAt { .. } + | FilesystemOperation::NodeStatAt { .. } + | FilesystemOperation::NodeLstatAt { .. } + | FilesystemOperation::ReadDirectoryAt { .. } + | FilesystemOperation::AccessAt { .. } + | FilesystemOperation::CreateDirectoryAt { .. } + | FilesystemOperation::CreateDirectoriesAt { .. } + | FilesystemOperation::MakeNodeAt { .. } + | FilesystemOperation::LinkAt { .. } + | FilesystemOperation::LinkDescriptorAt { .. } + | FilesystemOperation::RenameAt { .. } + | FilesystemOperation::SymlinkAt { .. } + | FilesystemOperation::ReadLinkAt { .. } + | FilesystemOperation::UnlinkAt { .. } + | FilesystemOperation::FilesystemStatsAt { .. } + | FilesystemOperation::Remount { .. } + | FilesystemOperation::Stat { + target: MetadataTarget::Path { .. }, + .. + } + | FilesystemOperation::SetTimes { + target: MetadataTarget::Path { .. }, + .. + } + | FilesystemOperation::SetMode { + target: MetadataTarget::Path { .. }, + .. + } + | FilesystemOperation::SetOwner { + target: MetadataTarget::Path { .. }, + .. + } + | FilesystemOperation::SetAttributesAt { .. } + | FilesystemOperation::SetPathLength { .. } + | FilesystemOperation::Xattr { + target: MetadataTarget::Path { .. }, + .. + } + ) +} + +fn filesystem_operation_is_read_only_mutation(operation: &FilesystemOperation) -> bool { + use agentos_kernel::fd_table::{O_CREAT, O_RDWR, O_TRUNC, O_WRONLY}; + + match operation { + FilesystemOperation::WriteFileAt { .. } + | FilesystemOperation::SetAttributesAt { .. } + | FilesystemOperation::CreateDirectoriesAt { .. } + | FilesystemOperation::CreateDirectoryAt { .. } => true, + FilesystemOperation::OpenAt { options, .. } => { + let requested_mutation_rights = match options.rights { + GuestOpenRights::Explicit { base, inheriting } => { + (base | inheriting) + & (WASI_RIGHT_FD_DATASYNC + | WASI_RIGHT_FD_SYNC + | WASI_RIGHT_FD_WRITE + | WASI_RIGHT_FD_ALLOCATE + | WASI_RIGHT_FD_FILESTAT_SET_SIZE + | WASI_RIGHT_FD_FILESTAT_SET_TIMES + | WASI_RIGHT_PATH_CREATE_DIRECTORY + | WASI_RIGHT_PATH_FILESTAT_SET_SIZE + | WASI_RIGHT_PATH_FILESTAT_SET_TIMES + | WASI_RIGHT_PATH_LINK_SOURCE + | WASI_RIGHT_PATH_LINK_TARGET + | WASI_RIGHT_PATH_RENAME_SOURCE + | WASI_RIGHT_PATH_RENAME_TARGET + | WASI_RIGHT_PATH_SYMLINK + | WASI_RIGHT_PATH_REMOVE_DIRECTORY + | WASI_RIGHT_PATH_UNLINK_FILE) + != 0 + } + GuestOpenRights::Synthesized => false, + }; + options.flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC) != 0 + || requested_mutation_rights + } + FilesystemOperation::OpenTmpfileAt { .. } + | FilesystemOperation::SetLength { .. } + | FilesystemOperation::SetPathLength { .. } + | FilesystemOperation::SetTimes { .. } + | FilesystemOperation::SetMode { .. } + | FilesystemOperation::SetOwner { .. } + | FilesystemOperation::MakeNodeAt { .. } + | FilesystemOperation::LinkAt { .. } + | FilesystemOperation::LinkDescriptorAt { .. } + | FilesystemOperation::RenameAt { .. } + | FilesystemOperation::SymlinkAt { .. } + | FilesystemOperation::UnlinkAt { .. } + | FilesystemOperation::Range { .. } + | FilesystemOperation::Remount { .. } => true, + FilesystemOperation::Xattr { operation, .. } => { + matches!( + operation, + XattrOperation::Set { .. } | XattrOperation::Remove + ) + } + _ => false, + } +} + +fn descriptor_write_targets_file( + kernel: &SidecarKernel, + pid: u32, + fd: u32, +) -> Result { + let entry = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + Ok(matches!( + entry.filetype, + agentos_kernel::fd_table::FILETYPE_REGULAR_FILE + | agentos_kernel::fd_table::FILETYPE_DIRECTORY + | agentos_kernel::fd_table::FILETYPE_SYMBOLIC_LINK + )) +} + +fn require_descriptor_right( + kernel: &SidecarKernel, + pid: u32, + fd: u32, + required: u64, + operation: &FilesystemOperation, +) -> Result<(), HostServiceError> { + let entry = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_host_error)?; + if entry.rights & required == required { + return Ok(()); + } + Err(HostServiceError::new( + "EACCES", + format!("descriptor {fd} lacks required WASI rights {required:#x} for {operation:?}"), + ) + .with_details(json!({ + "fd": fd, + "requiredRights": required, + "descriptorRights": entry.rights, + "operation": format!("{operation:?}"), + }))) +} + +fn require_path_right( + kernel: &SidecarKernel, + pid: u32, + dir_fd: u32, + _path: &BoundedString, + required: u64, + operation: &FilesystemOperation, +) -> Result<(), HostServiceError> { + if dir_fd == u32::MAX { + return Ok(()); + } + require_descriptor_right(kernel, pid, dir_fd, required, operation) +} + +fn authorize_filesystem_rights( + kernel: &SidecarKernel, + pid: u32, + operation: &FilesystemOperation, +) -> Result<(), HostServiceError> { + match operation { + FilesystemOperation::ReadFileAt { dir_fd, path, .. } + | FilesystemOperation::WriteFileAt { dir_fd, path, .. } + | FilesystemOperation::ReadDirectoryAt { dir_fd, path, .. } => { + require_path_right(kernel, pid, *dir_fd, path, WASI_RIGHT_PATH_OPEN, operation) + } + FilesystemOperation::OpenAt { dir_fd, path, .. } => { + require_path_right(kernel, pid, *dir_fd, path, WASI_RIGHT_PATH_OPEN, operation) + } + FilesystemOperation::OpenTmpfileAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_OPEN | WASI_RIGHT_PATH_CREATE_DIRECTORY, + operation, + ), + FilesystemOperation::Read { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_READ, operation) + } + FilesystemOperation::Write { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_WRITE, operation) + } + FilesystemOperation::Seek { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_SEEK, operation) + } + FilesystemOperation::Sync { fd, kind } => require_descriptor_right( + kernel, + pid, + *fd, + match kind { + DescriptorSyncKind::Data => WASI_RIGHT_FD_DATASYNC, + DescriptorSyncKind::All => WASI_RIGHT_FD_SYNC, + }, + operation, + ), + FilesystemOperation::DescriptorFileStat { fd } + | FilesystemOperation::DescriptorPath { fd, .. } + | FilesystemOperation::Extents { fd, .. } + | FilesystemOperation::ExtentAt { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_FILESTAT_GET, operation) + } + FilesystemOperation::SetDescriptorFlags { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_FDSTAT_SET_FLAGS, operation) + } + FilesystemOperation::SetLength { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_FILESTAT_SET_SIZE, operation) + } + FilesystemOperation::SetPathLength { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_SET_SIZE, + operation, + ), + FilesystemOperation::ReadDirectory { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_READDIR, operation) + } + FilesystemOperation::Stat { target, path } => match (target, path) { + (MetadataTarget::Descriptor(fd), _) => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_FILESTAT_GET, operation) + } + (MetadataTarget::Path { dir_fd, .. }, Some(path)) => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_GET, + operation, + ), + _ => Ok(()), + }, + FilesystemOperation::NodeStatAt { dir_fd, path } + | FilesystemOperation::NodeLstatAt { dir_fd, path } + | FilesystemOperation::AccessAt { dir_fd, path, .. } + | FilesystemOperation::FilesystemStatsAt { dir_fd, path } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_GET, + operation, + ), + FilesystemOperation::SetTimes { target, path, .. } => match (target, path) { + (MetadataTarget::Descriptor(fd), _) => require_descriptor_right( + kernel, + pid, + *fd, + WASI_RIGHT_FD_FILESTAT_SET_TIMES, + operation, + ), + (MetadataTarget::Path { dir_fd, .. }, Some(path)) => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_SET_TIMES, + operation, + ), + _ => Ok(()), + }, + FilesystemOperation::SetMode { target, path, .. } + | FilesystemOperation::SetOwner { target, path, .. } => match (target, path) { + (MetadataTarget::Descriptor(fd), _) => require_descriptor_right( + kernel, + pid, + *fd, + WASI_RIGHT_FD_FILESTAT_SET_TIMES, + operation, + ), + (MetadataTarget::Path { dir_fd, .. }, Some(path)) => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_SET_TIMES, + operation, + ), + _ => Ok(()), + }, + FilesystemOperation::SetAttributesAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_FILESTAT_SET_TIMES, + operation, + ), + FilesystemOperation::CreateDirectoryAt { dir_fd, path, .. } + | FilesystemOperation::CreateDirectoriesAt { dir_fd, path, .. } + | FilesystemOperation::MakeNodeAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_CREATE_DIRECTORY, + operation, + ), + FilesystemOperation::LinkAt { + old_dir_fd, + old_path, + new_dir_fd, + new_path, + .. + } => { + require_path_right( + kernel, + pid, + *old_dir_fd, + old_path, + WASI_RIGHT_PATH_LINK_SOURCE, + operation, + )?; + require_path_right( + kernel, + pid, + *new_dir_fd, + new_path, + WASI_RIGHT_PATH_LINK_TARGET, + operation, + ) + } + FilesystemOperation::LinkDescriptorAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_LINK_TARGET, + operation, + ), + FilesystemOperation::RenameAt { + old_dir_fd, + old_path, + new_dir_fd, + new_path, + .. + } => { + require_path_right( + kernel, + pid, + *old_dir_fd, + old_path, + WASI_RIGHT_PATH_RENAME_SOURCE, + operation, + )?; + require_path_right( + kernel, + pid, + *new_dir_fd, + new_path, + WASI_RIGHT_PATH_RENAME_TARGET, + operation, + ) + } + FilesystemOperation::SymlinkAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_SYMLINK, + operation, + ), + FilesystemOperation::ReadLinkAt { dir_fd, path, .. } => require_path_right( + kernel, + pid, + *dir_fd, + path, + WASI_RIGHT_PATH_READLINK, + operation, + ), + FilesystemOperation::UnlinkAt { + dir_fd, + path, + remove_directory, + } => require_path_right( + kernel, + pid, + *dir_fd, + path, + if *remove_directory { + WASI_RIGHT_PATH_REMOVE_DIRECTORY + } else { + WASI_RIGHT_PATH_UNLINK_FILE + }, + operation, + ), + FilesystemOperation::Range { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_ALLOCATE, operation) + } + FilesystemOperation::Xattr { + target, + path, + operation: xattr_operation, + .. + } => { + let mutation = matches!( + xattr_operation, + XattrOperation::Set { .. } | XattrOperation::Remove + ); + match (target, path) { + (MetadataTarget::Descriptor(fd), _) => require_descriptor_right( + kernel, + pid, + *fd, + if mutation { + WASI_RIGHT_FD_FILESTAT_SET_TIMES + } else { + WASI_RIGHT_FD_FILESTAT_GET + }, + operation, + ), + (MetadataTarget::Path { dir_fd, .. }, Some(path)) => require_path_right( + kernel, + pid, + *dir_fd, + path, + if mutation { + WASI_RIGHT_PATH_FILESTAT_SET_TIMES + } else { + WASI_RIGHT_PATH_FILESTAT_GET + }, + operation, + ), + _ => Ok(()), + } + } + FilesystemOperation::StdinRead { .. } => { + require_descriptor_right(kernel, pid, 0, WASI_RIGHT_FD_READ, operation) + } + FilesystemOperation::StdioWrite { fd, .. } => { + require_descriptor_right(kernel, pid, *fd, WASI_RIGHT_FD_WRITE, operation) + } + _ => Ok(()), + } +} + +pub(super) fn authorize_host_operation( + kernel: &SidecarKernel, + pid: u32, + operation: &HostOperation, +) -> Result<(), HostServiceError> { + if let HostOperation::Filesystem(operation) = operation { + authorize_filesystem_rights(kernel, pid, operation)?; + } + let tier = kernel + .process_permission_tier(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + if tier == ProcessPermissionTier::Full { + return Ok(()); + } + + match operation { + HostOperation::Filesystem(operation) => { + if matches!( + operation, + FilesystemOperation::Pipe + | FilesystemOperation::Duplicate { .. } + | FilesystemOperation::DuplicateTo { .. } + | FilesystemOperation::Remount { .. } + ) { + return Err(tier_denied("EACCES", tier, "filesystem", operation)); + } + if tier == ProcessPermissionTier::Isolated + && matches!( + operation, + FilesystemOperation::DuplicateMin { .. } + | FilesystemOperation::DescriptorFdFlags { .. } + | FilesystemOperation::SetDescriptorFdFlags { .. } + | FilesystemOperation::AdvisoryLock { .. } + | FilesystemOperation::RecordLock { .. } + | FilesystemOperation::CancelRecordLocks + ) + { + return Err(tier_denied("EACCES", tier, "filesystem", operation)); + } + if tier == ProcessPermissionTier::Isolated + && filesystem_operation_is_path_based(operation) + { + return Err(tier_denied("EACCES", tier, "filesystem", operation)); + } + if matches!( + tier, + ProcessPermissionTier::ReadOnly | ProcessPermissionTier::Isolated + ) { + let descriptor_file_write = match operation { + FilesystemOperation::Write { fd, .. } if *fd > 2 => { + descriptor_write_targets_file(kernel, pid, *fd)? + } + _ => false, + }; + if descriptor_file_write || filesystem_operation_is_read_only_mutation(operation) { + return Err(tier_denied("EROFS", tier, "filesystem", operation)); + } + } + if tier == ProcessPermissionTier::ReadWrite + && matches!( + operation, + FilesystemOperation::MakeNodeAt { .. } + | FilesystemOperation::LinkAt { .. } + | FilesystemOperation::LinkDescriptorAt { .. } + | FilesystemOperation::RenameAt { .. } + | FilesystemOperation::SymlinkAt { .. } + | FilesystemOperation::UnlinkAt { .. } + ) + { + return Err(tier_denied("EACCES", tier, "filesystem", operation)); + } + Ok(()) + } + HostOperation::Network( + NetworkOperation::KernelPoll { .. } | NetworkOperation::PosixPoll { .. }, + ) => Ok(()), + HostOperation::Network(operation) => Err(tier_denied("EACCES", tier, "network", operation)), + HostOperation::Process( + ProcessOperation::GetImage { .. } | ProcessOperation::SystemIdentity, + ) => Ok(()), + HostOperation::Process(ProcessOperation::GetResourceLimit { .. }) => Ok(()), + HostOperation::Process( + ProcessOperation::SetResourceLimit { .. } | ProcessOperation::Umask { .. }, + ) if tier != ProcessPermissionTier::Isolated => Ok(()), + HostOperation::Process(operation) => Err(tier_denied("EACCES", tier, "process", operation)), + HostOperation::Clock(ClockOperation::Time { .. } | ClockOperation::Resolution { .. }) => { + Ok(()) + } + HostOperation::Clock(operation) => Err(tier_denied("EACCES", tier, "clock", operation)), + HostOperation::Terminal(TerminalOperation::OpenPty) => Err(tier_denied( + "EACCES", + tier, + "terminal", + &TerminalOperation::OpenPty, + )), + HostOperation::Terminal(_) => Ok(()), + HostOperation::Signal( + SignalOperation::UpdateMask { + how: SignalMaskHow::Block, + set: SignalSetValue(0), + } + | SignalOperation::BeginDelivery + | SignalOperation::TakePublishedDelivery + | SignalOperation::EndDelivery { .. }, + ) => Ok(()), + HostOperation::Signal(operation) => Err(tier_denied("EACCES", tier, "signal", operation)), + HostOperation::Identity(_) | HostOperation::Entropy(_) => Ok(()), + _ => Err(HostServiceError::new( + "EACCES", + "process tier denies unknown host operation", + )), + } +} + +pub(super) fn decode_compatibility_host_call( + call: ExecutionHostCall, + full_filesystem: bool, + max_reply_bytes: usize, +) -> Result { + let remapped = remap_wasm_process_sync_rpc(&call.request)?; + let request = remapped.as_ref().unwrap_or(&call.request); + let Some(operation) = decode_host_operation(request, full_filesystem, max_reply_bytes)? else { + return Ok(ActiveExecutionEvent::HostRpcRequest(call)); + }; + Ok(ActiveExecutionEvent::Common(ExecutionEvent::HostCall { + operation, + reply: call.reply, + })) +} + +fn decode_host_operation( + request: &HostRpcRequest, + full_filesystem: bool, + max_reply_bytes: usize, +) -> Result, SidecarError> { + let operation = match request.method.as_str() { + "process.fd_preopens" => HostOperation::Filesystem(FilesystemOperation::Preopens), + "process.fd_close" => HostOperation::Filesystem(FilesystemOperation::Close { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_close fd")?, + }), + "process.fd_seek" => { + let raw_whence = javascript_sync_rpc_arg_u32(&request.args, 2, "fd_seek whence")?; + let whence = match raw_whence { + 0 => DescriptorWhence::Set, + 1 => DescriptorWhence::Current, + 2 => DescriptorWhence::End, + _ => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported fd_seek whence {raw_whence}"), + )); + } + }; + let offset = javascript_sync_rpc_arg_str(&request.args, 1, "fd_seek offset")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "fd_seek offset must be i64"))?; + HostOperation::Filesystem(FilesystemOperation::Seek { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "fd_seek fd")?, + offset, + whence, + }) + } + "process.fd_socketpair" => { + let raw_kind = javascript_sync_rpc_arg_u32(&request.args, 0, "socketpair kind")?; + let kind = match raw_kind { + 1 => SocketKind::Stream, + 2 => SocketKind::Datagram, + 3 => SocketKind::SeqPacket, + _ => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported socketpair kind {raw_kind}"), + )); + } + }; + HostOperation::Network(NetworkOperation::SocketPair { + kind, + nonblocking: javascript_sync_rpc_arg_bool( + &request.args, + 1, + "socketpair nonblocking", + )?, + close_on_exec: javascript_sync_rpc_arg_bool( + &request.args, + 2, + "socketpair close-on-exec", + )?, + }) + } + "process.hostnet_fd_open" => { + let (domain, kind_index, nonblocking_index, close_on_exec_index) = + if request.args.len() >= 4 { + let domain = match javascript_sync_rpc_arg_u32( + &request.args, + 0, + "host-network socket domain", + )? { + 1 => HostSocketDomain::Inet4, + 2 => HostSocketDomain::Inet6, + 3 => HostSocketDomain::Unix, + other => { + return Err(SidecarError::host( + "EAFNOSUPPORT", + format!("unsupported host-network socket domain {other}"), + )); + } + }; + (domain, 1, 2, 3) + } else { + // Compatibility with already-built V8 runners. The legacy + // request carried only a datagram boolean and had no + // executor-neutral address-family metadata. + (HostSocketDomain::Inet4, 0, 1, 2) + }; + let raw_kind = request.args.get(kind_index).ok_or_else(|| { + SidecarError::host("EINVAL", "host-network socket kind is required") + })?; + let kind = if request.args.len() >= 4 { + match raw_kind + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + { + Some(5) => SocketKind::Datagram, + Some(6) => SocketKind::Stream, + _ => { + return Err(SidecarError::host( + "EPROTONOSUPPORT", + "unsupported host-network socket kind", + )); + } + } + } else if raw_kind.as_bool().ok_or_else(|| { + SidecarError::host( + "EINVAL", + "legacy host-network datagram flag must be boolean", + ) + })? { + SocketKind::Datagram + } else { + SocketKind::Stream + }; + HostOperation::Network(NetworkOperation::Socket { + domain, + kind, + nonblocking: javascript_sync_rpc_arg_bool( + &request.args, + nonblocking_index, + "host-network nonblocking flag", + )?, + close_on_exec: javascript_sync_rpc_arg_bool( + &request.args, + close_on_exec_index, + "host-network close-on-exec flag", + )?, + }) + } + "process.hostnet_bind" => HostOperation::Network(NetworkOperation::Bind { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network bind fd")?, + address: decode_hostnet_socket_address( + request.args.get(1), + "host-network bind address", + )?, + }), + "process.hostnet_connect" => HostOperation::Network(NetworkOperation::Connect { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network connect fd")?, + address: decode_hostnet_socket_address( + request.args.get(1), + "host-network connect address", + )?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 2, + "host-network connect deadline", + )?, + }), + "process.hostnet_listen" => HostOperation::Network(NetworkOperation::Listen { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network listen fd")?, + backlog: javascript_sync_rpc_arg_u32(&request.args, 1, "host-network listen backlog")?, + }), + "process.hostnet_accept" => HostOperation::Network(NetworkOperation::Accept { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network accept fd")?, + nonblocking: javascript_sync_rpc_arg_bool( + &request.args, + 1, + "host-network accepted nonblocking flag", + )?, + close_on_exec: javascript_sync_rpc_arg_bool( + &request.args, + 2, + "host-network accepted close-on-exec flag", + )?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 3, + "host-network accept deadline", + )?, + }), + "process.hostnet_validate" => HostOperation::Network(NetworkOperation::Validate { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network validation fd")?, + requirement: if javascript_sync_rpc_arg_bool( + &request.args, + 1, + "host-network listening requirement", + )? { + SocketValidationRequirement::Listening + } else { + SocketValidationRequirement::Socket + }, + }), + "process.hostnet_recv" => { + let requested = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "host-network receive byte length", + )?) + .map_err(|_| SidecarError::host("E2BIG", "receive length exceeds usize"))?; + let limit = PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::Receive { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network receive fd")?, + max_bytes: BoundedUsize::try_new(requested, &limit).map_err(SidecarError::from)?, + flags: javascript_sync_rpc_arg_u32(&request.args, 2, "receive flags")?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 3, + "host-network receive deadline", + )?, + }) + } + "process.hostnet_send" => { + let bytes = + javascript_sync_rpc_request_bytes_arg(request, 1, "host-network send bytes")?; + let limit = PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::Send { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "host-network send fd")?, + bytes: BoundedBytes::try_new(bytes, &limit).map_err(SidecarError::from)?, + flags: javascript_sync_rpc_arg_u32(&request.args, 2, "send flags")?, + address: request + .args + .get(3) + .filter(|value| !value.is_null()) + .map(|value| decode_hostnet_socket_address(Some(value), "send address")) + .transpose()?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 4, + "host-network send deadline", + )?, + }) + } + "process.hostnet_local_address" => HostOperation::Network(NetworkOperation::LocalAddress { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "local-address fd")?, + }), + "process.hostnet_peer_address" => HostOperation::Network(NetworkOperation::PeerAddress { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "peer-address fd")?, + }), + "process.hostnet_get_option" => HostOperation::Network(NetworkOperation::GetOption { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "get-option fd")?, + name: decode_hostnet_socket_option(javascript_sync_rpc_arg_str( + &request.args, + 1, + "socket option", + )?)?, + }), + "process.hostnet_set_option" => HostOperation::Network(NetworkOperation::SetOption { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "set-option fd")?, + name: decode_hostnet_socket_option(javascript_sync_rpc_arg_str( + &request.args, + 1, + "socket option", + )?)?, + value: decode_hostnet_socket_option_value(request.args.get(2), "socket option value")?, + }), + "process.hostnet_poll" => { + let raw = request + .args + .first() + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::host("EINVAL", "host-network poll interests must be an array") + })?; + let interests = raw + .iter() + .map(|value| { + let entry = value.as_object().ok_or_else(|| { + SidecarError::host("EINVAL", "poll interest must be an object") + })?; + Ok(PollInterest { + fd: entry + .get("fd") + .and_then(Value::as_u64) + .and_then(|fd| u32::try_from(fd).ok()) + .ok_or_else(|| SidecarError::host("EINVAL", "poll fd must be u32"))?, + readable: entry + .get("readable") + .and_then(Value::as_bool) + .unwrap_or(false), + writable: entry + .get("writable") + .and_then(Value::as_bool) + .unwrap_or(false), + }) + }) + .collect::, SidecarError>>()?; + let limit = PayloadLimit::new("limits.kernel.maxPollDescriptors", 1024) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::Poll { + interests: BoundedVec::try_new(interests, &limit).map_err(SidecarError::from)?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "host-network poll deadline", + )?, + }) + } + "process.hostnet_tls_connect" => { + let name_limit = PayloadLimit::new("runtime.network.maxHostBytes", 253) + .map_err(SidecarError::from)?; + let alpn_limit = PayloadLimit::new("runtime.network.maxAlpnProtocols", 32) + .map_err(SidecarError::from)?; + let alpn_bytes_limit = PayloadLimit::new("runtime.network.maxAlpnProtocolBytes", 255) + .map_err(SidecarError::from)?; + let alpn = request + .args + .get(2) + .and_then(Value::as_array) + .map(|values| { + values + .iter() + .map(|value| { + BoundedBytes::try_new( + javascript_sync_rpc_bytes_arg( + std::slice::from_ref(value), + 0, + "ALPN protocol", + )?, + &alpn_bytes_limit, + ) + .map_err(SidecarError::from) + }) + .collect::, SidecarError>>() + }) + .transpose()? + .unwrap_or_default(); + HostOperation::Network(NetworkOperation::TlsConnect { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "TLS socket fd")?, + server_name: BoundedString::try_new( + javascript_sync_rpc_arg_str(&request.args, 1, "TLS server name")?.to_owned(), + &name_limit, + ) + .map_err(SidecarError::from)?, + alpn: BoundedVec::try_new(alpn, &alpn_limit).map_err(SidecarError::from)?, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 3, + "TLS connect deadline", + )?, + }) + } + "process.fd_socket_shutdown" => { + let how = match javascript_sync_rpc_arg_u32(&request.args, 1, "shutdown mode")? { + 0 => SocketShutdown::Read, + 1 => SocketShutdown::Write, + 2 => SocketShutdown::Both, + other => { + return Err(SidecarError::host( + "EINVAL", + format!("invalid shutdown mode {other}"), + )); + } + }; + HostOperation::Network(NetworkOperation::Shutdown { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "shutdown socket fd")?, + how, + }) + } + "process.fd_sendmsg_rights" => { + let raw_rights = request + .args + .get(2) + .and_then(Value::as_array) + .ok_or_else(|| SidecarError::host("EINVAL", "sendmsg rights must be an array"))?; + // Compatibility V8 host-network descriptions remain an explicit + // adapter projection. Plain kernel descriptor transfers are fully + // typed and are the contract used by engine-neutral executors. + if raw_rights.iter().any(|value| !value.is_u64()) { + return Ok(None); + } + let rights = raw_rights + .iter() + .map(|value| { + value + .as_u64() + .and_then(|fd| u32::try_from(fd).ok()) + .ok_or_else(|| SidecarError::host("EBADF", "sendmsg right must be u32")) + }) + .collect::, _>>()?; + let rights_limit = PayloadLimit::new("limits.network.maxScmRights", 253) + .map_err(SidecarError::from)?; + let bytes_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::SendDescriptorRights { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "sendmsg socket fd")?, + bytes: BoundedBytes::try_new( + javascript_sync_rpc_request_bytes_arg(request, 1, "sendmsg data")?, + &bytes_limit, + ) + .map_err(SidecarError::from)?, + rights: BoundedVec::try_new(rights, &rights_limit).map_err(SidecarError::from)?, + flags: javascript_sync_rpc_arg_u32_optional(&request.args, 3, "sendmsg flags")? + .unwrap_or_default(), + }) + } + "process.fd_recvmsg_rights" => { + let byte_limit = + PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + let rights_limit = PayloadLimit::new("limits.network.maxScmRights", 253) + .map_err(SidecarError::from)?; + let max_bytes = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "recvmsg maximum bytes", + )?) + .map_err(|_| SidecarError::host("E2BIG", "recvmsg byte limit exceeds usize"))?; + let max_rights = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 2, + "recvmsg maximum rights", + )?) + .map_err(|_| SidecarError::host("E2BIG", "recvmsg rights limit exceeds usize"))?; + HostOperation::Network(NetworkOperation::ReceiveDescriptorRights { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "recvmsg socket fd")?, + max_bytes: BoundedUsize::try_new(max_bytes, &byte_limit) + .map_err(SidecarError::from)?, + max_rights: BoundedUsize::try_new(max_rights, &rights_limit) + .map_err(SidecarError::from)?, + close_on_exec: javascript_sync_rpc_arg_bool( + &request.args, + 3, + "recvmsg close-on-exec", + )?, + peek: request + .args + .get(4) + .and_then(Value::as_bool) + .unwrap_or(false), + dontwait: request + .args + .get(5) + .and_then(Value::as_bool) + .unwrap_or(false), + waitall: request + .args + .get(6) + .and_then(Value::as_bool) + .unwrap_or(false), + }) + } + "__kernel_poll" => { + let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?; + let limit = PayloadLimit::new("limits.kernel.maxPollDescriptors", 1024) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::KernelPoll { + interests: BoundedVec::try_new( + fd_requests + .into_iter() + .map(|entry| KernelPollInterest { + fd: entry.fd, + events: entry.events, + }) + .collect(), + &limit, + ) + .map_err(SidecarError::from)?, + timeout_ms: (timeout_ms >= 0).then_some(timeout_ms as u64), + }) + } + "process.posix_poll" => { + let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?; + let limit = PayloadLimit::new("limits.kernel.maxPollDescriptors", 1024) + .map_err(SidecarError::from)?; + HostOperation::Network(NetworkOperation::PosixPoll { + interests: BoundedVec::try_new( + fd_requests + .into_iter() + .map(|entry| KernelPollInterest { + fd: entry.fd, + events: entry.events, + }) + .collect(), + &limit, + ) + .map_err(SidecarError::from)?, + timeout_ms: (timeout_ms >= 0).then_some(timeout_ms as u64), + signal_mask: request + .args + .get(2) + .filter(|value| !value.is_null()) + .map(|value| decode_signal_set(Some(value))) + .transpose()?, + }) + } + "dns.lookup" => { + let payload = request + .args + .first() + .and_then(Value::as_object) + .ok_or_else(|| { + SidecarError::host("EINVAL", "dns.lookup requires an object payload") + })?; + let hostname = payload + .get("hostname") + .and_then(Value::as_str) + .ok_or_else(|| SidecarError::host("EINVAL", "dns.lookup hostname is required"))?; + let family = match payload.get("family").and_then(Value::as_u64).unwrap_or(0) { + 0 => DnsAddressFamily::Any, + 4 => DnsAddressFamily::Inet4, + 6 => DnsAddressFamily::Inet6, + other => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported dns family {other}"), + )); + } + }; + HostOperation::Network(NetworkOperation::ResolveDns { + host: bounded_dns_value(hostname, "maxDnsNameBytes", 253)?, + port: None, + family, + max_results: BoundedUsize::try_new( + 64, + &PayloadLimit::new("runtime.network.maxDnsResults", 64) + .map_err(SidecarError::from)?, + ) + .map_err(SidecarError::from)?, + }) + } + "dns.resolveRawRr" => { + let payload = request + .args + .first() + .and_then(Value::as_object) + .ok_or_else(|| { + SidecarError::host("EINVAL", "dns.resolveRawRr requires an object payload") + })?; + let hostname = payload + .get("hostname") + .and_then(Value::as_str) + .ok_or_else(|| SidecarError::host("EINVAL", "DNS hostname is required"))?; + let record_type = payload.get("rrtype").and_then(Value::as_str).unwrap_or("A"); + HostOperation::Network(NetworkOperation::ResolveDnsRecord { + host: bounded_dns_value(hostname, "maxDnsNameBytes", 253)?, + record_type: bounded_dns_value(record_type, "maxDnsRecordTypeBytes", 16)?, + raw: true, + max_results: BoundedUsize::try_new( + 64, + &PayloadLimit::new("runtime.network.maxDnsResults", 64) + .map_err(SidecarError::from)?, + ) + .map_err(SidecarError::from)?, + }) + } + "child_process.spawn" => HostOperation::Process(ProcessOperation::Spawn( + decode_process_launch_request(&request.args, max_reply_bytes)?, + )), + "child_process.poll" => HostOperation::Process(ProcessOperation::PollChild { + child_id: bounded_child_process_id(javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.poll child id", + )?)?, + wait_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "child_process.poll wait ms", + )? + .unwrap_or_default(), + }), + "child_process.write_stdin" => { + let request_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + HostOperation::Process(ProcessOperation::WriteChildStdin { + child_id: bounded_child_process_id(javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.write_stdin child id", + )?)?, + chunk: BoundedBytes::try_new( + javascript_sync_rpc_request_bytes_arg( + request, + 1, + "child_process.write_stdin chunk", + )?, + &request_limit, + ) + .map_err(SidecarError::from)?, + }) + } + "child_process.close_stdin" => HostOperation::Process(ProcessOperation::CloseChildStdin { + child_id: bounded_child_process_id(javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.close_stdin child id", + )?)?, + }), + "process.exec" => HostOperation::Process(ProcessOperation::Exec( + decode_process_exec_request(&request.args, max_reply_bytes, false)?, + )), + "process.exec_fd_image_commit" => HostOperation::Process(ProcessOperation::Exec( + decode_process_exec_request(&request.args, max_reply_bytes, true)?, + )), + "process.exec_image_open" => { + let path_limit = PayloadLimit::new("runtime.filesystem.maxPathBytes", 4096) + .map_err(SidecarError::from)?; + HostOperation::Process(ProcessOperation::OpenExecutableImage { + source: ExecutableImageSource::Path( + BoundedString::try_new( + javascript_sync_rpc_arg_str( + &request.args, + 0, + "process.exec_image_open path", + )? + .to_owned(), + &path_limit, + ) + .map_err(SidecarError::from)?, + ), + }) + } + "process.exec_image_open_fd" => { + HostOperation::Process(ProcessOperation::OpenExecutableImage { + source: ExecutableImageSource::Descriptor(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "process.exec_image_open_fd fd", + )?), + }) + } + "process.exec_image_read" => { + let handle = + javascript_sync_rpc_arg_str(&request.args, 0, "process.exec_image_read handle")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "exec image handle must be u64"))?; + let offset = + javascript_sync_rpc_arg_str(&request.args, 1, "process.exec_image_read offset")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "exec image offset must be u64"))?; + let requested = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 2, + "process.exec_image_read byte length", + )?) + .map_err(|_| SidecarError::host("E2BIG", "exec image read length exceeds usize"))?; + let encoded_reply = requested + .checked_add(2) + .and_then(|value| value.checked_div(3)) + .and_then(|value| value.checked_mul(4)) + .and_then(|value| value.checked_add(37)) + .ok_or_else(|| { + SidecarError::host("E2BIG", "exec image encoded reply size overflows usize") + })?; + PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes) + .map_err(SidecarError::from)? + .admit(encoded_reply) + .map_err(SidecarError::from)?; + let raw_limit = PayloadLimit::new("maxExecImageReadBytes", max_reply_bytes) + .map_err(SidecarError::from)?; + HostOperation::Process(ProcessOperation::ReadExecutableImage { + handle, + offset, + max_bytes: BoundedUsize::try_new(requested, &raw_limit) + .map_err(SidecarError::from)?, + }) + } + "process.exec_image_close" => { + let handle = + javascript_sync_rpc_arg_str(&request.args, 0, "process.exec_image_close handle")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "exec image handle must be u64"))?; + HostOperation::Process(ProcessOperation::CloseExecutableImage { handle }) + } + "process.umask" => HostOperation::Process(ProcessOperation::Umask { + new_mask: javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?, + }), + "process.getrlimit" => { + let raw = javascript_sync_rpc_arg_u32(&request.args, 0, "process.getrlimit resource")?; + HostOperation::Process(ProcessOperation::GetResourceLimit { + kind: decode_resource_limit(raw)?, + }) + } + "process.setrlimit" => { + let raw = javascript_sync_rpc_arg_u32(&request.args, 0, "process.setrlimit resource")?; + let soft = decode_rlim(&request.args, 1, "process.setrlimit soft value")?; + let hard = decode_rlim(&request.args, 2, "process.setrlimit hard value")?; + HostOperation::Process(ProcessOperation::SetResourceLimit { + kind: decode_resource_limit(raw)?, + value: ResourceLimitValue { soft, hard }, + }) + } + "process.getpgid" => { + let pid = javascript_sync_rpc_arg_u32(&request.args, 0, "process getpgid pid")?; + HostOperation::Process(ProcessOperation::GetProcessGroup { + pid: (pid != 0).then_some(pid), + }) + } + "process.setpgid" => { + let pid = javascript_sync_rpc_arg_u32(&request.args, 0, "process setpgid pid")?; + let pgid = + javascript_sync_rpc_arg_u32(&request.args, 1, "process setpgid process group")?; + HostOperation::Process(ProcessOperation::SetProcessGroup { + pid: (pid != 0).then_some(pid), + pgid: (pgid != 0).then_some(pgid), + }) + } + "process.kill" => HostOperation::Process(ProcessOperation::Kill { + target: javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?, + signal: parse_signal(javascript_sync_rpc_arg_str( + &request.args, + 1, + "process.kill signal", + )?)?, + }), + "process.waitpid" => HostOperation::Process(ProcessOperation::Wait { + target: decode_wait_target(javascript_sync_rpc_arg_i32( + &request.args, + 0, + "waitpid selector", + )?)?, + options: decode_wait_options(javascript_sync_rpc_arg_u32( + &request.args, + 1, + "waitpid options", + )?)?, + deadline_ms: None, + temporary_mask: None, + }), + "process.waitpid_transition" => HostOperation::Process(ProcessOperation::WaitTransition { + target: decode_wait_target(javascript_sync_rpc_arg_i32( + &request.args, + 0, + "waitpid selector", + )?)?, + options: decode_wait_options(javascript_sync_rpc_arg_u32( + &request.args, + 1, + "waitpid options", + )?)?, + }), + "process.system_identity" => HostOperation::Process(ProcessOperation::SystemIdentity), + "process.image" => HostOperation::Process(ProcessOperation::GetImage { + max_reply_bytes: BoundedUsize::try_new( + max_reply_bytes, + &PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes) + .map_err(SidecarError::from)?, + ) + .map_err(SidecarError::from)?, + }), + "process.getuid" => identity_id(IdentityIdKind::RealUser), + "process.geteuid" => identity_id(IdentityIdKind::EffectiveUser), + "process.getgid" => identity_id(IdentityIdKind::RealGroup), + "process.getegid" => identity_id(IdentityIdKind::EffectiveGroup), + "process.getresuid" => HostOperation::Identity(IdentityOperation::GetUserIds), + "process.getresgid" => HostOperation::Identity(IdentityOperation::GetGroupIds), + "process.getgroups" => HostOperation::Identity(IdentityOperation::GetSupplementaryGroups), + "process.getpwuid" => HostOperation::Identity(IdentityOperation::PasswdById { + uid: javascript_sync_rpc_arg_u32(&request.args, 0, "passwd uid")?, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.getpwnam" => HostOperation::Identity(IdentityOperation::PasswdByName { + name: bounded_account_name(javascript_sync_rpc_arg_str( + &request.args, + 0, + "passwd name", + )?)?, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.getpwent" => HostOperation::Identity(IdentityOperation::NextPasswd { + index: javascript_sync_rpc_arg_u32(&request.args, 0, "passwd index")? as usize, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.getgrgid" => HostOperation::Identity(IdentityOperation::GroupById { + gid: javascript_sync_rpc_arg_u32(&request.args, 0, "group gid")?, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.getgrnam" => HostOperation::Identity(IdentityOperation::GroupByName { + name: bounded_account_name(javascript_sync_rpc_arg_str( + &request.args, + 0, + "group name", + )?)?, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.getgrent" => HostOperation::Identity(IdentityOperation::NextGroup { + index: javascript_sync_rpc_arg_u32(&request.args, 0, "group index")? as usize, + max_record_bytes: account_record_limit(max_reply_bytes)?, + }), + "process.setuid" => HostOperation::Identity(IdentityOperation::SetId { + kind: IdentityIdKind::RealUser, + value: Some(javascript_sync_rpc_arg_u32(&request.args, 0, "setuid uid")?), + }), + "process.seteuid" => HostOperation::Identity(IdentityOperation::SetId { + kind: IdentityIdKind::EffectiveUser, + value: Some(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "seteuid uid", + )?), + }), + "process.setreuid" => HostOperation::Identity(IdentityOperation::SetRealEffectiveUserIds { + real: optional_identity_id(&request.args, 0, "setreuid uid")?, + effective: optional_identity_id(&request.args, 1, "setreuid euid")?, + }), + "process.setresuid" => HostOperation::Identity(IdentityOperation::SetUserIds { + real: optional_identity_id(&request.args, 0, "setresuid uid")?, + effective: optional_identity_id(&request.args, 1, "setresuid euid")?, + saved: optional_identity_id(&request.args, 2, "setresuid suid")?, + }), + "process.setgid" => HostOperation::Identity(IdentityOperation::SetId { + kind: IdentityIdKind::RealGroup, + value: Some(javascript_sync_rpc_arg_u32(&request.args, 0, "setgid gid")?), + }), + "process.setegid" => HostOperation::Identity(IdentityOperation::SetId { + kind: IdentityIdKind::EffectiveGroup, + value: Some(javascript_sync_rpc_arg_u32( + &request.args, + 0, + "setegid gid", + )?), + }), + "process.setregid" => { + HostOperation::Identity(IdentityOperation::SetRealEffectiveGroupIds { + real: optional_identity_id(&request.args, 0, "setregid gid")?, + effective: optional_identity_id(&request.args, 1, "setregid egid")?, + }) + } + "process.setresgid" => HostOperation::Identity(IdentityOperation::SetGroupIds { + real: optional_identity_id(&request.args, 0, "setresgid gid")?, + effective: optional_identity_id(&request.args, 1, "setresgid egid")?, + saved: optional_identity_id(&request.args, 2, "setresgid sgid")?, + }), + "process.setgroups" => { + let values = request + .args + .first() + .and_then(Value::as_array) + .ok_or_else(|| SidecarError::host("EINVAL", "setgroups requires an array"))?; + if values.len() > MAX_SUPPLEMENTARY_GROUPS { + return Err(payload_limit_error( + "limits.process.maxSupplementaryGroups", + MAX_SUPPLEMENTARY_GROUPS, + values.len(), + )); + } + let groups = values + .iter() + .map(|value| { + value + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| { + SidecarError::host("EINVAL", "setgroups entries must be u32") + }) + }) + .collect::, _>>()?; + let limit = PayloadLimit::new( + "limits.process.maxSupplementaryGroups", + MAX_SUPPLEMENTARY_GROUPS, + ) + .map_err(SidecarError::from)?; + HostOperation::Identity(IdentityOperation::SetSupplementaryGroups { + groups: BoundedVec::try_new(groups, &limit).map_err(SidecarError::from)?, + }) + } + "process.clock_time" => { + let clock = decode_clock(javascript_sync_rpc_arg_u32(&request.args, 0, "clock id")?)?; + let precision_ns = request + .args + .get(1) + .and_then(Value::as_str) + .map(|value| value.parse::()) + .transpose() + .map_err(|_| SidecarError::host("EINVAL", "clock precision must be u64"))? + .unwrap_or_default(); + let deterministic_realtime_ns = if clock == GuestClockId::Realtime { + request + .args + .get(2) + .and_then(Value::as_str) + .map(str::parse::) + .transpose() + .map_err(|_| { + SidecarError::host( + "EINVAL", + "deterministic realtime must be u64 nanoseconds", + ) + })? + } else { + None + }; + HostOperation::Clock(ClockOperation::Time { + clock, + precision_ns, + deterministic_realtime_ns, + }) + } + "process.clock_resolution" => HostOperation::Clock(ClockOperation::Resolution { + clock: decode_clock(javascript_sync_rpc_arg_u32(&request.args, 0, "clock id")?)?, + }), + "process.sleep" => HostOperation::Clock(ClockOperation::Sleep { + duration_ms: javascript_sync_rpc_arg_u64( + &request.args, + 0, + "sleep duration milliseconds", + )?, + }), + "process.itimer_real" => { + match javascript_sync_rpc_arg_u32(&request.args, 0, "ITIMER_REAL operation")? { + 0 => HostOperation::Clock(ClockOperation::RealIntervalGet), + 1 => HostOperation::Clock(ClockOperation::RealIntervalSet { + initial_us: javascript_sync_rpc_arg_u64( + &request.args, + 1, + "ITIMER_REAL value microseconds", + )?, + interval_us: javascript_sync_rpc_arg_u64( + &request.args, + 2, + "ITIMER_REAL interval microseconds", + )?, + }), + other => { + return Err(SidecarError::host( + "EINVAL", + format!("invalid ITIMER_REAL operation {other}"), + )); + } + } + } + "process.take_signal" => HostOperation::Signal(SignalOperation::TakePublishedDelivery), + "process.signal_begin" => HostOperation::Signal(SignalOperation::BeginDelivery), + "process.signal_end" => HostOperation::Signal(SignalOperation::EndDelivery { + token: javascript_sync_rpc_arg_u64(&request.args, 0, "signal token")?, + }), + "process.signal_state" => { + let mask_json = + javascript_sync_rpc_arg_str(&request.args, 2, "process.signal_state mask")?; + if mask_json.len() > MAX_SIGNAL_STATE_MASK_JSON_BYTES { + return Err(payload_limit_error( + "maxSignalStateMaskJsonBytes", + MAX_SIGNAL_STATE_MASK_JSON_BYTES, + mask_json.len(), + )); + } + let (signal, registration) = + parse_process_signal_state_request(&request.args).map_err(SidecarError::from)?; + if registration.mask.len() > MAX_SIGNAL_SET_ENTRIES { + return Err(payload_limit_error( + "maxSignalSetEntries", + MAX_SIGNAL_SET_ENTRIES, + registration.mask.len(), + )); + } + HostOperation::Signal(SignalOperation::SetAction { + signal: i32::try_from(signal).map_err(|_| { + SidecarError::host("EINVAL", "process.signal_state signal exceeds i32") + })?, + action: SignalActionValue { + disposition: match registration.action { + SignalDispositionAction::Default => SignalDispositionValue::Default, + SignalDispositionAction::Ignore => SignalDispositionValue::Ignore, + SignalDispositionAction::User => SignalDispositionValue::User, + }, + flags: registration.flags, + mask: signal_set_from_u32(registration.mask)?, + }, + }) + } + "process.signal_mask" => { + let raw_how = javascript_sync_rpc_arg_u32(&request.args, 0, "signal-mask operation")?; + let set = decode_signal_set(request.args.get(1))?; + let how = match raw_how { + 0 => SignalMaskHow::Block, + 1 => SignalMaskHow::Unblock, + 2 => SignalMaskHow::Set, + // The compatibility query convention is a no-op block. + 3 if set.0 == 0 => SignalMaskHow::Block, + _ => { + return Err(SidecarError::host( + "EINVAL", + format!("invalid signal-mask operation {raw_how}"), + )); + } + }; + HostOperation::Signal(SignalOperation::UpdateMask { how, set }) + } + "process.signal_mask_scope_begin" => { + HostOperation::Signal(SignalOperation::BeginTemporaryMask { + mask: decode_signal_set(request.args.first())?, + }) + } + "process.signal_mask_scope_end" => { + HostOperation::Signal(SignalOperation::EndTemporaryMask { + token: javascript_sync_rpc_arg_u64( + &request.args, + 0, + "temporary signal-mask token", + )?, + }) + } + "process.random_get" => { + let requested = + javascript_sync_rpc_arg_u64(&request.args, 0, "process.random_get byte length")?; + let requested = usize::try_from(requested).map_err(|_| { + SidecarError::host("E2BIG", "process.random_get byte length exceeds usize") + })?; + let maximum = MAX_ENTROPY_CHUNK_BYTES.min(max_reply_bytes); + let limit = + PayloadLimit::new("maxEntropyChunkBytes", maximum).map_err(SidecarError::from)?; + HostOperation::Entropy(EntropyOperation { + length: BoundedUsize::try_new(requested, &limit).map_err(SidecarError::from)?, + }) + } + "__kernel_isatty" => HostOperation::Terminal(TerminalOperation::IsTerminal { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_isatty fd")?, + }), + "__kernel_tty_size" => HostOperation::Terminal(TerminalOperation::GetWindowSize { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_size fd")?, + }), + "__kernel_tty_set_size" => { + let columns = u16::try_from(javascript_sync_rpc_arg_u32( + &request.args, + 1, + "__kernel_tty_set_size cols", + )?) + .map_err(|_| SidecarError::host("EINVAL", "TTY columns exceed u16"))?; + let rows = u16::try_from(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "__kernel_tty_set_size rows", + )?) + .map_err(|_| SidecarError::host("EINVAL", "TTY rows exceed u16"))?; + HostOperation::Terminal(TerminalOperation::SetWindowSize { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_set_size fd")?, + size: TerminalWindowSize { + rows, + columns, + x_pixels: 0, + y_pixels: 0, + }, + }) + } + "__kernel_tcgetattr" => HostOperation::Terminal(TerminalOperation::GetAttributes { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetattr fd")?, + }), + "__kernel_tcsetattr" => { + let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "__kernel_tcsetattr flags")?; + let cc = request + .args + .get(2) + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::host("EINVAL", "TTY control characters must be an array") + })?; + if cc.len() != 7 { + return Err(SidecarError::host( + "EINVAL", + format!( + "TTY control character array must contain 7 bytes, observed {}", + cc.len() + ), + )); + } + let mut control_characters = [0_u8; 32]; + for (index, value) in cc.iter().enumerate() { + control_characters[index] = value + .as_u64() + .and_then(|value| u8::try_from(value).ok()) + .ok_or_else(|| { + SidecarError::host("EINVAL", "TTY control character must be a byte") + })?; + } + HostOperation::Terminal(TerminalOperation::SetAttributes { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcsetattr fd")?, + attributes: TerminalAttributes { + input_flags: flags & (1 << 0), + output_flags: flags & ((1 << 1) | (1 << 2)), + control_flags: 0, + local_flags: flags & ((1 << 3) | (1 << 4) | (1 << 5)), + line_discipline: 0, + control_characters, + input_speed: 0, + output_speed: 0, + }, + }) + } + "__kernel_tcgetpgrp" => { + HostOperation::Terminal(TerminalOperation::GetForegroundProcessGroup { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetpgrp fd")?, + }) + } + "__kernel_tcsetpgrp" => { + HostOperation::Terminal(TerminalOperation::SetForegroundProcessGroup { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcsetpgrp fd")?, + pgid: javascript_sync_rpc_arg_u32(&request.args, 1, "__kernel_tcsetpgrp pgid")?, + }) + } + "__kernel_tcgetsid" => HostOperation::Terminal(TerminalOperation::GetSession { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetsid fd")?, + }), + "__pty_set_raw_mode" => HostOperation::Terminal(TerminalOperation::SetRawMode { + fd: 0, + enabled: javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?, + }), + "process.pty_open" => HostOperation::Terminal(TerminalOperation::OpenPty), + _ if full_filesystem => { + if let Some(operation) = filesystem::decode(request, full_filesystem, max_reply_bytes)? + { + HostOperation::Filesystem(operation) + } else if let Some(operation) = + network_compat::decode_managed(request, max_reply_bytes)? + { + HostOperation::Network(operation) + } else { + return Ok(None); + } + } + _ => return Ok(None), + }; + Ok(Some(operation)) +} + +fn identity_id(kind: IdentityIdKind) -> HostOperation { + HostOperation::Identity(IdentityOperation::GetId { kind }) +} + +fn optional_identity_id( + args: &[Value], + index: usize, + label: &str, +) -> Result, SidecarError> { + if args.get(index).is_some_and(Value::is_null) { + return Ok(None); + } + let value = javascript_sync_rpc_arg_u32(args, index, label)?; + Ok((value != u32::MAX).then_some(value)) +} + +fn account_record_limit(max_reply_bytes: usize) -> Result { + let maximum = max_reply_bytes.min(MAX_ACCOUNT_RECORD_BYTES); + let limit = PayloadLimit::new("maxAccountRecordBytes", maximum).map_err(SidecarError::from)?; + BoundedUsize::try_new(maximum, &limit).map_err(SidecarError::from) +} + +fn bounded_account_name(name: &str) -> Result { + let limit = PayloadLimit::new("maxAccountNameBytes", MAX_ACCOUNT_NAME_BYTES) + .map_err(SidecarError::from)?; + BoundedString::try_new(name.to_owned(), &limit).map_err(SidecarError::from) +} + +fn bounded_child_process_id(value: &str) -> Result { + let limit = PayloadLimit::new("maxChildProcessIdBytes", MAX_CHILD_PROCESS_ID_BYTES) + .map_err(SidecarError::from)?; + BoundedString::try_new(value.to_owned(), &limit).map_err(SidecarError::from) +} + +#[derive(Debug, serde::Deserialize)] +struct LegacyProcessLaunchOptions { + #[serde(flatten)] + options: ProcessLaunchOptions, + #[serde(default, rename = "maxBuffer")] + _max_buffer: Option, +} + +fn decode_process_launch_request( + args: &[Value], + max_request_bytes: usize, +) -> Result { + let request_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_request_bytes) + .map_err(SidecarError::from)?; + request_limit.admit_json(args).map_err(SidecarError::from)?; + + if let Some(value) = args.first().cloned() { + if let Ok(request) = serde_json::from_value::(value) { + return BoundedProcessLaunchRequest::try_new(request, &request_limit) + .map_err(SidecarError::from); + } + } + + let command = javascript_sync_rpc_arg_str(args, 0, "process launch command")?.to_owned(); + let raw_args = javascript_sync_rpc_arg_str(args, 1, "process launch args")?; + let raw_options = javascript_sync_rpc_arg_str(args, 2, "process launch options")?; + let parsed_args = serde_json::from_str::>(raw_args).map_err(|error| { + SidecarError::host( + "EINVAL", + format!("invalid process launch args payload: {error}"), + ) + })?; + let parsed_options = + serde_json::from_str::(raw_options).map_err(|error| { + SidecarError::host( + "EINVAL", + format!("invalid process launch options payload: {error}"), + ) + })?; + BoundedProcessLaunchRequest::try_new( + ProcessLaunchRequest { + command, + args: parsed_args, + options: parsed_options.options, + }, + &request_limit, + ) + .map_err(SidecarError::from) +} + +fn decode_process_exec_request( + args: &[Value], + max_request_bytes: usize, + fd_image_commit: bool, +) -> Result { + let request = decode_process_launch_request(args, max_request_bytes)?; + let has_executable_fd = request.as_request().options.executable_fd.is_some(); + if has_executable_fd != fd_image_commit { + return Err(SidecarError::host( + "EINVAL", + if fd_image_commit { + "process.exec_fd_image_commit requires executableFd" + } else { + "executableFd is only valid for process.exec_fd_image_commit" + }, + )); + } + if fd_image_commit { + validate_wasm_fd_image_commit_request(request.as_request())?; + } + Ok(request) +} + +fn bounded_dns_value( + value: &str, + limit_name: &'static str, + maximum: usize, +) -> Result { + let limit = PayloadLimit::new(limit_name, maximum).map_err(SidecarError::from)?; + BoundedString::try_new(value.to_owned(), &limit).map_err(SidecarError::from) +} + +fn decode_hostnet_socket_address( + value: Option<&Value>, + label: &str, +) -> Result { + let value = value + .and_then(Value::as_object) + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} must be an object")))?; + let path_limit = + PayloadLimit::new("runtime.filesystem.maxPathBytes", 4096).map_err(SidecarError::from)?; + let host_limit = + PayloadLimit::new("runtime.network.maxHostBytes", 253).map_err(SidecarError::from)?; + match value.get("type").and_then(Value::as_str) { + Some("inet") => Ok(SocketAddress::Inet { + host: BoundedString::try_new( + value + .get("host") + .and_then(Value::as_str) + .ok_or_else(|| { + SidecarError::host("EINVAL", format!("{label} host is required")) + })? + .to_owned(), + &host_limit, + ) + .map_err(SidecarError::from)?, + port: value + .get("port") + .and_then(Value::as_u64) + .and_then(|port| u16::try_from(port).ok()) + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} port must be u16")))?, + }), + Some("unix-path") => Ok(SocketAddress::UnixPath( + BoundedString::try_new( + value + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| { + SidecarError::host("EINVAL", format!("{label} path is required")) + })? + .to_owned(), + &path_limit, + ) + .map_err(SidecarError::from)?, + )), + Some("unix-abstract") => { + let bytes = value + .get("hex") + .and_then(Value::as_str) + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} hex is required")))?; + if !bytes.len().is_multiple_of(2) { + return Err(SidecarError::host( + "EINVAL", + format!("{label} hex is invalid"), + )); + } + let decoded = bytes + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).map_err(|_| { + SidecarError::host("EINVAL", format!("{label} hex is invalid")) + })?; + u8::from_str_radix(pair, 16).map_err(|_| { + SidecarError::host("EINVAL", format!("{label} hex is invalid")) + }) + }) + .collect::, _>>()?; + Ok(SocketAddress::UnixAbstract( + BoundedBytes::try_new(decoded, &path_limit).map_err(SidecarError::from)?, + )) + } + Some("unix-autobind") => Ok(SocketAddress::UnixAutobind), + _ => Err(SidecarError::host( + "EINVAL", + format!("{label} has an unsupported type"), + )), + } +} + +fn decode_hostnet_socket_option(value: &str) -> Result { + match value { + "error" => Ok(SocketOptionName::Error), + "reuse-address" => Ok(SocketOptionName::ReuseAddress), + "reuse-port" => Ok(SocketOptionName::ReusePort), + "keep-alive" => Ok(SocketOptionName::KeepAlive), + "no-delay" => Ok(SocketOptionName::NoDelay), + "broadcast" => Ok(SocketOptionName::Broadcast), + "receive-buffer" => Ok(SocketOptionName::ReceiveBuffer), + "send-buffer" => Ok(SocketOptionName::SendBuffer), + "linger" => Ok(SocketOptionName::Linger), + "receive-timeout" => Ok(SocketOptionName::ReceiveTimeout), + "send-timeout" => Ok(SocketOptionName::SendTimeout), + "ipv6-only" => Ok(SocketOptionName::Ipv6Only), + "multicast-ttl" => Ok(SocketOptionName::MulticastTtl), + "multicast-loop" => Ok(SocketOptionName::MulticastLoop), + _ => Err(SidecarError::host( + "ENOPROTOOPT", + format!("unsupported socket option {value}"), + )), + } +} + +fn decode_hostnet_socket_option_value( + value: Option<&Value>, + label: &str, +) -> Result { + let value = + value.ok_or_else(|| SidecarError::host("EINVAL", format!("{label} is required")))?; + if let Some(value) = value.as_bool() { + return Ok(SocketOptionValue::Bool(value)); + } + if let Some(value) = value.as_i64() { + return Ok(SocketOptionValue::Integer(value)); + } + if value.is_null() { + return Ok(SocketOptionValue::DurationMs(None)); + } + let object = value + .as_object() + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} is invalid")))?; + if let Some(duration) = object.get("durationMs") { + return Ok(SocketOptionValue::DurationMs(if duration.is_null() { + None + } else { + Some(duration.as_u64().ok_or_else(|| { + SidecarError::host("EINVAL", format!("{label} durationMs must be u64")) + })?) + })); + } + if let Some(enabled) = object.get("enabled").and_then(Value::as_bool) { + let seconds = object + .get("seconds") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} seconds must be u32")))?; + return Ok(SocketOptionValue::Linger { enabled, seconds }); + } + Err(SidecarError::host("EINVAL", format!("{label} is invalid"))) +} + +fn payload_limit_error(limit_name: &'static str, limit: usize, requested: usize) -> SidecarError { + HostServiceError::new("E2BIG", format!("request exceeds {limit_name} ({limit})")) + .with_details(json!({ + "limitName": limit_name, + "limit": limit, + "requested": requested, + })) + .into() +} + +fn decode_clock(raw: u32) -> Result { + match raw { + 0 => Ok(GuestClockId::Realtime), + 1 => Ok(GuestClockId::Monotonic), + 2 => Ok(GuestClockId::ProcessCpu), + 3 => Ok(GuestClockId::ThreadCpu), + _ => Err(SidecarError::host( + "EINVAL", + format!("unsupported clock id {raw}"), + )), + } +} + +fn decode_resource_limit(raw: u32) -> Result { + match raw { + 0 => Ok(ResourceLimitKind::Cpu), + 1 => Ok(ResourceLimitKind::FileSize), + 2 => Ok(ResourceLimitKind::Data), + 3 => Ok(ResourceLimitKind::Stack), + 4 => Ok(ResourceLimitKind::Core), + 5 => Ok(ResourceLimitKind::ResidentSet), + 6 => Ok(ResourceLimitKind::Processes), + 7 => Ok(ResourceLimitKind::OpenFiles), + 8 => Ok(ResourceLimitKind::LockedMemory), + 9 => Ok(ResourceLimitKind::AddressSpace), + _ => Err(SidecarError::host( + "EINVAL", + format!("unsupported resource limit {raw}"), + )), + } +} + +fn decode_wait_target(selector: i32) -> Result { + match selector { + -1 => Ok(WaitTarget::Any), + 0 => Ok(WaitTarget::ProcessGroup(0)), + selector if selector > 0 => Ok(WaitTarget::Pid(selector as u32)), + selector => selector + .checked_abs() + .and_then(|value| u32::try_from(value).ok()) + .map(WaitTarget::ProcessGroup) + .ok_or_else(|| SidecarError::host("EINVAL", "invalid waitpid selector")), + } +} + +fn decode_wait_options(options: u32) -> Result { + let invalid = options & !(1 | 2 | 8); + if invalid != 0 { + return Err(SidecarError::host( + "EINVAL", + format!("invalid waitpid option bits {invalid:#x}"), + )); + } + Ok(options) +} + +fn decode_rlim(args: &[Value], index: usize, label: &str) -> Result, SidecarError> { + let Some(value) = args.get(index) else { + return Err(SidecarError::host("EINVAL", format!("{label} is required"))); + }; + let value = if let Some(text) = value.as_str() { + text.parse::() + .map_err(|_| SidecarError::host("EINVAL", format!("{label} must be u64")))? + } else { + javascript_sync_rpc_arg_u64(args, index, label)? + }; + Ok((value != u64::MAX).then_some(value)) +} + +fn decode_signal_set(value: Option<&Value>) -> Result { + let signals = value + .and_then(Value::as_array) + .ok_or_else(|| SidecarError::host("EINVAL", "signal-mask set must be an array"))?; + if signals.len() > MAX_SIGNAL_SET_ENTRIES { + return Err(payload_limit_error( + "maxSignalSetEntries", + MAX_SIGNAL_SET_ENTRIES, + signals.len(), + )); + } + let signals = signals + .iter() + .map(|signal| { + signal + .as_i64() + .and_then(|signal| u32::try_from(signal).ok()) + .ok_or_else(|| { + SidecarError::host( + "EINVAL", + "signal-mask entries must be integers between 1 and 64", + ) + }) + }) + .collect::, _>>()?; + signal_set_from_u32(signals) +} + +fn signal_set_from_u32( + signals: impl IntoIterator, +) -> Result { + let mut bits = 0_u64; + for signal in signals { + if !(1..=64).contains(&signal) { + return Err(SidecarError::host( + "EINVAL", + "signal-mask entries must be integers between 1 and 64", + )); + } + bits |= 1_u64 << (signal - 1); + } + Ok(SignalSetValue(bits)) +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(super) struct HostOperationEffects { + pub(super) may_make_fd_readable: bool, + pub(super) may_make_fd_writable: bool, +} + +pub(super) fn dispatch_host_operation( + generation: u64, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: HostOperation, + reply: DirectHostReplyHandle, +) -> Result { + let identity = reply.identity(); + if identity.generation != generation || identity.pid != process.kernel_pid { + reply + .fail( + HostServiceError::new( + "ESTALE", + "host call identity does not match the active kernel process", + ) + .with_details(json!({ + "expectedGeneration": generation, + "expectedPid": process.kernel_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) + .map_err(SidecarError::from)?; + return Ok(HostOperationEffects::default()); + } + if let Err(error) = authorize_host_operation(kernel, process.kernel_pid, &operation) { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(HostOperationEffects::default()); + } + if requires_context_host_dispatch(&operation) { + reply + .fail(HostServiceError::new( + "ERR_AGENTOS_CONTEXT_DISPATCH_REQUIRED", + "VM-scoped host operation bypassed context dispatch", + )) + .map_err(SidecarError::from)?; + return Ok(HostOperationEffects::default()); + } + + let effects = host_operation_effects(&operation); + let result = match operation { + HostOperation::Filesystem(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Network(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Process(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Terminal(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Signal(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Identity(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Clock(operation) => { + execute::(kernel, process, operation, &reply) + } + HostOperation::Entropy(operation) => { + execute::(kernel, process, operation, &reply) + } + other => Err(unsupported("host", other)), + }; + match result { + Ok(Some(response)) => { + reply.succeed(response).map_err(SidecarError::from)?; + Ok(effects) + } + Ok(None) => Ok(HostOperationEffects::default()), + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + Ok(HostOperationEffects::default()) + } + } +} + +/// VM-scoped operations must run before the kernel/process-only fallback. +/// Keeping the classifier next to both dispatchers prevents a new executor or +/// event pump from silently losing DNS, reactor, wait, or process context. +pub(super) fn requires_context_host_dispatch(operation: &HostOperation) -> bool { + matches!( + operation, + HostOperation::Filesystem( + FilesystemOperation::Close { .. } + | FilesystemOperation::CloseFrom { .. } + | FilesystemOperation::Renumber { .. } + | FilesystemOperation::DuplicateTo { .. } + | FilesystemOperation::Move { .. } + | FilesystemOperation::Read { + offset: None, + deadline_ms: Some(_), + .. + } + | FilesystemOperation::StdinRead { .. }, + ) | HostOperation::Network( + NetworkOperation::HttpRequest { .. } + | NetworkOperation::ResolveDns { .. } + | NetworkOperation::ResolveDnsRecord { .. } + | NetworkOperation::Socket { .. } + | NetworkOperation::Bind { .. } + | NetworkOperation::Connect { .. } + | NetworkOperation::Listen { .. } + | NetworkOperation::Accept { .. } + | NetworkOperation::Validate { .. } + | NetworkOperation::Receive { .. } + | NetworkOperation::Send { .. } + | NetworkOperation::LocalAddress { .. } + | NetworkOperation::PeerAddress { .. } + | NetworkOperation::GetOption { .. } + | NetworkOperation::SetOption { .. } + | NetworkOperation::Poll { .. } + | NetworkOperation::TlsConnect { .. } + | NetworkOperation::KernelPoll { .. } + | NetworkOperation::PosixPoll { .. } + | NetworkOperation::ManagedUdpPoll { .. } + | NetworkOperation::ManagedBindUnix { .. } + | NetworkOperation::ManagedBindConnectedUnix { .. } + | NetworkOperation::ManagedReserveTcpPort { .. } + | NetworkOperation::ManagedReleaseTcpPort { .. } + | NetworkOperation::ManagedConnect { .. } + | NetworkOperation::ManagedListen { .. } + | NetworkOperation::ManagedPoll { .. } + | NetworkOperation::ManagedWaitConnect { .. } + | NetworkOperation::ManagedRead { .. } + | NetworkOperation::ManagedWrite { .. } + | NetworkOperation::ManagedDestroy { .. } + | NetworkOperation::ManagedAccept { .. } + | NetworkOperation::ManagedCloseListener { .. } + | NetworkOperation::ManagedTlsUpgrade { .. } + | NetworkOperation::ManagedUdpCreate { .. } + | NetworkOperation::ManagedUdpBind { .. } + | NetworkOperation::ManagedUdpSend { .. } + | NetworkOperation::ManagedUdpClose { .. } + | NetworkOperation::SendDescriptorRights { .. } + | NetworkOperation::ReceiveDescriptorRights { .. } + ) | HostOperation::Process( + ProcessOperation::Spawn(_) + | ProcessOperation::RunCaptured { .. } + | ProcessOperation::Exec(_) + | ProcessOperation::PollChild { .. } + | ProcessOperation::WriteChildStdin { .. } + | ProcessOperation::CloseChildStdin { .. } + | ProcessOperation::Wait { .. } + ) | HostOperation::Clock(ClockOperation::Sleep { .. }) + ) +} + +/// Dispatch operations that need VM-scoped sidecar capabilities in addition +/// to the kernel/process pair. Waiting variants use this seam as well: they +/// retain only owned operation data and the direct reply capability. +pub(super) async fn dispatch_context_host_operation( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: HostOperation, + reply: DirectHostReplyHandle, +) -> Result, SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(None); + } + let vm = sidecar + .vms + .get(vm_id) + .expect("validated host-call VM remains registered"); + if let Err(error) = authorize_host_operation(&vm.kernel, reply.identity().pid, &operation) { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(None); + } + match operation { + HostOperation::Network(operation @ NetworkOperation::HttpRequest { .. }) => { + dispatch_context_http_operation(sidecar, vm_id, process_id, operation, reply)?; + Ok(None) + } + HostOperation::Filesystem(FilesystemOperation::Close { fd }) => { + network_compat::dispatch_context_close_with_managed_retirement( + sidecar, vm_id, process_id, fd, reply, + )?; + Ok(None) + } + HostOperation::Filesystem(FilesystemOperation::CloseFrom { min_fd, exact_fds }) => { + network_compat::dispatch_context_closefrom_with_managed_retirement( + sidecar, vm_id, process_id, min_fd, exact_fds, reply, + )?; + Ok(None) + } + HostOperation::Filesystem( + operation @ (FilesystemOperation::Renumber { .. } + | FilesystemOperation::DuplicateTo { .. } + | FilesystemOperation::Move { .. }), + ) => { + network_compat::dispatch_context_descriptor_replacement_with_managed_retirement( + sidecar, vm_id, process_id, operation, reply, + )?; + Ok(None) + } + HostOperation::Filesystem( + operation @ (FilesystemOperation::Read { + offset: None, + deadline_ms: Some(_), + .. + } + | FilesystemOperation::StdinRead { .. }), + ) => { + filesystem::dispatch_context_deferred_kernel_read( + sidecar, vm_id, process_id, operation, reply, + )?; + Ok(None) + } + HostOperation::Network( + operation @ (NetworkOperation::ResolveDns { .. } + | NetworkOperation::ResolveDnsRecord { .. }), + ) => { + dispatch_context_dns_operation(sidecar, vm_id, process_id, operation, reply)?; + Ok(None) + } + HostOperation::Network(operation @ NetworkOperation::KernelPoll { .. }) => { + network::dispatch_context_kernel_poll(sidecar, vm_id, process_id, operation, reply)?; + Ok(None) + } + HostOperation::Network(operation @ NetworkOperation::PosixPoll { .. }) => { + network_compat::dispatch_context_posix_poll( + sidecar, vm_id, process_id, operation, reply, + )?; + Ok(None) + } + HostOperation::Network(operation @ NetworkOperation::ManagedUdpPoll { .. }) => { + network_compat::dispatch_context_udp_poll( + sidecar, vm_id, process_id, operation, reply, + )?; + Ok(None) + } + HostOperation::Network( + operation @ (NetworkOperation::Socket { .. } + | NetworkOperation::Bind { .. } + | NetworkOperation::Connect { .. } + | NetworkOperation::Listen { .. } + | NetworkOperation::Accept { .. } + | NetworkOperation::Validate { .. } + | NetworkOperation::Receive { .. } + | NetworkOperation::Send { .. } + | NetworkOperation::LocalAddress { .. } + | NetworkOperation::PeerAddress { .. } + | NetworkOperation::GetOption { .. } + | NetworkOperation::SetOption { .. } + | NetworkOperation::Poll { .. } + | NetworkOperation::TlsConnect { .. } + | NetworkOperation::ManagedBindUnix { .. } + | NetworkOperation::ManagedBindConnectedUnix { .. } + | NetworkOperation::ManagedReserveTcpPort { .. } + | NetworkOperation::ManagedReleaseTcpPort { .. } + | NetworkOperation::ManagedConnect { .. } + | NetworkOperation::ManagedListen { .. } + | NetworkOperation::ManagedPoll { .. } + | NetworkOperation::ManagedWaitConnect { .. } + | NetworkOperation::ManagedRead { .. } + | NetworkOperation::ManagedWrite { .. } + | NetworkOperation::ManagedDestroy { .. } + | NetworkOperation::ManagedAccept { .. } + | NetworkOperation::ManagedCloseListener { .. } + | NetworkOperation::ManagedTlsUpgrade { .. } + | NetworkOperation::ManagedUdpCreate { .. } + | NetworkOperation::ManagedUdpBind { .. } + | NetworkOperation::ManagedUdpSend { .. } + | NetworkOperation::ManagedUdpClose { .. } + | NetworkOperation::SendDescriptorRights { .. } + | NetworkOperation::ReceiveDescriptorRights { .. }), + ) => { + network_compat::dispatch_context_managed_network_operation( + sidecar, vm_id, process_id, operation, reply, + ) + .await?; + Ok(None) + } + HostOperation::Process( + operation @ (ProcessOperation::Spawn(_) + | ProcessOperation::RunCaptured { .. } + | ProcessOperation::Exec(_) + | ProcessOperation::PollChild { .. } + | ProcessOperation::WriteChildStdin { .. } + | ProcessOperation::CloseChildStdin { .. }), + ) => { + dispatch_context_process_operation(sidecar, vm_id, process_id, operation, reply) + .await?; + Ok(None) + } + HostOperation::Process(ProcessOperation::Wait { + target, + options, + deadline_ms, + temporary_mask, + }) => { + if temporary_mask.is_some() { + reply + .fail(HostServiceError::new( + "EINVAL", + "waitpid does not accept a temporary signal mask", + )) + .map_err(SidecarError::from)?; + return Ok(None); + } + let deadline = match deadline_ms + .map(checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(None); + } + }; + dispatch_context_guest_wait( + sidecar, + vm_id, + process_id, + DeferredGuestWaitKind::Process { target, options }, + deadline, + reply, + )?; + Ok(None) + } + HostOperation::Clock(ClockOperation::Sleep { duration_ms }) => { + let deadline = match checked_deferred_guest_wait_deadline(duration_ms) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(None); + } + }; + dispatch_context_guest_wait( + sidecar, + vm_id, + process_id, + DeferredGuestWaitKind::Sleep, + Some(deadline), + reply, + )?; + Ok(None) + } + operation => Ok(Some((operation, reply))), + } +} + +fn dispatch_context_guest_wait( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + kind: DeferredGuestWaitKind, + deadline: Option, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let notify = Arc::clone(&sidecar.process_event_notify); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated VM must remain borrowed"); + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.process_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes + .get_mut(process_id) + .expect("validated process must remain borrowed"); + service_deferred_guest_wait( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + Some((kind, deadline, reply)), + ) +} + +pub(super) fn service_deferred_guest_wait( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::process_table::ProcessWaitHandle, + notify: Arc, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<( + DeferredGuestWaitKind, + Option, + DirectHostReplyHandle, + )>, +) -> Result<(), SidecarError> { + let newly_admitted = incoming.is_some(); + if let Some((kind, deadline, reply)) = incoming { + if process.deferred_guest_wait.is_some() { + reply + .fail(HostServiceError::new( + "EBUSY", + "process already owns a deferred wait", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let identity = reply.identity(); + if identity.generation != generation || identity.pid != process.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "deferred wait identity does not match the active kernel process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + process.deferred_guest_wait = Some(DeferredGuestWait { + kind, + reply, + deadline, + wake_task: None, + }); + } + + let now = Instant::now(); + let should_probe = process.deferred_guest_wait.as_ref().is_some_and(|wait| { + newly_admitted + || process.deferred_guest_wait_interrupted + || wait.deadline.is_some_and(|deadline| now >= deadline) + || wait + .wake_task + .as_ref() + .is_none_or(tokio::task::JoinHandle::is_finished) + }); + if !should_probe { + return Ok(()); + } + + let mut wait = process + .deferred_guest_wait + .take() + .expect("deferred wait checked above"); + if let Some(task) = wait.wake_task.take() { + task.abort(); + } + let interrupted = std::mem::take(&mut process.deferred_guest_wait_interrupted); + // Snapshot before the destructive readiness probe. A transition racing + // the probe changes this generation, so the waiter returns immediately + // instead of absorbing the only wake and sleeping forever. + let observed = wait_handle.snapshot(); + let result = match wait.kind { + DeferredGuestWaitKind::Process { target, options } => { + match process::probe_process_wait(kernel, process.kernel_pid, target, options) { + Ok(value) if !value.is_null() || options & 1 != 0 => Some(Ok(value)), + Ok(_) if interrupted => Some(Err(HostServiceError::new( + "EINTR", + "blocking waitpid interrupted by a caught signal", + ))), + Ok(_) if wait.deadline.is_some_and(|deadline| now >= deadline) => { + Some(Ok(Value::Null)) + } + Ok(_) => None, + Err(error) => Some(Err(error)), + } + } + DeferredGuestWaitKind::Sleep => { + if interrupted { + Some(Err(HostServiceError::new( + "EINTR", + "sleep interrupted by a caught signal", + ))) + } else if wait.deadline.is_none_or(|deadline| now >= deadline) { + Some(Ok(Value::Null)) + } else { + None + } + } + }; + if let Some(result) = result { + return match result { + Ok(value) => wait + .reply + .succeed(HostCallReply::Json(value)) + .map_err(SidecarError::from), + Err(error) => wait.reply.fail(error).map_err(SidecarError::from), + }; + } + + let deadline = wait.deadline; + let task_class = if wait.kind == DeferredGuestWaitKind::Sleep { + agentos_runtime::TaskClass::Timer + } else { + agentos_runtime::TaskClass::Vm + }; + let wake_task = runtime.spawn(task_class, async move { + match deadline { + Some(deadline) => { + let delay = deadline.saturating_duration_since(Instant::now()); + tokio::select! { + _ = wait_handle.wait_for_change_async(observed) => {} + _ = tokio::time::sleep(delay) => {} + } + } + None => wait_handle.wait_for_change_async(observed).await, + } + notify.notify_one(); + }); + match wake_task { + Ok(task) => { + wait.wake_task = Some(task); + process.deferred_guest_wait = Some(wait); + Ok(()) + } + Err(error) => wait + .reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from), + } +} + +fn validate_context_host_call( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + reply: &DirectHostReplyHandle, +) -> Result { + let Some(vm) = sidecar.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(false); + }; + let Some(process) = vm.active_processes.get(process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(false); + }; + let identity = reply.identity(); + if identity.generation != vm.generation || identity.pid != process.kernel_pid { + reply + .fail( + HostServiceError::new( + "ESTALE", + "host call identity does not match the active kernel process", + ) + .with_details(json!({ + "expectedGeneration": vm.generation, + "expectedPid": process.kernel_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) + .map_err(SidecarError::from)?; + return Ok(false); + } + Ok(true) +} + +fn claim_context_host_work(reply: &DirectHostReplyHandle) -> Result { + match reply.claim() { + Ok(claimed) => Ok(claimed), + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + Ok(false) + } + } +} + +fn settle_context_preflight( + reply: &DirectHostReplyHandle, + result: Result, +) -> Result, SidecarError> { + match result { + Ok(value) => Ok(Some(value)), + Err(error) => { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + Ok(None) + } + } +} + +fn dispatch_context_dns_operation( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + if !claim_context_host_work(&reply)? { + return Ok(()); + } + let vm = sidecar + .vms + .get(vm_id) + .expect("validated VM must remain borrowed"); + let runtime = vm.runtime_context.clone(); + let response = service_host_dns_operation( + sidecar.bridge.clone(), + &vm.kernel, + vm_id.to_owned(), + vm.dns.clone(), + operation, + ); + let task_reply = reply.clone(); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Dns, async move { + let settled = match response.await { + Ok(response) => task_reply.succeed(response), + Err(error) => task_reply.fail(error), + }; + if let Err(error) = settled { + eprintln!("ERR_AGENTOS_DNS_DIRECT_REPLY: {error}"); + } + }) { + let error = SidecarError::from(error); + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + } + Ok(()) +} + +fn dispatch_context_http_operation( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let NetworkOperation::HttpRequest { + url, + method, + headers, + body, + max_response_bytes, + max_header_bytes, + max_body_bytes, + } = operation + else { + return Err(SidecarError::host( + "EINVAL", + "HTTP dispatcher received a non-HTTP operation", + )); + }; + let bridge = sidecar.bridge.clone(); + let preflight = (|| { + let url = Url::parse(url.as_str()) + .map_err(|error| SidecarError::host("ERR_INVALID_URL", error.to_string()))?; + if !matches!(url.scheme(), "http" | "https") { + return Err(SidecarError::host( + "ERR_INVALID_URL", + format!("unsupported outbound HTTP scheme {}", url.scheme()), + )); + } + let host = url + .host_str() + .ok_or_else(|| SidecarError::host("ERR_INVALID_URL", "outbound HTTP URL has no host"))? + .to_owned(); + let port = url.port_or_known_default().ok_or_else(|| { + SidecarError::host("ERR_INVALID_URL", "outbound HTTP URL has no port") + })?; + validate_http_request_metadata(method.as_str(), headers.as_slice())?; + bridge.require_network_access( + vm_id, + crate::execution::NetworkOperation::Http, + format_tcp_resource(&host, port), + )?; + Ok((url, host, port)) + })(); + let Some((url, host, port)) = settle_context_preflight(&reply, preflight)? else { + return Ok(()); + }; + if !claim_context_host_work(&reply)? { + return Ok(()); + } + let prepared = (|| { + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated HTTP host-call VM remains registered"); + let process = vm + .active_processes + .get(process_id) + .expect("validated HTTP host-call process remains registered"); + let kernel_pid = process.kernel_pid; + let policy_body_limit = process.limits.http.max_fetch_response_bytes; + let runtime = vm.runtime_context.clone(); + let pinned_addresses = if let Ok(literal_ip) = host.parse::() { + filter_dns_safe_ip_addrs(vec![literal_ip], &host)? + } else { + filter_dns_safe_ip_addrs( + resolve_dns_ip_addrs( + &bridge, + &vm.kernel, + vm_id, + &vm.dns, + &host, + DnsLookupPolicy::SkipPermissions, + )?, + &host, + )? + }; + bridge.require_resolved_network_access( + vm_id, + crate::execution::NetworkOperation::Http, + &format_tcp_resource(&host, port), + &pinned_addresses + .iter() + .map(|ip| format_tcp_resource(&ip.to_string(), port)) + .collect::>(), + )?; + let default_ca_bundle = if url.scheme() == "https" { + read_vm_default_ca_bundle(&mut vm.kernel, kernel_pid)? + } else { + Vec::new() + }; + Ok(( + runtime, + policy_body_limit, + pinned_addresses, + default_ca_bundle, + )) + })(); + let Some((runtime, policy_body_limit, pinned_addresses, default_ca_bundle)) = + settle_context_preflight(&reply, prepared)? + else { + return Ok(()); + }; + let max_response_bytes = max_response_bytes.get(); + let max_header_bytes = max_header_bytes.get().min(max_response_bytes); + let max_body_bytes = max_body_bytes + .get() + .min(policy_body_limit) + .min(max_response_bytes); + let reserved_bytes = url + .as_str() + .len() + .saturating_add(method.as_str().len()) + .saturating_add(body.len()) + .saturating_add(default_ca_bundle.len()) + .saturating_add(max_response_bytes) + .saturating_add( + headers + .as_slice() + .iter() + .map(|header| { + header + .name + .as_str() + .len() + .saturating_add(header.value.as_str().len()) + }) + .sum::(), + ); + let request = BoundedHttpRequest { + url, + method: method.into_string(), + headers: headers.into_vec(), + body: body.into_vec(), + pinned_addresses, + default_ca_bundle, + max_response_bytes, + max_header_bytes, + max_body_bytes, + }; + let task_reply = reply.clone(); + let task_runtime = runtime.clone(); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Socket, async move { + let result = task_runtime + .blocking() + .run(reserved_bytes, move || issue_bounded_http_request(request)) + .await + .map_err(|error| host_service_error(&SidecarError::from(error))) + .and_then(|result| result.map_err(|error| host_service_error(&error))) + .map(HostCallReply::Json); + let settled = match result { + Ok(response) => task_reply.succeed(response), + Err(error) => task_reply.fail(error), + }; + if let Err(error) = settled { + eprintln!("ERR_AGENTOS_HTTP_DIRECT_REPLY: {error}"); + } + }) { + let error = SidecarError::from(error); + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + } + Ok(()) +} + +async fn dispatch_context_process_operation( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: ProcessOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + + match operation { + ProcessOperation::Spawn(request) => { + let mut request = request.into_request(); + if let Err(error) = merge_process_internal_bootstrap_env(sidecar, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, false)) + { + settle_context_process_reply(&reply, Err(error))?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let result = sidecar + .spawn_child_process(vm_id, process_id, request) + .await; + settle_context_process_reply(&reply, result.map(HostCallReply::Json))?; + } + ProcessOperation::RunCaptured { + request, + max_buffer, + } => { + let mut request = request.into_request(); + if let Err(error) = merge_process_internal_bootstrap_env(sidecar, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, false)) + { + settle_context_process_reply(&reply, Err(error))?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let completion = PendingChildProcessSyncCompletion::Direct(reply.clone()); + if let Err(error) = sidecar + .begin_javascript_child_process_sync( + vm_id, + process_id, + request, + Some(max_buffer.get()), + completion, + ) + .await + { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + } + } + ProcessOperation::PollChild { child_id, wait_ms } => { + if let Err(error) = + sidecar.validate_child_poll_target(vm_id, process_id, &[], child_id.as_str()) + { + settle_context_process_reply(&reply, Err(error))?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + // Polling is deliberately nonblocking in the sidecar. Settle the + // claimed reply in this event turn so the guest can re-poll after + // a concurrent child HostCall without depending on another edge + // from the shared process broker. + let result = sidecar + .poll_child_process(vm_id, process_id, child_id.as_str(), wait_ms) + .await + .map(HostCallReply::Json); + settle_context_process_reply(&reply, result)?; + } + ProcessOperation::WriteChildStdin { child_id, chunk } => { + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let result = sidecar + .write_child_process_stdin(vm_id, process_id, child_id.as_str(), chunk.as_slice()) + .map(|()| HostCallReply::Json(Value::Null)); + settle_context_process_reply(&reply, result)?; + } + ProcessOperation::CloseChildStdin { child_id } => { + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let result = sidecar + .close_child_process_stdin(vm_id, process_id, child_id.as_str()) + .map(|()| HostCallReply::Json(Value::Null)); + settle_context_process_reply(&reply, result)?; + } + ProcessOperation::Exec(request) => { + let mut request = request.into_request(); + let fd_image_commit = request.options.executable_fd.is_some(); + let preflight = if fd_image_commit { + validate_wasm_fd_image_commit_request(&request) + } else { + merge_process_internal_bootstrap_env(sidecar, vm_id, &mut request) + .and_then(|()| validate_process_launch_request(&request, true)) + }; + if let Err(error) = preflight { + settle_context_process_reply(&reply, Err(error))?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let local_replacement = request.options.local_replacement; + let result = if fd_image_commit { + sidecar.commit_wasm_fd_process_image(vm_id, process_id, &[], request) + } else { + sidecar.exec_process_image(vm_id, process_id, &[], request) + }; + match result { + Ok(()) if local_replacement => reply + .succeed_json(json!({ "committed": true })) + .map_err(SidecarError::from)?, + Ok(()) => reply.dismiss_claimed().map_err(SidecarError::from)?, + Err(error) => reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?, + } + } + other => { + reply + .fail(unsupported("process context", other)) + .map_err(SidecarError::from)?; + } + } + Ok(()) +} + +pub(super) fn merge_process_internal_bootstrap_env( + sidecar: &NativeSidecar, + vm_id: &str, + request: &mut ProcessLaunchRequest, +) -> Result<(), SidecarError> { + let vm = sidecar + .vms + .get(vm_id) + .ok_or_else(|| SidecarError::host("ESTALE", "host call VM no longer exists"))?; + let mut internal = sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env); + internal.extend(sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + )); + request.options.internal_bootstrap_env = internal; + Ok(()) +} + +fn settle_context_process_reply( + reply: &DirectHostReplyHandle, + result: Result, +) -> Result<(), SidecarError> { + match result { + Ok(response) => reply.succeed(response), + Err(error) => reply.fail(host_service_error(&error)), + } + .map_err(SidecarError::from) +} + +fn host_operation_effects(operation: &HostOperation) -> HostOperationEffects { + match operation { + HostOperation::Filesystem( + FilesystemOperation::Close { .. } | FilesystemOperation::CloseFrom { .. }, + ) => HostOperationEffects { + may_make_fd_readable: true, + may_make_fd_writable: false, + }, + _ => HostOperationEffects::default(), + } +} + +fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: Operation, + reply: &DirectHostReplyHandle, +) -> Result, HostServiceError> +where + C: SidecarHostCapability, +{ + if C::requires_claim(&operation) && !reply.claim()? { + return Ok(None); + } + C::execute(kernel, process, operation).map(Some) +} + +fn kernel_host_error(error: KernelError) -> HostServiceError { + HostServiceError::new(error.code(), error.to_string()) +} + +fn unsupported(family: &str, operation: impl fmt::Debug) -> HostServiceError { + HostServiceError::new( + "ENOSYS", + format!("{family} host operation is not implemented by the shared sidecar: {operation:?}"), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::{BTreeSet, HashMap}; + use std::sync::{Arc, Mutex}; + + #[derive(Default)] + struct RecordingReplyTarget { + replies: Mutex)>>, + } + + struct RejectingClaimTarget; + + impl agentos_execution::backend::DirectHostReplyTarget for RejectingClaimTarget { + fn claim(&self, _call_id: u64) -> Result { + Ok(false) + } + + fn respond( + &self, + _call_id: u64, + _claimed: bool, + _result: Result, + ) -> Result<(), HostServiceError> { + panic!("a rejected claim must not be settled again") + } + } + + impl agentos_execution::backend::DirectHostReplyTarget for RecordingReplyTarget { + fn claim(&self, _call_id: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _call_id: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + self.replies + .lock() + .expect("reply lock") + .push((claimed, result)); + Ok(()) + } + } + + fn recording_reply( + target: Arc, + ) -> agentos_execution::backend::DirectHostReplyHandle { + agentos_execution::backend::DirectHostReplyHandle::new( + agentos_execution::backend::HostCallIdentity { + generation: 1, + pid: 2, + call_id: 3, + }, + target, + 1024, + ) + .expect("reply") + } + + fn request(method: &str, args: Vec) -> HostRpcRequest { + HostRpcRequest { + id: 1, + method: method.to_owned(), + args, + raw_bytes_args: HashMap::new(), + } + } + + #[test] + fn rejected_context_claim_performs_no_dns_or_http_work() { + let reply = agentos_execution::backend::DirectHostReplyHandle::new( + agentos_execution::backend::HostCallIdentity { + generation: 1, + pid: 2, + call_id: 9, + }, + Arc::new(RejectingClaimTarget), + 1024, + ) + .expect("reply"); + let mut work_started = false; + if claim_context_host_work(&reply).expect("claim") { + work_started = true; + } + assert!(!work_started); + } + + #[test] + fn context_preflight_failure_settles_the_original_typed_error() { + let target = Arc::new(RecordingReplyTarget::default()); + let reply = recording_reply(Arc::clone(&target)); + assert!(settle_context_preflight::<()>( + &reply, + Err(SidecarError::host("ERR_INVALID_URL", "invalid URL")), + ) + .expect("settle preflight") + .is_none()); + let replies = target.replies.lock().expect("replies"); + assert!(!replies[0].0); + assert_eq!( + replies[0].1.as_ref().expect_err("typed error").code, + "ERR_INVALID_URL" + ); + } + + #[test] + fn deferred_guest_wait_deadlines_are_bounded_and_checked() { + assert!(checked_deferred_guest_wait_deadline(MAX_DEFERRED_GUEST_WAIT_MS).is_ok()); + let error = checked_deferred_guest_wait_deadline(MAX_DEFERRED_GUEST_WAIT_MS + 1) + .expect_err("duration above the ABI bound must fail"); + assert_eq!(error.code, "EINVAL"); + let details = error.details.expect("duration limit details"); + assert_eq!(details["limitName"], "guestWaitDurationMs"); + assert_eq!(details["limit"], MAX_DEFERRED_GUEST_WAIT_MS); + assert_eq!(details["observed"], MAX_DEFERRED_GUEST_WAIT_MS + 1); + } + + #[test] + fn typed_identity_route_accepts_explicit_unchanged_ids() { + let operation = decode_host_operation( + &request( + "process.setresgid", + vec![Value::Null, json!(1000), Value::Null], + ), + true, + 1024, + ) + .expect("decode identity operation") + .expect("typed identity route"); + + assert!(matches!( + operation, + HostOperation::Identity(IdentityOperation::SetGroupIds { + real: None, + effective: Some(1000), + saved: None, + }) + )); + } + + fn operation_family(operation: &HostOperation) -> inventory::HostCapabilityFamily { + match operation { + HostOperation::Terminal(_) => inventory::HostCapabilityFamily::Terminal, + HostOperation::Signal(_) => inventory::HostCapabilityFamily::Signal, + HostOperation::Identity(_) => inventory::HostCapabilityFamily::Identity, + HostOperation::Clock(_) => inventory::HostCapabilityFamily::Clock, + HostOperation::Entropy(_) => inventory::HostCapabilityFamily::Entropy, + other => panic!("unexpected capability family for assigned RPC: {other:?}"), + } + } + + #[test] + fn every_live_identity_terminal_signal_clock_and_entropy_rpc_has_a_typed_route() { + use inventory::HostCapabilityFamily::{Clock, Entropy, Identity, Signal, Terminal}; + + let cases = vec![ + ("process.getuid", vec![], Identity), + ("process.geteuid", vec![], Identity), + ("process.getgid", vec![], Identity), + ("process.getegid", vec![], Identity), + ("process.getresuid", vec![], Identity), + ("process.getresgid", vec![], Identity), + ("process.getgroups", vec![], Identity), + ("process.getpwuid", vec![json!(1000)], Identity), + ("process.getpwnam", vec![json!("agentos")], Identity), + ("process.getpwent", vec![json!(0)], Identity), + ("process.getgrgid", vec![json!(1000)], Identity), + ("process.getgrnam", vec![json!("agentos")], Identity), + ("process.getgrent", vec![json!(0)], Identity), + ("process.setuid", vec![json!(1000)], Identity), + ("process.seteuid", vec![json!(1000)], Identity), + ("process.setreuid", vec![json!(1000), json!(1000)], Identity), + ( + "process.setresuid", + vec![json!(1000), json!(1000), json!(1000)], + Identity, + ), + ("process.setgid", vec![json!(1000)], Identity), + ("process.setegid", vec![json!(1000)], Identity), + ("process.setregid", vec![json!(1000), json!(1000)], Identity), + ( + "process.setresgid", + vec![json!(1000), json!(1000), json!(1000)], + Identity, + ), + ("process.setgroups", vec![json!([1000])], Identity), + ("__kernel_isatty", vec![json!(0)], Terminal), + ("__kernel_tty_size", vec![json!(0)], Terminal), + ( + "__kernel_tty_set_size", + vec![json!(0), json!(80), json!(24)], + Terminal, + ), + ("__kernel_tcgetattr", vec![json!(0)], Terminal), + ( + "__kernel_tcsetattr", + vec![json!(0), json!(63), json!([1, 2, 3, 4, 5, 6, 7])], + Terminal, + ), + ("__kernel_tcgetpgrp", vec![json!(0)], Terminal), + ("__kernel_tcsetpgrp", vec![json!(0), json!(42)], Terminal), + ("__kernel_tcgetsid", vec![json!(0)], Terminal), + ("__pty_set_raw_mode", vec![json!(true)], Terminal), + ("process.pty_open", vec![], Terminal), + ("process.signal_begin", vec![], Signal), + ("process.signal_end", vec![json!(7)], Signal), + ("process.signal_mask", vec![json!(3), json!([])], Signal), + ( + "process.signal_mask_scope_begin", + vec![json!([2, 15])], + Signal, + ), + ("process.signal_mask_scope_end", vec![json!(7)], Signal), + ( + "process.signal_state", + vec![json!(15), json!("user"), json!("[2]"), json!(0)], + Signal, + ), + ("process.take_signal", vec![], Signal), + ( + "process.clock_time", + vec![json!(1), json!("1"), Value::Null], + Clock, + ), + ("process.clock_resolution", vec![json!(1)], Clock), + ("process.itimer_real", vec![json!(0)], Clock), + ("process.sleep", vec![json!(1)], Clock), + ("process.random_get", vec![json!(1024)], Entropy), + ]; + + let mut covered = BTreeSet::new(); + for (method, args, expected_family) in cases { + let operation = decode_host_operation(&request(method, args), true, 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {method}: {error}")) + .unwrap_or_else(|| panic!("{method} fell through to the legacy dispatcher")); + assert_eq!(operation_family(&operation), expected_family, "{method}"); + assert!(covered.insert(method), "duplicate coverage for {method}"); + } + + let assigned_families = [Identity, Terminal, Signal, Clock, Entropy]; + let expected = inventory::semantic_rpc_inventory() + .into_iter() + .filter(|method| { + inventory::capability_family(method) + .is_some_and(|family| assigned_families.contains(&family)) + }) + .collect::>(); + assert_eq!(covered, expected); + } + + #[test] + fn exec_decoder_keeps_path_and_prepared_fd_commit_modes_distinct() { + let prepared = json!({ + "command": "/proc/self/fd/3", + "options": { + "executableFd": 3, + "localReplacement": true, + }, + }); + assert!( + decode_host_operation(&request("process.exec", vec![prepared]), true, 1024 * 1024,) + .is_err() + ); + assert!(decode_host_operation( + &request( + "process.exec_fd_image_commit", + vec![json!({ "command": "/bin/true" })], + ), + true, + 1024 * 1024, + ) + .is_err()); + } + + #[test] + fn compatibility_decoder_emits_each_shared_capability_family_it_supports() { + let cases = [ + request("process.fd_preopens", vec![]), + request( + "process.fd_socketpair", + vec![json!(1), json!(false), json!(true)], + ), + request("process.umask", vec![Value::Null]), + request("__kernel_tty_size", vec![json!(1)]), + request("process.signal_end", vec![json!(7)]), + request("process.getresuid", vec![]), + request("process.clock_resolution", vec![json!(1)]), + ]; + let operations = cases + .iter() + .map(|request| { + decode_host_operation(request, true, 1024 * 1024) + .expect("decode") + .expect("typed route") + }) + .collect::>(); + assert!(matches!(operations[0], HostOperation::Filesystem(_))); + assert!(matches!(operations[1], HostOperation::Network(_))); + assert!(matches!(operations[2], HostOperation::Process(_))); + assert!(matches!(operations[3], HostOperation::Terminal(_))); + assert!(matches!(operations[4], HostOperation::Signal(_))); + assert!(matches!(operations[5], HostOperation::Identity(_))); + assert!(matches!(operations[6], HostOperation::Clock(_))); + } + + #[test] + fn compatibility_adapter_selects_unpositioned_write_progress_without_runtime_branching() { + let write = request("process.fd_write", vec![json!(4), json!("data")]); + let positioned = request( + "process.fd_pwrite", + vec![json!(4), json!("data"), json!("0")], + ); + + assert!(matches!( + decode_host_operation(&write, true, 1024).expect("decode WASM write"), + Some(HostOperation::Filesystem(FilesystemOperation::Write { + nonblocking: true, + offset: None, + .. + })) + )); + assert!( + decode_host_operation(&write, false, 1024) + .expect("decode non-WASM adapter write") + .is_none(), + "non-WASM adapters retain their existing adapter-local write path" + ); + assert!(matches!( + decode_host_operation(&positioned, true, 1024).expect("decode positioned write"), + Some(HostOperation::Filesystem(FilesystemOperation::Write { + nonblocking: false, + offset: Some(0), + .. + })) + )); + } + + #[test] + fn compatibility_decoder_routes_owned_process_lifecycle_operations() { + let launch = json!({ + "command": "/opt/agentos/bin/ls", + "args": ["-la"], + "options": { "cwd": "/tmp", "stdio": ["pipe", "pipe", "pipe"] }, + }); + let cases = [ + request("child_process.spawn", vec![launch.clone()]), + request("child_process.poll", vec![json!("child-1"), json!(5000)]), + request( + "child_process.write_stdin", + vec![json!("child-1"), json!("input")], + ), + request("child_process.close_stdin", vec![json!("child-1")]), + request("process.exec", vec![launch]), + ]; + let operations = cases + .iter() + .map(|request| { + decode_host_operation(request, true, 1024 * 1024) + .expect("decode process operation") + .expect("typed process route") + }) + .collect::>(); + assert!(matches!( + operations[0], + HostOperation::Process(ProcessOperation::Spawn(_)) + )); + assert!(matches!( + operations[1], + HostOperation::Process(ProcessOperation::PollChild { wait_ms: 5000, .. }) + )); + assert!(matches!( + operations[2], + HostOperation::Process(ProcessOperation::WriteChildStdin { .. }) + )); + assert!(matches!( + operations[3], + HostOperation::Process(ProcessOperation::CloseChildStdin { .. }) + )); + assert!(matches!( + operations[4], + HostOperation::Process(ProcessOperation::Exec(_)) + )); + } + + #[test] + fn every_frozen_process_rpc_is_typed_except_reviewed_adapter_calls() { + let launch = json!({ "command": "/opt/agentos/bin/true" }); + let cases = vec![ + ("child_process.close_stdin", vec![json!("child-1")]), + ("child_process.poll", vec![json!("child-1"), json!(0)]), + ("child_process.spawn", vec![launch.clone()]), + ( + "child_process.write_stdin", + vec![json!("child-1"), json!("input")], + ), + ("process.exec", vec![launch]), + ( + "process.exec_fd_image_commit", + vec![json!({ + "command": "/proc/self/fd/3", + "options": { + "executableFd": 3, + "localReplacement": true, + }, + })], + ), + ("process.exec_image_open", vec![json!("/bin/sh")]), + ("process.exec_image_open_fd", vec![json!(3)]), + ( + "process.exec_image_read", + vec![json!("1"), json!("0"), json!(1024)], + ), + ("process.exec_image_close", vec![json!("1")]), + ("process.image", vec![]), + ("process.getpgid", vec![json!(0)]), + ("process.getrlimit", vec![json!(7)]), + ("process.kill", vec![json!(42), json!("SIGTERM")]), + ("process.setpgid", vec![json!(0), json!(0)]), + ( + "process.setrlimit", + vec![json!(7), json!("64"), json!("64")], + ), + ("process.system_identity", vec![]), + ("process.umask", vec![Value::Null]), + ("process.waitpid", vec![json!(-1), json!(1)]), + ("process.waitpid_transition", vec![json!(-1), json!(1)]), + ]; + let mut covered = BTreeSet::new(); + for (method, args) in cases { + let operation = decode_host_operation(&request(method, args), true, 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {method}: {error}")) + .unwrap_or_else(|| panic!("{method} fell through to the legacy dispatcher")); + assert!( + matches!(operation, HostOperation::Process(_)), + "{method} decoded outside the process capability: {operation:?}" + ); + assert!(covered.insert(method), "duplicate process case {method}"); + } + + let expected = inventory::semantic_rpc_inventory() + .into_iter() + .filter(|method| { + inventory::capability_family(method) + == Some(inventory::HostCapabilityFamily::Process) + && !inventory::WASM_ADAPTER_ONLY_RPCS.contains(method) + }) + .collect::>(); + assert_eq!(covered, expected); + } + + #[test] + fn process_preflight_and_post_claim_failures_preserve_the_original_errno() { + let preflight_target = Arc::new(RecordingReplyTarget::default()); + let preflight_reply = recording_reply(Arc::clone(&preflight_target)); + settle_context_process_reply( + &preflight_reply, + Err(SidecarError::host("EINVAL", "invalid launch options")), + ) + .expect("settle preflight failure"); + let preflight = preflight_target.replies.lock().expect("preflight reply"); + assert!(!preflight[0].0); + assert!(matches!(&preflight[0].1, Err(error) if error.code == "EINVAL")); + + let claimed_target = Arc::new(RecordingReplyTarget::default()); + let claimed_reply = recording_reply(Arc::clone(&claimed_target)); + assert!(claimed_reply.claim().expect("claim side effect")); + settle_context_process_reply( + &claimed_reply, + Err(SidecarError::host("EPIPE", "child stdin closed")), + ) + .expect("settle claimed failure"); + let claimed = claimed_target.replies.lock().expect("claimed reply"); + assert!(claimed[0].0); + assert!(matches!(&claimed[0].1, Err(error) if error.code == "EPIPE")); + } + + #[test] + fn compatibility_decoder_preserves_only_unknown_calls_for_legacy_service() { + assert!( + decode_host_operation(&request("fs.readSync", vec![]), true, 1024) + .expect("unknown method") + .is_none() + ); + assert!(matches!( + decode_host_operation( + &request( + "process.fd_socketpair", + vec![json!(3), json!(false), json!(false)], + ), + true, + 1024 + ) + .expect("seqpacket decode"), + Some(HostOperation::Network(NetworkOperation::SocketPair { + kind: SocketKind::SeqPacket, + nonblocking: false, + close_on_exec: false, + })) + )); + } + + #[test] + fn assigned_decoders_reject_unbounded_or_malformed_payloads_before_dispatch() { + let too_many_groups = vec![json!(1000); MAX_SUPPLEMENTARY_GROUPS + 1]; + assert!(decode_host_operation( + &request("process.setgroups", vec![Value::Array(too_many_groups)]), + true, + 1024 * 1024, + ) + .is_err()); + + assert!(decode_host_operation( + &request( + "process.getpwnam", + vec![json!("x".repeat(MAX_ACCOUNT_NAME_BYTES + 1))], + ), + true, + 1024 * 1024, + ) + .is_err()); + + let too_many_signals = vec![json!(2); MAX_SIGNAL_SET_ENTRIES + 1]; + assert!(decode_host_operation( + &request( + "process.signal_mask_scope_begin", + vec![Value::Array(too_many_signals)], + ), + true, + 1024 * 1024, + ) + .is_err()); + + assert!(decode_host_operation( + &request( + "process.signal_state", + vec![ + json!(15), + json!("user"), + json!(" ".repeat(MAX_SIGNAL_STATE_MASK_JSON_BYTES + 1)), + json!(0), + ], + ), + true, + 1024 * 1024, + ) + .is_err()); + + assert!(decode_host_operation( + &request( + "__kernel_tcsetattr", + vec![json!(0), json!(0), json!([1, 2, 3, 4, 5, 6, 7, 8])], + ), + true, + 1024 * 1024, + ) + .is_err()); + + assert!(decode_host_operation( + &request( + "process.random_get", + vec![json!(MAX_ENTROPY_CHUNK_BYTES + 1)], + ), + true, + 1024 * 1024, + ) + .is_err()); + assert!(decode_host_operation( + &request("process.random_get", vec![json!(1025)]), + true, + 1024, + ) + .is_err()); + + assert!(decode_host_operation( + &request( + "child_process.write_stdin", + vec![json!("child-1"), json!("x".repeat(1025))], + ), + true, + 1024, + ) + .is_err()); + assert!(decode_host_operation( + &request( + "child_process.spawn", + vec![json!({ + "command": "/bin/echo", + "args": ["x".repeat(1024)], + })], + ), + true, + 128, + ) + .is_err()); + } + + #[test] + fn account_record_decoder_caps_output_to_the_reply_transport_limit() { + let operation = + decode_host_operation(&request("process.getpwuid", vec![json!(1000)]), true, 128) + .expect("decode account lookup") + .expect("typed account lookup"); + assert!(matches!( + operation, + HostOperation::Identity(IdentityOperation::PasswdById { + max_record_bytes, + .. + }) if max_record_bytes.get() == 128 + )); + } + + #[test] + fn resource_limit_decoder_preserves_numeric_and_string_wire_values() { + for value in [json!(64), json!("64")] { + let operation = decode_host_operation( + &request("process.setrlimit", vec![json!(7), value.clone(), value]), + true, + 1024, + ) + .expect("decode rlimit") + .expect("typed rlimit route"); + assert!(matches!( + operation, + HostOperation::Process(ProcessOperation::SetResourceLimit { + kind: ResourceLimitKind::OpenFiles, + value: ResourceLimitValue { + soft: Some(64), + hard: Some(64), + }, + }) + )); + } + } + + #[test] + fn side_effecting_typed_operations_claim_the_direct_reply_before_execution() { + assert!(process::ProcessCapability::requires_claim( + &ProcessOperation::Kill { + target: 42, + signal: 15, + } + )); + assert!(process::ProcessCapability::requires_claim( + &ProcessOperation::SetProcessGroup { + pid: Some(42), + pgid: Some(42), + } + )); + assert!(process::ProcessCapability::requires_claim( + &ProcessOperation::Wait { + target: WaitTarget::Any, + options: 1, + deadline_ms: None, + temporary_mask: None, + } + )); + assert!(clock::ClockCapability::requires_claim( + &ClockOperation::RealIntervalSet { + initial_us: 1, + interval_us: 1, + } + )); + assert!(identity::IdentityCapability::requires_claim( + &IdentityOperation::SetId { + kind: IdentityIdKind::EffectiveUser, + value: Some(1), + } + )); + assert!(terminal::TerminalCapability::requires_claim( + &TerminalOperation::SetRawMode { + fd: 0, + enabled: true, + } + )); + assert!(signal::SignalCapability::requires_claim( + &SignalOperation::SetAction { + signal: 15, + action: SignalActionValue { + disposition: SignalDispositionValue::User, + flags: 0, + mask: SignalSetValue::default(), + }, + } + )); + assert!(signal::SignalCapability::requires_claim( + &SignalOperation::BeginTemporaryMask { + mask: SignalSetValue::default(), + } + )); + assert!(entropy::EntropyCapability::requires_claim( + &EntropyOperation { + length: BoundedUsize::try_new( + 1, + &PayloadLimit::new("maxEntropyChunkBytes", 1).expect("limit"), + ) + .expect("bounded entropy"), + } + )); + assert!(network::NetworkCapability::requires_claim( + &NetworkOperation::SocketPair { + kind: SocketKind::Stream, + nonblocking: true, + close_on_exec: true, + } + )); + + assert!(!process::ProcessCapability::requires_claim( + &ProcessOperation::GetPid + )); + assert!(!clock::ClockCapability::requires_claim( + &ClockOperation::RealIntervalGet + )); + assert!(!identity::IdentityCapability::requires_claim( + &IdentityOperation::GetUserIds + )); + assert!(!terminal::TerminalCapability::requires_claim( + &TerminalOperation::GetAttributes { fd: 0 } + )); + assert!(!network::NetworkCapability::requires_claim( + &NetworkOperation::ResolveDns { + host: BoundedString::try_new( + String::from("example.test"), + &PayloadLimit::new("maxDnsNameBytes", 253).expect("limit"), + ) + .expect("host"), + port: None, + family: agentos_execution::host::DnsAddressFamily::Any, + max_results: BoundedUsize::try_new( + 16, + &PayloadLimit::new("maxDnsResults", 16).expect("limit"), + ) + .expect("results"), + } + )); + } + + #[test] + fn every_vm_scoped_host_family_is_classified_before_kernel_only_dispatch() { + let cases = [ + request("__kernel_stdin_read", vec![json!(4096), json!(0)]), + request( + "dns.lookup", + vec![json!({"hostname":"localhost","family":4})], + ), + request( + "__kernel_poll", + vec![json!([{ "fd": 0, "events": 1 }]), json!(0)], + ), + request("net.connect", vec![json!({"host":"127.0.0.1","port":80})]), + request("dgram.poll", vec![json!("udp-1"), json!(0)]), + request( + "child_process.spawn", + vec![json!({"command":"/opt/agentos/bin/true"})], + ), + request("process.waitpid", vec![json!(-1), json!(1)]), + request("process.sleep", vec![json!(1)]), + ]; + for request in cases { + let operation = decode_host_operation(&request, true, 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {}: {error}", request.method)) + .unwrap_or_else(|| panic!("{} fell through to legacy dispatch", request.method)); + assert!( + requires_context_host_dispatch(&operation), + "{} must traverse context dispatch before kernel-only fallback", + request.method + ); + } + + assert!(!requires_context_host_dispatch(&HostOperation::Process( + ProcessOperation::GetPid, + ))); + assert!(!requires_context_host_dispatch(&HostOperation::Clock( + ClockOperation::Resolution { + clock: GuestClockId::Monotonic, + }, + ))); + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/network.rs b/crates/native-sidecar/src/execution/host_dispatch/network.rs new file mode 100644 index 0000000000..af75b02e54 --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/network.rs @@ -0,0 +1,272 @@ +use super::*; +use agentos_kernel::poll::{PollEvents, PollFd}; +use agentos_kernel::socket_table::SocketType; +use std::time::Instant; + +pub(super) struct NetworkCapability; + +impl SidecarHostCapability for NetworkCapability { + fn requires_claim(operation: &NetworkOperation) -> bool { + matches!( + operation, + NetworkOperation::SocketPair { .. } | NetworkOperation::Shutdown { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: NetworkOperation, + ) -> Result { + match operation { + NetworkOperation::SocketPair { + kind, + nonblocking, + close_on_exec, + } => { + let socket_type = match kind { + SocketKind::Stream => SocketType::Stream, + SocketKind::Datagram => SocketType::Datagram, + SocketKind::SeqPacket => SocketType::SeqPacket, + }; + let (first_fd, second_fd) = kernel + .fd_socketpair( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + socket_type, + nonblocking, + close_on_exec, + ) + .map_err(kernel_host_error)?; + Ok(HostCallReply::Json(json!({ + "firstFd": first_fd, + "secondFd": second_fd, + }))) + } + NetworkOperation::Shutdown { fd, how } => { + let how = match how { + SocketShutdown::Read => KernelSocketShutdown::Read, + SocketShutdown::Write => KernelSocketShutdown::Write, + SocketShutdown::Both => KernelSocketShutdown::Both, + }; + kernel + .fd_socket_shutdown(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, how) + .map_err(kernel_host_error)?; + Ok(HostCallReply::Json(Value::Null)) + } + other => Err(unsupported("network", other)), + } + } +} + +/// Typed kernel poll: owner-thread probes are synchronous and waits retain +/// only the cloneable notifier, an absolute deadline, typed interests, and the +/// generation-bound direct reply capability. +pub(super) fn dispatch_context_kernel_poll( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let NetworkOperation::KernelPoll { + interests, + timeout_ms, + } = operation + else { + return Err(SidecarError::host( + "EINVAL", + "kernel poll dispatcher received a different network operation", + )); + }; + + let deadline = match timeout_ms + .map(checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + }; + let notify = Arc::clone(&sidecar.process_event_notify); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated kernel-poll VM remains registered"); + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes + .get_mut(process_id) + .expect("validated kernel-poll process remains registered"); + service_deferred_kernel_poll( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + Some((interests, deadline, reply)), + ) +} + +pub(super) fn typed_kernel_poll_response( + kernel: &SidecarKernel, + kernel_pid: u32, + interests: &[KernelPollInterest], +) -> Result { + let fds = interests + .iter() + .map(|entry| PollFd { + fd: entry.fd, + events: PollEvents::from_bits(entry.events), + revents: PollEvents::empty(), + }) + .collect(); + let result = kernel + .poll_fds(EXECUTION_DRIVER_NAME, kernel_pid, fds, 0) + .map_err(kernel_error)?; + Ok(json!({ + "readyCount": result.ready_count, + "fds": result.fds.into_iter().map(|entry| json!({ + "fd": entry.fd, + "events": entry.events.bits(), + "revents": entry.revents.bits(), + })).collect::>(), + })) +} + +/// Park a descendant executor's typed kernel poll without blocking the +/// sidecar actor or a Tokio worker. Readiness is always re-probed on the owner +/// thread; the spawned task retains only the cloneable kernel notifier and an +/// optional absolute deadline. +pub(in crate::execution) fn service_deferred_kernel_poll( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::poll::PollWaitHandle, + notify: Arc, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<( + BoundedVec, + Option, + DirectHostReplyHandle, + )>, +) -> Result<(), SidecarError> { + let newly_admitted = incoming.is_some(); + if let Some((interests, deadline, reply)) = incoming { + if process.deferred_kernel_poll.is_some() { + reply + .fail(HostServiceError::new( + "EBUSY", + "process already owns a deferred kernel poll", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let identity = reply.identity(); + if identity.generation != generation || identity.pid != process.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "deferred kernel poll identity does not match the active kernel process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + process.deferred_kernel_poll = Some(DeferredKernelPoll { + interests, + reply, + deadline, + wake_task: None, + temporary_signal_mask_token: None, + combined: false, + }); + } + + let now = Instant::now(); + let should_probe = process.deferred_kernel_poll.as_ref().is_some_and(|poll| { + newly_admitted + || poll.deadline.is_some_and(|deadline| now >= deadline) + || poll + .wake_task + .as_ref() + .is_none_or(tokio::task::JoinHandle::is_finished) + }); + if !should_probe { + return Ok(()); + } + + let mut poll = process + .deferred_kernel_poll + .take() + .expect("deferred kernel poll checked above"); + if let Some(task) = poll.wake_task.take() { + task.abort(); + } + // Snapshot before probing so a readiness edge racing the probe cannot be + // absorbed before the off-actor waiter starts. + let observed = wait_handle.snapshot(); + let response = + match typed_kernel_poll_response(kernel, process.kernel_pid, poll.interests.as_slice()) { + Ok(response) => response, + Err(error) => { + return poll + .reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from); + } + }; + let ready = response + .get("readyCount") + .and_then(Value::as_u64) + .unwrap_or_default() + > 0; + if ready || poll.deadline.is_some_and(|deadline| now >= deadline) { + return poll + .reply + .succeed(HostCallReply::Json(response)) + .map_err(SidecarError::from); + } + + let deadline = poll.deadline; + let wake_task = runtime.spawn(agentos_runtime::TaskClass::Vm, async move { + match deadline { + Some(deadline) => { + let delay = deadline.saturating_duration_since(Instant::now()); + tokio::select! { + _ = wait_handle.wait_for_change_async(observed) => {} + _ = tokio::time::sleep(delay) => {} + } + } + None => { + wait_handle.wait_for_change_async(observed).await; + } + } + notify.notify_one(); + }); + match wake_task { + Ok(task) => { + poll.wake_task = Some(task); + process.deferred_kernel_poll = Some(poll); + Ok(()) + } + Err(error) => poll + .reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from), + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs new file mode 100644 index 0000000000..af9f6dc2e1 --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs @@ -0,0 +1,4854 @@ +use super::*; +use crate::state::{ + DeferredRpcError, ManagedHostNetDescription, ManagedHostNetDescriptionRegistry, + ManagedHostNetRoute, ManagedStreamReadRecheck, ManagedUdpPollRecheck, +}; +use agentos_execution::host::SocketDomain as HostSocketDomain; +use agentos_kernel::process_table::SignalSet; +use agentos_kernel::socket_table::SocketType; +use std::time::{Duration, Instant}; + +const MANAGED_ID_BYTES: usize = 256; +const HOST_BYTES: usize = 253; +const UNIX_PATH_BYTES: usize = 4096; + +const POSIX_POLLIN: u16 = 0x001; +const POSIX_POLLOUT: u16 = 0x004; +const POSIX_POLLNVAL: u16 = 0x020; +const POSIX_POLLRDNORM: u16 = 0x040; +const POSIX_POLLWRNORM: u16 = 0x100; +const POSIX_READ_EVENTS: u16 = POSIX_POLLIN | POSIX_POLLRDNORM; +const POSIX_WRITE_EVENTS: u16 = POSIX_POLLOUT | POSIX_POLLWRNORM; + +fn posix_signal_set(set: SignalSetValue) -> Result { + SignalSet::from_signals((1..=64).filter(|signal| set.0 & (1_u64 << (signal - 1)) != 0)) + .map_err(|error| SidecarError::host(error.code(), error.to_string())) +} + +fn managed_posix_poll_response( + socket_paths: &SocketPathContext, + kernel_readiness: KernelSocketReadinessRegistry, + capabilities: CapabilityRegistry, + managed_descriptions: &ManagedHostNetDescriptionRegistry, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + interests: &[KernelPollInterest], +) -> Result { + let mut kernel_interests = Vec::new(); + let mut managed_revents = BTreeMap::::new(); + let trace_enabled = net_tcp_trace_enabled(&process.env); + + for interest in interests { + let description_id = match kernel.fd_description_identity( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + interest.fd, + ) { + Ok((description_id, _)) => description_id, + Err(error) if error.code() == "EBADF" => { + managed_revents.insert(interest.fd, POSIX_POLLNVAL); + continue; + } + Err(error) => return Err(kernel_error(error)), + }; + let route = managed_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .get(&description_id) + .and_then(|description| description.route_for(process.kernel_pid).cloned()); + let Some(route) = route else { + kernel_interests.push(*interest); + continue; + }; + + let mut revents = 0_u16; + if interest.events & POSIX_WRITE_EVENTS != 0 + && matches!( + route, + ManagedHostNetRoute::TcpSocket(_) + | ManagedHostNetRoute::UnixSocket(_) + | ManagedHostNetRoute::UdpSocket(_) + ) + { + revents |= interest.events & POSIX_WRITE_EVENTS; + } + if interest.events & POSIX_READ_EVENTS != 0 { + let readable = match route { + ManagedHostNetRoute::TcpSocket(ref socket_id) + | ManagedHostNetRoute::UnixSocket(ref socket_id) => { + let mut context = ManagedNetworkServiceContext { + vm_id: "posix-poll", + socket_paths, + kernel, + kernel_readiness: kernel_readiness.clone(), + process, + capabilities: capabilities.clone(), + }; + probe_managed_socket_readable(&mut context, socket_id)? + } + ManagedHostNetRoute::TcpListener(ref listener_id) => process + .tcp_listeners + .get_mut(listener_id) + .ok_or_else(|| SidecarError::host("EBADF", "managed TCP listener is stale"))? + .probe_readable(kernel, process.kernel_pid, trace_enabled)?, + ManagedHostNetRoute::UnixListener(ref listener_id) => process + .unix_listeners + .get_mut(listener_id) + .ok_or_else(|| SidecarError::host("EBADF", "managed Unix listener is stale"))? + .probe_readable()?, + ManagedHostNetRoute::UdpSocket(ref socket_id) => { + let socket = process.udp_sockets.get(socket_id).ok_or_else(|| { + SidecarError::host("EBADF", "managed UDP socket is stale") + })?; + socket + .pending_datagram + .lock() + .map_err(|_| { + SidecarError::host("EIO", "UDP pending datagram lock poisoned") + })? + .is_some() + || socket + .native_read_wake_pending + .load(std::sync::atomic::Ordering::Acquire) + || socket.kernel_readable(kernel, process.kernel_pid)? + } + ManagedHostNetRoute::Unbound + | ManagedHostNetRoute::TcpBound { .. } + | ManagedHostNetRoute::UnixBound { .. } => false, + }; + if readable { + revents |= interest.events & POSIX_READ_EVENTS; + } + } + managed_revents.insert(interest.fd, revents); + } + + let kernel_response = + super::network::typed_kernel_poll_response(kernel, process.kernel_pid, &kernel_interests)?; + let kernel_fds = kernel_response + .get("fds") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + let mut ready_count = 0_u64; + let fds = interests + .iter() + .map(|interest| { + let revents = managed_revents + .get(&interest.fd) + .copied() + .unwrap_or_else(|| { + kernel_fds + .iter() + .find(|entry| { + entry.get("fd").and_then(Value::as_u64) == Some(u64::from(interest.fd)) + }) + .and_then(|entry| entry.get("revents")) + .and_then(Value::as_u64) + .and_then(|value| u16::try_from(value).ok()) + .unwrap_or_default() + }); + if revents != 0 { + ready_count += 1; + } + json!({ + "fd": interest.fd, + "events": interest.events, + "revents": revents, + }) + }) + .collect::>(); + Ok(json!({ "readyCount": ready_count, "fds": fds })) +} + +fn native_tcp_listener_waiters( + managed_descriptions: &ManagedHostNetDescriptionRegistry, + kernel: &SidecarKernel, + process: &ActiveProcess, + interests: &[KernelPollInterest], +) -> Result>, SidecarError> { + let descriptions = managed_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))?; + let mut waiters = Vec::new(); + for interest in interests + .iter() + .filter(|interest| interest.events & POSIX_READ_EVENTS != 0) + { + let Ok((description_id, _)) = + kernel.fd_description_identity(EXECUTION_DRIVER_NAME, process.kernel_pid, interest.fd) + else { + continue; + }; + let Some(ManagedHostNetRoute::TcpListener(listener_id)) = descriptions + .get(&description_id) + .and_then(|description| description.route_for(process.kernel_pid)) + else { + continue; + }; + let Some(listener) = process + .tcp_listeners + .get(listener_id) + .and_then(|listener| listener.listener.as_ref()) + else { + continue; + }; + waiters.push( + tokio::io::unix::AsyncFd::new(listener.try_clone().map_err(|error| { + SidecarError::host("EIO", format!("clone TCP listener for poll: {error}")) + })?) + .map_err(|error| { + SidecarError::host("EIO", format!("register TCP listener poll waiter: {error}")) + })?, + ); + } + Ok(waiters) +} + +pub(super) fn dispatch_context_posix_poll( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let NetworkOperation::PosixPoll { + interests, + timeout_ms, + signal_mask, + } = operation + else { + return Err(SidecarError::host( + "EINVAL", + "POSIX poll dispatcher received a different network operation", + )); + }; + let deadline = match timeout_ms + .map(checked_deferred_guest_wait_deadline) + .transpose() + { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + }; + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated POSIX-poll VM remains registered"), + )?; + let notify = Arc::clone(&sidecar.process_event_notify); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated POSIX-poll VM remains registered"); + let generation = vm.generation; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated POSIX-poll process remains registered"); + service_deferred_posix_poll( + generation, + &runtime, + wait_handle, + notify, + &socket_paths, + kernel_readiness, + capabilities, + managed_descriptions, + &mut vm.kernel, + process, + Some((interests, deadline, signal_mask, reply)), + ) +} + +fn restore_deferred_posix_mask( + process: &mut ActiveProcess, + poll: &mut DeferredKernelPoll, +) -> Result<(), HostServiceError> { + let Some(token) = poll.temporary_signal_mask_token.take() else { + return Ok(()); + }; + process + .kernel_handle + .end_temporary_signal_mask(token) + .map_err(|error| HostServiceError::new(error.code(), error.to_string())) +} + +fn fail_deferred_posix_poll( + process: &mut ActiveProcess, + mut poll: DeferredKernelPoll, + failure: HostServiceError, +) -> Result<(), SidecarError> { + let failure = match restore_deferred_posix_mask(process, &mut poll) { + Ok(()) => failure, + Err(restore_error) => { + eprintln!( + "ERR_AGENTOS_PPOLL_MASK_RESTORE: {}; original poll failure: {}", + restore_error, failure + ); + restore_error + } + }; + poll.reply.fail(failure).map_err(SidecarError::from) +} + +#[allow(clippy::too_many_arguments)] +pub(in crate::execution) fn service_deferred_posix_poll( + generation: u64, + runtime: &agentos_runtime::RuntimeContext, + wait_handle: agentos_kernel::poll::PollWaitHandle, + notify: Arc, + socket_paths: &SocketPathContext, + kernel_readiness: KernelSocketReadinessRegistry, + capabilities: CapabilityRegistry, + managed_descriptions: ManagedHostNetDescriptionRegistry, + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + incoming: Option<( + BoundedVec, + Option, + Option, + DirectHostReplyHandle, + )>, +) -> Result<(), SidecarError> { + let newly_admitted = incoming.is_some(); + if let Some((interests, deadline, signal_mask, reply)) = incoming { + if process.deferred_kernel_poll.is_some() { + reply + .fail(HostServiceError::new( + "EBUSY", + "process already owns a deferred POSIX poll", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let identity = reply.identity(); + if identity.generation != generation || identity.pid != process.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "deferred POSIX poll identity does not match the active kernel process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + // A checkpoint published before admission already owns a handler frame; + // a temporary mask cannot retroactively block it. + if process.guest_signal_checkpoint_pending || process.runtime_control.pending().checkpoint { + reply + .fail(HostServiceError::new( + "EINTR", + "caught signal was pending before POSIX poll admission", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let temporary_signal_mask_token = match signal_mask { + Some(mask) => { + let mask = match posix_signal_set(mask) { + Ok(mask) => mask, + Err(error) => { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + }; + match process.kernel_handle.begin_temporary_signal_mask(mask) { + Ok(token) => Some(token), + Err(error) => { + reply + .fail(HostServiceError::new(error.code(), error.to_string())) + .map_err(SidecarError::from)?; + return Ok(()); + } + } + } + None => None, + }; + process.deferred_kernel_poll = Some(DeferredKernelPoll { + interests, + reply, + deadline, + wake_task: None, + temporary_signal_mask_token, + combined: true, + }); + // Installing a ppoll mask can make a previously blocked signal + // deliverable. Publish it while the guest is still parked. + if let Err(error) = process.apply_runtime_controls() { + if let Some(poll) = process.clear_deferred_kernel_poll() { + fail_deferred_posix_poll( + process, + poll, + HostServiceError::new( + "EIO", + format!("failed to publish POSIX-poll signal checkpoint: {error}"), + ), + )?; + } + return Err(error); + } + if process.deferred_kernel_poll.is_none() { + return Ok(()); + } + } + + let now = Instant::now(); + let should_probe = process.deferred_kernel_poll.as_ref().is_some_and(|poll| { + poll.combined + && (newly_admitted + || poll.deadline.is_some_and(|deadline| now >= deadline) + || poll + .wake_task + .as_ref() + .is_none_or(tokio::task::JoinHandle::is_finished)) + }); + if !should_probe { + return Ok(()); + } + let mut poll = process + .deferred_kernel_poll + .take() + .expect("deferred POSIX poll checked above"); + if let Some(task) = poll.wake_task.take() { + task.abort(); + } + let observed = wait_handle.snapshot(); + let response = managed_posix_poll_response( + socket_paths, + kernel_readiness, + capabilities, + &managed_descriptions, + kernel, + process, + poll.interests.as_slice(), + ); + let response = match response { + Ok(response) => response, + Err(error) => { + return fail_deferred_posix_poll(process, poll, host_service_error(&error)); + } + }; + let ready = response + .get("readyCount") + .and_then(Value::as_u64) + .unwrap_or_default() + > 0; + if ready || poll.deadline.is_some_and(|deadline| now >= deadline) { + if let Err(error) = restore_deferred_posix_mask(process, &mut poll) { + return poll.reply.fail(error).map_err(SidecarError::from); + } + if poll.combined { + // Publish signals released only by mask restoration before the + // successful result wakes guest code. Such a signal does not + // rewrite the already-observed poll result to EINTR. + if let Err(error) = process.apply_runtime_controls() { + poll.reply + .fail(HostServiceError::new( + "EIO", + format!("failed to publish restored ppoll signal checkpoint: {error}"), + )) + .map_err(SidecarError::from)?; + return Err(error); + } + } + return poll + .reply + .succeed(HostCallReply::Json(response)) + .map_err(SidecarError::from); + } + + let deadline = poll.deadline; + let native_listener_waiters = match native_tcp_listener_waiters( + &managed_descriptions, + kernel, + process, + poll.interests.as_slice(), + ) { + Ok(waiters) => waiters, + Err(error) => { + return fail_deferred_posix_poll(process, poll, host_service_error(&error)); + } + }; + let task_notify = Arc::clone(¬ify); + let wake_task = runtime.spawn(agentos_runtime::TaskClass::Vm, async move { + let native_listener_ready = std::future::poll_fn(|cx| { + if native_listener_waiters.is_empty() { + return std::task::Poll::Pending; + } + for waiter in &native_listener_waiters { + if let std::task::Poll::Ready(_) = waiter.poll_read_ready(cx) { + return std::task::Poll::Ready(()); + } + } + std::task::Poll::Pending + }); + tokio::pin!(native_listener_ready); + match deadline { + Some(deadline) => { + let delay = deadline.saturating_duration_since(Instant::now()); + tokio::select! { + _ = wait_handle.wait_for_change_async(observed) => {} + _ = task_notify.notified() => {} + _ = &mut native_listener_ready => {} + _ = tokio::time::sleep(delay) => {} + } + } + None => { + tokio::select! { + _ = wait_handle.wait_for_change_async(observed) => {} + _ = task_notify.notified() => {} + _ = &mut native_listener_ready => {} + } + } + } + task_notify.notify_one(); + }); + match wake_task { + Ok(task) => { + poll.wake_task = Some(task); + process.deferred_kernel_poll = Some(poll); + Ok(()) + } + Err(error) => fail_deferred_posix_poll( + process, + poll, + host_service_error(&SidecarError::from(error)), + ), + } +} + +pub(super) fn dispatch_context_close_with_managed_retirement( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + fd: u32, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated close VM remains registered"), + )?; + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let bridge = sidecar.bridge.clone(); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated close VM remains registered"); + let process = vm + .active_processes + .get(process_id) + .expect("validated close process remains registered"); + let kernel_pid = process.kernel_pid; + let result = close_with_managed_retirement(&bridge, vm_id, &socket_paths, vm, kernel_pid, fd); + match result { + Ok(response) => reply.succeed(response).map_err(SidecarError::from), + Err(error) => reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from), + } +} + +pub(in crate::execution) fn close_with_managed_retirement( + bridge: &SharedBridge, + vm_id: &str, + socket_paths: &SocketPathContext, + vm: &mut VmState, + kernel_pid: u32, + fd: u32, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let description_id = vm + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .ok() + .map(|identity| identity.0); + vm.kernel + .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)?; + prune_managed_descriptions_after_fd_mutation( + bridge, + vm_id, + socket_paths, + vm, + kernel_pid, + description_id.into_iter(), + )?; + Ok(HostCallReply::Json(Value::Null)) +} + +pub(super) fn dispatch_context_closefrom_with_managed_retirement( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + min_fd: u32, + exact_fds: Option>, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated closefrom VM remains registered"), + )?; + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let bridge = sidecar.bridge.clone(); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated closefrom VM remains registered"); + let process = vm + .active_processes + .get(process_id) + .expect("validated closefrom process remains registered"); + let kernel_pid = process.kernel_pid; + let response = closefrom_with_managed_retirement( + &bridge, + vm_id, + &socket_paths, + vm, + kernel_pid, + min_fd, + exact_fds, + ); + match response { + Ok(response) => reply.succeed(response).map_err(SidecarError::from), + Err(error) => reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from), + } +} + +pub(in crate::execution) fn closefrom_with_managed_retirement( + bridge: &SharedBridge, + vm_id: &str, + socket_paths: &SocketPathContext, + vm: &mut VmState, + kernel_pid: u32, + min_fd: u32, + exact_fds: Option>, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if let (Some(fds), Some(limit)) = (exact_fds.as_ref(), vm.kernel.resource_limits().max_open_fds) + { + if fds.len() > limit { + return Err(SidecarError::host( + "E2BIG", + format!( + "fd_closefrom canonical target list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + fds.len() + ), + )); + } + } + let exact_set = exact_fds + .as_ref() + .map(|fds| fds.as_slice().iter().copied().collect::>()); + let candidate_descriptions = match vm + .kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error) + { + Ok(snapshot) => snapshot + .into_iter() + .filter_map(|entry| { + exact_set + .as_ref() + .map_or(entry.fd >= min_fd, |fds| fds.contains(&entry.fd)) + .then_some(entry.description_id) + }) + .collect::>(), + Err(error) => return Err(error), + }; + + // `fd_close_from` removes all matching table entries before it performs + // fallible resource cleanup. Always reconcile managed routes from the + // post-mutation alias counts, including that cleanup-error path. + let close_result = if let Some(fds) = exact_fds { + vm.kernel + .fd_close_exact(EXECUTION_DRIVER_NAME, kernel_pid, fds.into_vec()) + } else { + vm.kernel + .fd_close_from(EXECUTION_DRIVER_NAME, kernel_pid, min_fd) + } + .map_err(kernel_error); + let prune_result = prune_managed_descriptions_after_fd_mutation( + &bridge, + vm_id, + &socket_paths, + vm, + kernel_pid, + candidate_descriptions, + ); + match (close_result, prune_result) { + (Ok(closed_fds), Ok(())) => Ok(HostCallReply::Json(json!({ + "closedFds": closed_fds, + }))), + (Err(error), _) | (Ok(_), Err(error)) => Err(error), + } +} + +pub(super) fn dispatch_context_descriptor_replacement_with_managed_retirement( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: FilesystemOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated descriptor-mutation VM remains registered"), + )?; + let bridge = sidecar.bridge.clone(); + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated descriptor-mutation VM remains registered"); + let process = vm + .active_processes + .get(process_id) + .expect("validated descriptor-mutation process remains registered"); + let pid = process.kernel_pid; + let result = replace_descriptor_with_managed_retirement( + &bridge, + vm_id, + &socket_paths, + vm, + pid, + operation, + ); + match result { + Ok(response) => reply.succeed(response), + Err(error) => reply.fail(host_service_error(&error)), + } + .map_err(SidecarError::from) +} + +pub(in crate::execution) fn replace_descriptor_with_managed_retirement( + bridge: &SharedBridge, + vm_id: &str, + socket_paths: &SocketPathContext, + vm: &mut VmState, + pid: u32, + operation: FilesystemOperation, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let replaced_description_id = match operation { + FilesystemOperation::Renumber { to, .. } + | FilesystemOperation::DuplicateTo { target_fd: to, .. } => vm + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, pid, to) + .ok() + .map(|identity| identity.0), + FilesystemOperation::Move { + replaced_fd: Some(to), + .. + } => vm + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, pid, to) + .ok() + .map(|identity| identity.0), + FilesystemOperation::Move { + replaced_fd: None, .. + } => None, + _ => { + return Err(SidecarError::host( + "EINVAL", + "invalid descriptor replacement operation", + )) + } + }; + let result = match operation { + FilesystemOperation::Renumber { from, to } => vm + .kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, from, to) + .and_then(|()| vm.kernel.fd_close(EXECUTION_DRIVER_NAME, pid, from)) + .map(|()| Value::Null), + FilesystemOperation::DuplicateTo { fd, target_fd } => vm + .kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, fd, target_fd) + .map(|()| Value::Null), + FilesystemOperation::Move { fd, replaced_fd } => vm + .kernel + .fd_renumber_projection(EXECUTION_DRIVER_NAME, pid, fd, replaced_fd) + .map(Value::from), + _ => unreachable!("validated descriptor replacement operation"), + } + .map_err(kernel_error); + match result { + Ok(value) => { + prune_managed_descriptions_after_fd_mutation( + bridge, + vm_id, + socket_paths, + vm, + pid, + replaced_description_id.into_iter(), + )?; + Ok(HostCallReply::Json(value)) + } + Err(error) => Err(error), + } +} + +fn prune_managed_descriptions_after_fd_mutation( + bridge: &SharedBridge, + vm_id: &str, + socket_paths: &SocketPathContext, + vm: &mut VmState, + kernel_pid: u32, + candidates: impl IntoIterator, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let candidates = candidates.into_iter().collect::>(); + if candidates.is_empty() { + return Ok(()); + } + let alias_counts = candidates + .into_iter() + .map(|description_id| { + vm.kernel + .fd_description_alias_count(EXECUTION_DRIVER_NAME, kernel_pid, description_id) + .map(|aliases| (description_id, aliases)) + .map_err(kernel_error) + }) + .collect::, _>>()?; + let (retired_routes, retired) = { + let mut descriptions = vm + .managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))?; + prune_managed_registry_routes(&mut descriptions, kernel_pid, alias_counts) + }; + for description in retired_routes { + retire_managed_description_routes(bridge, vm_id, socket_paths, vm, description); + } + for description in retired { + retire_managed_description_routes(bridge, vm_id, socket_paths, vm, description); + } + Ok(()) +} + +fn prune_managed_registry_routes( + descriptions: &mut BTreeMap, + kernel_pid: u32, + alias_counts: impl IntoIterator, +) -> ( + Vec, + Vec, +) { + let mut retired_routes = Vec::new(); + let mut retired_descriptions = Vec::new(); + for (description_id, process_aliases) in alias_counts { + if process_aliases == 0 { + if let Some(description) = descriptions.get_mut(&description_id) { + if let Some(route) = description.routes.remove(&kernel_pid) { + let mut route_description = description.clone(); + route_description.routes.clear(); + route_description.routes.insert(kernel_pid, route); + retired_routes.push(route_description); + } + } + } + if descriptions + .get(&description_id) + .is_some_and(|description| description.lease.ref_count() == 1) + { + if let Some(description) = descriptions.remove(&description_id) { + retired_descriptions.push(description); + } + } + } + (retired_routes, retired_descriptions) +} + +fn active_process_by_kernel_pid_mut( + processes: &mut BTreeMap, + kernel_pid: u32, +) -> Option<&mut ActiveProcess> { + for process in processes.values_mut() { + if process.kernel_pid == kernel_pid { + return Some(process); + } + if let Some(found) = + active_process_by_kernel_pid_mut(&mut process.child_processes, kernel_pid) + { + return Some(found); + } + } + None +} + +fn retire_managed_description_routes( + bridge: &SharedBridge, + vm_id: &str, + socket_paths: &SocketPathContext, + vm: &mut VmState, + description: ManagedHostNetDescription, +) where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let capabilities = vm.capabilities.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let dns = vm.dns.clone(); + for (kernel_pid, route) in description.routes { + let Some(process) = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + else { + continue; + }; + let result = match route { + ManagedHostNetRoute::Unbound => Ok(Value::Null.into()), + ManagedHostNetRoute::TcpBound { reservation_id } => { + process.tcp_port_reservations.remove(&reservation_id); + Ok(Value::Null.into()) + } + ManagedHostNetRoute::TcpSocket(socket_id) + | ManagedHostNetRoute::UnixSocket(socket_id) => { + service_managed_network_operation( + ManagedNetworkServiceContext { + vm_id, + socket_paths, + kernel: &mut vm.kernel, + kernel_readiness: kernel_readiness.clone(), + process, + capabilities: capabilities.clone(), + }, + NetworkOperation::ManagedDestroy { + socket_id: match bounded_managed_id(socket_id) { + Ok(id) => id, + Err(error) => { + eprintln!("ERR_AGENTOS_SOCKET_CLEANUP: invalid retired socket id: {error}"); + continue; + } + }, + }, + ) + } + ManagedHostNetRoute::TcpListener(listener_id) + | ManagedHostNetRoute::UnixListener(listener_id) + | ManagedHostNetRoute::UnixBound { listener_id } => { + service_managed_network_operation( + ManagedNetworkServiceContext { + vm_id, + socket_paths, + kernel: &mut vm.kernel, + kernel_readiness: kernel_readiness.clone(), + process, + capabilities: capabilities.clone(), + }, + NetworkOperation::ManagedCloseListener { + listener_id: match bounded_managed_id(listener_id) { + Ok(id) => id, + Err(error) => { + eprintln!("ERR_AGENTOS_SOCKET_CLEANUP: invalid retired listener id: {error}"); + continue; + } + }, + }, + ) + } + ManagedHostNetRoute::UdpSocket(socket_id) => service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge, + kernel: &mut vm.kernel, + vm_id, + dns: &dns, + socket_paths, + process, + kernel_readiness: kernel_readiness.clone(), + capabilities: capabilities.clone(), + }, + NetworkOperation::ManagedUdpClose { + socket_id: match bounded_managed_id(socket_id) { + Ok(id) => id, + Err(error) => { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: invalid retired UDP id: {error}" + ); + continue; + } + }, + }, + ), + }; + match result { + Ok(HostServiceResponse::Deferred { receiver, task_class, .. }) => { + if let Err(error) = vm.runtime_context.spawn(task_class, async move { + if let Err(error) = receiver.await.unwrap_or_else(|_| { + Err(DeferredRpcError { + code: "ECANCELED".to_owned(), + message: "retired network cleanup completion channel closed".to_owned(), + details: None, + }) + }) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: deferred retired network cleanup failed: {}: {}", + error.code, error.message + ); + } + }) { + eprintln!("ERR_AGENTOS_SOCKET_CLEANUP: failed to schedule retired cleanup: {error}"); + } + } + Ok(_) => {} + Err(error) => eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to retire managed description route: {error}" + ), + } + } +} + +pub(in crate::execution) fn retire_managed_process_routes( + bridge: &SharedBridge, + vm_id: &str, + vm: &mut VmState, + kernel_pid: u32, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let socket_paths = build_socket_path_context(vm)?; + let retired_routes = { + let mut descriptions = vm + .managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))?; + descriptions + .values_mut() + .filter_map(|description| { + let route = description.routes.remove(&kernel_pid)?; + let mut route_description = description.clone(); + route_description.routes.clear(); + route_description.routes.insert(kernel_pid, route); + Some(route_description) + }) + .collect::>() + }; + for description in retired_routes { + retire_managed_description_routes(bridge, vm_id, &socket_paths, vm, description); + } + Ok(()) +} + +pub(in crate::execution) fn prune_managed_process_routes_without_aliases( + bridge: &SharedBridge, + vm_id: &str, + vm: &mut VmState, + kernel_pid: u32, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let candidates = vm + .managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .iter() + .filter_map(|(description_id, description)| { + description + .routes + .contains_key(&kernel_pid) + .then_some(*description_id) + }) + .collect::>(); + let socket_paths = build_socket_path_context(vm)?; + prune_managed_descriptions_after_fd_mutation( + bridge, + vm_id, + &socket_paths, + vm, + kernel_pid, + candidates, + ) +} + +pub(in crate::execution) fn retire_orphaned_managed_descriptions( + vm: &mut VmState, +) -> Result<(), SidecarError> { + vm.managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .retain(|_, description| { + !(description.routes.is_empty() && description.lease.ref_count() == 1) + }); + Ok(()) +} + +#[derive(serde::Deserialize)] +struct ManagedConnectWire { + #[serde(default)] + host: Option, + #[serde(default)] + port: Option, + #[serde(default)] + path: Option, + #[serde(rename = "abstractPathHex", default)] + abstract_path_hex: Option, + #[serde(rename = "boundServerId", default)] + bound_server_id: Option, + #[serde(rename = "localAddress", default)] + local_address: Option, + #[serde(rename = "localPort", default)] + local_port: Option, + #[serde(rename = "localReservation", default)] + local_reservation: Option, +} + +#[derive(serde::Deserialize)] +struct ManagedBindConnectedUnixWire { + #[serde(rename = "socketId")] + socket_id: String, + #[serde(default)] + path: Option, + #[serde(rename = "abstractPathHex", default)] + abstract_path_hex: Option, + #[serde(default)] + autobind: bool, +} + +#[derive(serde::Deserialize)] +struct ManagedReserveTcpPortWire { + #[serde(default)] + host: Option, + #[serde(default)] + port: Option, +} + +#[derive(serde::Deserialize)] +struct ManagedListenWire { + #[serde(default)] + host: Option, + #[serde(default)] + port: Option, + #[serde(default)] + path: Option, + #[serde(rename = "abstractPathHex", default)] + abstract_path_hex: Option, + #[serde(rename = "boundServerId", default)] + bound_server_id: Option, + #[serde(default)] + autobind: bool, + #[serde(default)] + backlog: Option, + #[serde(rename = "localReservation", default)] + local_reservation: Option, +} + +#[derive(serde::Deserialize)] +struct ManagedUdpCreateWire { + #[serde(rename = "type")] + socket_type: String, +} + +#[derive(serde::Deserialize)] +struct ManagedUdpBindWire { + #[serde(default)] + address: Option, + #[serde(default)] + port: u16, +} + +#[derive(serde::Deserialize)] +struct ManagedUdpSendWire { + #[serde(default)] + address: Option, + #[serde(default)] + port: Option, +} + +pub(super) fn decode_managed( + request: &HostRpcRequest, + max_payload_bytes: usize, +) -> Result, SidecarError> { + let id_limit = PayloadLimit::new("runtime.network.maxCapabilityIdBytes", MANAGED_ID_BYTES) + .map_err(SidecarError::Host)?; + let host_limit = PayloadLimit::new("runtime.network.maxHostBytes", HOST_BYTES) + .map_err(SidecarError::Host)?; + let path_limit = PayloadLimit::new("runtime.network.maxUnixPathBytes", UNIX_PATH_BYTES) + .map_err(SidecarError::Host)?; + let payload_limit = + PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_payload_bytes) + .map_err(SidecarError::Host)?; + let id = |index, label| bounded_arg(request, index, label, &id_limit); + let host = |value: Option| bounded_optional(value, &host_limit); + let unix = |path, abstract_hex, autobind| { + decode_unix_address(path, abstract_hex, autobind, &path_limit) + }; + let operation = match request.method.as_str() { + "net.bind_unix" => { + let payload: ManagedListenWire = decode_value_arg(request, 0, "net.bind_unix")?; + NetworkOperation::ManagedBindUnix { + address: unix(payload.path, payload.abstract_path_hex, payload.autobind)?, + } + } + "net.bind_connected_unix" => { + let payload: ManagedBindConnectedUnixWire = + decode_value_arg(request, 0, "net.bind_connected_unix")?; + NetworkOperation::ManagedBindConnectedUnix { + socket_id: BoundedString::try_new(payload.socket_id, &id_limit) + .map_err(SidecarError::Host)?, + address: unix(payload.path, payload.abstract_path_hex, payload.autobind)?, + } + } + "net.reserve_tcp_port" => { + let payload: ManagedReserveTcpPortWire = + decode_value_arg(request, 0, "net.reserve_tcp_port")?; + NetworkOperation::ManagedReserveTcpPort { + host: host(payload.host)?, + port: payload.port, + } + } + "net.release_tcp_port" => NetworkOperation::ManagedReleaseTcpPort { + reservation_id: id(0, "reservation id")?, + }, + "net.connect" => { + let payload: ManagedConnectWire = decode_value_arg(request, 0, "net.connect")?; + NetworkOperation::ManagedConnect { + endpoint: ManagedTcpEndpoint { + host: host(payload.host)?, + port: payload.port, + unix: decode_optional_unix( + payload.path, + payload.abstract_path_hex, + false, + &path_limit, + )?, + bound_server_id: bounded_optional(payload.bound_server_id, &id_limit)?, + local_address: host(payload.local_address)?, + local_port: payload.local_port, + local_reservation: bounded_optional(payload.local_reservation, &id_limit)?, + backlog: None, + }, + } + } + "net.listen" => { + let payload: ManagedListenWire = decode_value_or_json_arg(request, 0, "net.listen")?; + NetworkOperation::ManagedListen { + endpoint: ManagedTcpEndpoint { + host: host(payload.host)?, + port: payload.port, + unix: decode_optional_unix( + payload.path, + payload.abstract_path_hex, + payload.autobind, + &path_limit, + )?, + bound_server_id: bounded_optional(payload.bound_server_id, &id_limit)?, + local_address: None, + local_port: None, + local_reservation: bounded_optional(payload.local_reservation, &id_limit)?, + backlog: payload.backlog, + }, + } + } + "net.poll" => NetworkOperation::ManagedPoll { + socket_id: id(0, "socket id")?, + wait_ms: optional_u64(request, 1, "wait ms")?, + }, + "net.socket_wait_connect" => NetworkOperation::ManagedWaitConnect { + socket_id: id(0, "socket id")?, + }, + "net.socket_read" => NetworkOperation::ManagedRead { + socket_id: id(0, "socket id")?, + max_bytes: optional_u64(request, 1, "maximum read bytes")?, + peek: optional_bool(request, 2, "peek flag")?, + wait_ms: optional_u64(request, 3, "wait ms")?, + }, + "net.write" => NetworkOperation::ManagedWrite { + socket_id: id(0, "socket id")?, + bytes: bounded_bytes_arg(request, 1, "net.write bytes", &payload_limit)?, + }, + "net.destroy" => NetworkOperation::ManagedDestroy { + socket_id: id(0, "socket id")?, + }, + "net.server_accept" => NetworkOperation::ManagedAccept { + listener_id: id(0, "listener id")?, + }, + "net.server_close" => NetworkOperation::ManagedCloseListener { + listener_id: id(0, "listener id")?, + }, + "net.socket_upgrade_tls" => { + let options = javascript_sync_rpc_arg_str(&request.args, 1, "TLS options")?; + serde_json::from_str::(options).map_err(|error| { + SidecarError::host("EINVAL", format!("invalid TLS options: {error}")) + })?; + NetworkOperation::ManagedTlsUpgrade { + socket_id: id(0, "socket id")?, + options_json: BoundedString::try_new(options.to_owned(), &payload_limit) + .map_err(SidecarError::Host)?, + } + } + "dgram.createSocket" => { + let payload: ManagedUdpCreateWire = decode_value_arg(request, 0, "dgram.createSocket")?; + NetworkOperation::ManagedUdpCreate { + family: match payload.socket_type.as_str() { + "udp4" => ManagedUdpFamily::Inet4, + "udp6" => ManagedUdpFamily::Inet6, + other => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported UDP type {other}"), + )) + } + }, + } + } + "dgram.bind" => { + let payload: ManagedUdpBindWire = decode_value_arg(request, 1, "dgram.bind")?; + NetworkOperation::ManagedUdpBind { + socket_id: id(0, "socket id")?, + host: host(payload.address)?, + port: payload.port, + } + } + "dgram.send" => { + let payload: ManagedUdpSendWire = decode_value_arg(request, 2, "dgram.send")?; + NetworkOperation::ManagedUdpSend { + socket_id: id(0, "socket id")?, + bytes: bounded_bytes_arg(request, 1, "UDP bytes", &payload_limit)?, + host: host(payload.address)?, + port: payload.port, + } + } + "dgram.poll" => NetworkOperation::ManagedUdpPoll { + socket_id: id(0, "socket id")?, + wait_ms: optional_u64(request, 1, "wait ms")?, + peek: optional_bool(request, 2, "peek flag")?, + max_bytes: None, + }, + "dgram.close" => NetworkOperation::ManagedUdpClose { + socket_id: id(0, "socket id")?, + }, + _ => return Ok(None), + }; + Ok(Some(operation)) +} + +fn bounded_arg( + request: &HostRpcRequest, + index: usize, + label: &str, + limit: &PayloadLimit, +) -> Result { + BoundedString::try_new( + javascript_sync_rpc_arg_str(&request.args, index, label)?.to_owned(), + limit, + ) + .map_err(SidecarError::Host) +} +fn bounded_optional( + value: Option, + limit: &PayloadLimit, +) -> Result, SidecarError> { + value + .map(|value| BoundedString::try_new(value, limit).map_err(SidecarError::Host)) + .transpose() +} +fn bounded_bytes_arg( + request: &HostRpcRequest, + index: usize, + label: &str, + limit: &PayloadLimit, +) -> Result { + BoundedBytes::try_new( + javascript_sync_rpc_request_bytes_arg(request, index, label)?, + limit, + ) + .map_err(SidecarError::Host) +} +fn optional_u64(request: &HostRpcRequest, index: usize, label: &str) -> Result { + Ok(javascript_sync_rpc_arg_u64_optional(&request.args, index, label)?.unwrap_or_default()) +} +fn optional_bool( + request: &HostRpcRequest, + index: usize, + label: &str, +) -> Result { + match request.args.get(index) { + None | Some(Value::Null) => Ok(false), + Some(Value::Bool(value)) => Ok(*value), + Some(_) => Err(SidecarError::host( + "EINVAL", + format!("{label} must be a boolean"), + )), + } +} +fn decode_value_arg( + request: &HostRpcRequest, + index: usize, + label: &str, +) -> Result { + serde_json::from_value( + request + .args + .get(index) + .cloned() + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} payload is required")))?, + ) + .map_err(|error| SidecarError::host("EINVAL", format!("invalid {label} payload: {error}"))) +} +fn decode_value_or_json_arg( + request: &HostRpcRequest, + index: usize, + label: &str, +) -> Result { + let value = request + .args + .get(index) + .cloned() + .ok_or_else(|| SidecarError::host("EINVAL", format!("{label} payload is required")))?; + match value { + Value::String(json) => serde_json::from_str(&json), + other => serde_json::from_value(other), + } + .map_err(|error| SidecarError::host("EINVAL", format!("invalid {label} payload: {error}"))) +} +fn decode_optional_unix( + path: Option, + abstract_hex: Option, + autobind: bool, + limit: &PayloadLimit, +) -> Result, SidecarError> { + if path.is_none() && abstract_hex.is_none() && !autobind { + Ok(None) + } else { + decode_unix_address(path, abstract_hex, autobind, limit).map(Some) + } +} +fn decode_unix_address( + path: Option, + abstract_hex: Option, + autobind: bool, + limit: &PayloadLimit, +) -> Result { + match (path, abstract_hex, autobind) { + (Some(path), None, false) => BoundedString::try_new(path, limit) + .map(ManagedUnixAddress::Path) + .map_err(SidecarError::Host), + (None, Some(hex), false) => BoundedString::try_new(hex, limit) + .map(ManagedUnixAddress::AbstractHex) + .map_err(SidecarError::Host), + (None, None, true) => Ok(ManagedUnixAddress::Autobind), + _ => Err(SidecarError::host( + "EINVAL", + "exactly one Unix address is required", + )), + } +} + +#[allow(dead_code)] +pub(super) struct NetworkCapability; + +fn dispatch_context_stream_read( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + socket_id: String, + max_bytes: u64, + peek: bool, + wait_ms: u64, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let deadline = match checked_deferred_guest_wait_deadline(wait_ms) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + }; + dispatch_claimed_context_stream_read( + sidecar, + vm_id, + process_id, + ManagedStreamReadRecheck { + root_process_id: process_id.to_owned(), + process_path: Vec::new(), + socket_id, + max_bytes, + peek, + deadline, + reply, + }, + ) +} + +pub(in crate::execution) fn dispatch_descendant_context_stream_read( + sidecar: &mut NativeSidecar, + vm_id: &str, + root_process_id: &str, + process_path: &[&str], + socket_id: String, + max_bytes: u64, + peek: bool, + wait_ms: u64, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let deadline = match checked_deferred_guest_wait_deadline(wait_ms) { + Ok(deadline) => deadline, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(()); + } + }; + dispatch_claimed_context_stream_read( + sidecar, + vm_id, + root_process_id, + ManagedStreamReadRecheck { + root_process_id: root_process_id.to_owned(), + process_path: process_path + .iter() + .map(|segment| (*segment).to_owned()) + .collect(), + socket_id, + max_bytes, + peek, + deadline, + reply, + }, + ) +} + +pub(in crate::execution) fn dispatch_claimed_context_stream_read( + sidecar: &mut NativeSidecar, + vm_id: &str, + root_process_id: &str, + pending: ManagedStreamReadRecheck, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if pending.root_process_id != root_process_id { + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "managed stream read re-entry used the wrong root process lane", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?, + )?; + let (runtime, connection_id, session_id, notify, response) = { + let vm = sidecar + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get_mut(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = + NativeSidecar::::active_process_by_owned_path_mut(root, &pending.process_path) + .ok_or_else(|| { + SidecarError::host("ESTALE", "managed stream read target process disappeared") + })?; + let identity = pending.reply.identity(); + if identity.generation != vm.generation || identity.pid != process.kernel_pid { + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "managed stream read identity no longer matches its process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + let notify = if let Some(socket) = process.tcp_sockets.get(&pending.socket_id) { + Arc::clone(&socket.read_event_notify) + } else if let Some(socket) = process.unix_sockets.get(&pending.socket_id) { + Arc::clone(&socket.read_event_notify) + } else { + pending + .reply + .fail(HostServiceError::new( + "EBADF", + format!("unknown managed socket {}", pending.socket_id), + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let connection_id = vm.connection_id.clone(); + let session_id = vm.session_id.clone(); + let capabilities = vm.capabilities.clone(); + let response = service_managed_network_operation( + ManagedNetworkServiceContext { + vm_id, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness: Arc::clone(&vm.kernel_socket_readiness), + process, + capabilities, + }, + NetworkOperation::ManagedRead { + socket_id: bounded_managed_id(pending.socket_id.clone())?, + max_bytes: pending.max_bytes, + peek: pending.peek, + wait_ms: 0, + }, + ); + (runtime, connection_id, session_id, notify, response) + }; + let would_block = matches!( + response.as_ref().ok().and_then(response_value), + Some(Value::Object(fields)) if fields.get("kind").and_then(Value::as_str) == Some("wouldBlock") + ); + if !would_block || Instant::now() >= pending.deadline { + return settle_execution_host_call(&pending.reply, response); + } + + let sender = sidecar.process_event_sender.clone(); + let event_notify = Arc::clone(&sidecar.process_event_notify); + let vm_id_owned = vm_id.to_owned(); + let root_process_id_owned = root_process_id.to_owned(); + let task_reply = pending.reply.clone(); + let spawn = runtime.spawn(agentos_runtime::TaskClass::Socket, async move { + let remaining = pending.deadline.saturating_duration_since(Instant::now()); + if !remaining.is_zero() { + tokio::select! { + _ = notify.notified() => {} + _ = tokio::time::sleep(remaining) => {} + } + } + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: vm_id_owned, + process_id: root_process_id_owned, + event: ActiveExecutionEvent::ManagedStreamReadRecheck(Box::new(pending)), + }; + if let Err(error) = sender.send(envelope).await { + if let ActiveExecutionEvent::ManagedStreamReadRecheck(pending) = error.0.event { + if let Err(error) = pending.reply.fail(HostServiceError::new( + "ECANCELED", + "managed stream read re-entry lane closed", + )) { + eprintln!( + "ERR_AGENTOS_HOST_REPLY_SETTLEMENT: failed to cancel managed stream read: {error}" + ); + } + } + } else { + event_notify.notify_one(); + } + }); + if let Err(error) = spawn { + task_reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + Ok(()) +} + +pub(super) async fn dispatch_context_managed_network_operation( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + if let NetworkOperation::ManagedRead { + socket_id, + max_bytes, + peek, + wait_ms, + } = &operation + { + return dispatch_context_stream_read( + sidecar, + vm_id, + process_id, + socket_id.as_str().to_owned(), + *max_bytes, + *peek, + *wait_ms, + reply, + ); + } + if let NetworkOperation::Receive { + fd, + max_bytes, + flags, + deadline_ms, + .. + } = &operation + { + let vm = sidecar + .vms + .get(vm_id) + .expect("validated fd-network VM remains registered"); + let process = vm + .active_processes + .get(process_id) + .expect("validated fd-network process remains registered"); + let description_id = vm + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, process.kernel_pid, *fd) + .map_err(kernel_error)? + .0; + let route = vm + .managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .get(&description_id) + .and_then(|description| description.route_for(process.kernel_pid).cloned()); + if let Some(ManagedHostNetRoute::UdpSocket(socket_id)) = route { + return dispatch_context_udp_poll( + sidecar, + vm_id, + process_id, + NetworkOperation::ManagedUdpPoll { + socket_id: bounded_managed_id(socket_id)?, + wait_ms: deadline_ms.unwrap_or_default(), + peek: flags & 0x0002 != 0, + max_bytes: Some(*max_bytes), + }, + reply, + ); + } + if let Some( + ManagedHostNetRoute::TcpSocket(socket_id) | ManagedHostNetRoute::UnixSocket(socket_id), + ) = route + { + return dispatch_context_stream_read( + sidecar, + vm_id, + process_id, + socket_id, + max_bytes.get() as u64, + flags & 0x0002 != 0, + deadline_ms.unwrap_or_default(), + reply, + ); + } + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + if is_managed_fd_operation(&operation) { + let bridge = sidecar.bridge.clone(); + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated fd-network VM remains registered"), + )?; + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated fd-network VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let dns = vm.dns.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated fd-network process remains registered"); + let response = service_managed_fd_network_operation( + ManagedFdNetworkServiceContext { + bridge: &bridge, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + capabilities, + managed_descriptions, + call_id: reply.identity().call_id, + }, + operation, + ); + return settle_managed_network_response( + sidecar, + vm_id, + process_id, + runtime, + reply, + "managed fd network", + response, + ); + } + if matches!( + operation, + NetworkOperation::ManagedUdpCreate { .. } + | NetworkOperation::ManagedUdpBind { .. } + | NetworkOperation::ManagedUdpSend { .. } + | NetworkOperation::ManagedUdpClose { .. } + ) { + let bridge = sidecar.bridge.clone(); + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated managed UDP VM remains registered"), + )?; + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated managed UDP VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let dns = vm.dns.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated managed UDP process remains registered"); + let response = service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge: &bridge, + kernel: &mut vm.kernel, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + process, + kernel_readiness, + capabilities, + }, + operation, + ); + return settle_managed_network_response( + sidecar, + vm_id, + process_id, + runtime, + reply, + "managed UDP", + response, + ); + } + if matches!( + operation, + NetworkOperation::ManagedPoll { .. } + | NetworkOperation::ManagedWaitConnect { .. } + | NetworkOperation::ManagedRead { .. } + | NetworkOperation::ManagedWrite { .. } + | NetworkOperation::ManagedDestroy { .. } + | NetworkOperation::ManagedAccept { .. } + | NetworkOperation::ManagedCloseListener { .. } + | NetworkOperation::ManagedTlsUpgrade { .. } + ) { + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated managed-network VM remains registered"), + )?; + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated managed-network VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated managed-network process remains registered"); + let response = service_managed_network_operation( + ManagedNetworkServiceContext { + vm_id, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + capabilities, + }, + operation, + ); + return settle_managed_network_response( + sidecar, + vm_id, + process_id, + runtime, + reply, + "managed network", + response, + ); + } + if is_managed_endpoint_operation(&operation) { + let bridge = sidecar.bridge.clone(); + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated managed-endpoint VM remains registered"), + )?; + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated managed-endpoint VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let dns = vm.dns.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated managed-endpoint process remains registered"); + let response = service_managed_endpoint_operation( + ManagedEndpointServiceContext { + bridge: &bridge, + vm_id, + dns: &dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + capabilities, + call_id: reply.identity().call_id, + }, + operation, + ); + return settle_managed_network_response( + sidecar, + vm_id, + process_id, + runtime, + reply, + "managed endpoint", + response, + ); + } + if is_direct_managed_operation(&operation) { + return Err(SidecarError::host( + "EINVAL", + "typed managed-network operation missed its direct executor", + )); + } + let bridge = sidecar.bridge.clone(); + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated managed-network VM remains registered"), + )?; + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated managed-network VM remains registered"); + let runtime = vm.runtime_context.clone(); + let capabilities = vm.capabilities.clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let dns = vm.dns.clone(); + let process = vm + .active_processes + .get_mut(process_id) + .expect("validated managed-network process remains registered"); + let response = service_descriptor_rights_compat_operation( + &bridge, + vm_id, + &dns, + &socket_paths, + &mut vm.kernel, + kernel_readiness, + process, + capabilities, + Arc::clone(&vm.managed_host_net_descriptions), + reply.identity().call_id, + operation, + ) + .await; + + settle_managed_network_response( + sidecar, + vm_id, + process_id, + runtime, + reply, + "descriptor rights", + response, + ) +} + +struct ManagedFdNetworkServiceContext<'a, B> { + bridge: &'a SharedBridge, + vm_id: &'a str, + dns: &'a VmDnsConfig, + socket_paths: &'a SocketPathContext, + kernel: &'a mut SidecarKernel, + kernel_readiness: KernelSocketReadinessRegistry, + process: &'a mut ActiveProcess, + capabilities: CapabilityRegistry, + managed_descriptions: crate::state::ManagedHostNetDescriptionRegistry, + call_id: u64, +} + +#[allow(clippy::too_many_arguments)] +pub(in crate::execution) fn service_descendant_managed_fd_network_operation( + bridge: &SharedBridge, + vm_id: &str, + dns: &VmDnsConfig, + socket_paths: &SocketPathContext, + kernel: &mut SidecarKernel, + kernel_readiness: KernelSocketReadinessRegistry, + process: &mut ActiveProcess, + capabilities: CapabilityRegistry, + managed_descriptions: crate::state::ManagedHostNetDescriptionRegistry, + call_id: u64, + operation: NetworkOperation, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + service_managed_fd_network_operation( + ManagedFdNetworkServiceContext { + bridge, + vm_id, + dns, + socket_paths, + kernel, + kernel_readiness, + process, + capabilities, + managed_descriptions, + call_id, + }, + operation, + ) +} + +fn managed_fd_description_id( + context: &ManagedFdNetworkServiceContext<'_, B>, + fd: u32, +) -> Result { + let (description_id, _) = context + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, context.process.kernel_pid, fd) + .map_err(kernel_error)?; + if !context + .managed_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .contains_key(&description_id) + { + return Err(SidecarError::host( + "ENOTSOCK", + format!("fd {fd} is not a managed socket"), + )); + } + Ok(description_id) +} + +fn managed_description( + context: &ManagedFdNetworkServiceContext<'_, B>, + description_id: u64, +) -> Result { + context + .managed_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .get(&description_id) + .cloned() + .ok_or_else(|| SidecarError::host("ENOTSOCK", "managed socket description disappeared")) +} + +fn update_managed_description( + context: &ManagedFdNetworkServiceContext<'_, B>, + description_id: u64, + update: impl FnOnce(&mut ManagedHostNetDescription), +) -> Result<(), SidecarError> { + let mut descriptions = context + .managed_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))?; + let description = descriptions + .get_mut(&description_id) + .ok_or_else(|| SidecarError::host("ENOTSOCK", "managed socket description disappeared"))?; + update(description); + Ok(()) +} + +fn managed_route( + context: &ManagedFdNetworkServiceContext<'_, B>, + description_id: u64, +) -> Result { + managed_description(context, description_id)? + .route_for(context.process.kernel_pid) + .cloned() + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + "managed socket has no reactor projection for this process", + ) + }) +} + +fn managed_endpoint_context<'a, B>( + context: &'a mut ManagedFdNetworkServiceContext<'_, B>, +) -> ManagedEndpointServiceContext<'a, B> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + ManagedEndpointServiceContext { + bridge: context.bridge, + vm_id: context.vm_id, + dns: context.dns, + socket_paths: context.socket_paths, + kernel: &mut *context.kernel, + kernel_readiness: context.kernel_readiness.clone(), + process: &mut *context.process, + capabilities: context.capabilities.clone(), + call_id: context.call_id, + } +} + +fn managed_socket_context<'a, B>( + context: &'a mut ManagedFdNetworkServiceContext<'_, B>, +) -> ManagedNetworkServiceContext<'a> { + ManagedNetworkServiceContext { + vm_id: context.vm_id, + socket_paths: context.socket_paths, + kernel: &mut *context.kernel, + kernel_readiness: context.kernel_readiness.clone(), + process: &mut *context.process, + capabilities: context.capabilities.clone(), + } +} + +fn endpoint_for_address(address: &SocketAddress) -> ManagedTcpEndpoint { + match address { + SocketAddress::Inet { host, port } => ManagedTcpEndpoint { + host: Some(host.clone()), + port: Some(*port), + unix: None, + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + SocketAddress::UnixPath(path) => ManagedTcpEndpoint { + host: None, + port: None, + unix: Some(ManagedUnixAddress::Path(path.clone())), + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + SocketAddress::UnixAbstract(bytes) => ManagedTcpEndpoint { + host: None, + port: None, + unix: Some(ManagedUnixAddress::AbstractHex( + BoundedString::try_new( + encode_hex_bytes(bytes.as_slice()), + &PayloadLimit::new("runtime.filesystem.maxPathBytes", 8192) + .expect("static nonzero path limit"), + ) + .expect("bounded abstract address remains bounded after hex encoding"), + )), + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + SocketAddress::UnixAutobind => ManagedTcpEndpoint { + host: None, + port: None, + unix: Some(ManagedUnixAddress::Autobind), + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + } +} + +fn managed_unix_address(address: &SocketAddress) -> Result { + match endpoint_for_address(address).unix { + Some(address) => Ok(address), + None => Err(SidecarError::host( + "EAFNOSUPPORT", + "expected an AF_UNIX address", + )), + } +} + +fn response_value(response: &HostServiceResponse) -> Option { + let HostServiceResponse::Json(value) = response else { + return None; + }; + match value { + Value::String(encoded) => serde_json::from_str(encoded).ok(), + value => Some(value.clone()), + } +} + +fn response_string_field(response: &HostServiceResponse, field: &str) -> Option { + response_value(response)? + .get(field) + .and_then(Value::as_str) + .map(str::to_owned) +} + +pub(in crate::execution) fn managed_socket_address_from_info( + info: &Value, + peer: bool, +) -> Result, SidecarError> { + let (inet_address, inet_port, unix_path, unix_abstract) = if peer { + ( + "remoteAddress", + "remotePort", + "remotePath", + "remoteAbstractPathHex", + ) + } else { + ( + "localAddress", + "localPort", + "localPath", + "localAbstractPathHex", + ) + }; + if let (Some(host), Some(port)) = ( + info.get(inet_address).and_then(Value::as_str), + info.get(inet_port).and_then(Value::as_u64), + ) { + let port = u16::try_from(port) + .map_err(|_| SidecarError::host("EINVAL", "socket endpoint port exceeds u16"))?; + let host = BoundedString::try_new( + host.to_owned(), + &PayloadLimit::new("runtime.network.maxHostBytes", HOST_BYTES) + .expect("static nonzero host limit"), + ) + .map_err(SidecarError::from)?; + return Ok(Some(SocketAddress::Inet { host, port })); + } + if let Some(hex) = info.get(unix_abstract).and_then(Value::as_str) { + let bytes = decode_abstract_unix_name(hex)?; + return BoundedBytes::try_new( + bytes, + &PayloadLimit::new("runtime.network.maxUnixAddressBytes", UNIX_PATH_BYTES) + .expect("static nonzero Unix address limit"), + ) + .map(SocketAddress::UnixAbstract) + .map(Some) + .map_err(SidecarError::from); + } + if let Some(path) = info.get(unix_path).and_then(Value::as_str) { + let path = BoundedString::try_new( + path.to_owned(), + &PayloadLimit::new("runtime.network.maxUnixPathBytes", UNIX_PATH_BYTES) + .expect("static nonzero Unix path limit"), + ) + .map_err(SidecarError::from)?; + return Ok(Some(SocketAddress::UnixPath(path))); + } + Ok(None) +} + +fn response_socket_address( + response: &HostServiceResponse, + peer: bool, +) -> Result, SidecarError> { + response_value(response) + .as_ref() + .map(|value| managed_socket_address_from_info(value, peer)) + .transpose() + .map(Option::flatten) +} + +fn rollback_managed_stream_socket( + context: &mut ManagedFdNetworkServiceContext<'_, B>, + socket_id: &str, +) { + if let Some(socket) = context.process.tcp_sockets.remove(socket_id) { + release_tcp_socket_handle( + context.process, + socket_id, + socket, + context.kernel, + &context.kernel_readiness, + ); + } else if let Some(socket) = context.process.unix_sockets.remove(socket_id) { + release_unix_socket_handle( + context.process, + socket_id, + socket, + &context.socket_paths.unix_bound_addresses, + ); + } +} + +fn service_managed_fd_network_operation( + mut context: ManagedFdNetworkServiceContext<'_, B>, + operation: NetworkOperation, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + match operation { + NetworkOperation::Validate { fd, requirement } => { + if requirement == SocketValidationRequirement::Socket { + context + .kernel + .fd_validate_socket( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + false, + ) + .map_err(kernel_error)?; + return Ok(Value::Null.into()); + } + + let description_id = context + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, context.process.kernel_pid, fd) + .map_err(kernel_error)? + .0; + let managed_route = context + .managed_descriptions + .lock() + .map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })? + .get(&description_id) + .and_then(|description| description.route_for(context.process.kernel_pid)) + .cloned(); + match managed_route { + Some( + ManagedHostNetRoute::TcpListener(_) | ManagedHostNetRoute::UnixListener(_), + ) => Ok(Value::Null.into()), + Some(_) => Err(SidecarError::host( + "EINVAL", + format!("socket file descriptor {fd} is not listening"), + )), + None => { + context + .kernel + .fd_validate_socket( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + true, + ) + .map_err(kernel_error)?; + Ok(Value::Null.into()) + } + } + } + NetworkOperation::Socket { + domain, + kind, + nonblocking, + close_on_exec, + } => { + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let (fd, description_id) = context + .kernel + .fd_open_external_socket( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + kind == SocketKind::Datagram, + nonblocking, + close_on_exec, + ) + .map_err(kernel_error)?; + let lease = match context.kernel.fd_transfer( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + ) { + Ok(lease) => lease, + Err(error) => { + if let Err(close_error) = context.kernel.fd_close( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to roll back managed socket fd after transfer failure: {close_error}" + ); + } + return Err(kernel_error(error)); + } + }; + if descriptions.contains_key(&description_id) { + if let Err(close_error) = + context + .kernel + .fd_close(EXECUTION_DRIVER_NAME, context.process.kernel_pid, fd) + { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close duplicate managed socket description after registry collision: {close_error}" + ); + } + return Err(SidecarError::host( + "EEXIST", + "kernel reused a live managed socket description id", + )); + } + descriptions.insert( + description_id, + ManagedHostNetDescription::new(domain, kind, lease, context.process.kernel_pid), + ); + if kind == SocketKind::Datagram { + let family = if domain == HostSocketDomain::Inet6 { + ManagedUdpFamily::Inet6 + } else { + ManagedUdpFamily::Inet4 + }; + let existing_udp_ids = context + .process + .udp_sockets + .keys() + .cloned() + .collect::>(); + let created = match service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge: context.bridge, + kernel: &mut *context.kernel, + vm_id: context.vm_id, + dns: context.dns, + socket_paths: context.socket_paths, + process: &mut *context.process, + kernel_readiness: context.kernel_readiness.clone(), + capabilities: context.capabilities.clone(), + }, + NetworkOperation::ManagedUdpCreate { family }, + ) { + Ok(created) => created, + Err(error) => { + descriptions.remove(&description_id); + if let Err(close_error) = context.kernel.fd_close( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to roll back managed UDP fd after create failure: {close_error}" + ); + } + return Err(error); + } + }; + let socket_id = response_string_field(&created, "socketId").filter(|socket_id| { + context.process.udp_sockets.contains_key(socket_id) + && !existing_udp_ids.contains(socket_id) + }); + let Some(socket_id) = socket_id else { + let new_ids = context + .process + .udp_sockets + .keys() + .filter(|socket_id| !existing_udp_ids.contains(*socket_id)) + .cloned() + .collect::>(); + for socket_id in new_ids { + if let Some(socket) = context.process.udp_sockets.remove(&socket_id) { + if let Err(error) = release_udp_socket_handle( + context.process, + &socket_id, + socket, + context.kernel, + &context.kernel_readiness, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to roll back malformed UDP create result: {error}" + ); + } + } + } + descriptions.remove(&description_id); + if let Err(error) = context.kernel.fd_close( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + fd, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to roll back malformed managed UDP fd: {error}" + ); + } + return Err(SidecarError::host( + "EIO", + "UDP create returned an invalid socket id", + )); + }; + descriptions + .get_mut(&description_id) + .expect("new managed UDP description remains locked") + .routes + .insert( + context.process.kernel_pid, + ManagedHostNetRoute::UdpSocket(socket_id), + ); + } + Ok(json!({ + "fd": fd, + "descriptionId": description_id.to_string(), + }) + .into()) + } + NetworkOperation::Bind { fd, address } => { + let description_id = context + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, context.process.kernel_pid, fd) + .map_err(kernel_error)? + .0; + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let description = descriptions.get(&description_id).cloned().ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed socket description disappeared") + })?; + let current_route = description + .route_for(context.process.kernel_pid) + .cloned() + .ok_or_else(|| SidecarError::host("ESTALE", "managed socket route is missing"))?; + if current_route != ManagedHostNetRoute::Unbound + && !(description.kind == SocketKind::Datagram + && matches!(current_route, ManagedHostNetRoute::UdpSocket(_))) + { + return Err(SidecarError::host("EINVAL", "socket is already bound")); + } + let response = match (description.domain, description.kind) { + (HostSocketDomain::Unix, SocketKind::Stream) => service_managed_endpoint_operation( + managed_endpoint_context(&mut context), + NetworkOperation::ManagedBindUnix { + address: managed_unix_address(&address)?, + }, + )?, + (HostSocketDomain::Inet4 | HostSocketDomain::Inet6, SocketKind::Stream) => { + let SocketAddress::Inet { host, port } = &address else { + return Err(SidecarError::host( + "EAFNOSUPPORT", + "INET socket requires INET address", + )); + }; + service_managed_endpoint_operation( + managed_endpoint_context(&mut context), + NetworkOperation::ManagedReserveTcpPort { + host: Some(host.clone()), + port: Some(*port), + }, + )? + } + (HostSocketDomain::Inet4 | HostSocketDomain::Inet6, SocketKind::Datagram) => { + let ManagedHostNetRoute::UdpSocket(socket_id) = ¤t_route else { + return Err(SidecarError::host("EIO", "UDP socket route is missing")); + }; + let socket_id = socket_id.clone(); + let SocketAddress::Inet { host, port } = &address else { + return Err(SidecarError::host( + "EAFNOSUPPORT", + "UDP socket requires INET address", + )); + }; + let response = service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge: context.bridge, + kernel: &mut *context.kernel, + vm_id: context.vm_id, + dns: context.dns, + socket_paths: context.socket_paths, + process: &mut *context.process, + kernel_readiness: context.kernel_readiness.clone(), + capabilities: context.capabilities.clone(), + }, + NetworkOperation::ManagedUdpBind { + socket_id: bounded_managed_id(socket_id.clone())?, + host: Some(host.clone()), + port: *port, + }, + )?; + response + } + _ => return Err(SidecarError::host("EOPNOTSUPP", "unsupported socket bind")), + }; + let route = match (description.domain, description.kind) { + (HostSocketDomain::Unix, SocketKind::Stream) => ManagedHostNetRoute::UnixBound { + listener_id: response_string_field(&response, "serverId").ok_or_else(|| { + SidecarError::host("EIO", "Unix bind omitted listener id") + })?, + }, + (HostSocketDomain::Inet4 | HostSocketDomain::Inet6, SocketKind::Stream) => { + ManagedHostNetRoute::TcpBound { + reservation_id: response_string_field(&response, "reservationId") + .ok_or_else(|| { + SidecarError::host("EIO", "TCP bind omitted reservation id") + })?, + } + } + (_, SocketKind::Datagram) => { + let ManagedHostNetRoute::UdpSocket(id) = current_route else { + unreachable!("UDP bind validated its route") + }; + ManagedHostNetRoute::UdpSocket(id) + } + _ => unreachable!(), + }; + let local_address = + response_socket_address(&response, false)?.or_else(|| Some(address.clone())); + let kernel_pid = context.process.kernel_pid; + let description = descriptions + .get_mut(&description_id) + .expect("managed bind description remains locked"); + description.bound_address = Some(address); + description.local_address = local_address; + description.routes.insert(kernel_pid, route); + Ok(response) + } + NetworkOperation::Connect { fd, address, .. } => { + let description_id = managed_fd_description_id(&context, fd)?; + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let description = descriptions.get(&description_id).cloned().ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed socket description disappeared") + })?; + if description.kind != SocketKind::Stream { + return Err(SidecarError::host( + "EOPNOTSUPP", + "connect requires a stream socket", + )); + } + let mut endpoint = endpoint_for_address(&address); + match description + .route_for(context.process.kernel_pid) + .cloned() + .ok_or_else(|| SidecarError::host("ESTALE", "managed socket route is missing"))? + { + ManagedHostNetRoute::Unbound => {} + ManagedHostNetRoute::TcpBound { reservation_id } => { + endpoint.local_reservation = Some(bounded_managed_id(reservation_id)?); + if let Some(SocketAddress::Inet { host, port }) = + description.local_address.or(description.bound_address) + { + endpoint.local_address = Some(host); + endpoint.local_port = Some(port); + } + } + ManagedHostNetRoute::UnixBound { listener_id } => { + endpoint.bound_server_id = Some(bounded_managed_id(listener_id)?); + } + ManagedHostNetRoute::TcpListener(_) | ManagedHostNetRoute::UnixListener(_) => { + return Err(SidecarError::host( + "EINVAL", + "listening socket cannot connect", + )) + } + _ => return Err(SidecarError::host("EISCONN", "socket is already connected")), + } + let response = service_managed_endpoint_operation( + managed_endpoint_context(&mut context), + NetworkOperation::ManagedConnect { endpoint }, + )?; + if let Some(socket_id) = response_string_field(&response, "socketId") { + let route = if description.domain == HostSocketDomain::Unix { + ManagedHostNetRoute::UnixSocket(socket_id) + } else { + ManagedHostNetRoute::TcpSocket(socket_id) + }; + let local_address = response_socket_address(&response, false)?; + let peer_address = + response_socket_address(&response, true)?.or_else(|| Some(address.clone())); + let description = descriptions + .get_mut(&description_id) + .expect("managed connect description remains locked"); + description.routes.insert(context.process.kernel_pid, route); + description.local_address = local_address; + description.peer_address = peer_address; + } else if matches!(response, HostServiceResponse::Deferred { .. }) { + context + .process + .pending_managed_host_net_connects + .insert(context.call_id, description_id); + } + Ok(response) + } + NetworkOperation::Listen { fd, backlog } => { + let description_id = managed_fd_description_id(&context, fd)?; + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let description = descriptions.get(&description_id).cloned().ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed socket description disappeared") + })?; + let mut endpoint = match description.bound_address.as_ref() { + Some(address) => endpoint_for_address(address), + None if description.domain != HostSocketDomain::Unix => ManagedTcpEndpoint { + host: None, + port: Some(0), + unix: None, + bound_server_id: None, + local_address: None, + local_port: None, + local_reservation: None, + backlog: None, + }, + None => return Err(SidecarError::host("EINVAL", "Unix listener must be bound")), + }; + endpoint.backlog = Some(backlog); + match description + .route_for(context.process.kernel_pid) + .cloned() + .ok_or_else(|| SidecarError::host("ESTALE", "managed socket route is missing"))? + { + ManagedHostNetRoute::TcpBound { reservation_id } => { + endpoint.local_reservation = Some(bounded_managed_id(reservation_id)?); + } + ManagedHostNetRoute::UnixBound { listener_id } => { + endpoint = ManagedTcpEndpoint { + host: None, + port: None, + unix: None, + bound_server_id: Some(bounded_managed_id(listener_id)?), + local_address: None, + local_port: None, + local_reservation: None, + backlog: Some(backlog), + }; + } + ManagedHostNetRoute::UnixListener(listener_id) => { + return relisten_managed_unix_endpoint( + managed_endpoint_context(&mut context), + &listener_id, + backlog, + ) + } + ManagedHostNetRoute::Unbound => {} + _ => { + return Err(SidecarError::host( + "EINVAL", + "socket cannot enter listen state", + )) + } + } + let response = service_managed_endpoint_operation( + managed_endpoint_context(&mut context), + NetworkOperation::ManagedListen { endpoint }, + )?; + let listener_id = response_string_field(&response, "serverId") + .ok_or_else(|| SidecarError::host("EIO", "listen omitted listener id"))?; + let route = if description.domain == HostSocketDomain::Unix { + ManagedHostNetRoute::UnixListener(listener_id) + } else { + ManagedHostNetRoute::TcpListener(listener_id) + }; + let local_address = response_socket_address(&response, false)? + .or(description.local_address) + .or(description.bound_address); + let description = descriptions + .get_mut(&description_id) + .expect("managed listen description remains locked"); + description.routes.insert(context.process.kernel_pid, route); + description.local_address = local_address; + Ok(response) + } + NetworkOperation::Accept { + fd, + nonblocking, + close_on_exec, + .. + } => { + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let description_id = context + .kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, context.process.kernel_pid, fd) + .map_err(kernel_error)? + .0; + let parent = descriptions.get(&description_id).cloned().ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed listener description disappeared") + })?; + let parent_route = parent + .route_for(context.process.kernel_pid) + .cloned() + .ok_or_else(|| SidecarError::host("ESTALE", "managed listener route is missing"))?; + let listener_id = match &parent_route { + ManagedHostNetRoute::TcpListener(id) | ManagedHostNetRoute::UnixListener(id) => { + id.clone() + } + _ => { + return Err(SidecarError::host( + "EINVAL", + "accept requires a listening socket", + )) + } + }; + let response = service_managed_network_operation( + managed_socket_context(&mut context), + NetworkOperation::ManagedAccept { + listener_id: bounded_managed_id(listener_id)?, + }, + )?; + let Some(socket_id) = response_string_field(&response, "socketId") else { + return Ok(response); + }; + let mut value = match response_value(&response) { + Some(Value::Object(fields)) => Value::Object(fields), + _ => { + rollback_managed_stream_socket(&mut context, &socket_id); + return Err(SidecarError::host( + "EIO", + "accept returned an invalid response", + )); + } + }; + let address_info = value.get("info").unwrap_or(&value); + let local_address = match managed_socket_address_from_info(address_info, false) { + Ok(address) => address, + Err(error) => { + rollback_managed_stream_socket(&mut context, &socket_id); + return Err(error); + } + }; + let peer_address = match managed_socket_address_from_info(address_info, true) { + Ok(address) => address, + Err(error) => { + rollback_managed_stream_socket(&mut context, &socket_id); + return Err(error); + } + }; + let expected_socket_present = if parent.domain == HostSocketDomain::Unix { + context.process.unix_sockets.contains_key(&socket_id) + } else { + context.process.tcp_sockets.contains_key(&socket_id) + }; + if !expected_socket_present { + rollback_managed_stream_socket(&mut context, &socket_id); + return Err(SidecarError::host( + "EIO", + "accept returned an unknown socket id", + )); + } + let (accepted_fd, accepted_description_id) = + match context.kernel.fd_open_external_socket( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + false, + nonblocking, + close_on_exec, + ) { + Ok(opened) => opened, + Err(error) => { + rollback_managed_stream_socket(&mut context, &socket_id); + return Err(kernel_error(error)); + } + }; + let accepted_lease = match context.kernel.fd_transfer( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + accepted_fd, + ) { + Ok(lease) => lease, + Err(error) => { + rollback_managed_stream_socket(&mut context, &socket_id); + if let Err(close_error) = context.kernel.fd_close( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + accepted_fd, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close accepted fd after transfer failure: {close_error}" + ); + } + return Err(kernel_error(error)); + } + }; + let mut accepted = ManagedHostNetDescription::new( + parent.domain, + SocketKind::Stream, + accepted_lease, + context.process.kernel_pid, + ); + accepted.receive_timeout_ms = parent.receive_timeout_ms; + accepted.no_delay = parent.no_delay; + accepted.keep_alive = parent.keep_alive; + accepted.local_address = local_address; + accepted.peer_address = peer_address; + let accepted_route = if parent.domain == HostSocketDomain::Unix { + ManagedHostNetRoute::UnixSocket(socket_id.clone()) + } else { + ManagedHostNetRoute::TcpSocket(socket_id.clone()) + }; + accepted + .routes + .insert(context.process.kernel_pid, accepted_route); + if descriptions.contains_key(&accepted_description_id) { + rollback_managed_stream_socket(&mut context, &socket_id); + if let Err(error) = context.kernel.fd_close( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + accepted_fd, + ) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close accepted fd after description collision: {error}" + ); + } + return Err(SidecarError::host( + "EEXIST", + "kernel reused a live accepted socket description id", + )); + } + descriptions.insert(accepted_description_id, accepted); + let Value::Object(fields) = &mut value else { + unreachable!("accept response validated as an object") + }; + fields.insert("fd".to_owned(), json!(accepted_fd)); + fields.insert( + "descriptionId".to_owned(), + json!(accepted_description_id.to_string()), + ); + Ok(HostServiceResponse::Json(value)) + } + NetworkOperation::Receive { + fd, + max_bytes, + flags, + deadline_ms, + } => { + let description_id = managed_fd_description_id(&context, fd)?; + let route = managed_route(&context, description_id)?; + let peek = flags & 0x0002 != 0; + let wait_ms = deadline_ms.unwrap_or_default(); + match route { + ManagedHostNetRoute::TcpSocket(socket_id) + | ManagedHostNetRoute::UnixSocket(socket_id) => service_managed_network_operation( + managed_socket_context(&mut context), + NetworkOperation::ManagedRead { + socket_id: bounded_managed_id(socket_id)?, + max_bytes: max_bytes.get() as u64, + peek, + wait_ms, + }, + ), + ManagedHostNetRoute::UdpSocket(_) => Err(SidecarError::host( + "ERR_AGENTOS_CONTEXT_DISPATCH_REQUIRED", + "UDP receive requires the asynchronous fd poll dispatcher", + )), + _ => Err(SidecarError::host("ENOTCONN", "socket is not connected")), + } + } + NetworkOperation::Send { + fd, + bytes, + flags: _, + address, + .. + } => { + let description_id = managed_fd_description_id(&context, fd)?; + let route = managed_route(&context, description_id)?; + match route { + ManagedHostNetRoute::TcpSocket(socket_id) + | ManagedHostNetRoute::UnixSocket(socket_id) => service_managed_network_operation( + managed_socket_context(&mut context), + NetworkOperation::ManagedWrite { + socket_id: bounded_managed_id(socket_id)?, + bytes, + }, + ), + ManagedHostNetRoute::UdpSocket(socket_id) => { + let (host, port) = match address { + Some(SocketAddress::Inet { host, port }) => (Some(host), Some(port)), + None => (None, None), + _ => { + return Err(SidecarError::host( + "EAFNOSUPPORT", + "UDP send requires INET address", + )) + } + }; + let response = service_managed_udp_operation( + ManagedUdpServiceRequest { + bridge: context.bridge, + kernel: &mut *context.kernel, + vm_id: context.vm_id, + dns: context.dns, + socket_paths: context.socket_paths, + process: &mut *context.process, + kernel_readiness: context.kernel_readiness.clone(), + capabilities: context.capabilities.clone(), + }, + NetworkOperation::ManagedUdpSend { + socket_id: bounded_managed_id(socket_id.clone())?, + bytes, + host, + port, + }, + )?; + let local_address = context + .process + .udp_sockets + .get(&socket_id) + .and_then(ActiveUdpSocket::local_addr) + .map(|address| SocketAddress::Inet { + host: BoundedString::try_new( + address.ip().to_string(), + &PayloadLimit::new("runtime.network.maxHostBytes", HOST_BYTES) + .expect("static nonzero host limit"), + ) + .expect("IP address remains within host limit"), + port: address.port(), + }); + if local_address.is_some() { + update_managed_description(&context, description_id, |description| { + description.local_address = local_address; + })?; + } + Ok(response) + } + _ => Err(SidecarError::host("ENOTCONN", "socket is not connected")), + } + } + NetworkOperation::LocalAddress { fd } | NetworkOperation::PeerAddress { fd } => { + let peer = matches!(operation, NetworkOperation::PeerAddress { .. }); + let description_id = managed_fd_description_id(&context, fd)?; + let description = managed_description(&context, description_id)?; + let address = if peer { + description.peer_address + } else { + description.local_address.or(description.bound_address) + }; + if let Some(address) = address { + return Ok(HostServiceResponse::Json(socket_address_value(address))); + } + if peer { + return Err(SidecarError::host("ENOTCONN", "socket has no peer address")); + } + if description.domain == HostSocketDomain::Unix { + // Linux reports an unbound AF_UNIX socket as an unnamed Unix + // address. An empty value is deliberately encoded by the + // executor adapter as `unix-unnamed`. + return Ok(HostServiceResponse::Json(json!({}))); + } + Err(SidecarError::host("EINVAL", "socket is not bound")) + } + NetworkOperation::SetOption { fd, name, value } => { + let description_id = managed_fd_description_id(&context, fd)?; + let registry = Arc::clone(&context.managed_descriptions); + let mut descriptions = registry.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + match (name, value) { + (SocketOptionName::ReceiveTimeout, SocketOptionValue::DurationMs(value)) => { + descriptions + .get_mut(&description_id) + .ok_or_else(|| { + SidecarError::host("ENOTSOCK", "managed socket description disappeared") + })? + .receive_timeout_ms = value; + } + (SocketOptionName::NoDelay, SocketOptionValue::Bool(value)) => { + let route = descriptions + .get(&description_id) + .and_then(|description| { + description.route_for(context.process.kernel_pid).cloned() + }) + .ok_or_else(|| { + SidecarError::host("ESTALE", "managed socket route is missing") + })?; + let ManagedHostNetRoute::TcpSocket(id) = &route else { + return Err(SidecarError::host( + "ENOPROTOOPT", + "TCP_NODELAY requires TCP", + )); + }; + context + .process + .tcp_sockets + .get_mut(id) + .ok_or_else(|| SidecarError::host("EBADF", "managed TCP route is stale"))? + .set_no_delay(value)?; + descriptions + .get_mut(&description_id) + .expect("managed socket description remains locked") + .no_delay = value; + } + (SocketOptionName::KeepAlive, SocketOptionValue::Bool(value)) => { + let route = descriptions + .get(&description_id) + .and_then(|description| { + description.route_for(context.process.kernel_pid).cloned() + }) + .ok_or_else(|| { + SidecarError::host("ESTALE", "managed socket route is missing") + })?; + let ManagedHostNetRoute::TcpSocket(id) = &route else { + return Err(SidecarError::host( + "ENOPROTOOPT", + "SO_KEEPALIVE requires TCP", + )); + }; + context + .process + .tcp_sockets + .get_mut(id) + .ok_or_else(|| SidecarError::host("EBADF", "managed TCP route is stale"))? + .set_keep_alive(value, None)?; + descriptions + .get_mut(&description_id) + .expect("managed socket description remains locked") + .keep_alive = value; + } + _ => { + return Err(SidecarError::host( + "ENOPROTOOPT", + "socket option is unsupported for this transport", + )) + } + } + Ok(Value::Null.into()) + } + NetworkOperation::GetOption { fd, name } => { + let description_id = managed_fd_description_id(&context, fd)?; + let description = managed_description(&context, description_id)?; + let value = match name { + SocketOptionName::Error => json!(0), + SocketOptionName::ReceiveTimeout => { + json!({ "durationMs": description.receive_timeout_ms }) + } + SocketOptionName::NoDelay => json!(description.no_delay), + SocketOptionName::KeepAlive => json!(description.keep_alive), + _ => { + return Err(SidecarError::host( + "ENOPROTOOPT", + "socket option getter is unsupported", + )) + } + }; + Ok(value.into()) + } + NetworkOperation::TlsConnect { + fd, + server_name, + alpn, + .. + } => { + let description_id = managed_fd_description_id(&context, fd)?; + let route = managed_route(&context, description_id)?; + let ManagedHostNetRoute::TcpSocket(socket_id) = route else { + return Err(SidecarError::host( + "ENOTCONN", + "TLS upgrade requires a connected TCP socket", + )); + }; + let protocols = alpn + .into_vec() + .into_iter() + .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).into_owned()) + .collect::>(); + service_managed_network_operation( + managed_socket_context(&mut context), + NetworkOperation::ManagedTlsUpgrade { + socket_id: bounded_managed_id(socket_id)?, + options_json: bounded_managed_payload( + json!({ + "servername": server_name.as_str(), + "ALPNProtocols": protocols, + }) + .to_string(), + )?, + }, + ) + } + NetworkOperation::Poll { + interests, + deadline_ms: _, + } => { + let mut ready = Vec::new(); + for interest in interests.into_vec() { + let description_id = managed_fd_description_id(&context, interest.fd)?; + let readable = if interest.readable { + match &managed_route(&context, description_id)? { + ManagedHostNetRoute::TcpSocket(id) + | ManagedHostNetRoute::UnixSocket(id) => { + let response = service_managed_network_operation( + managed_socket_context(&mut context), + NetworkOperation::ManagedPoll { + socket_id: bounded_managed_id(id.clone())?, + wait_ms: 0, + }, + )?; + !matches!(response_value(&response), Some(Value::Null) | None) + } + ManagedHostNetRoute::TcpListener(_) + | ManagedHostNetRoute::UnixListener(_) => false, + ManagedHostNetRoute::UdpSocket(id) => context + .process + .udp_sockets + .get(id) + .and_then(|socket| socket.pending_datagram.lock().ok()) + .is_some_and(|pending| pending.is_some()), + _ => false, + } + } else { + false + }; + ready.push(json!({ + "fd": interest.fd, + "readable": readable, + "writable": interest.writable, + })); + } + Ok(json!({ "ready": ready }).into()) + } + other => Err(SidecarError::host( + "EINVAL", + format!("unsupported managed fd operation: {other:?}"), + )), + } +} + +fn bounded_managed_id(value: String) -> Result { + BoundedString::try_new( + value, + &PayloadLimit::new("runtime.network.maxCapabilityIdBytes", MANAGED_ID_BYTES) + .map_err(SidecarError::from)?, + ) + .map_err(SidecarError::from) +} + +fn bounded_managed_payload(value: String) -> Result { + BoundedString::try_new( + value, + &PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", 1024 * 1024) + .map_err(SidecarError::from)?, + ) + .map_err(SidecarError::from) +} + +fn socket_address_value(address: SocketAddress) -> Value { + match address { + SocketAddress::Inet { host, port } => json!({ "address": host.as_str(), "port": port }), + SocketAddress::UnixPath(path) => json!({ "path": path.as_str() }), + SocketAddress::UnixAbstract(bytes) => { + json!({ "abstractPathHex": encode_hex_bytes(bytes.as_slice()) }) + } + SocketAddress::UnixAutobind => json!({ "autobind": true }), + } +} + +fn encode_hex_bytes(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut encoded = String::with_capacity(bytes.len().saturating_mul(2)); + for byte in bytes { + encoded.push(char::from(HEX[usize::from(byte >> 4)])); + encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + encoded +} + +fn is_managed_fd_operation(operation: &NetworkOperation) -> bool { + matches!( + operation, + NetworkOperation::Socket { .. } + | NetworkOperation::Bind { .. } + | NetworkOperation::Connect { .. } + | NetworkOperation::Listen { .. } + | NetworkOperation::Accept { .. } + | NetworkOperation::Validate { .. } + | NetworkOperation::Receive { .. } + | NetworkOperation::Send { .. } + | NetworkOperation::LocalAddress { .. } + | NetworkOperation::PeerAddress { .. } + | NetworkOperation::GetOption { .. } + | NetworkOperation::SetOption { .. } + | NetworkOperation::Poll { .. } + | NetworkOperation::TlsConnect { .. } + ) +} + +fn settle_managed_network_response( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + runtime: agentos_runtime::RuntimeContext, + reply: DirectHostReplyHandle, + operation: &str, + response: Result, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let response = match response { + Ok(HostServiceResponse::Deferred { + receiver, + timeout, + task_class, + }) => { + enqueue_deferred_host_service_completion( + sidecar, vm_id, process_id, runtime, reply, operation, receiver, timeout, + task_class, + )?; + return Ok(()); + } + other => other, + }; + settle_execution_host_call(&reply, response) +} + +fn is_managed_endpoint_operation(operation: &NetworkOperation) -> bool { + matches!( + operation, + NetworkOperation::ManagedBindUnix { .. } + | NetworkOperation::ManagedBindConnectedUnix { .. } + | NetworkOperation::ManagedReserveTcpPort { .. } + | NetworkOperation::ManagedReleaseTcpPort { .. } + | NetworkOperation::ManagedConnect { .. } + | NetworkOperation::ManagedListen { .. } + ) +} + +fn is_direct_managed_operation(operation: &NetworkOperation) -> bool { + matches!( + operation, + NetworkOperation::ManagedBindUnix { .. } + | NetworkOperation::ManagedBindConnectedUnix { .. } + | NetworkOperation::ManagedReserveTcpPort { .. } + | NetworkOperation::ManagedReleaseTcpPort { .. } + | NetworkOperation::ManagedConnect { .. } + | NetworkOperation::ManagedListen { .. } + | NetworkOperation::ManagedPoll { .. } + | NetworkOperation::ManagedWaitConnect { .. } + | NetworkOperation::ManagedRead { .. } + | NetworkOperation::ManagedWrite { .. } + | NetworkOperation::ManagedDestroy { .. } + | NetworkOperation::ManagedAccept { .. } + | NetworkOperation::ManagedCloseListener { .. } + | NetworkOperation::ManagedTlsUpgrade { .. } + | NetworkOperation::ManagedUdpCreate { .. } + | NetworkOperation::ManagedUdpBind { .. } + | NetworkOperation::ManagedUdpSend { .. } + | NetworkOperation::ManagedUdpPoll { .. } + | NetworkOperation::ManagedUdpClose { .. } + ) +} + +pub(super) fn dispatch_context_udp_poll( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let NetworkOperation::ManagedUdpPoll { + socket_id, + wait_ms, + peek, + max_bytes, + } = operation + else { + return Err(SidecarError::host( + "EINVAL", + "UDP poll dispatcher received a different network operation", + )); + }; + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let socket = sidecar + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|process| process.udp_sockets.get(socket_id.as_str())) + .ok_or_else(|| SidecarError::host("EBADF", "unknown UDP socket")); + let socket = match socket { + Ok(socket) => socket, + Err(error) => { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(()); + } + }; + let requested_wait = Duration::from_millis(wait_ms); + let operation_deadline = socket.poll_handle().operation_deadline(); + let wait = requested_wait.min(operation_deadline); + dispatch_claimed_context_udp_poll( + sidecar, + vm_id, + process_id, + ManagedUdpPollRecheck { + root_process_id: process_id.to_owned(), + process_path: Vec::new(), + socket_id: socket_id.into_string(), + peek, + max_bytes, + deadline: Instant::now() + wait, + operation_deadline: (requested_wait >= operation_deadline) + .then_some(operation_deadline), + deadline_warning_emitted: false, + native_probe_completed: false, + native_event: None, + reply, + fair_turn: None, + }, + ) +} + +pub(in crate::execution) fn dispatch_descendant_context_udp_poll( + sidecar: &mut NativeSidecar, + vm_id: &str, + root_process_id: &str, + process_path: &[&str], + operation: NetworkOperation, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let NetworkOperation::ManagedUdpPoll { + socket_id, + wait_ms, + peek, + max_bytes, + } = operation + else { + return Err(SidecarError::host( + "EINVAL", + "descendant UDP poll dispatcher received a different network operation", + )); + }; + let mut pending = ManagedUdpPollRecheck { + root_process_id: root_process_id.to_owned(), + process_path: process_path + .iter() + .map(|entry| (*entry).to_owned()) + .collect(), + socket_id: socket_id.into_string(), + peek, + max_bytes, + deadline: Instant::now(), + operation_deadline: None, + deadline_warning_emitted: false, + native_probe_completed: false, + native_event: None, + reply, + fair_turn: None, + }; + if !validate_udp_poll_target(sidecar, vm_id, &pending)? { + return Ok(()); + } + if !pending.reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let socket = sidecar + .vms + .get(vm_id) + .and_then(|vm| udp_poll_target_process::(vm, &pending)) + .and_then(|process| process.udp_sockets.get(&pending.socket_id)); + let Some(socket) = socket else { + pending + .reply + .fail(HostServiceError::new("EBADF", "unknown UDP socket")) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let requested_wait = Duration::from_millis(wait_ms); + let operation_deadline = socket.poll_handle().operation_deadline(); + let wait = requested_wait.min(operation_deadline); + pending.deadline = Instant::now() + wait; + pending.operation_deadline = + (requested_wait >= operation_deadline).then_some(operation_deadline); + dispatch_claimed_context_udp_poll(sidecar, vm_id, root_process_id, pending) +} + +pub(in crate::execution) fn dispatch_claimed_context_udp_poll( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + mut pending: ManagedUdpPollRecheck, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if process_id != pending.root_process_id || !validate_udp_poll_target(sidecar, vm_id, &pending)? + { + return Ok(()); + } + + let socket_paths = build_socket_path_context( + sidecar + .vms + .get(vm_id) + .expect("validated UDP-poll VM remains registered"), + )?; + let vm = sidecar + .vms + .get(vm_id) + .expect("validated UDP-poll VM remains registered"); + let wait_handle = vm.kernel.poll_wait_handle(); + let observed_generation = wait_handle.snapshot(); + let process = udp_poll_target_process::(vm, &pending) + .expect("validated UDP-poll process remains registered"); + let socket = match process.udp_sockets.get(&pending.socket_id) { + Some(socket) => socket, + None => { + if let Some(turn) = pending.fair_turn.take() { + turn.complete(FairBudget::default(), false) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + } + pending + .reply + .fail(HostServiceError::new("EBADF", "unknown UDP socket")) + .map_err(SidecarError::from)?; + return Ok(()); + } + }; + let poll_handle = socket.poll_handle(); + if let Some(event) = pending.native_event.take() { + return settle_or_buffer_udp_poll_event( + &pending.reply, + &socket_paths, + &poll_handle.pending_datagram, + Some(event), + pending.peek, + pending.max_bytes, + ); + } + if let Some(event) = buffered_udp_poll_event(&poll_handle, pending.peek)? { + return settle_udp_poll_event( + &pending.reply, + &socket_paths, + Some(event), + pending.max_bytes, + ); + } + let kernel_readable = socket.kernel_readable(&vm.kernel, process.kernel_pid)?; + + if kernel_readable { + if let Some(turn) = pending.fair_turn.take() { + let vm = sidecar + .vms + .get_mut(vm_id) + .expect("validated UDP-poll VM remains registered"); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = udp_poll_target_process_mut::(active_processes, &pending) + .expect("validated UDP-poll process remains registered"); + let socket = process + .udp_sockets + .get(&pending.socket_id) + .expect("validated UDP socket remains registered"); + let event = socket.consume_kernel_datagram(kernel, process.kernel_pid, turn)?; + return settle_or_buffer_udp_poll_event( + &pending.reply, + &socket_paths, + &socket.pending_datagram, + event, + pending.peek, + pending.max_bytes, + ); + } + return spawn_udp_fair_reentry(sidecar, vm_id, process_id, poll_handle, pending); + } + + if let Some(turn) = pending.fair_turn.take() { + turn.complete(FairBudget::default(), false) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + } + if poll_handle.native_commands.is_none() { + if let Some(limit) = pending.operation_deadline { + let mut deadline = crate::execution::OperationDeadlineTracker::from_deadline( + pending.deadline, + limit, + pending.deadline_warning_emitted, + ); + deadline.observe("managed UDP poll wait"); + pending.deadline_warning_emitted = deadline.warning_emitted(); + } + } + if Instant::now() >= pending.deadline + && (poll_handle.native_commands.is_none() || pending.native_probe_completed) + { + return settle_udp_poll_event(&pending.reply, &socket_paths, None, pending.max_bytes); + } + spawn_udp_wait_reentry( + sidecar, + vm_id, + process_id, + poll_handle, + wait_handle, + observed_generation, + pending, + ) +} + +fn udp_poll_target_process<'a, B>( + vm: &'a VmState, + pending: &ManagedUdpPollRecheck, +) -> Option<&'a ActiveProcess> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let root = vm.active_processes.get(&pending.root_process_id)?; + if pending.process_path.is_empty() { + return Some(root); + } + let path = pending + .process_path + .iter() + .map(String::as_str) + .collect::>(); + NativeSidecar::::active_process_by_path(root, &path) +} + +fn udp_poll_target_process_mut<'a, B>( + active_processes: &'a mut BTreeMap, + pending: &ManagedUdpPollRecheck, +) -> Option<&'a mut ActiveProcess> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let root = active_processes.get_mut(&pending.root_process_id)?; + if pending.process_path.is_empty() { + return Some(root); + } + let path = pending + .process_path + .iter() + .map(String::as_str) + .collect::>(); + NativeSidecar::::active_process_by_path_mut(root, &path) +} + +fn validate_udp_poll_target( + sidecar: &NativeSidecar, + vm_id: &str, + pending: &ManagedUdpPollRecheck, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let Some(vm) = sidecar.vms.get(vm_id) else { + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "UDP poll VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(false); + }; + let Some(process) = udp_poll_target_process::(vm, pending) else { + pending + .reply + .fail(HostServiceError::new( + "ESTALE", + "UDP poll process target no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(false); + }; + let identity = pending.reply.identity(); + if identity.generation != vm.generation || identity.pid != process.kernel_pid { + pending + .reply + .fail( + HostServiceError::new( + "ESTALE", + "UDP poll identity does not match the generation-bound process target", + ) + .with_details(json!({ + "expectedGeneration": vm.generation, + "expectedPid": process.kernel_pid, + "observedGeneration": identity.generation, + "observedPid": identity.pid, + })), + ) + .map_err(SidecarError::from)?; + return Ok(false); + } + Ok(true) +} + +fn spawn_udp_fair_reentry( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + poll_handle: ActiveUdpPollHandle, + pending: ManagedUdpPollRecheck, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let (runtime, connection_id, session_id) = udp_reentry_context(sidecar, vm_id)?; + let sender = sidecar.process_event_sender.clone(); + let notify = Arc::clone(&sidecar.process_event_notify); + let vm_id = vm_id.to_owned(); + let process_id = process_id.to_owned(); + let failure_reply = pending.reply.clone(); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Udp, async move { + let mut pending = pending; + match poll_handle.acquire_fair_turn().await { + Ok(turn) => pending.fair_turn = Some(turn), + Err(error) => { + if let Err(reply_error) = pending.reply.fail(host_service_error(&error)) { + eprintln!( + "ERR_AGENTOS_HOST_REPLY_SETTLEMENT: failed to publish managed UDP fair-turn error: {reply_error}" + ); + } + return; + } + } + send_udp_reentry( + sender, + notify, + connection_id, + session_id, + vm_id, + process_id, + pending, + ) + .await; + }) { + failure_reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn spawn_udp_wait_reentry( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + poll_handle: ActiveUdpPollHandle, + wait_handle: agentos_kernel::poll::PollWaitHandle, + observed_generation: u64, + pending: ManagedUdpPollRecheck, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let (runtime, connection_id, session_id) = udp_reentry_context(sidecar, vm_id)?; + let sender = sidecar.process_event_sender.clone(); + let notify = Arc::clone(&sidecar.process_event_notify); + let vm_id = vm_id.to_owned(); + let process_id = process_id.to_owned(); + let failure_reply = pending.reply.clone(); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Udp, async move { + let mut pending = pending; + // Register readiness before the native owner probe so an event arriving + // in the gap remains observable after an empty completion. + let native_ready = poll_handle.read_event_notify.notified(); + match poll_handle.poll_native_once().await { + Ok(Some(event)) => { + pending.native_probe_completed = true; + pending.native_event = Some(event); + send_udp_reentry( + sender, + notify, + connection_id, + session_id, + vm_id, + process_id, + pending, + ) + .await; + return; + } + Ok(None) => pending.native_probe_completed = true, + Err(error) => { + pending.native_probe_completed = true; + pending.native_event = Some(DatagramEvent::Error { + code: Some(host_service_error_code(&error)), + message: host_service_error_message(&error), + }); + send_udp_reentry( + sender, + notify, + connection_id, + session_id, + vm_id, + process_id, + pending, + ) + .await; + return; + } + } + let remaining = if let Some(limit) = pending.operation_deadline { + let mut deadline = crate::execution::OperationDeadlineTracker::from_deadline( + pending.deadline, + limit, + pending.deadline_warning_emitted, + ); + deadline.observe("managed UDP poll wait"); + pending.deadline_warning_emitted = deadline.warning_emitted(); + if deadline.expired() { + send_udp_reentry( + sender, + notify, + connection_id, + session_id, + vm_id, + process_id, + pending, + ) + .await; + return; + } + deadline.remaining_until_next_edge() + } else { + pending.deadline.saturating_duration_since(Instant::now()) + }; + if !remaining.is_zero() { + tokio::select! { + _ = native_ready => {} + _ = wait_handle.wait_for_change_async(observed_generation) => {} + _ = tokio::time::sleep(remaining) => {} + } + } + send_udp_reentry( + sender, + notify, + connection_id, + session_id, + vm_id, + process_id, + pending, + ) + .await; + }) { + failure_reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + Ok(()) +} + +fn udp_reentry_context( + sidecar: &NativeSidecar, + vm_id: &str, +) -> Result<(agentos_runtime::RuntimeContext, String, String), SidecarError> { + let vm = sidecar + .vms + .get(vm_id) + .ok_or_else(|| SidecarError::host("ESTALE", "UDP-poll VM no longer exists"))?; + Ok(( + vm.runtime_context.clone(), + vm.connection_id.clone(), + vm.session_id.clone(), + )) +} + +#[allow(clippy::too_many_arguments)] +async fn send_udp_reentry( + sender: tokio::sync::mpsc::Sender, + notify: Arc, + connection_id: String, + session_id: String, + vm_id: String, + process_id: String, + pending: ManagedUdpPollRecheck, +) { + debug_assert_eq!(process_id, pending.root_process_id); + let process_id = pending.root_process_id.clone(); + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id, + process_id, + event: ActiveExecutionEvent::ManagedUdpPollRecheck(Box::new(pending)), + }; + if let Err(error) = sender.send(envelope).await { + if let ActiveExecutionEvent::ManagedUdpPollRecheck(pending) = error.0.event { + if let Err(reply_error) = pending.reply.fail(HostServiceError::new( + "ECANCELED", + "UDP poll re-entry lane closed", + )) { + eprintln!( + "ERR_AGENTOS_HOST_REPLY_SETTLEMENT: failed to cancel UDP poll after re-entry lane closure: {reply_error}" + ); + } + } + eprintln!("ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: UDP poll could not re-enter"); + } else { + notify.notify_one(); + } +} + +fn settle_udp_poll_event( + reply: &DirectHostReplyHandle, + socket_paths: &SocketPathContext, + event: Option, + max_bytes: Option, +) -> Result<(), SidecarError> { + match event { + Some(DatagramEvent::Message { + data, + remote_addr, + _byte_reservation, + _datagram_reservation, + _udp_byte_reservation, + _udp_datagram_reservation, + }) => { + let family = SocketFamily::from_ip(remote_addr.ip()); + let guest_port = if is_loopback_ip(remote_addr.ip()) { + socket_paths + .guest_udp_port_for_host_port(family, remote_addr.port()) + .unwrap_or(remote_addr.port()) + } else { + remote_addr.port() + }; + let mut value = remote_endpoint_value(&remote_addr, guest_port); + if let Value::Object(fields) = &mut value { + fields.insert("type".to_owned(), Value::String("message".to_owned())); + let data = max_bytes + .map(|limit| &data[..data.len().min(limit.get())]) + .unwrap_or(data.as_slice()); + fields.insert("data".to_owned(), host_bytes_value(data)); + } + reply + .succeed_retained( + HostCallReply::Json(value), + ( + _byte_reservation, + _datagram_reservation, + _udp_byte_reservation, + _udp_datagram_reservation, + ), + ) + .map_err(SidecarError::from) + } + Some(DatagramEvent::Error { code, message }) => reply + .fail(HostServiceError::new( + code.as_deref().unwrap_or("EIO"), + message, + )) + .map_err(SidecarError::from), + None => reply + .succeed(HostCallReply::Json(if max_bytes.is_some() { + json!({ "kind": "wouldBlock" }) + } else { + Value::Null + })) + .map_err(SidecarError::from), + } +} + +fn buffered_udp_poll_event( + poll_handle: &ActiveUdpPollHandle, + peek: bool, +) -> Result, SidecarError> { + let mut pending = poll_handle.pending_datagram.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_UDP_READ_STATE_POISONED", + "managed UDP pending-datagram lock poisoned", + ) + })?; + Ok(if peek { + pending.clone() + } else { + pending.take() + }) +} + +fn settle_or_buffer_udp_poll_event( + reply: &DirectHostReplyHandle, + socket_paths: &SocketPathContext, + pending_datagram: &Arc>>, + event: Option, + peek: bool, + max_bytes: Option, +) -> Result<(), SidecarError> { + if peek { + if let Some(event) = event.as_ref() { + let mut pending = pending_datagram.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_UDP_READ_STATE_POISONED", + "managed UDP pending-datagram lock poisoned", + ) + })?; + if pending.is_none() { + *pending = Some(event.clone()); + } + } + } + settle_udp_poll_event(reply, socket_paths, event, max_bytes) +} + +impl SidecarHostCapability for NetworkCapability { + fn requires_claim(operation: &NetworkOperation) -> bool { + matches!( + operation, + NetworkOperation::SocketPair { .. } + | NetworkOperation::Shutdown { .. } + | NetworkOperation::ManagedBindUnix { .. } + | NetworkOperation::ManagedBindConnectedUnix { .. } + | NetworkOperation::ManagedReserveTcpPort { .. } + | NetworkOperation::ManagedReleaseTcpPort { .. } + | NetworkOperation::ManagedConnect { .. } + | NetworkOperation::ManagedListen { .. } + | NetworkOperation::ManagedPoll { .. } + | NetworkOperation::ManagedRead { .. } + | NetworkOperation::ManagedWrite { .. } + | NetworkOperation::ManagedDestroy { .. } + | NetworkOperation::ManagedAccept { .. } + | NetworkOperation::ManagedCloseListener { .. } + | NetworkOperation::ManagedTlsUpgrade { .. } + | NetworkOperation::ManagedUdpCreate { .. } + | NetworkOperation::ManagedUdpBind { .. } + | NetworkOperation::ManagedUdpSend { .. } + | NetworkOperation::ManagedUdpPoll { .. } + | NetworkOperation::ManagedUdpClose { .. } + | NetworkOperation::SendDescriptorRights { .. } + | NetworkOperation::ReceiveDescriptorRights { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: NetworkOperation, + ) -> Result { + match operation { + NetworkOperation::SocketPair { + kind, + nonblocking, + close_on_exec, + } => { + let socket_type = match kind { + SocketKind::Stream => SocketType::Stream, + SocketKind::Datagram => SocketType::Datagram, + SocketKind::SeqPacket => SocketType::SeqPacket, + }; + let (first_fd, second_fd) = kernel + .fd_socketpair( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + socket_type, + nonblocking, + close_on_exec, + ) + .map_err(kernel_host_error)?; + Ok(HostCallReply::Json(json!({ + "firstFd": first_fd, + "secondFd": second_fd, + }))) + } + NetworkOperation::Shutdown { fd, how } => { + let how = match how { + SocketShutdown::Read => KernelSocketShutdown::Read, + SocketShutdown::Write => KernelSocketShutdown::Write, + SocketShutdown::Both => KernelSocketShutdown::Both, + }; + kernel + .fd_socket_shutdown(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, how) + .map_err(kernel_host_error)?; + Ok(HostCallReply::Json(Value::Null)) + } + other => Err(unsupported("network", other)), + } + } +} + +#[cfg(test)] +mod managed_tests { + use super::*; + use crate::execution::host_dispatch::inventory::WASM_RUNNER_RPC_INVENTORY; + use agentos_execution::backend::{DirectHostReplyTarget, HostCallIdentity}; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::MemoryFileSystem; + use std::collections::HashMap; + use std::sync::Mutex; + + #[derive(Default)] + struct ReplyTarget { + result: Mutex)>>, + } + + impl DirectHostReplyTarget for ReplyTarget { + fn claim(&self, _: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _: u64, + claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + *self.result.lock().expect("reply result") = Some((claimed, result)); + Ok(()) + } + } + + fn request(method: &str) -> HostRpcRequest { + let args = match method { + "net.bind_unix" => vec![json!({"path":"/tmp/s"})], + "net.bind_connected_unix" => vec![json!({"socketId":"s1","path":"/tmp/s"})], + "net.connect" => vec![json!({"host":"127.0.0.1","port":80})], + "net.listen" => vec![json!({"host":"127.0.0.1","port":0,"backlog":16})], + "net.reserve_tcp_port" => vec![json!({"host":"127.0.0.1","port":0})], + "net.release_tcp_port" => vec![json!("r1")], + "net.poll" => vec![json!("s1"), json!(0)], + "net.socket_wait_connect" | "net.socket_read" | "net.destroy" => vec![json!("s1")], + "net.write" => vec![json!("s1"), json!("x")], + "net.server_accept" | "net.server_close" => vec![json!("l1")], + "net.socket_upgrade_tls" => vec![json!("s1"), json!("{}")], + "dgram.createSocket" => vec![json!({"type":"udp4"})], + "dgram.bind" => vec![json!("u1"), json!({"address":"127.0.0.1","port":0})], + "dgram.send" => vec![ + json!("u1"), + json!("x"), + json!({"address":"127.0.0.1","port":7}), + ], + "dgram.poll" => vec![json!("u1"), json!(0)], + "dgram.close" => vec![json!("u1")], + other => panic!("missing managed network fixture for {other}"), + }; + HostRpcRequest { + id: 1, + method: method.to_owned(), + args, + raw_bytes_args: HashMap::new(), + } + } + + fn managed_test_kernel(name: &str) -> SidecarKernel { + let mut config = KernelVmConfig::new(name); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register managed-network test driver"); + kernel + } + + #[test] + fn common_managed_network_and_completion_state_are_executor_neutral() { + let managed_source = include_str!("network_compat.rs"); + let state_source = include_str!("../../state.rs"); + for forbidden in [ + ["Java", "script", "Net"].concat(), + ["Java", "script", "Sync", "Rpc"].concat(), + ["Py", "thon"].concat(), + ] { + assert!( + !managed_source.contains(&forbidden), + "common managed network source contains executor-specific type {forbidden}" + ); + } + assert!(state_source.contains("struct HostCallCompletion")); + assert!(!state_source.contains(&["Java", "script", "Sync", "RpcCompletion"].concat())); + } + + #[test] + fn parent_route_retires_while_child_and_queued_scm_right_retain_description() { + let mut kernel = managed_test_kernel("managed-parent-child-scm-lifecycle"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(EXECUTION_DRIVER_NAME.to_owned()), + ..SpawnOptions::default() + }, + ) + .expect("spawn parent"); + let (fd, description_id) = kernel + .fd_open_external_socket(EXECUTION_DRIVER_NAME, parent.pid(), false, false, false) + .expect("open managed external socket"); + let lease = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, parent.pid(), fd) + .expect("capture canonical description lease"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(EXECUTION_DRIVER_NAME.to_owned()), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .expect("spawn inheriting child"); + assert_eq!( + kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, child.pid(), fd) + .expect("child inherits external socket") + .0, + description_id + ); + + let mut description = ManagedHostNetDescription::new( + HostSocketDomain::Inet4, + SocketKind::Stream, + lease, + parent.pid(), + ); + description + .routes + .insert(child.pid(), ManagedHostNetRoute::Unbound); + let mut descriptions = BTreeMap::from([(description_id, description)]); + let queued_scm_right = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, parent.pid(), fd) + .expect("queue SCM_RIGHTS description lease"); + + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent.pid(), fd) + .expect("parent final close"); + let (retired_parent_routes, retired_descriptions) = + prune_managed_registry_routes(&mut descriptions, parent.pid(), [(description_id, 0)]); + assert_eq!(retired_parent_routes.len(), 1); + assert!(retired_descriptions.is_empty()); + assert_eq!( + descriptions + .get(&description_id) + .expect("global description retained") + .routes + .keys() + .copied() + .collect::>(), + [child.pid()] + ); + drop(retired_parent_routes); + + drop(queued_scm_right); + kernel + .fd_close(EXECUTION_DRIVER_NAME, child.pid(), fd) + .expect("child final close"); + let (retired_child_routes, retired_descriptions) = + prune_managed_registry_routes(&mut descriptions, child.pid(), [(description_id, 0)]); + assert_eq!(retired_child_routes.len(), 1); + assert_eq!(retired_descriptions.len(), 1); + assert!(descriptions.is_empty(), "final close retires registry row"); + } + + #[test] + fn exec_cloexec_retires_managed_route_and_description() { + let mut kernel = managed_test_kernel("managed-exec-cloexec-lifecycle"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(EXECUTION_DRIVER_NAME.to_owned()), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let (fd, description_id) = kernel + .fd_open_external_socket(EXECUTION_DRIVER_NAME, process.pid(), false, false, true) + .expect("open CLOEXEC managed socket"); + let lease = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, process.pid(), fd) + .expect("capture description lease"); + let mut descriptions = BTreeMap::from([( + description_id, + ManagedHostNetDescription::new( + HostSocketDomain::Inet4, + SocketKind::Stream, + lease, + process.pid(), + ), + )]); + kernel + .exec_process_retaining_internal_fds( + EXECUTION_DRIVER_NAME, + process.pid(), + WASM_COMMAND, + Vec::new(), + BTreeMap::new(), + String::new(), + &[], + &[], + None, + None, + ) + .expect("exec closes CLOEXEC fd"); + let aliases = kernel + .fd_description_alias_count(EXECUTION_DRIVER_NAME, process.pid(), description_id) + .expect("query post-exec aliases"); + assert_eq!(aliases, 0); + let (_, retired) = prune_managed_registry_routes( + &mut descriptions, + process.pid(), + [(description_id, aliases)], + ); + assert_eq!(retired.len(), 1); + assert!(descriptions.is_empty()); + } + + #[test] + fn process_exit_final_fd_cleanup_retires_managed_description() { + let mut kernel = managed_test_kernel("managed-process-exit-lifecycle"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(EXECUTION_DRIVER_NAME.to_owned()), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let (fd, description_id) = kernel + .fd_open_external_socket(EXECUTION_DRIVER_NAME, process.pid(), false, false, false) + .expect("open managed socket"); + let lease = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, process.pid(), fd) + .expect("capture description lease"); + let mut descriptions = BTreeMap::from([( + description_id, + ManagedHostNetDescription::new( + HostSocketDomain::Inet4, + SocketKind::Stream, + lease, + process.pid(), + ), + )]); + process.finish(0); + kernel.waitpid(process.pid()).expect("reap process"); + assert_eq!( + descriptions + .get(&description_id) + .expect("description exists before sidecar retirement") + .lease + .ref_count(), + 1, + "kernel process cleanup released its final fd reference" + ); + let (_, retired) = + prune_managed_registry_routes(&mut descriptions, process.pid(), [(description_id, 0)]); + assert_eq!(retired.len(), 1); + assert!(descriptions.is_empty()); + } + + fn frozen_network_request(method: &str) -> HostRpcRequest { + match method { + "__kernel_poll" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!([{ "fd": 0, "events": 1 }]), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.posix_poll" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!([{ "fd": 0, "events": 1 }]), json!(0), Value::Null], + raw_bytes_args: HashMap::new(), + }, + "dns.lookup" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!({ "hostname": "localhost", "family": 4 })], + raw_bytes_args: HashMap::new(), + }, + "dns.resolveRawRr" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!({ "hostname": "localhost", "rrtype": "A" })], + raw_bytes_args: HashMap::new(), + }, + "process.fd_socketpair" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(1), json!(true), json!(true)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_fd_open" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(2), json!(6), json!(true), json!(true)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_bind" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![ + json!(3), + json!({ "type": "inet", "host": "127.0.0.1", "port": 0 }), + ], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_connect" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![ + json!(3), + json!({ "type": "inet", "host": "127.0.0.1", "port": 80 }), + json!(0), + ], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_listen" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!(16)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_accept" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!(true), json!(true), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_recv" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!(1), json!(0), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_send" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!("x"), json!(0), Value::Null, json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_local_address" | "process.hostnet_peer_address" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_get_option" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!("error")], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_set_option" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!("reuse-address"), json!(true)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_poll" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!([{ "fd": 3, "readable": true }]), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_tls_connect" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!("localhost"), json!([]), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.hostnet_validate" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!(true)], + raw_bytes_args: HashMap::new(), + }, + "process.fd_socket_shutdown" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!(2)], + raw_bytes_args: HashMap::new(), + }, + "process.fd_sendmsg_rights" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![json!(3), json!("x"), json!([4]), json!(0)], + raw_bytes_args: HashMap::new(), + }, + "process.fd_recvmsg_rights" => HostRpcRequest { + id: 1, + method: method.to_owned(), + args: vec![ + json!(3), + json!(64), + json!(4), + json!(true), + json!(false), + json!(true), + json!(false), + ], + raw_bytes_args: HashMap::new(), + }, + _ => request(method), + } + } + + #[test] + fn guest_wait_deadline_rejects_unbounded_duration_without_panicking() { + let error = checked_deferred_guest_wait_deadline(u64::MAX) + .expect_err("an unbounded guest wait must be rejected"); + + assert_eq!(error.code, "EINVAL"); + assert!(error.message.contains("guestWaitDurationMs")); + } + + #[test] + fn every_frozen_net_and_dgram_rpc_has_a_bounded_typed_route() { + for method in WASM_RUNNER_RPC_INVENTORY + .iter() + .copied() + .filter(|method| method.starts_with("net.") || method.starts_with("dgram.")) + { + let operation = decode_managed(&request(method), 1024 * 1024) + .unwrap_or_else(|error| panic!("decode {method}: {error}")); + assert!( + operation.is_some(), + "{method} must not use legacy fallthrough" + ); + let operation = operation.expect("checked typed operation"); + assert!( + is_direct_managed_operation(&operation), + "{method} must execute directly without reconstructing an RPC request" + ); + assert!( + descriptor_rights_compat_request(1, operation).is_err(), + "{method} must be rejected by the descriptor-rights-only compatibility adapter" + ); + } + } + + #[test] + fn managed_read_preserves_the_complete_runner_request() { + let request = HostRpcRequest { + id: 1, + method: "net.socket_read".to_owned(), + args: vec![json!("s1"), json!(4096), json!(true), json!(17)], + raw_bytes_args: HashMap::new(), + }; + assert!(matches!( + decode_managed(&request, 1024).expect("decode managed read"), + Some(NetworkOperation::ManagedRead { + max_bytes: 4096, + peek: true, + wait_ms: 17, + .. + }) + )); + } + + #[test] + fn every_frozen_network_family_rpc_has_a_typed_operation_route() { + use crate::execution::host_dispatch::inventory::{capability_family, HostCapabilityFamily}; + for method in WASM_RUNNER_RPC_INVENTORY + .iter() + .copied() + .filter(|method| capability_family(method) == Some(HostCapabilityFamily::Network)) + { + let decoded = super::super::decode_host_operation( + &frozen_network_request(method), + true, + 1024 * 1024, + ) + .unwrap_or_else(|error| panic!("decode frozen Network RPC {method}: {error}")); + assert!( + matches!(decoded, Some(HostOperation::Network(_))), + "{method} must route to a typed Network operation" + ); + } + } + + #[test] + fn host_network_scm_rights_metadata_stays_an_explicit_compatibility_projection() { + let mut request = frozen_network_request("process.fd_sendmsg_rights"); + request.args[2] = json!([{ + "kind": "hostNet", + "domain": 1, + "socketType": 1, + "protocol": 0, + "nonblocking": false, + "listening": false + }]); + assert!( + super::super::decode_host_operation(&request, true, 1024) + .expect("decode compatibility host-network transfer") + .is_none(), + "V8-owned host-network description metadata is adapter state, not a neutral kernel right" + ); + } + + #[test] + fn managed_decoder_rejects_oversized_id_path_and_bytes() { + let mut id = request("net.destroy"); + id.args[0] = json!("x".repeat(MANAGED_ID_BYTES + 1)); + assert_eq!( + decode_managed(&id, 1024).unwrap_err().code(), + Some("ENAMETOOLONG") + ); + let mut path = request("net.bind_unix"); + path.args[0] = json!({"path":"x".repeat(UNIX_PATH_BYTES + 1)}); + assert_eq!( + decode_managed(&path, 1024 * 1024).unwrap_err().code(), + Some("ENAMETOOLONG") + ); + let mut bytes = request("net.write"); + bytes.args[1] = json!("12345"); + assert_eq!(decode_managed(&bytes, 4).unwrap_err().code(), Some("E2BIG")); + } + + #[test] + fn kernel_poll_is_a_bounded_typed_network_operation() { + let request = HostRpcRequest { + id: 1, + method: "__kernel_poll".to_owned(), + args: vec![ + json!([ + { "fd": 0, "events": 1 }, + { "fd": 1, "events": 4 } + ]), + Value::Null, + ], + raw_bytes_args: HashMap::new(), + }; + assert!(matches!( + super::super::decode_host_operation(&request, true, 1024).expect("decode poll"), + Some(HostOperation::Network(NetworkOperation::KernelPoll { + timeout_ms: None, + .. + })) + )); + + let mut oversized = request; + oversized.args[0] = Value::Array( + (0..=1024) + .map(|fd| json!({ "fd": fd, "events": 1 })) + .collect(), + ); + let error = super::super::decode_host_operation(&oversized, true, 1024) + .expect_err("oversized poll set must fail"); + assert_eq!(error.code(), Some("E2BIG")); + + let ppoll = HostRpcRequest { + id: 2, + method: "process.posix_poll".to_owned(), + args: vec![ + json!([{ "fd": 7, "events": POSIX_POLLIN }]), + Value::Null, + json!([2, 15]), + ], + raw_bytes_args: HashMap::new(), + }; + assert!(matches!( + super::super::decode_host_operation(&ppoll, true, 1024).expect("decode ppoll"), + Some(HostOperation::Network(NetworkOperation::PosixPoll { + timeout_ms: None, + signal_mask: Some(SignalSetValue(bits)), + .. + })) if bits == (1_u64 << 1) | (1_u64 << 14) + )); + } + + #[test] + fn closed_udp_reentry_lane_cancels_the_claimed_reply() { + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create UDP re-entry test runtime"); + let target = Arc::new(ReplyTarget::default()); + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 3, + pid: 9, + call_id: 11, + }, + target.clone(), + 4096, + ) + .expect("create direct reply"); + assert!(reply.claim().expect("claim reply")); + let (sender, receiver) = tokio::sync::mpsc::channel(1); + drop(receiver); + runtime.context().handle().block_on(send_udp_reentry( + sender, + Arc::new(tokio::sync::Notify::new()), + "connection".to_owned(), + "session".to_owned(), + "vm".to_owned(), + "process".to_owned(), + ManagedUdpPollRecheck { + root_process_id: "process".to_owned(), + process_path: Vec::new(), + socket_id: "udp-1".to_owned(), + peek: false, + max_bytes: None, + deadline: Instant::now(), + operation_deadline: None, + deadline_warning_emitted: false, + native_probe_completed: false, + native_event: None, + reply, + fair_turn: None, + }, + )); + let result = target + .result + .lock() + .expect("reply result") + .take() + .expect("closed lane settles reply"); + assert!(result.0, "reply must stay claimed through cancellation"); + assert_eq!(result.1.expect_err("cancellation error").code, "ECANCELED"); + } + + #[test] + fn descendant_udp_reentry_preserves_root_lane_and_process_path() { + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create UDP re-entry test runtime"); + let target = Arc::new(ReplyTarget::default()); + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 3, + pid: 9, + call_id: 12, + }, + target, + 4096, + ) + .expect("create direct reply"); + let (sender, mut receiver) = tokio::sync::mpsc::channel(1); + runtime.context().handle().block_on(send_udp_reentry( + sender, + Arc::new(tokio::sync::Notify::new()), + "connection".to_owned(), + "session".to_owned(), + "vm".to_owned(), + "root".to_owned(), + ManagedUdpPollRecheck { + root_process_id: "root".to_owned(), + process_path: vec!["child-1".to_owned(), "child-2".to_owned()], + socket_id: "udp-1".to_owned(), + peek: false, + max_bytes: None, + deadline: Instant::now(), + operation_deadline: None, + deadline_warning_emitted: false, + native_probe_completed: false, + native_event: None, + reply, + fair_turn: None, + }, + )); + + let envelope = receiver.try_recv().expect("UDP re-entry envelope"); + assert_eq!(envelope.process_id, "root"); + let ActiveExecutionEvent::ManagedUdpPollRecheck(pending) = envelope.event else { + panic!("expected UDP poll re-entry event"); + }; + assert_eq!(pending.root_process_id, "root"); + assert_eq!(pending.process_path, ["child-1", "child-2"]); + assert_eq!(pending.reply.identity().generation, 3); + assert_eq!(pending.reply.identity().pid, 9); + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/process.rs b/crates/native-sidecar/src/execution/host_dispatch/process.rs new file mode 100644 index 0000000000..fceec43821 --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/process.rs @@ -0,0 +1,415 @@ +use super::*; +use agentos_kernel::kernel::{WaitPidEvent, WaitPidFlags}; +use agentos_kernel::process_runtime::ProcessExit; +use agentos_kernel::process_table::{ProcessResourceLimit, ProcessResourceLimitKind}; +use agentos_kernel::resource_accounting::{ + DEFAULT_MAX_PROCESS_ARGV_BYTES, DEFAULT_MAX_PROCESS_ENV_BYTES, +}; + +pub(super) struct ProcessCapability; + +impl SidecarHostCapability for ProcessCapability { + fn requires_claim(operation: &ProcessOperation) -> bool { + matches!( + operation, + ProcessOperation::Spawn(_) + | ProcessOperation::RunCaptured { .. } + | ProcessOperation::Exec(_) + | ProcessOperation::OpenExecutableImage { .. } + | ProcessOperation::CloseExecutableImage { .. } + | ProcessOperation::PollChild { .. } + | ProcessOperation::WriteChildStdin { .. } + | ProcessOperation::CloseChildStdin { .. } + | ProcessOperation::SetResourceLimit { .. } + | ProcessOperation::Umask { new_mask: Some(_) } + | ProcessOperation::SetProcessGroup { .. } + | ProcessOperation::Kill { .. } + | ProcessOperation::Wait { .. } + | ProcessOperation::WaitTransition { .. } + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: ProcessOperation, + ) -> Result { + let value = match operation { + ProcessOperation::OpenExecutableImage { source } => { + if process.executable_image.is_some() { + return Err(HostServiceError::new( + "EBUSY", + "this process already owns an executable-image snapshot", + )); + } + let image = match source { + ExecutableImageSource::Path(path) => { + let path = if path.as_str().starts_with('/') { + normalize_path(path.as_str()) + } else { + normalize_path(&format!("{}/{}", process.guest_cwd, path.as_str())) + }; + kernel.load_process_runtime_image( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + process.limits.wasm.max_module_file_bytes, + ) + } + ExecutableImageSource::Descriptor(fd) => kernel + .load_process_runtime_image_from_fd( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + process.limits.wasm.max_module_file_bytes, + ), + } + .map_err(kernel_host_error)?; + let size = image.bytes.len(); + let mode = image.mode; + let canonical_path = image.canonical_path; + let retained_bytes = process + .runtime_context + .resources() + .reserve(ResourceClass::ExecutorBytes, size) + .map_err(executable_image_limit_error)?; + let handle = process.install_executable_image(image.bytes, retained_bytes)?; + json!({ + "handle": handle.to_string(), + "canonicalPath": canonical_path, + "size": size, + "mode": mode, + }) + } + ProcessOperation::ReadExecutableImage { + handle, + offset, + max_bytes, + } => { + let bytes = process.read_executable_image(handle, offset, max_bytes.get())?; + return Ok(HostCallReply::Json(host_bytes_value(bytes))); + } + ProcessOperation::CloseExecutableImage { handle } => { + process.close_executable_image(handle)?; + Value::Null + } + ProcessOperation::GetImage { max_reply_bytes } => { + let image = bounded_committed_process_image( + kernel, + process.kernel_pid, + max_reply_bytes.get(), + )?; + json!({ + "argv": image + .argv + .into_vec() + .into_iter() + .map(BoundedString::into_string) + .collect::>(), + "env": image + .env + .into_vec() + .into_iter() + .map(|(key, value)| { + vec![key.into_string(), value.into_string()] + }) + .collect::>(), + }) + } + ProcessOperation::GetPid => json!(kernel + .getpid(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map_err(kernel_host_error)?), + ProcessOperation::GetParentPid => json!(kernel + .getppid(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map_err(kernel_host_error)?), + ProcessOperation::GetProcessGroup { pid } => json!(kernel + .getpgid(EXECUTION_DRIVER_NAME, pid.unwrap_or(process.kernel_pid),) + .map_err(kernel_host_error)?), + ProcessOperation::SetProcessGroup { pid, pgid } => { + let pid = pid.unwrap_or(process.kernel_pid); + kernel + .setpgid(EXECUTION_DRIVER_NAME, pid, pgid.unwrap_or(pid)) + .map_err(kernel_host_error)?; + Value::Null + } + ProcessOperation::Kill { target, signal } => { + // Linux permits signaling any same-credential process. The + // per-VM kernel and requester-driver ownership checks are the + // isolation boundary; restricting this to self/direct children + // incorrectly rejects ordinary shell jobs signaling a parent + // or sibling process. + kernel + .signal_process(EXECUTION_DRIVER_NAME, target, signal) + .map_err(kernel_host_error)?; + Value::Null + } + ProcessOperation::Wait { + target, + options, + deadline_ms, + temporary_mask, + } => { + if deadline_ms.is_some() || temporary_mask.is_some() { + return Err(HostServiceError::new( + "EINVAL", + "synchronous waitpid does not accept a deadline or temporary mask", + )); + } + probe_process_wait(kernel, process.kernel_pid, target, options)? + } + ProcessOperation::WaitTransition { target, options } => { + let selector = wait_selector(kernel, process.kernel_pid, target)?; + match kernel + .take_nonterminal_wait_event( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + selector, + wait_flags(options), + ) + .map_err(kernel_host_error)? + { + Some(event) => { + let status = match event.event { + WaitPidEvent::Stopped => ((event.status as u32 & 0xff) << 8) | 0x7f, + WaitPidEvent::Continued => 0xffff, + WaitPidEvent::Exited => { + return Err(HostServiceError::new( + "EIO", + "terminal wait event escaped nonterminal query", + )); + } + }; + json!({ "pid": event.pid, "status": status }) + } + None => Value::Null, + } + } + ProcessOperation::Umask { new_mask } => json!(kernel + .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask) + .map_err(kernel_host_error)?), + ProcessOperation::GetResourceLimit { kind } => { + let limit = kernel + .get_resource_limit( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + resource_kind(kind), + ) + .map_err(kernel_host_error)?; + json!({ + "soft": limit.soft.unwrap_or(u64::MAX).to_string(), + "hard": limit.hard.unwrap_or(u64::MAX).to_string(), + }) + } + ProcessOperation::SetResourceLimit { kind, value } => { + kernel + .set_resource_limit( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + resource_kind(kind), + ProcessResourceLimit { + soft: value.soft, + hard: value.hard, + }, + ) + .map_err(kernel_host_error)?; + Value::Null + } + ProcessOperation::SystemIdentity => { + let identity = kernel.system_identity(); + json!({ + "hostname": identity.hostname, + "type": identity.os_type, + "release": identity.os_release, + "version": identity.os_version, + "machine": identity.machine, + "domainName": identity.domain_name, + }) + } + other => return Err(unsupported("process", other)), + }; + Ok(HostCallReply::Json(value)) + } +} + +fn bounded_committed_process_image( + kernel: &SidecarKernel, + pid: u32, + max_reply_bytes: usize, +) -> Result { + #[derive(serde::Serialize)] + struct Reply<'a> { + argv: &'a [String], + env: &'a [(String, String)], + } + + let reply_limit = PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes)?; + let limits = kernel.resource_limits(); + let argv_maximum = limits + .max_process_argv_bytes + .unwrap_or(DEFAULT_MAX_PROCESS_ARGV_BYTES) + .max(1); + let env_maximum = limits + .max_process_env_bytes + .unwrap_or(DEFAULT_MAX_PROCESS_ENV_BYTES) + .max(1); + let image = kernel + .process_image(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + // Count the exact wire JSON with a bounded streaming writer before + // constructing a serde_json::Value. This remains safe when a trusted VM + // raises the kernel argv/env limits above the bridge response limit. + reply_limit.admit_json(&Reply { + argv: &image.argv, + env: &image.env, + })?; + let argv_limit = PayloadLimit::new("limits.resources.maxProcessArgvBytes", argv_maximum)?; + let env_limit = PayloadLimit::new("limits.resources.maxProcessEnvBytes", env_maximum)?; + let argv = image + .argv + .into_iter() + .map(|value| BoundedString::try_new(value, &argv_limit)) + .collect::, _>>()?; + let env = image + .env + .into_iter() + .map(|(key, value)| { + Ok(( + BoundedString::try_new(key, &env_limit)?, + BoundedString::try_new(value, &env_limit)?, + )) + }) + .collect::, HostServiceError>>()?; + Ok(CommittedProcessImage { + argv: BoundedVec::try_new(argv, &argv_limit)?, + env: BoundedVec::try_new(env, &env_limit)?, + }) +} + +pub(super) fn probe_process_wait( + kernel: &mut SidecarKernel, + caller_pid: u32, + target: WaitTarget, + options: u32, +) -> Result { + let selector = wait_selector(kernel, caller_pid, target)?; + wait_result_value( + kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + caller_pid, + selector, + wait_flags(options), + ) + .map_err(kernel_host_error)?, + ) +} + +fn executable_image_limit_error(error: LimitError) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_RESOURCE_LIMIT", error.to_string()).with_details(json!({ + "scope": error.scope, + "resource": error.resource.name(), + "used": error.used, + "requested": error.requested, + "limit": error.limit, + "limitName": error.config_path, + })) +} + +fn wait_flags(options: u32) -> WaitPidFlags { + let mut flags = WaitPidFlags::WNOHANG; + if options & 2 != 0 { + flags |= WaitPidFlags::WUNTRACED; + } + if options & 8 != 0 { + flags |= WaitPidFlags::WCONTINUED; + } + flags +} + +fn wait_selector( + _kernel: &SidecarKernel, + _caller_pid: u32, + target: WaitTarget, +) -> Result { + match target { + WaitTarget::Any => Ok(-1), + WaitTarget::Pid(pid) => i32::try_from(pid) + .map_err(|_| HostServiceError::new("EINVAL", "waitpid PID exceeds i32")), + // waitpid(0, ...) is relative to the caller's process group. Preserve + // selector zero so the kernel evaluates it with the caller identity; + // converting pgid 1 to -1 would incorrectly mean "any child". + WaitTarget::ProcessGroup(0) => Ok(0), + WaitTarget::ProcessGroup(pgid) => i32::try_from(pgid) + .map(|pgid| -pgid) + .map_err(|_| HostServiceError::new("EINVAL", "process group exceeds i32")), + } +} + +fn wait_result_value( + transition: Option, +) -> Result { + let Some(transition) = transition else { + return Ok(Value::Null); + }; + let (event, raw_status, exit_code, signal, core_dumped) = + match (transition.event, transition.termination) { + (WaitPidEvent::Exited, Some(ProcessExit::Exited(code))) => ( + "exit", + (code as u32 & 0xff) << 8, + code as u32 & 0xff, + 0, + false, + ), + ( + WaitPidEvent::Exited, + Some(ProcessExit::Signaled { + signal, + core_dumped, + }), + ) => ( + "exit", + (signal as u32 & 0x7f) | if core_dumped { 0x80 } else { 0 }, + 0, + signal as u32 & 0x7f, + core_dumped, + ), + (WaitPidEvent::Stopped, _) => ( + "stopped", + ((transition.status as u32 & 0xff) << 8) | 0x7f, + 0, + transition.status as u32 & 0xff, + false, + ), + (WaitPidEvent::Continued, _) => ("continued", 0xffff, 0, 0, false), + (WaitPidEvent::Exited, None) => { + return Err(HostServiceError::new( + "EIO", + "kernel terminal wait transition omitted exact termination", + )); + } + }; + Ok(json!({ + "pid": transition.pid, + "event": event, + "status": transition.status, + "rawStatus": raw_status, + "exitCode": exit_code, + "signal": signal, + "coreDumped": core_dumped, + })) +} + +fn resource_kind(kind: ResourceLimitKind) -> ProcessResourceLimitKind { + match kind { + ResourceLimitKind::AddressSpace => ProcessResourceLimitKind::AddressSpace, + ResourceLimitKind::Core => ProcessResourceLimitKind::Core, + ResourceLimitKind::Cpu => ProcessResourceLimitKind::Cpu, + ResourceLimitKind::Data => ProcessResourceLimitKind::Data, + ResourceLimitKind::FileSize => ProcessResourceLimitKind::FileSize, + ResourceLimitKind::LockedMemory => ProcessResourceLimitKind::LockedMemory, + ResourceLimitKind::OpenFiles => ProcessResourceLimitKind::OpenFiles, + ResourceLimitKind::Processes => ProcessResourceLimitKind::Processes, + ResourceLimitKind::ResidentSet => ProcessResourceLimitKind::ResidentSet, + ResourceLimitKind::Stack => ProcessResourceLimitKind::Stack, + } +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/signal.rs b/crates/native-sidecar/src/execution/host_dispatch/signal.rs new file mode 100644 index 0000000000..5f8e33a51f --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/signal.rs @@ -0,0 +1,164 @@ +use super::*; +use agentos_kernel::process_table::{SigmaskHow, SignalAction, SignalDisposition, SignalSet}; + +pub(super) struct SignalCapability; + +impl SidecarHostCapability for SignalCapability { + fn requires_claim(operation: &SignalOperation) -> bool { + matches!( + operation, + SignalOperation::SetAction { .. } + | SignalOperation::UpdateMask { .. } + | SignalOperation::BeginDelivery + | SignalOperation::TakePublishedDelivery + | SignalOperation::EndDelivery { .. } + | SignalOperation::BeginTemporaryMask { .. } + | SignalOperation::EndTemporaryMask { .. } + ) + } + + fn execute( + _: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: SignalOperation, + ) -> Result { + let value = match operation { + SignalOperation::GetAction { signal } => { + let action = process + .kernel_handle + .signal_action(signal, None) + .map_err(kernel_host_error)?; + signal_action_value(action) + } + SignalOperation::SetAction { signal, action } => { + process + .kernel_handle + .signal_action(signal, Some(kernel_signal_action(action)?)) + .map_err(kernel_host_error)?; + Value::Null + } + SignalOperation::BeginDelivery => { + materialize_real_timer_signal(process); + process + .kernel_handle + .begin_signal_delivery() + .map_err(kernel_host_error)? + .map(|delivery| { + json!({ + "signal": delivery.signal, + "token": delivery.token, + "flags": delivery.action.flags, + }) + }) + .unwrap_or(Value::Null) + } + SignalOperation::TakePublishedDelivery => { + materialize_real_timer_signal(process); + let identity = process.kernel_handle.runtime_identity(); + let delivery = ExecutionBackend::take_signal_checkpoint( + &process.execution, + ExecutionWakeIdentity { + generation: identity.generation, + pid: identity.pid, + }, + )?; + if delivery.is_none() { + process.guest_signal_checkpoint_pending = false; + } + delivery + .map(|delivery| { + json!({ + "signal": delivery.signal, + "token": delivery.delivery_token, + "flags": delivery.flags, + }) + }) + .unwrap_or(Value::Null) + } + SignalOperation::EndDelivery { token } => { + process + .kernel_handle + .end_signal_delivery(token) + .map_err(kernel_host_error)?; + Value::Null + } + SignalOperation::UpdateMask { how, set } => { + let previous = process + .kernel_handle + .sigprocmask(kernel_mask_how(how), kernel_signal_set(set)?) + .map_err(kernel_host_error)?; + json!({ "signals": previous.signals() }) + } + SignalOperation::Pending => json!({ + "signals": process + .kernel_handle + .sigpending() + .map_err(kernel_host_error)? + .signals(), + }), + SignalOperation::BeginTemporaryMask { mask } => { + let token = process + .kernel_handle + .begin_temporary_signal_mask(kernel_signal_set(mask)?) + .map_err(kernel_host_error)?; + json!(token) + } + SignalOperation::EndTemporaryMask { token } => { + process + .kernel_handle + .end_temporary_signal_mask(token) + .map_err(kernel_host_error)?; + Value::Null + } + other => return Err(unsupported("signal", other)), + }; + Ok(HostCallReply::Json(value)) + } +} + +fn materialize_real_timer_signal(process: &ActiveProcess) { + if process.real_interval_timer.take_expiry() { + process.kernel_handle.kill(libc::SIGALRM); + } +} + +fn kernel_signal_action(action: SignalActionValue) -> Result { + Ok(SignalAction { + disposition: match action.disposition { + SignalDispositionValue::Default => SignalDisposition::Default, + SignalDispositionValue::Ignore => SignalDisposition::Ignore, + SignalDispositionValue::User => SignalDisposition::User, + }, + mask: kernel_signal_set(action.mask)?, + flags: action.flags, + }) +} + +fn signal_action_value(action: SignalAction) -> Value { + let disposition = match action.disposition { + SignalDisposition::Default => "default", + SignalDisposition::Ignore => "ignore", + SignalDisposition::User => "user", + }; + json!({ + "action": disposition, + "mask": action.mask.signals(), + "flags": action.flags, + }) +} + +fn kernel_mask_how(how: SignalMaskHow) -> SigmaskHow { + match how { + SignalMaskHow::Block => SigmaskHow::Block, + SignalMaskHow::Unblock => SigmaskHow::Unblock, + SignalMaskHow::Set => SigmaskHow::SetMask, + } +} + +fn kernel_signal_set(set: SignalSetValue) -> Result { + let signals = (1..=64) + .filter(|signal| set.0 & (1_u64 << (signal - 1)) != 0) + .collect::>(); + SignalSet::from_signals(signals) + .map_err(|error| HostServiceError::new(error.code(), error.to_string())) +} diff --git a/crates/native-sidecar/src/execution/host_dispatch/terminal.rs b/crates/native-sidecar/src/execution/host_dispatch/terminal.rs new file mode 100644 index 0000000000..5680b6ba3c --- /dev/null +++ b/crates/native-sidecar/src/execution/host_dispatch/terminal.rs @@ -0,0 +1,132 @@ +use super::*; +use agentos_kernel::pty::{PartialTermios, PartialTermiosControlChars}; + +const TTY_IFLAG_ICRNL: u32 = 1 << 0; +const TTY_OFLAG_OPOST: u32 = 1 << 1; +const TTY_OFLAG_ONLCR: u32 = 1 << 2; +const TTY_LFLAG_ICANON: u32 = 1 << 3; +const TTY_LFLAG_ECHO: u32 = 1 << 4; +const TTY_LFLAG_ISIG: u32 = 1 << 5; + +pub(super) struct TerminalCapability; + +impl SidecarHostCapability for TerminalCapability { + fn requires_claim(operation: &TerminalOperation) -> bool { + matches!( + operation, + TerminalOperation::SetAttributes { .. } + | TerminalOperation::SetWindowSize { .. } + | TerminalOperation::SetForegroundProcessGroup { .. } + | TerminalOperation::SetRawMode { .. } + | TerminalOperation::OpenPty + ) + } + + fn execute( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + operation: TerminalOperation, + ) -> Result { + let value = match operation { + TerminalOperation::IsTerminal { fd } => json!(kernel + .isatty(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_host_error)?), + TerminalOperation::GetAttributes { fd } => { + let attributes = kernel + .tcgetattr(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_host_error)?; + let mut flags = 0_u32; + flags |= TTY_IFLAG_ICRNL * u32::from(attributes.icrnl); + flags |= TTY_OFLAG_OPOST * u32::from(attributes.opost); + flags |= TTY_OFLAG_ONLCR * u32::from(attributes.onlcr); + flags |= TTY_LFLAG_ICANON * u32::from(attributes.icanon); + flags |= TTY_LFLAG_ECHO * u32::from(attributes.echo); + flags |= TTY_LFLAG_ISIG * u32::from(attributes.isig); + json!({ + "flags": flags, + "cc": [ + attributes.cc.vintr, + attributes.cc.vquit, + attributes.cc.vsusp, + attributes.cc.veof, + attributes.cc.verase, + attributes.cc.vkill, + attributes.cc.vwerase, + ], + }) + } + TerminalOperation::SetAttributes { fd, attributes } => { + let cc = attributes.control_characters; + kernel + .tcsetattr( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + PartialTermios { + icrnl: Some(attributes.input_flags & TTY_IFLAG_ICRNL != 0), + opost: Some(attributes.output_flags & TTY_OFLAG_OPOST != 0), + onlcr: Some(attributes.output_flags & TTY_OFLAG_ONLCR != 0), + icanon: Some(attributes.local_flags & TTY_LFLAG_ICANON != 0), + echo: Some(attributes.local_flags & TTY_LFLAG_ECHO != 0), + isig: Some(attributes.local_flags & TTY_LFLAG_ISIG != 0), + cc: Some(PartialTermiosControlChars { + vintr: Some(cc[0]), + vquit: Some(cc[1]), + vsusp: Some(cc[2]), + veof: Some(cc[3]), + verase: Some(cc[4]), + vkill: Some(cc[5]), + vwerase: Some(cc[6]), + }), + }, + ) + .map_err(kernel_host_error)?; + Value::Null + } + TerminalOperation::GetWindowSize { fd } => { + let size = kernel + .pty_window_size(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_host_error)?; + json!({ "cols": size.cols, "rows": size.rows }) + } + TerminalOperation::SetWindowSize { fd, size } => { + kernel + .pty_resize( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + size.columns, + size.rows, + ) + .map_err(kernel_host_error)?; + Value::Null + } + TerminalOperation::GetForegroundProcessGroup { fd } => json!(kernel + .tcgetpgrp(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_host_error)?), + TerminalOperation::SetForegroundProcessGroup { fd, pgid } => { + kernel + .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, pgid) + .map_err(kernel_host_error)?; + Value::Null + } + TerminalOperation::GetSession { fd } => json!(kernel + .tcgetsid(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_host_error)?), + TerminalOperation::SetRawMode { fd, enabled } => { + process.tty_raw_mode_generation = kernel + .pty_set_raw_mode(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, enabled) + .map_err(kernel_host_error)?; + Value::Null + } + TerminalOperation::OpenPty => { + let (master_fd, slave_fd, path) = kernel + .open_pty(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map_err(kernel_host_error)?; + json!({ "masterFd": master_fd, "slaveFd": slave_fd, "path": path }) + } + other => return Err(unsupported("terminal", other)), + }; + Ok(HostCallReply::Json(value)) + } +} diff --git a/crates/native-sidecar/src/execution/javascript/crypto.rs b/crates/native-sidecar/src/execution/javascript/crypto.rs index 1606636264..418def355c 100644 --- a/crates/native-sidecar/src/execution/javascript/crypto.rs +++ b/crates/native-sidecar/src/execution/javascript/crypto.rs @@ -27,7 +27,7 @@ struct JavascriptScryptOptions { pub(crate) fn service_javascript_crypto_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { match request.method.as_str() { "crypto.hashDigest" => { @@ -186,7 +186,7 @@ pub(crate) fn service_javascript_crypto_sync_rpc( fn service_javascript_crypto_hash_create_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { ensure_per_process_state_handle_capacity(process.hash_sessions.len(), "hash session")?; let algorithm = @@ -204,7 +204,7 @@ fn service_javascript_crypto_hash_create_sync_rpc( fn service_javascript_crypto_hash_update_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.hashUpdate session id")?; let data = request @@ -226,7 +226,7 @@ fn service_javascript_crypto_hash_update_sync_rpc( fn service_javascript_crypto_hash_final_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.hashFinal session id")?; let mut session = process.hash_sessions.remove(&session_id).ok_or_else(|| { @@ -383,20 +383,20 @@ struct JavascriptDirectKeyInput { } fn service_javascript_crypto_cipheriv_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { service_javascript_crypto_cipheriv_inner(request, false) } fn service_javascript_crypto_decipheriv_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { service_javascript_crypto_cipheriv_inner(request, true) } fn service_javascript_crypto_cipheriv_create_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?; let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?; @@ -424,7 +424,7 @@ fn service_javascript_crypto_cipheriv_create_sync_rpc( fn service_javascript_crypto_cipheriv_update_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?; @@ -443,7 +443,7 @@ fn service_javascript_crypto_cipheriv_update_sync_rpc( fn service_javascript_crypto_cipheriv_final_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?; @@ -471,7 +471,7 @@ fn service_javascript_crypto_cipheriv_final_sync_rpc( } fn service_javascript_crypto_sign_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let algorithm = request.args.first().and_then(Value::as_str); let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?; @@ -498,7 +498,7 @@ fn service_javascript_crypto_sign_sync_rpc( } fn service_javascript_crypto_verify_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let algorithm = request.args.first().and_then(Value::as_str); let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?; @@ -522,7 +522,7 @@ fn service_javascript_crypto_verify_sync_rpc( } fn service_javascript_crypto_asymmetric_op_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?; let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?; @@ -577,7 +577,7 @@ fn service_javascript_crypto_asymmetric_op_sync_rpc( } fn service_javascript_crypto_create_key_object_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?; @@ -604,7 +604,7 @@ fn service_javascript_crypto_create_key_object_sync_rpc( } fn service_javascript_crypto_generate_key_pair_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?; @@ -678,7 +678,7 @@ fn service_javascript_crypto_generate_key_pair_sync_rpc( } fn service_javascript_crypto_generate_key_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?; let options = javascript_crypto_parse_serialized_options_arg( @@ -716,7 +716,7 @@ fn service_javascript_crypto_generate_key_sync_rpc( } fn service_javascript_crypto_generate_prime_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let bits = javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32; @@ -763,7 +763,7 @@ fn service_javascript_crypto_generate_prime_sync_rpc( } fn service_javascript_crypto_diffie_hellman_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?; let parsed: Value = serde_json::from_str(options).map_err(|error| { @@ -808,7 +808,7 @@ fn service_javascript_crypto_diffie_hellman_sync_rpc( } fn service_javascript_crypto_diffie_hellman_group_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?; let params = javascript_crypto_named_dh_group(name)?; @@ -831,7 +831,7 @@ fn service_javascript_crypto_diffie_hellman_group_sync_rpc( fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { ensure_per_process_state_handle_capacity( process.diffie_hellman_sessions.len(), @@ -900,7 +900,7 @@ fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc( fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64( &request.args, @@ -954,7 +954,7 @@ fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc( fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let session_id = javascript_sync_rpc_arg_u64( &request.args, @@ -971,7 +971,7 @@ fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc( } fn service_javascript_crypto_subtle_sync_rpc( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?; let parsed: Value = serde_json::from_str(raw).map_err(|error| { @@ -1414,7 +1414,7 @@ fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result Result { let label = if decrypt { diff --git a/crates/native-sidecar/src/execution/javascript/http.rs b/crates/native-sidecar/src/execution/javascript/http.rs index 291303eca5..5b346852da 100644 --- a/crates/native-sidecar/src/execution/javascript/http.rs +++ b/crates/native-sidecar/src/execution/javascript/http.rs @@ -1,12 +1,11 @@ use super::super::*; -use crate::state::DeferredRpcError; const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const VM_FETCH_STREAM_CHUNK_MAX_BYTES: usize = 64 * 1024; const VM_FETCH_STREAM_COUNT_LIMIT: usize = 256; type VmFetchResponseHead = (u16, String, Vec<(String, String)>, VmFetchBodyMode); -fn http_loopback_request_timeout() -> Duration { +pub(in crate::execution) fn http_loopback_request_timeout() -> Duration { std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV) .ok() .and_then(|value| value.parse::().ok()) @@ -48,29 +47,10 @@ pub(in crate::execution) struct HttpHeaderCollection { raw_pairs: Vec<(String, String)>, } -struct LoopbackHttpResponseWaitRequest<'a, B> { - bridge: &'a SharedBridge, - vm_id: &'a str, - dns: &'a VmDnsConfig, - socket_paths: &'a JavascriptSocketPathContext, - kernel: &'a mut SidecarKernel, - kernel_readiness: KernelSocketReadinessRegistry, - process: &'a mut ActiveProcess, - request_key: (u64, u64), - capabilities: CapabilityRegistry, -} - -pub(crate) struct LoopbackHttpDispatchRequest<'a, B> { - pub(crate) bridge: &'a SharedBridge, - pub(crate) vm_id: &'a str, - pub(crate) dns: &'a VmDnsConfig, - pub(crate) socket_paths: &'a JavascriptSocketPathContext, - pub(crate) kernel: &'a mut SidecarKernel, - pub(crate) kernel_readiness: KernelSocketReadinessRegistry, +pub(crate) struct LoopbackHttpDispatchRequest<'a> { pub(crate) process: &'a mut ActiveProcess, pub(crate) server_id: u64, pub(crate) request_json: &'a str, - pub(crate) capabilities: CapabilityRegistry, } pub(in crate::execution) fn parse_http_header_collection( @@ -168,7 +148,7 @@ pub(in crate::execution) fn serialize_http_loopback_request( "rawHeaders": http_raw_headers_json(headers), "bodyBase64": body_base64, })) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) + .map_err(|error| SidecarError::host("ERR_AGENTOS_NODE_SYNC_RPC", format!("{error}"))) } fn http_request_target(url: &Url) -> String { @@ -489,7 +469,7 @@ async fn service_host_fetch_target_event( bridge: &SharedBridge, vm_id: &str, dns: &VmDnsConfig, - socket_paths: &JavascriptSocketPathContext, + socket_paths: &SocketPathContext, kernel: &mut SidecarKernel, kernel_readiness: &KernelSocketReadinessRegistry, process: &mut ActiveProcess, @@ -500,32 +480,28 @@ where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { + let identity = process.kernel_handle.runtime_identity(); + let max_reply_bytes = process.limits.reactor.max_bridge_response_bytes; let event = if wait.is_zero() { process .execution - .try_poll_event() - .map_err(|error| SidecarError::Execution(error.to_string()))? + .try_poll_event(identity, max_reply_bytes)? } else { process .execution - .poll_event(wait) - .await - .map_err(|error| SidecarError::Execution(error.to_string()))? + .poll_event(identity, max_reply_bytes, wait) + .await? }; let Some(event) = event else { return Ok(false) }; match event { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - if request.method == "net.http_wait" => - { + ActiveExecutionEvent::HostRpcRequest(request) if request.method == "net.http_wait" => { // The listener wait intentionally remains pending until server // close. A nested vm.fetch pump must not steal it from the main // sidecar dispatcher or wait for it inline. - process.queue_pending_execution_event( - ActiveExecutionEvent::JavascriptSyncRpcRequest(request), - )?; + process.queue_pending_execution_event(ActiveExecutionEvent::HostRpcRequest(request))?; } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::HostRpcRequest(request) => { let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { bridge, vm_id, @@ -536,9 +512,10 @@ where process, sync_request: &request, capabilities: capabilities.clone(), + managed_descriptions: None, }) .await; - settle_nested_javascript_sync_rpc(process, &request, response).await?; + settle_execution_host_call(&request.reply, response)?; } ActiveExecutionEvent::Exited(code) => { return Err(SidecarError::Execution(format!( @@ -552,81 +529,18 @@ where Ok(true) } -async fn settle_nested_javascript_sync_rpc( - process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, - response: Result, -) -> Result<(), SidecarError> { - let response = match response { - Ok(JavascriptSyncRpcServiceResponse::Deferred { - receiver, timeout, .. - }) => { - let receive = async { - receiver.await.unwrap_or_else(|_| { - Err(DeferredRpcError { - code: String::from("ERR_AGENTOS_DEFERRED_RPC_RESPONSE_CHANNEL_CLOSED"), - message: format!( - "deferred sync RPC response channel closed for {}", - request.method - ), - }) - }) - }; - let result = match timeout { - Some(timeout) => match tokio::time::timeout(timeout, receive).await { - Ok(result) => result, - Err(_) => Err(DeferredRpcError { - code: String::from("ERR_AGENTOS_DEFERRED_RPC_TIMEOUT"), - message: format!( - "{} deferred response timed out after {} ms", - request.method, - timeout.as_millis() - ), - }), - }, - None => receive.await, - }; - match result { - Ok(value) => Ok(JavascriptSyncRpcServiceResponse::Json(value)), - Err(error) => { - return process - .execution - .respond_javascript_sync_rpc_error(request.id, error.code, error.message) - .or_else(ignore_stale_javascript_sync_rpc_response); - } - } - } - other => other, - }; - match response { - Ok(result) => process - .execution - .respond_javascript_sync_rpc_response(request.id, result) - .or_else(ignore_stale_javascript_sync_rpc_response), - Err(error) => process - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response), - } -} - async fn drain_host_fetch_target_events( bridge: &SharedBridge, vm_id: &str, vm: &mut VmState, target_process_id: &str, - socket_paths: &JavascriptSocketPathContext, + socket_paths: &SocketPathContext, ) -> Result<(), SidecarError> where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let mut idle_turns = 0; - for _ in 0..64 { + for _ in 0..32 { let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let capabilities = vm.capabilities.clone(); @@ -646,13 +560,11 @@ where ) .await?; if !serviced { - idle_turns += 1; - if idle_turns >= 8 { - break; - } - tokio::task::yield_now().await; - } else { - idle_turns = 0; + // A just-closed client socket may need another bounded reactor + // turn before the guest observes EOF and retires its accepted + // socket. Keep probing within this fixed 32 ms cleanup budget + // instead of returning after the first empty 1 ms poll. + continue; } } Ok(()) @@ -675,10 +587,10 @@ where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let socket_paths = build_javascript_socket_path_context(vm)?; - // This is an outbound connection, so bind port zero and let the kernel - // reserve a distinct ephemeral source port. The JavaScript listen-port - // allocator is for servers and does not track active client sockets. + let socket_paths = build_socket_path_context(vm)?; + // Client source ports belong to the kernel socket table. The listen-port + // allocator does not reserve active client sockets and can hand the same + // source port to concurrent requests. let local_port = 0; let pending_capability = reserve_capability(&vm.capabilities, CapabilityKind::TcpSocket)?; @@ -725,10 +637,25 @@ where } else { Ok(()) }; - match (result, close_result) { - (Ok(response), Ok(())) => cleanup_result.map(|()| response), - (Err(error), _) => Err(error), - (Ok(_), Err(error)) => Err(error), + match result { + Ok(response) => { + close_result?; + cleanup_result?; + Ok(response) + } + Err(error) => { + if let Err(close_error) = close_result { + eprintln!( + "ERR_AGENTOS_HTTP_FETCH_CLEANUP: failed to close kernel socket {socket_id} after fetch error: {close_error}" + ); + } + if let Err(cleanup_error) = cleanup_result { + eprintln!( + "ERR_AGENTOS_HTTP_FETCH_CLEANUP: failed to drain target events after fetch error: {cleanup_error}" + ); + } + Err(error) + } } } @@ -756,7 +683,7 @@ where VM_FETCH_STREAM_COUNT_LIMIT ))); } - let socket_paths = build_javascript_socket_path_context(vm)?; + let socket_paths = build_socket_path_context(vm)?; // Keep the source port kernel-owned for the lifetime of the stream. Using // the listen-port allocator here can return the same port to every active // request because client sockets are not part of its reservation table. @@ -829,11 +756,14 @@ where let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let capabilities = vm.capabilities.clone(); - let process = vm.active_processes.get_mut(target_process_id).ok_or_else(|| { - SidecarError::InvalidState(format!( - "vm.fetch target process disappeared: {target_process_id}" - )) - })?; + let process = vm + .active_processes + .get_mut(target_process_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "vm.fetch target process disappeared: {target_process_id}" + )) + })?; service_host_fetch_target_event( bridge, vm_id, @@ -852,7 +782,10 @@ where .poll_targets( EXECUTION_DRIVER_NAME, kernel_pid, - vec![PollTargetEntry::socket(socket_id, POLLIN | POLLHUP | POLLERR)], + vec![PollTargetEntry::socket( + socket_id, + POLLIN | POLLHUP | POLLERR, + )], 0, ) .map_err(kernel_error)?; @@ -924,16 +857,25 @@ where "statusText": status_text, "headers": response_headers, })) - .map_err(|error| SidecarError::Execution(format!( - "ERR_AGENTOS_VM_FETCH_SERIALIZE: failed to serialize response head: {error}" - ))) + .map_err(|error| { + SidecarError::Execution(format!( + "ERR_AGENTOS_VM_FETCH_SERIALIZE: failed to serialize response head: {error}" + )) + }) } .await; if result.is_err() { - let _ = vm + if let Err(error) = vm .kernel - .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id); + .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) + { + tracing::error!( + socket_id, + error = %error, + "failed to close kernel socket after VM fetch stream start error" + ); + } } result } @@ -954,7 +896,7 @@ where .socket_close(EXECUTION_DRIVER_NAME, state.kernel_pid, state.socket_id) .map_err(kernel_error); drop(state); - let socket_paths = build_javascript_socket_path_context(vm)?; + let socket_paths = build_socket_path_context(vm)?; let cleanup_result = drain_host_fetch_target_events(bridge, vm_id, vm, &target_process_id, &socket_paths).await; close_result.and(cleanup_result) @@ -988,7 +930,7 @@ where http_loopback_request_timeout().as_millis() ))); } - let socket_paths = build_javascript_socket_path_context(vm)?; + let socket_paths = build_socket_path_context(vm)?; { let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); @@ -1085,9 +1027,11 @@ where "body": base64::engine::general_purpose::STANDARD.encode(body), "done": done, })) - .map_err(|error| SidecarError::Execution(format!( - "ERR_AGENTOS_VM_FETCH_SERIALIZE: failed to serialize stream chunk: {error}" - )))?; + .map_err(|error| { + SidecarError::Execution(format!( + "ERR_AGENTOS_VM_FETCH_SERIALIZE: failed to serialize stream chunk: {error}" + )) + })?; Ok((response, done)) } .await; @@ -1143,7 +1087,7 @@ async fn dispatch_kernel_http_fetch_with_socket( options: &JavascriptHttpRequestOptions, headers: &HttpHeaderCollection, body_bytes: Option<&[u8]>, - socket_paths: &JavascriptSocketPathContext, + socket_paths: &SocketPathContext, max_fetch_response_bytes: usize, ) -> Result where @@ -1273,223 +1217,7 @@ where } } -fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result { - let status = response.status(); - let status_text = response.status_text().to_owned(); - let mut header_pairs = Vec::new(); - let mut raw_headers = Vec::new(); - for raw_name in response.headers_names() { - for value in response.all(&raw_name) { - header_pairs.push(json!([raw_name.to_ascii_lowercase(), value])); - raw_headers.push(Value::String(raw_name.clone())); - raw_headers.push(Value::String(value.to_owned())); - } - } - let mut reader = response.into_reader(); - let mut body = Vec::new(); - reader.read_to_end(&mut body).map_err(|error| { - SidecarError::Execution(format!("failed to read HTTP response: {error}")) - })?; - serde_json::to_string(&json!({ - "status": status, - "statusText": status_text, - "headers": header_pairs, - "rawHeaders": raw_headers, - "body": base64::engine::general_purpose::STANDARD.encode(body), - "bodyEncoding": "base64", - "url": url.as_str(), - })) - .map(Value::String) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) -} - -/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6 -/// brackets) into its host and port components. Returns `None` if the port is -/// missing or unparseable. -fn split_netloc(netloc: &str) -> Option<(&str, u16)> { - let (host, port) = netloc.rsplit_once(':')?; - let port: u16 = port.parse().ok()?; - let host = host - .strip_prefix('[') - .and_then(|rest| rest.strip_suffix(']')) - .unwrap_or(host); - Some((host, port)) -} - -pub(in crate::execution) fn issue_outbound_http_request( - url: &Url, - options: &JavascriptHttpRequestOptions, - headers: &HttpHeaderCollection, - pinned_addresses: &[IpAddr], - default_ca_bundle: &[u8], -) -> Result { - let method = options.method.as_deref().unwrap_or("GET"); - if pinned_addresses.is_empty() { - return Err(SidecarError::Execution(String::from( - "EACCES: no egress-vetted address available for outbound HTTP request", - ))); - } - // Pin the underlying resolver to the egress-vetted addresses. ureq performs - // its own DNS resolution for the TCP/TLS connect; without this override an - // https:// request would re-resolve the hostname through the host resolver - // (a rebinding DNS server could then return a private/metadata IP that the - // earlier range check would have rejected). The pinned resolver returns only - // the vetted addresses and refuses any host it was not vetted for, while the - // request URL keeps the original hostname so TLS SNI and the Host header stay - // correct. - let pinned_host = url.host_str().map(str::to_owned); - let pinned: Vec = pinned_addresses.to_vec(); - let resolver = move |netloc: &str| -> std::io::Result> { - let (host, port) = split_netloc(netloc).ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("invalid network location: {netloc}"), - ) - })?; - let expected_host = pinned_host.as_deref(); - if expected_host != Some(host) { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - format!( - "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}" - ), - )); - } - if pinned.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "EACCES: no egress-vetted address available for outbound HTTP request", - )); - } - Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect()) - }; - let mut agent_builder = ureq::AgentBuilder::new() - .resolver(resolver) - .timeout_connect(Duration::from_secs(5)) - .timeout_read(Duration::from_secs(15)) - .timeout_write(Duration::from_secs(15)); - if url.scheme() == "https" { - let tls_options = JavascriptTlsBridgeOptions { - is_server: false, - servername: url.host_str().map(str::to_owned), - alpn_protocols: Some(vec![String::from("http/1.1")]), - reject_unauthorized: options.reject_unauthorized, - ..JavascriptTlsBridgeOptions::default() - }; - agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config( - &tls_options, - default_ca_bundle, - )?)); - } - let agent = agent_builder.build(); - let mut request = agent.request_url(method, url); - for (name, values) in &headers.normalized { - if name == "host" { - continue; - } - let header_value = values.join(", "); - request = request.set(name, &header_value); - } - let response = match options.body.as_deref() { - Some(body) => request.send_string(body), - None => request.call(), - }; - - match response { - Ok(response) => outbound_http_response_json(url, response), - Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response), - Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!( - "ERR_HTTP_REQUEST_FAILED: {error}" - ))), - } -} - -async fn wait_for_loopback_http_response( - request: LoopbackHttpResponseWaitRequest<'_, B>, -) -> Result -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - let LoopbackHttpResponseWaitRequest { - bridge, - vm_id, - dns, - socket_paths, - kernel, - kernel_readiness, - process, - request_key, - capabilities, - } = request; - let deadline = Instant::now() + http_loopback_request_timeout(); - loop { - let response = match process.pending_http_requests.get(&request_key) { - Some(PendingHttpRequest::Buffered(response)) => response.clone(), - Some(PendingHttpRequest::Deferred(_)) | None => None, - }; - if let Some(response) = response { - process.pending_http_requests.remove(&request_key); - return Ok(response); - } - - if Instant::now() >= deadline { - process.pending_http_requests.remove(&request_key); - return Err(SidecarError::Execution(String::from( - "HTTP loopback request timed out waiting for net.http_respond", - ))); - } - - let remaining = deadline.saturating_duration_since(Instant::now()); - let Some(event) = process - .execution - .poll_event(remaining) - .await - .map_err(|error| SidecarError::Execution(error.to_string()))? - else { - continue; - }; - - match event { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - if request.method == "net.http_wait" => - { - process.queue_pending_execution_event( - ActiveExecutionEvent::JavascriptSyncRpcRequest(request), - )?; - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { - bridge, - vm_id, - dns, - socket_paths, - kernel, - kernel_readiness: Arc::clone(&kernel_readiness), - process, - sync_request: &request, - capabilities: capabilities.clone(), - }) - .await; - settle_nested_javascript_sync_rpc(process, &request, response).await?; - } - ActiveExecutionEvent::Exited(code) => { - process.pending_http_requests.remove(&request_key); - return Err(SidecarError::Execution(format!( - "HTTP loopback server exited before responding (exit code {code})" - ))); - } - ActiveExecutionEvent::Stdout(_) - | ActiveExecutionEvent::Stderr(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) - | ActiveExecutionEvent::SignalState { .. } => {} - } - } -} - -fn begin_loopback_http_request( +pub(in crate::execution) fn begin_loopback_http_request( process: &mut ActiveProcess, server_id: u64, request_json: &str, @@ -1519,6 +1247,18 @@ fn begin_loopback_http_request( Ok((server_id, request_id)) } +pub(in crate::execution) fn take_loopback_http_response( + process: &mut ActiveProcess, + request_key: (u64, u64), +) -> Option { + let response = match process.pending_http_requests.get(&request_key) { + Some(PendingHttpRequest::Buffered(response)) => response.clone(), + Some(PendingHttpRequest::Deferred(_)) | None => None, + }?; + process.pending_http_requests.remove(&request_key); + Some(response) +} + pub(in crate::execution) fn complete_loopback_http_request( process: &mut ActiveProcess, request_key: (u64, u64), @@ -1553,49 +1293,9 @@ pub(in crate::execution) fn complete_loopback_http_request( Ok(()) } -pub(crate) async fn dispatch_loopback_http_request( - request: LoopbackHttpDispatchRequest<'_, B>, -) -> Result -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - let LoopbackHttpDispatchRequest { - bridge, - vm_id, - dns, - socket_paths, - kernel, - kernel_readiness, - process, - server_id, - request_json, - capabilities, - } = request; - let request_key = begin_loopback_http_request(process, server_id, request_json, || { - PendingHttpRequest::Buffered(None) - })?; - wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest { - bridge, - vm_id, - dns, - socket_paths, - kernel, - kernel_readiness, - process, - request_key, - capabilities, - }) - .await -} - -pub(crate) fn dispatch_loopback_http_request_deferred( - request: LoopbackHttpDispatchRequest<'_, B>, -) -> Result -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ +pub(crate) fn dispatch_loopback_http_request_deferred( + request: LoopbackHttpDispatchRequest<'_>, +) -> Result { let LoopbackHttpDispatchRequest { process, server_id, @@ -1606,7 +1306,7 @@ where begin_loopback_http_request(process, server_id, request_json, || { PendingHttpRequest::Deferred(respond_to) })?; - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: Some(http_loopback_request_timeout()), task_class: agentos_runtime::TaskClass::Listener, @@ -1636,109 +1336,3 @@ pub(crate) fn ensure_vm_fetch_response_frame_within_limit( .map(|_| ()) .map_err(|error| SidecarError::FrameTooLarge(error.to_string())) } - -/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the -/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard -/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its -/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could -/// previously make the second lookup land on a private/link-local/metadata IP -/// the first check rejected. The fix pins `ureq`'s resolver to the vetted -/// address set; these tests prove the connect is pinned and refuses any other -/// host or an empty (fully-rejected) address set. -#[cfg(test)] -mod dns_rebinding_pin_tests { - use super::{ - issue_outbound_http_request, serialize_kernel_http_fetch_request, split_netloc, - JavascriptHttpRequestOptions, - }; - use std::collections::BTreeMap; - use std::io::{Read, Write}; - use std::net::{IpAddr, Ipv4Addr, TcpListener}; - use std::thread; - use url::Url; - - fn empty_headers() -> super::HttpHeaderCollection { - super::parse_http_header_collection(&BTreeMap::new(), "test headers") - .expect("empty header collection") - } - - fn options() -> JavascriptHttpRequestOptions { - JavascriptHttpRequestOptions { - method: Some(String::from("GET")), - headers: BTreeMap::new(), - body: None, - reject_unauthorized: None, - } - } - - #[test] - fn split_netloc_handles_hostnames_and_bracketed_ipv6() { - assert_eq!( - split_netloc("attacker.example:80"), - Some(("attacker.example", 80)) - ); - assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443))); - assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080))); - assert_eq!(split_netloc("no-port"), None); - assert_eq!(split_netloc("host:notaport"), None); - } - - #[test] - fn vm_fetch_serializes_exactly_one_leading_path_slash() { - for path in ["hello?q=1", "/hello?q=1", "//hello?q=1"] { - let request = - serialize_kernel_http_fetch_request(3080, path, &options(), &empty_headers(), None); - assert!( - request.starts_with(b"GET /hello?q=1 HTTP/1.1\r\n"), - "unexpected request line for {path:?}: {}", - String::from_utf8_lossy(&request) - ); - } - } - - /// A loopback HTTP server stands in for the egress-vetted target. The - /// request URL uses a *different* hostname (`attacker.example`) whose real - /// DNS would resolve elsewhere; pinning forces the connect onto the vetted - /// IP only. If the resolver were unpinned, the request would fail to reach - /// this server (and on a real host could land on a private/metadata IP). - #[test] - fn outbound_http_connect_is_pinned_to_vetted_ip() { - let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server"); - let port = listener.local_addr().expect("local addr").port(); - let server = thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("accept"); - let mut buf = [0u8; 1024]; - let _ = stream.read(&mut buf); - stream - .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi") - .expect("write response"); - let _ = stream.flush(); - }); - - let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url"); - let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]; - let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned, &[]) - .expect("pinned request should reach the vetted loopback target"); - let payload = result.as_str().expect("string payload"); - assert!( - payload.contains("\"status\":200"), - "expected 200 from pinned target, got: {payload}" - ); - server.join().expect("server thread"); - } - - /// With no vetted address (every resolved IP was rejected by the range - /// guard, or the literal IP was a blocked range), the pinned resolver must - /// refuse rather than fall back to the host resolver. - #[test] - fn outbound_http_refuses_when_no_vetted_address() { - let url = Url::parse("https://attacker.example/").expect("url"); - let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[], &[]) - .expect_err("empty pinned set must be refused"); - let message = error.to_string(); - assert!( - message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"), - "expected an egress refusal, got: {message}" - ); - } -} diff --git a/crates/native-sidecar/src/execution/javascript/mod.rs b/crates/native-sidecar/src/execution/javascript/mod.rs index c85911688b..faf899f791 100644 --- a/crates/native-sidecar/src/execution/javascript/mod.rs +++ b/crates/native-sidecar/src/execution/javascript/mod.rs @@ -3,17 +3,16 @@ pub(crate) use self::rpc::*; #[cfg(test)] #[allow(unused_imports)] pub(crate) use self::rpc::{ - clamp_javascript_net_poll_wait, service_javascript_net_sync_rpc, - JavascriptNetSyncRpcServiceRequest, + clamp_javascript_net_poll_wait, service_javascript_net_sync_rpc, NetServiceRequest, }; pub(crate) use self::rpc::{ - error_code, ignore_stale_javascript_sync_rpc_response, javascript_sync_rpc_arg_bool, - javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, - javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, + error_code, host_bytes_value, host_service_error, host_service_error_code, + javascript_sync_rpc_arg_bool, javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, + javascript_sync_rpc_arg_u32, javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg, - javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, javascript_sync_rpc_error_code, - javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, service_javascript_sync_rpc, - JavascriptSyncRpcServiceRequest, JavascriptSyncRpcServiceResponse, KernelPollFdRequest, + javascript_sync_rpc_encoding, javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, + service_javascript_sync_rpc, HostServiceResponse, JavascriptSyncRpcServiceRequest, + KernelPollFdRequest, }; mod crypto; pub(crate) use self::crypto::service_javascript_crypto_sync_rpc; @@ -22,6 +21,6 @@ pub(in crate::execution) use self::sqlite::*; mod http; pub(in crate::execution) use self::http::*; pub(crate) use self::http::{ - dispatch_loopback_http_request, dispatch_loopback_http_request_deferred, - ensure_vm_fetch_response_frame_within_limit, LoopbackHttpDispatchRequest, + dispatch_loopback_http_request_deferred, ensure_vm_fetch_response_frame_within_limit, + LoopbackHttpDispatchRequest, }; diff --git a/crates/native-sidecar/src/execution/javascript/rpc.rs b/crates/native-sidecar/src/execution/javascript/rpc.rs index 2074204375..3b7d75518d 100644 --- a/crates/native-sidecar/src/execution/javascript/rpc.rs +++ b/crates/native-sidecar/src/execution/javascript/rpc.rs @@ -1,11 +1,23 @@ use super::super::*; -use crate::filesystem::{ - javascript_sync_rpc_path_arg, remove_process_shadow_path, rename_process_shadow_path, -}; +use crate::filesystem::javascript_sync_rpc_path_arg; +use crate::state::ManagedHostNetRoute; use agentos_kernel::vfs::{VirtualTimeSpec, VirtualUtimeSpec}; const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "process.umask", + "process.exec_image_open", + "process.exec_image_open_fd", + "process.exec_image_read", + "process.exec_image_close", + "process.image", + "__kernel_tcgetattr", + "__kernel_tcsetattr", + "__kernel_tcgetpgrp", + "__kernel_tcsetpgrp", + "__kernel_tcgetsid", + "__kernel_tty_set_size", + "process.getrlimit", + "process.setrlimit", "process.getuid", "process.getgid", "process.geteuid", @@ -35,6 +47,11 @@ const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "fs.collapseRangeSync", "fs.fallocateSync", "fs.fiemapSync", + "fs.fiemapAtSync", + "fs.fgetxattrSync", + "fs.flistxattrSync", + "fs.fsetxattrSync", + "fs.fremovexattrSync", "fs.getxattrSync", "fs.insertRangeSync", "fs.lchownSync", @@ -54,12 +71,19 @@ const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "process.getpgid", "process.setpgid", "process.waitpid_transition", + "process.waitpid", "process.itimer_real", + "process.signal_begin", + "process.signal_end", + "process.signal_mask", + "process.signal_mask_scope_begin", + "process.signal_mask_scope_end", "process.fd_pipe", "process.fd_open", "process.path_open_at", "process.path_mkdir_at", "process.path_stat_at", + "process.path_chmod_at", "process.path_chown_at", "process.path_utimes_at", "process.path_link_at", @@ -68,7 +92,31 @@ const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "process.path_rename_at", "process.path_symlink_at", "process.path_unlink_at", + "process.random_get", + "process.clock_time", + "process.clock_resolution", + "process.sleep", + "process.system_identity", "process.fd_snapshot", + "process.hostnet_fd_open", + "process.hostnet_bind", + "process.hostnet_connect", + "process.hostnet_listen", + "process.hostnet_accept", + "process.hostnet_validate", + "process.hostnet_recv", + "process.hostnet_send", + "process.hostnet_local_address", + "process.hostnet_peer_address", + "process.hostnet_get_option", + "process.hostnet_set_option", + "process.hostnet_poll", + "process.hostnet_tls_connect", + "process.posix_poll", + "process.fd_description_identity", + "process.fd_description_alias_count", + "process.fd_preopens", + "process.fd_preopen", "process.fd_read", "process.fd_pread", "process.fd_write", @@ -77,6 +125,7 @@ const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "process.fd_datasync", "process.fd_readdir", "process.fd_close", + "process.fd_closefrom", "process.fd_stat", "process.fd_filestat", "process.fd_chmod", @@ -91,18 +140,183 @@ const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ "process.fd_dup", "process.fd_dup2", "process.fd_dup_min", + "process.fd_move", "process.fd_seek", + "process.fd_path", "process.fd_chdir_path", "process.fd_socketpair", + "process.pty_open", "process.fd_sendmsg_rights", "process.fd_recvmsg_rights", "process.fd_socket_shutdown", "dns.resolveRawRr", ]; -fn remap_wasm_process_sync_rpc( - request: &JavascriptSyncRpcRequest, -) -> Result, SidecarError> { +fn decode_javascript_dgram_operation( + request: &HostRpcRequest, +) -> Result { + let string = + |index, label| javascript_sync_rpc_arg_str(&request.args, index, label).map(str::to_owned); + match request.method.as_str() { + "dgram.createSocket" => { + let payload: DgramCreateSocketOptions = + serde_json::from_value(request.args.first().cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dgram.createSocket requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!( + "invalid dgram.createSocket payload: {error}" + )) + })?; + Ok(DgramOperation::Create { + family: UdpFamily::from_socket_type(&payload.socket_type)?, + }) + } + "dgram.bind" => { + let payload: DgramBindOptions = + serde_json::from_value(request.args.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dgram.bind requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}")) + })?; + Ok(DgramOperation::Bind { + socket_id: string(0, "dgram.bind socket id")?, + address: payload.address, + port: payload.port, + }) + } + "dgram.send" => { + let payload: DgramSendOptions = + serde_json::from_value(request.args.get(2).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dgram.send requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!("invalid dgram.send payload: {error}")) + })?; + Ok(DgramOperation::Send { + socket_id: string(0, "dgram.send socket id")?, + bytes: javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?, + address: payload.address, + port: payload.port, + }) + } + "dgram.connect" => { + let payload: DgramConnectOptions = + serde_json::from_value(request.args.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dgram.connect requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!("invalid dgram.connect payload: {error}")) + })?; + Ok(DgramOperation::Connect { + socket_id: string(0, "dgram.connect socket id")?, + address: payload.address, + port: payload.port, + }) + } + "dgram.disconnect" => Ok(DgramOperation::Disconnect { + socket_id: string(0, "dgram.disconnect socket id")?, + }), + "dgram.remoteAddress" => Ok(DgramOperation::RemoteAddress { + socket_id: string(0, "dgram.remoteAddress socket id")?, + }), + "dgram.close" => Ok(DgramOperation::Close { + socket_id: string(0, "dgram.close socket id")?, + }), + "dgram.address" => Ok(DgramOperation::Address { + socket_id: string(0, "dgram.address socket id")?, + }), + "dgram.setOption" => Ok(DgramOperation::SetOption { + socket_id: string(0, "dgram.setOption socket id")?, + name: string(1, "dgram.setOption option name")?, + payload: request.args.get(2).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dgram.setOption requires an option payload", + )) + })?, + }), + "dgram.setBufferSize" => { + let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?; + Ok(DgramOperation::SetBufferSize { + socket_id: string(0, "dgram.setBufferSize socket id")?, + which: string(1, "dgram.setBufferSize buffer kind")?, + size: usize::try_from(size).map_err(|_| { + SidecarError::InvalidState(String::from( + "dgram.setBufferSize size must fit within usize", + )) + })?, + }) + } + "dgram.getBufferSize" => Ok(DgramOperation::GetBufferSize { + socket_id: string(0, "dgram.getBufferSize socket id")?, + which: string(1, "dgram.getBufferSize buffer kind")?, + }), + other => Err(SidecarError::InvalidState(format!( + "unsupported JavaScript dgram sync RPC method {other}" + ))), + } +} + +fn decode_javascript_dns_operation(request: &HostRpcRequest) -> Result { + match request.method.as_str() { + "dns.lookup" => { + let payload: JavascriptDnsLookupRequest = + serde_json::from_value(request.args.first().cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dns.lookup requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}")) + })?; + Ok(DnsOperation::Lookup { + hostname: payload.hostname, + family: payload.family, + }) + } + "dns.resolve" | "dns.resolve4" | "dns.resolve6" | "dns.resolveRawRr" => { + let payload: JavascriptDnsResolveRequest = + serde_json::from_value(request.args.first().cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "dns.resolve requires a request payload", + )) + })?) + .map_err(|error| { + SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}")) + })?; + let requested_type = match request.method.as_str() { + "dns.resolve4" => String::from("A"), + "dns.resolve6" => String::from("AAAA"), + _ => payload + .rrtype + .as_deref() + .unwrap_or("A") + .to_ascii_uppercase(), + }; + Ok(DnsOperation::Resolve { + hostname: payload.hostname, + requested_type, + raw_record: request.method == "dns.resolveRawRr", + }) + } + other => Err(SidecarError::InvalidState(format!( + "unsupported JavaScript dns sync RPC method {other}" + ))), + } +} + +pub(crate) fn remap_wasm_process_sync_rpc( + request: &HostRpcRequest, +) -> Result, SidecarError> { if request.method != "process.wasm_sync_rpc" { return Ok(None); } @@ -112,7 +326,7 @@ fn remap_wasm_process_sync_rpc( "unsupported WASM process sync RPC method {method}" ))); } - Ok(Some(JavascriptSyncRpcRequest { + Ok(Some(HostRpcRequest { id: request.id, method: method.to_owned(), args: request.args[1..].to_vec(), @@ -128,7 +342,7 @@ fn remap_wasm_process_sync_rpc( /// Whether a successful sync RPC can transition a pipe/socket descriptor from /// not-readable to readable (including EOF). Wrapped WASM RPCs carry the real /// method name as argument zero. -pub(crate) fn javascript_sync_rpc_may_make_fd_readable(request: &JavascriptSyncRpcRequest) -> bool { +pub(crate) fn javascript_sync_rpc_may_make_fd_readable(request: &HostRpcRequest) -> bool { let method = if request.method == "process.wasm_sync_rpc" { request .args @@ -151,7 +365,7 @@ pub(crate) fn javascript_sync_rpc_may_make_fd_readable(request: &JavascriptSyncR /// Whether a successful sync RPC can free capacity in a pipe and therefore /// make a parked writer runnable. -pub(crate) fn javascript_sync_rpc_may_make_fd_writable(request: &JavascriptSyncRpcRequest) -> bool { +pub(crate) fn javascript_sync_rpc_may_make_fd_writable(request: &HostRpcRequest) -> bool { let method = if request.method == "process.wasm_sync_rpc" { request .args @@ -165,8 +379,8 @@ pub(crate) fn javascript_sync_rpc_may_make_fd_writable(request: &JavascriptSyncR } pub(crate) fn deferred_child_kernel_wait_request( - request: &JavascriptSyncRpcRequest, -) -> Result, SidecarError> { + request: &HostRpcRequest, +) -> Result, SidecarError> { if matches!( request.method.as_str(), "__kernel_stdin_read" @@ -192,7 +406,7 @@ pub(crate) fn deferred_child_kernel_wait_request( if method != "process.fd_read" && method != "process.fd_write" { return Ok(None); } - Ok(Some(JavascriptSyncRpcRequest { + Ok(Some(HostRpcRequest { id: request.id, method: method.to_owned(), args: request.args[1..].to_vec(), @@ -206,13 +420,13 @@ pub(crate) fn deferred_child_kernel_wait_request( } /// Normalize embedded-Node `fs.write*` calls only when they target a kernel -/// pipe. Regular-file writes must retain the filesystem service's host-shadow -/// synchronization, while a full pipe must never block the sidecar actor. +/// pipe. Regular-file writes already use the kernel-backed filesystem service, +/// while a full pipe must never block the sidecar actor. pub(crate) fn deferred_kernel_wait_request_for_process( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, kernel: &SidecarKernel, process: &ActiveProcess, -) -> Result, SidecarError> { +) -> Result, SidecarError> { if let Some(request) = deferred_child_kernel_wait_request(request)? { return Ok(Some(request)); } @@ -225,19 +439,13 @@ pub(crate) fn deferred_kernel_wait_request_for_process( return Ok(None); } let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem write fd")?; - // Projected host files live in the process-local mapped-fd table rather - // than the kernel fd table. They are regular files and can never require - // the nonblocking pipe-write path below. - if process.mapped_host_fd(fd).is_some() { - return Ok(None); - } - let stat = kernel - .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + let is_pipe = kernel + .fd_is_pipe(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) .map_err(kernel_error)?; - if stat.filetype != agentos_kernel::fd_table::FILETYPE_PIPE { + if !is_pipe { return Ok(None); } - Ok(Some(JavascriptSyncRpcRequest { + Ok(Some(HostRpcRequest { id: request.id, method: String::from("process.fd_write"), args: vec![ @@ -257,15 +465,107 @@ pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> { pub(crate) bridge: &'a SharedBridge, pub(crate) vm_id: &'a str, pub(crate) dns: &'a VmDnsConfig, - pub(crate) socket_paths: &'a JavascriptSocketPathContext, + pub(crate) socket_paths: &'a SocketPathContext, pub(crate) kernel: &'a mut SidecarKernel, pub(crate) kernel_readiness: KernelSocketReadinessRegistry, pub(crate) process: &'a mut ActiveProcess, - pub(crate) sync_request: &'a JavascriptSyncRpcRequest, + pub(crate) sync_request: &'a HostRpcRequest, pub(crate) capabilities: CapabilityRegistry, + pub(crate) managed_descriptions: Option, +} + +pub(in crate::execution) fn descriptor_rights_compat_request( + id: u64, + operation: agentos_execution::host::NetworkOperation, +) -> Result { + let mut raw_bytes_args = std::collections::HashMap::new(); + let (method, args) = match operation { + agentos_execution::host::NetworkOperation::SendDescriptorRights { + fd, + bytes, + rights, + flags, + } => { + raw_bytes_args.insert(1, bytes.into_vec()); + ( + "process.fd_sendmsg_rights", + vec![ + json!(fd), + Value::Null, + json!(rights.into_vec()), + json!(flags), + ], + ) + } + agentos_execution::host::NetworkOperation::ReceiveDescriptorRights { + fd, + max_bytes, + max_rights, + close_on_exec, + peek, + dontwait, + waitall, + } => ( + "process.fd_recvmsg_rights", + vec![ + json!(fd), + json!(max_bytes.get()), + json!(max_rights.get()), + json!(close_on_exec), + json!(peek), + json!(dontwait), + json!(waitall), + ], + ), + other => { + return Err(SidecarError::host( + "EINVAL", + format!("descriptor-rights adapter received unsupported operation: {other:?}"), + )) + } + }; + Ok(HostRpcRequest { + id, + method: method.to_owned(), + args, + raw_bytes_args, + }) +} + +pub(in crate::execution) async fn service_descriptor_rights_compat_operation( + bridge: &SharedBridge, + vm_id: &str, + dns: &VmDnsConfig, + socket_paths: &SocketPathContext, + kernel: &mut SidecarKernel, + kernel_readiness: KernelSocketReadinessRegistry, + process: &mut ActiveProcess, + capabilities: CapabilityRegistry, + managed_descriptions: crate::state::ManagedHostNetDescriptionRegistry, + call_id: u64, + operation: agentos_execution::host::NetworkOperation, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let request = descriptor_rights_compat_request(call_id, operation)?; + service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { + bridge, + vm_id, + dns, + socket_paths, + kernel, + kernel_readiness, + process, + sync_request: &request, + capabilities, + managed_descriptions: Some(managed_descriptions), + }) + .await } -pub(crate) enum JavascriptSyncRpcServiceResponse { +pub(crate) enum HostServiceResponse { Json(Value), Deferred { receiver: tokio::sync::oneshot::Receiver>, @@ -283,13 +583,13 @@ pub(crate) enum JavascriptSyncRpcServiceResponse { }, } -impl From for JavascriptSyncRpcServiceResponse { +impl From for HostServiceResponse { fn from(value: Value) -> Self { Self::Json(value) } } -impl JavascriptSyncRpcServiceResponse { +impl HostServiceResponse { pub(in crate::execution) fn as_json(&self) -> Option<&Value> { match self { Self::Json(value) => Some(value), @@ -301,15 +601,15 @@ impl JavascriptSyncRpcServiceResponse { } } -pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> { +pub(crate) struct NetServiceRequest<'a, B> { pub(crate) bridge: &'a SharedBridge, pub(crate) vm_id: &'a str, pub(crate) dns: &'a VmDnsConfig, - pub(crate) socket_paths: &'a JavascriptSocketPathContext, + pub(crate) socket_paths: &'a SocketPathContext, pub(crate) kernel: &'a mut SidecarKernel, pub(crate) kernel_readiness: KernelSocketReadinessRegistry, pub(crate) process: &'a mut ActiveProcess, - pub(crate) sync_request: &'a JavascriptSyncRpcRequest, + pub(crate) sync_request: &'a HostRpcRequest, pub(crate) capabilities: CapabilityRegistry, } @@ -457,6 +757,44 @@ pub(crate) fn javascript_sync_rpc_arg_u64( .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument"))) } +fn javascript_sync_rpc_arg_rlim( + args: &[Value], + index: usize, + label: &str, +) -> Result { + let Some(value) = args.get(index) else { + return Err(SidecarError::InvalidState(format!("{label} is required"))); + }; + if let Some(text) = value.as_str() { + return text + .parse::() + .map_err(|_| SidecarError::InvalidState(format!("{label} must be a u64"))); + } + javascript_sync_rpc_arg_u64(args, index, label) +} + +fn process_resource_limit_kind( + resource: u32, +) -> Result { + use agentos_kernel::kernel::ProcessResourceLimitKind; + match resource { + 0 => Ok(ProcessResourceLimitKind::Cpu), + 1 => Ok(ProcessResourceLimitKind::FileSize), + 2 => Ok(ProcessResourceLimitKind::Data), + 3 => Ok(ProcessResourceLimitKind::Stack), + 4 => Ok(ProcessResourceLimitKind::Core), + 5 => Ok(ProcessResourceLimitKind::ResidentSet), + 6 => Ok(ProcessResourceLimitKind::Processes), + 7 => Ok(ProcessResourceLimitKind::OpenFiles), + 8 => Ok(ProcessResourceLimitKind::LockedMemory), + 9 => Ok(ProcessResourceLimitKind::AddressSpace), + _ => Err(SidecarError::host( + "EINVAL", + format!("unknown resource limit {resource}"), + )), + } +} + pub(crate) fn javascript_sync_rpc_arg_u64_optional( args: &[Value], index: usize, @@ -485,10 +823,33 @@ pub(crate) fn javascript_sync_rpc_bytes_arg( } decode_encoded_bytes_value(value) - .map_err(|error| SidecarError::InvalidState(format!("{label} {error}"))) + .map_err(|error| SidecarError::host("EINVAL", format!("{label} {error}"))) } -pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value { +/// Decode an owned byte argument from either the bridge's lossless CBOR byte +/// lane or its JSON compatibility projection. V8 removes byte strings from +/// `args` and records them in `raw_bytes_args`; engine-neutral decoders must +/// prefer that lane so a Buffer does not turn into a null/opaque placeholder. +pub(crate) fn javascript_sync_rpc_request_bytes_arg( + request: &HostRpcRequest, + index: usize, + label: &str, +) -> Result, SidecarError> { + if let Some(bytes) = request.raw_bytes_args.get(&index) { + return Ok(bytes.clone()); + } + let Some(value) = request.args.get(index) else { + return Err(SidecarError::InvalidState(format!("{label} is required"))); + }; + if let Some(text) = value.as_str() { + return Ok(text.as_bytes().to_vec()); + } + decode_encoded_bytes_value(value) + .or_else(|_| decode_bridge_buffer_value(value)) + .map_err(|error| SidecarError::host("EINVAL", format!("{label} {error}"))) +} + +pub(crate) fn host_bytes_value(bytes: &[u8]) -> Value { encoded_bytes_value(bytes) } @@ -556,9 +917,10 @@ fn wasm_process_resolve_at_path( .fd_stat(EXECUTION_DRIVER_NAME, pid, dir_fd) .map_err(kernel_error)?; if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: file descriptor {dir_fd} is not a directory" - ))); + return Err(SidecarError::host( + "ENOTDIR", + format!("file descriptor {dir_fd} is not a directory"), + )); } let base = kernel .fd_path(EXECUTION_DRIVER_NAME, pid, dir_fd) @@ -579,7 +941,12 @@ fn wasm_process_path_stat_value(stat: agentos_kernel::vfs::VirtualStat) -> Value "ino": stat.ino, "filetype": filetype, "nlink": stat.nlink, + "mode": stat.mode, + "uid": stat.uid, + "gid": stat.gid, "size": stat.size, + "blocks": stat.blocks, + "rdev": stat.rdev, "atimeMs": stat.atime_ms, "mtimeMs": stat.mtime_ms, "ctimeMs": stat.ctime_ms, @@ -597,19 +964,18 @@ fn wasm_process_utime_spec( if !explicit { return Ok(VirtualUtimeSpec::Omit); } - let nanoseconds = nanoseconds.parse::().map_err(|_| { - SidecarError::InvalidState("EINVAL: pathname timestamp must be u64 nanoseconds".into()) - })?; - let seconds = i64::try_from(nanoseconds / 1_000_000_000).map_err(|_| { - SidecarError::InvalidState("EINVAL: pathname timestamp exceeds i64 seconds".into()) - })?; + let nanoseconds = nanoseconds + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "pathname timestamp must be u64 nanoseconds"))?; + let seconds = i64::try_from(nanoseconds / 1_000_000_000) + .map_err(|_| SidecarError::host("EINVAL", "pathname timestamp exceeds i64 seconds"))?; VirtualTimeSpec::new(seconds, (nanoseconds % 1_000_000_000) as u32) .map(VirtualUtimeSpec::Set) - .map_err(|error| SidecarError::InvalidState(format!("EINVAL: {error}"))) + .map_err(|error| SidecarError::host("EINVAL", error.to_string())) } pub(crate) async fn service_javascript_sync_rpc( request: JavascriptSyncRpcServiceRequest<'_, B>, -) -> Result +) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, @@ -624,6 +990,7 @@ where process, sync_request: original_request, capabilities, + managed_descriptions, } = request; let remapped_request = remap_wasm_process_sync_rpc(original_request)?; let request = remapped_request.as_ref().unwrap_or(original_request); @@ -634,7 +1001,7 @@ where if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readSync" { let kernel_pid = process.kernel_pid; let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?; - return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes)); + return Ok(HostServiceResponse::Raw(bytes)); } if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readFileRangeSync" { @@ -661,13 +1028,13 @@ where length, ) .map_err(kernel_error)?; - return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes)); + return Ok(HostServiceResponse::Raw(bytes)); } if request.method == "fs.readdirSync" { let kernel_pid = process.kernel_pid; let bytes = service_javascript_fs_readdir_raw_sync_rpc(kernel, process, kernel_pid, request)?; - return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes)); + return Ok(HostServiceResponse::Raw(bytes)); } let response = match request.method.as_str() { "__bench.noop" => Ok(Value::Null), @@ -693,24 +1060,7 @@ where service_javascript_internal_bridge_sync_rpc(process, request) } "__kernel_stdin_read" => { - // A TTY (PTY-backed) JavaScript process must read its stdin from the - // kernel PTY slave (fd 0) so cooked-mode line discipline (echo, - // VERASE/VKILL/VWERASE, ICRNL, VEOF) applies exactly as it does for - // wasm/python. Non-TTY JS keeps using the in-process local stdin - // bridge (piped stdin fed via process.execution.write_stdin). - let js_local_bridge = matches!(process.execution, ActiveExecution::Javascript(_)) - && process.tty_master_fd.is_none() - && !process.direct_posix_stdin; - if js_local_bridge { - match &process.execution { - ActiveExecution::Javascript(execution) => execution - .read_kernel_stdin_sync_rpc(request) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => unreachable!("js_local_bridge implies a JavaScript execution"), - } - } else { - service_javascript_kernel_stdin_sync_rpc(kernel, process, request) - } + service_javascript_kernel_stdin_sync_rpc(kernel, process, request) } "__kernel_stdio_write" => { service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request) @@ -719,6 +1069,24 @@ where "__kernel_tty_size" => { service_javascript_kernel_tty_size_sync_rpc(kernel, process, request) } + "__kernel_tty_set_size" => { + service_javascript_kernel_tty_set_size_sync_rpc(kernel, process, request) + } + "__kernel_tcgetattr" => { + service_javascript_kernel_tcgetattr_sync_rpc(kernel, process, request) + } + "__kernel_tcsetattr" => { + service_javascript_kernel_tcsetattr_sync_rpc(kernel, process, request) + } + "__kernel_tcgetpgrp" => { + service_javascript_kernel_tcgetpgrp_sync_rpc(kernel, process, request) + } + "__kernel_tcsetpgrp" => { + service_javascript_kernel_tcsetpgrp_sync_rpc(kernel, process, request) + } + "__kernel_tcgetsid" => { + service_javascript_kernel_tcgetsid_sync_rpc(kernel, process, request) + } "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request), "__pty_set_raw_mode" => { service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request) @@ -750,10 +1118,16 @@ where | "crypto.diffieHellmanSessionDestroy" | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request), "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" | "dns.resolveRawRr" => { - service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request) + service_dns_operation( + bridge, + kernel, + vm_id, + dns, + decode_javascript_dns_operation(request)?, + ) } "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => { - return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest { + return service_javascript_net_sync_rpc_response(NetServiceRequest { bridge, vm_id, dns, @@ -787,7 +1161,7 @@ where | "net.http2_stream_pause" | "net.http2_stream_resume" | "net.http2_stream_respond_with_file" => { - return service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest { + return service_javascript_http2_sync_rpc(Http2ServiceRequest { bridge, kernel, vm_id, @@ -824,7 +1198,7 @@ where | "net.destroy" | "net.server_close" | "tls.get_ciphers" => { - return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest { + return service_javascript_net_sync_rpc_response(NetServiceRequest { bridge, vm_id, dns, @@ -851,7 +1225,8 @@ where | "dgram.setOption" | "dgram.setBufferSize" | "dgram.getBufferSize" => { - return service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest { + let operation = decode_javascript_dgram_operation(request)?; + return service_dgram_operation(DgramServiceRequest { bridge, kernel, vm_id, @@ -859,7 +1234,7 @@ where socket_paths, process, kernel_readiness, - sync_request: request, + operation, capabilities, }); } @@ -883,16 +1258,107 @@ where | "sqlite.statement.finalize" => { service_javascript_sqlite_sync_rpc(kernel, process, request) } - "process.take_signal" => { - let signal = if process.real_interval_timer.take_expiry() { - Some(libc::SIGALRM) - } else { - process.pending_wasm_signals.pop_first() + "process.take_signal" | "process.signal_begin" => { + if process.real_interval_timer.take_expiry() { + process.kernel_handle.kill(libc::SIGALRM); + } + let delivery = process + .kernel_handle + .begin_signal_delivery() + .map_err(kernel_error)?; + Ok(delivery + .map(|delivery| { + json!({ + "signal": delivery.signal, + "token": delivery.token, + "flags": delivery.action.flags, + }) + }) + .unwrap_or(Value::Null)) + } + "process.signal_end" => { + let token = javascript_sync_rpc_arg_u64(&request.args, 0, "signal token")?; + process + .kernel_handle + .end_signal_delivery(token) + .map_err(kernel_error)?; + Ok(Value::Null) + } + "process.signal_mask" => { + let operation = javascript_sync_rpc_arg_u32(&request.args, 0, "signal-mask operation")?; + let signals = request + .args + .get(1) + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::host("EINVAL", String::from("signal-mask set must be an array", + )) + })? + .iter() + .map(|value| { + value + .as_i64() + .and_then(|signal| i32::try_from(signal).ok()) + .ok_or_else(|| { + SidecarError::host("EINVAL", String::from("signal-mask entries must be 32-bit integers", + )) + }) + }) + .collect::, _>>()?; + let set = SignalSet::from_signals(signals) + .map_err(|error| SidecarError::host(error.code(), error.to_string()))?; + let how = match operation { + 0 => SigmaskHow::Block, + 1 => SigmaskHow::Unblock, + 2 => SigmaskHow::SetMask, + 3 if set.is_empty() => SigmaskHow::Block, + _ => { + return Err(SidecarError::host("EINVAL", format!("invalid signal-mask operation {operation}" + ))) + } }; + let previous = process + .kernel_handle + .sigprocmask(how, set) + .map_err(kernel_error)?; + Ok(json!({ "signals": previous.signals() })) + } + "process.signal_mask_scope_begin" => { + let signals = request + .args + .first() + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::host("EINVAL", String::from("temporary signal mask must be an array", + )) + })? + .iter() + .map(|value| { + value + .as_i64() + .and_then(|signal| i32::try_from(signal).ok()) + .ok_or_else(|| { + SidecarError::host("EINVAL", String::from("temporary signal-mask entries must be 32-bit integers", + )) + }) + }) + .collect::, _>>()?; + let mask = SignalSet::from_signals(signals) + .map_err(|error| SidecarError::host(error.code(), error.to_string()))?; + let token = process + .kernel_handle + .begin_temporary_signal_mask(mask) + .map_err(kernel_error)?; + Ok(Value::from(token)) + } + "process.signal_mask_scope_end" => { + let token = + javascript_sync_rpc_arg_u64(&request.args, 0, "temporary signal-mask token")?; process - .pending_wasm_signals_gauge - .observe_depth(process.pending_wasm_signals.len()); - Ok(signal.map(Value::from).unwrap_or(Value::Null)) + .kernel_handle + .end_temporary_signal_mask(token) + .map_err(kernel_error)?; + Ok(Value::Null) } "process.itimer_real" => { let operation = javascript_sync_rpc_arg_u32(&request.args, 0, "ITIMER_REAL operation")?; @@ -912,8 +1378,7 @@ where process.real_interval_timer.set(value_us, interval_us) } other => { - return Err(SidecarError::InvalidState(format!( - "EINVAL: invalid ITIMER_REAL operation {other}" + return Err(SidecarError::host("EINVAL", format!("invalid ITIMER_REAL operation {other}" ))) } }; @@ -926,8 +1391,7 @@ where let selector = javascript_sync_rpc_arg_i32(&request.args, 0, "waitpid selector")?; let options = javascript_sync_rpc_arg_u32(&request.args, 1, "waitpid options")?; if options & !(1 | 2 | 8) != 0 { - return Err(SidecarError::InvalidState(format!( - "EINVAL: invalid waitpid option bits {:#x}", + return Err(SidecarError::host("EINVAL", format!("invalid waitpid option bits {:#x}", options & !(1 | 2 | 8) ))); } @@ -964,6 +1428,89 @@ where None => Ok(Value::Null), } } + "process.waitpid" => { + let selector = javascript_sync_rpc_arg_i32(&request.args, 0, "waitpid selector")?; + let options = javascript_sync_rpc_arg_u32(&request.args, 1, "waitpid options")?; + if options & !(1 | 2 | 8) != 0 { + return Err(SidecarError::host( + "EINVAL", + format!("invalid waitpid option bits {:#x}", options & !(1 | 2 | 8)), + )); + } + // The synchronous runner bridge must never park the sidecar + // dispatcher. Managed runners cooperatively service child I/O and + // retry; WNOHANG here describes bridge admission, not guest policy. + let mut flags = WaitPidFlags::WNOHANG; + if options & 2 != 0 { + flags |= WaitPidFlags::WUNTRACED; + } + if options & 8 != 0 { + flags |= WaitPidFlags::WCONTINUED; + } + let transition = kernel + .waitpid_detailed_with_options( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + selector, + flags, + ) + .map_err(kernel_error)?; + Ok(match transition { + None => Value::Null, + Some(transition) => { + let (event, raw_status, exit_code, signal, core_dumped) = + match (transition.event, transition.termination) { + ( + agentos_kernel::kernel::WaitPidEvent::Exited, + Some(agentos_kernel::process_runtime::ProcessExit::Exited(code)), + ) => ( + "exit", + ((code as u32 & 0xff) << 8), + code as u32 & 0xff, + 0, + false, + ), + ( + agentos_kernel::kernel::WaitPidEvent::Exited, + Some(agentos_kernel::process_runtime::ProcessExit::Signaled { + signal, + core_dumped, + }), + ) => ( + "exit", + (signal as u32 & 0x7f) | if core_dumped { 0x80 } else { 0 }, + 0, + signal as u32 & 0x7f, + core_dumped, + ), + (agentos_kernel::kernel::WaitPidEvent::Stopped, _) => ( + "stopped", + ((transition.status as u32 & 0xff) << 8) | 0x7f, + 0, + transition.status as u32 & 0xff, + false, + ), + (agentos_kernel::kernel::WaitPidEvent::Continued, _) => { + ("continued", 0xffff, 0, 0, false) + } + (agentos_kernel::kernel::WaitPidEvent::Exited, None) => { + return Err(SidecarError::InvalidState(String::from( + "kernel terminal wait transition omitted exact termination", + ))) + } + }; + json!({ + "pid": transition.pid, + "event": event, + "status": transition.status, + "rawStatus": raw_status, + "exitCode": exit_code, + "signal": signal, + "coreDumped": core_dumped, + }) + } + }) + } "process.fd_pipe" => kernel .open_pipe(EXECUTION_DRIVER_NAME, process.kernel_pid) .map(|(read_fd, write_fd)| json!({ "readFd": read_fd, "writeFd": write_fd })) @@ -972,14 +1519,26 @@ where let path = javascript_sync_rpc_arg_str(&request.args, 0, "fd_open path")?; let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_open flags")?; let mode = javascript_sync_rpc_arg_u32_optional(&request.args, 2, "fd_open mode")?; + let rights_base = javascript_sync_rpc_arg_str(&request.args, 3, "fd_open base rights")? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "fd_open base rights must be u64"))?; + let rights_inheriting = javascript_sync_rpc_arg_str( + &request.args, + 4, + "fd_open inheriting rights", + )? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "fd_open inheriting rights must be u64"))?; let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, 0, path)?; kernel - .fd_open( + .fd_open_with_rights( EXECUTION_DRIVER_NAME, process.kernel_pid, + None, &path, flags, mode, + Some((rights_base, rights_inheriting)), ) .map(Value::from) .map_err(kernel_error) @@ -989,14 +1548,32 @@ where let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_open_at path")?; let flags = javascript_sync_rpc_arg_u32(&request.args, 2, "path_open_at flags")?; let mode = javascript_sync_rpc_arg_u32_optional(&request.args, 3, "path_open_at mode")?; + let rights_base = javascript_sync_rpc_arg_str( + &request.args, + 4, + "path_open_at base rights", + )? + .parse::() + .map_err(|_| SidecarError::host("EINVAL", "path_open_at base rights must be u64"))?; + let rights_inheriting = javascript_sync_rpc_arg_str( + &request.args, + 5, + "path_open_at inheriting rights", + )? + .parse::() + .map_err(|_| { + SidecarError::host("EINVAL", "path_open_at inheriting rights must be u64") + })?; let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; kernel - .fd_open( + .fd_open_with_rights( EXECUTION_DRIVER_NAME, process.kernel_pid, + Some(dir_fd), &path, flags, mode, + Some((rights_base, rights_inheriting)), ) .map(Value::from) .map_err(kernel_error) @@ -1029,6 +1606,16 @@ where .map_err(kernel_error)?; Ok(wasm_process_path_stat_value(stat)) } + "process.path_chmod_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_chmod_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_chmod_at path")?; + let mode = javascript_sync_rpc_arg_u32(&request.args, 2, "path_chmod_at mode")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .chmod_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path, mode) + .map(|()| Value::Null) + .map_err(kernel_error) + } "process.path_chown_at" => { let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_chown_at dir fd")?; let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_chown_at path")?; @@ -1058,13 +1645,112 @@ where let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; let atime = wasm_process_utime_spec(atime_ns, fst_flags & 1 != 0, fst_flags & 2 != 0)?; let mtime = wasm_process_utime_spec(mtime_ns, fst_flags & 4 != 0, fst_flags & 8 != 0)?; - if follow { - kernel.utimes_spec(&path, atime, mtime) - } else { - kernel.lutimes(&path, atime, mtime) - } + kernel.utimes_spec_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + atime, + mtime, + follow, + ) .map(|()| Value::Null) - .map_err(kernel_error) + .map_err(kernel_error) + } + "process.random_get" => { + let length = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 0, + "random_get length", + )?) + .map_err(|_| SidecarError::InvalidState("random_get length is too large".into()))?; + let maximum = process.limits.wasm.sync_read_limit_bytes; + if length > maximum { + return Err(SidecarError::Host( + HostServiceError::limit( + "E2BIG", + "limits.wasm.syncReadLimitBytes", + maximum as u64, + length as u64, + ) + .with_details(json!({ + "limitName": "limits.wasm.syncReadLimitBytes", + "limit": maximum, + "observed": length, + "hint": "raise limits.wasm.syncReadLimitBytes if needed", + })), + )); + } + let mut bytes = vec![0_u8; length]; + getrandom::getrandom(&mut bytes).map_err(|error| { + SidecarError::Io(format!("failed to read system random bytes: {error}")) + })?; + Ok(host_bytes_value(&bytes)) + } + "process.clock_time" => { + let clock_id = javascript_sync_rpc_arg_u32(&request.args, 0, "clock id")?; + let clock = match clock_id { + 0 => KernelClockId::Realtime, + 1 => KernelClockId::Monotonic, + 2 => KernelClockId::ProcessCpu, + 3 => KernelClockId::ThreadCpu, + _ => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported clock id {clock_id}"), + )) + } + }; + let deterministic_realtime_ns = if clock == KernelClockId::Realtime { + request + .args + .get(2) + .and_then(Value::as_str) + .map(|value| { + value.parse::().map_err(|_| { + SidecarError::host( + "EINVAL", + "deterministic realtime must be u64 nanoseconds", + ) + }) + }) + .transpose()? + } else { + None + }; + kernel + .clock_time_ns(clock, deterministic_realtime_ns) + .map(|nanoseconds| json!(nanoseconds.to_string())) + .map_err(kernel_error) + } + "process.clock_resolution" => { + let clock_id = javascript_sync_rpc_arg_u32(&request.args, 0, "clock id")?; + let clock = match clock_id { + 0 => KernelClockId::Realtime, + 1 => KernelClockId::Monotonic, + 2 => KernelClockId::ProcessCpu, + 3 => KernelClockId::ThreadCpu, + _ => { + return Err(SidecarError::host( + "EINVAL", + format!("unsupported clock id {clock_id}"), + )) + } + }; + kernel + .clock_resolution_ns(clock) + .map(|nanoseconds| json!(nanoseconds.to_string())) + .map_err(kernel_error) + } + "process.system_identity" => { + let identity = kernel.system_identity(); + Ok(json!({ + "hostname": identity.hostname, + "type": identity.os_type, + "release": identity.os_release, + "version": identity.os_version, + "machine": identity.machine, + "domainName": identity.domain_name, + })) } "process.path_link_at" => { let old_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_link_at old fd")?; @@ -1082,7 +1768,12 @@ where .map_err(kernel_error)?; } kernel - .link(&old_path, &new_path) + .link_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &old_path, + &new_path, + ) .map(|()| Value::Null) .map_err(kernel_error) } @@ -1100,9 +1791,10 @@ where javascript_sync_rpc_arg_u32(&request.args, 0, "path_remove_dir_at dir fd")?; let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_remove_dir_at path")?; let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; - kernel.remove_dir(&path).map_err(kernel_error)?; - remove_process_shadow_path(process, &path)?; - Ok(Value::Null) + kernel + .remove_dir_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path) + .map(|()| Value::Null) + .map_err(kernel_error) } "process.path_rename_at" => { let old_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_rename_at old fd")?; @@ -1115,9 +1807,15 @@ where wasm_process_resolve_at_path(kernel, process.kernel_pid, old_fd, old_path)?; let new_path = wasm_process_resolve_at_path(kernel, process.kernel_pid, new_fd, new_path)?; - kernel.rename(&old_path, &new_path).map_err(kernel_error)?; - rename_process_shadow_path(process, &old_path, &new_path)?; - Ok(Value::Null) + kernel + .rename_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &old_path, + &new_path, + ) + .map(|()| Value::Null) + .map_err(kernel_error) } "process.path_symlink_at" => { let target = javascript_sync_rpc_arg_str(&request.args, 0, "path_symlink_at target")?; @@ -1125,7 +1823,12 @@ where let path = javascript_sync_rpc_arg_str(&request.args, 2, "path_symlink_at path")?; let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; kernel - .symlink(target, &path) + .symlink_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + target, + &path, + ) .map(|()| Value::Null) .map_err(kernel_error) } @@ -1133,9 +1836,46 @@ where let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_unlink_at dir fd")?; let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_unlink_at path")?; let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; - kernel.remove_file(&path).map_err(kernel_error)?; - remove_process_shadow_path(process, &path)?; - Ok(Value::Null) + kernel + .remove_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_preopens" => kernel + .initialize_wasi_preopens(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map(|preopens| { + Value::Array( + preopens + .into_iter() + .map(|preopen| { + json!({ + "fd": preopen.fd, + "guestPath": preopen.guest_path, + "rightsBase": preopen.rights_base, + "rightsInheriting": preopen.rights_inheriting, + }) + }) + .collect(), + ) + }) + .map_err(kernel_error), + "process.fd_preopen" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_preopen fd")?; + kernel + .wasi_preopen(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|preopen| { + preopen + .map(|preopen| { + json!({ + "fd": preopen.fd, + "guestPath": preopen.guest_path, + "rightsBase": preopen.rights_base, + "rightsInheriting": preopen.rights_inheriting, + }) + }) + .unwrap_or(Value::Null) + }) + .map_err(kernel_error) } "process.fd_snapshot" => kernel .fd_snapshot(EXECUTION_DRIVER_NAME, process.kernel_pid) @@ -1146,9 +1886,12 @@ where .map(|entry| { json!({ "fd": entry.fd, + "descriptionId": entry.description_id.to_string(), "fdFlags": entry.fd_flags, "statusFlags": entry.status_flags, "filetype": entry.filetype, + "rightsBase": entry.rights_base, + "rightsInheriting": entry.rights_inheriting, "kind": if entry.is_socket { "socket" } else if entry.is_pipe { @@ -1164,6 +1907,70 @@ where ) }) .map_err(kernel_error), + "process.hostnet_fd_open" => { + let datagram = javascript_sync_rpc_arg_bool( + &request.args, + 0, + "host-network datagram flag", + )?; + let nonblocking = javascript_sync_rpc_arg_bool( + &request.args, + 1, + "host-network nonblocking flag", + )?; + let close_on_exec = javascript_sync_rpc_arg_bool( + &request.args, + 2, + "host-network close-on-exec flag", + )?; + kernel + .fd_open_external_socket( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + datagram, + nonblocking, + close_on_exec, + ) + .map(|(fd, description_id)| { + json!({ "fd": fd, "descriptionId": description_id.to_string() }) + }) + .map_err(kernel_error) + } + "process.fd_description_identity" => { + let fd = javascript_sync_rpc_arg_u32( + &request.args, + 0, + "fd description identity fd", + )?; + kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|(description_id, aliases)| { + json!({ + "descriptionId": description_id.to_string(), + "aliases": aliases, + }) + }) + .map_err(kernel_error) + } + "process.fd_description_alias_count" => { + let description_id = javascript_sync_rpc_arg_str( + &request.args, + 0, + "fd description id", + )? + .parse::() + .map_err(|_| { + SidecarError::host("EINVAL", "fd description id must be a u64 decimal string") + })?; + kernel + .fd_description_alias_count( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + description_id, + ) + .map(Value::from) + .map_err(kernel_error) + } "process.fd_read" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?; // A previous read may have freed capacity in fd 0's pipe. Refill @@ -1193,7 +2000,7 @@ where .map(Option::unwrap_or_default), None => kernel.fd_read(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, length), } - .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map(|bytes| host_bytes_value(&bytes)) .map_err(kernel_error) } "process.fd_pread" => { @@ -1215,7 +2022,7 @@ where length, offset, ) - .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map(|bytes| host_bytes_value(&bytes)) .map_err(kernel_error) } "process.fd_write" => { @@ -1225,25 +2032,18 @@ where // blocking pipe write: the reader's RPC must be serviced here as // well. The runner polls and retries when a logically blocking fd // reports EAGAIN; genuinely nonblocking fds surface EAGAIN. - let written = if process.runtime == GuestRuntimeKind::WebAssembly { - kernel.fd_write_nonblocking(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data) - } else { - kernel.fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data) - } - .map_err(kernel_error)?; - if kernel - .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) - .map_err(kernel_error)? - .filetype - == agentos_kernel::fd_table::FILETYPE_REGULAR_FILE - { - crate::filesystem::mirror_kernel_fd_contents_to_process_shadow( - kernel, - process, + let written = match process.execution.synchronous_fd_write_policy() { + SynchronousFdWritePolicy::NonblockingRetry => kernel.fd_write_nonblocking( + EXECUTION_DRIVER_NAME, process.kernel_pid, fd, - )?; + &data, + ), + SynchronousFdWritePolicy::Blocking => { + kernel.fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data) + } } + .map_err(kernel_error)?; Ok(Value::from(written)) } "process.fd_pwrite" => { @@ -1255,19 +2055,6 @@ where let written = kernel .fd_pwrite(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data, offset) .map_err(kernel_error)?; - if kernel - .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) - .map_err(kernel_error)? - .filetype - == agentos_kernel::fd_table::FILETYPE_REGULAR_FILE - { - crate::filesystem::mirror_kernel_fd_contents_to_process_shadow( - kernel, - process, - process.kernel_pid, - fd, - )?; - } Ok(Value::from(written)) } "process.fd_sync" | "process.fd_datasync" => { @@ -1335,7 +2122,9 @@ where json!({ "filetype": stat.filetype, "flags": stat.flags, - "rights": stat.rights, + "rightsBase": stat.rights, + "rightsInheriting": stat.rights_inheriting, + "preopenPath": stat.wasi_preopen_path, }) }) .map_err(kernel_error) @@ -1357,6 +2146,8 @@ where "uid": stat.uid, "gid": stat.gid, "size": stat.size, + "blocks": stat.blocks, + "rdev": stat.rdev, "atimeMs": stat.atime_ms, "mtimeMs": stat.mtime_ms, "ctimeMs": stat.ctime_ms, @@ -1386,16 +2177,10 @@ where let length = javascript_sync_rpc_arg_str(&request.args, 1, "fd_truncate length")? .parse::() .map_err(|_| SidecarError::InvalidState("fd_truncate length must be u64".into()))?; - kernel - .fd_truncate(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, length) - .map_err(kernel_error)?; - crate::filesystem::mirror_kernel_fd_contents_to_process_shadow( - kernel, - process, - process.kernel_pid, - fd, - )?; - Ok(Value::Null) + kernel + .fd_truncate(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, length) + .map(|()| Value::Null) + .map_err(kernel_error) } "process.fd_set_flags" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_set_flags fd")?; @@ -1454,20 +2239,19 @@ where let start = javascript_sync_rpc_arg_str(&request.args, 3, "fd_record_lock start")? .parse::() .map_err(|_| { - SidecarError::InvalidState("EINVAL: fd_record_lock start must be u64".into()) + SidecarError::host("EINVAL", "fd_record_lock start must be u64") })?; let length = javascript_sync_rpc_arg_str(&request.args, 4, "fd_record_lock length")? .parse::() .map_err(|_| { - SidecarError::InvalidState("EINVAL: fd_record_lock length must be u64".into()) + SidecarError::host("EINVAL", "fd_record_lock length must be u64") })?; let lock_type = match raw_lock_type { 0 => agentos_kernel::fd_table::RecordLockType::Read, 1 => agentos_kernel::fd_table::RecordLockType::Write, 2 => agentos_kernel::fd_table::RecordLockType::Unlock, _ => { - return Err(SidecarError::InvalidState( - "EINVAL: fd_record_lock type must be F_RDLCK, F_WRLCK, or F_UNLCK".into(), + return Err(SidecarError::host("EINVAL", "fd_record_lock type must be F_RDLCK, F_WRLCK, or F_UNLCK", )) } }; @@ -1501,8 +2285,7 @@ where ) .map(|()| None), _ => { - return Err(SidecarError::InvalidState(format!( - "EINVAL: unsupported fd_record_lock command {command}" + return Err(SidecarError::host("EINVAL", format!("unsupported fd_record_lock command {command}" ))) } } @@ -1558,6 +2341,23 @@ where .map(Value::from) .map_err(kernel_error) } + "process.fd_move" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_move fd")?; + let replaced_fd = javascript_sync_rpc_arg_u32_optional( + &request.args, + 1, + "fd_move replaced fd", + )?; + kernel + .fd_renumber_projection( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + replaced_fd, + ) + .map(Value::from) + .map_err(kernel_error) + } "process.fd_seek" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_seek fd")?; let offset = javascript_sync_rpc_arg_str(&request.args, 1, "fd_seek offset")? @@ -1580,14 +2380,20 @@ where .map(|next| Value::String(next.to_string())) .map_err(kernel_error) } + "process.fd_path" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_path fd")?; + kernel + .fd_path(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(Value::String) + .map_err(kernel_error) + } "process.fd_chdir_path" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fchdir fd")?; let stat = kernel .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) .map_err(kernel_error)?; if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { - return Err(SidecarError::InvalidState(format!( - "ENOTDIR: file descriptor {fd} is not a directory" + return Err(SidecarError::host("ENOTDIR", format!("file descriptor {fd} is not a directory" ))); } kernel @@ -1622,9 +2428,15 @@ where .map(|(first_fd, second_fd)| json!({ "firstFd": first_fd, "secondFd": second_fd })) .map_err(kernel_error) } + "process.pty_open" => kernel + .open_pty(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map(|(master_fd, slave_fd, path)| { + json!({ "masterFd": master_fd, "slaveFd": slave_fd, "path": path }) + }) + .map_err(kernel_error), "process.fd_sendmsg_rights" => { let socket_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "sendmsg socket fd")?; - let data = javascript_sync_rpc_bytes_arg(&request.args, 1, "sendmsg data")?; + let data = javascript_sync_rpc_request_bytes_arg(request, 1, "sendmsg data")?; let raw_rights = request .args .get(2) @@ -1635,14 +2447,12 @@ where ) })?; if raw_rights.len() > LINUX_SCM_MAX_FD { - return Err(SidecarError::InvalidState(format!( - "EINVAL: SCM_RIGHTS accepts at most {LINUX_SCM_MAX_FD} descriptors" + return Err(SidecarError::host("EINVAL", format!("SCM_RIGHTS accepts at most {LINUX_SCM_MAX_FD} descriptors" ))); } if let Some(limit) = kernel.resource_limits().max_open_fds { if raw_rights.len() > limit { - return Err(SidecarError::InvalidState(format!( - "EMFILE: SCM_RIGHTS descriptor list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + return Err(SidecarError::host("EMFILE", format!("SCM_RIGHTS descriptor list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", raw_rights.len() ))); } @@ -1668,6 +2478,90 @@ where "sendmsg rights entries must be kernel fds or hostNet descriptions".into(), )); } + let managed_fd = value + .get("fd") + .and_then(Value::as_u64) + .and_then(|fd| u32::try_from(fd).ok()); + let managed_description_id = value + .get("descriptionId") + .and_then(Value::as_str) + .map(|description_id| { + description_id.parse::().map_err(|_| { + SidecarError::host( + "EINVAL", + "SCM_RIGHTS host-network descriptionId must be a u64 decimal string", + ) + }) + }) + .transpose()?; + if let (Some(fd), Some(description_id)) = + (managed_fd, managed_description_id) + { + let (actual_description_id, _) = kernel + .fd_description_identity( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + ) + .map_err(kernel_error)?; + if actual_description_id != description_id { + return Err(SidecarError::host( + "EINVAL", + "SCM_RIGHTS host-network fd and descriptionId disagree", + )); + } + let description = managed_descriptions + .as_ref() + .ok_or_else(|| { + SidecarError::host( + "ENOTSOCK", + "managed SCM_RIGHTS registry is unavailable", + ) + })? + .lock() + .map_err(|_| { + SidecarError::host( + "EIO", + "managed description registry lock poisoned", + ) + })? + .get(&description_id) + .cloned() + .ok_or_else(|| { + SidecarError::host( + "ENOTSOCK", + "managed SCM_RIGHTS description is unknown", + ) + })?; + let transferred = prepare_managed_transferred_host_net_resource( + kernel, + process, + description_id, + fd, + &description, + "managed SCM_RIGHTS host-network", + )?; + register_host_net_transfer_description( + &socket_paths.host_net_transfer_descriptions, + &transferred, + )?; + let transfer = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_error)?; + rights.push(FdTransferRequest::Opaque(Arc::new( + ManagedTransferredHostNetSocket { + resource: transferred, + transfer, + }, + ))); + continue; + } + if managed_fd.is_some() || managed_description_id.is_some() { + return Err(SidecarError::host( + "EINVAL", + "SCM_RIGHTS host-network fd and descriptionId must be provided together", + )); + } let source = scm_rights_host_net_source(value)?; let transferred = if let Some(source) = source { prepare_transferred_host_net_resource( @@ -1689,12 +2583,13 @@ where TransferredHostNetSocket::Pending { metadata, description_handles: Arc::new(()), + tcp_reservation: None, } }; register_host_net_transfer_description( &socket_paths.host_net_transfer_descriptions, &transferred, - ); + )?; rights.push(FdTransferRequest::Opaque(Arc::new(transferred))); } check_spawn_host_net_resource_limit( @@ -1776,17 +2671,77 @@ where rights.push(json!({ "kind": "kernel", "fd": fd })); } ReceivedFdRight::Opaque(resource) => { - let transferred = Arc::downcast::(resource) - .map_err(|_| { - SidecarError::InvalidState( - "received unknown SCM_RIGHTS resource type".into(), - ) - })?; - let transferred = match Arc::try_unwrap(transferred) { - Ok(transferred) => transferred, - Err(shared) => shared.clone_for_fd_transfer()?, + let (transferred, managed_transfer) = match Arc::downcast::< + ManagedTransferredHostNetSocket, + >(resource) + { + Ok(managed) => { + let managed = match Arc::try_unwrap(managed) { + Ok(managed) => managed, + Err(shared) => shared.clone_for_fd_transfer()?, + }; + (managed.resource, Some(managed.transfer)) + } + Err(resource) => { + let transferred = Arc::downcast::( + resource, + ) + .map_err(|_| { + SidecarError::InvalidState( + "received unknown SCM_RIGHTS resource type".into(), + ) + })?; + let transferred = match Arc::try_unwrap(transferred) { + Ok(transferred) => transferred, + Err(shared) => shared.clone_for_fd_transfer()?, + }; + (transferred, None) + } + }; + let managed_transfer = managed_transfer + .or_else(|| transferred.kernel_transfer_guard()); + let managed_description_id = managed_transfer + .as_ref() + .map(TransferredFd::description_id); + let mut managed_registry = if let Some(description_id) = managed_description_id { + let registry = managed_descriptions.as_ref().ok_or_else(|| { + SidecarError::host( + "ENOTSOCK", + "managed SCM_RIGHTS registry is unavailable", + ) + })?; + let descriptions = registry.lock().map_err(|_| { + SidecarError::host( + "EIO", + "managed description registry lock poisoned", + ) + })?; + if !descriptions.contains_key(&description_id) { + return Err(SidecarError::host( + "ESTALE", + "managed SCM_RIGHTS description disappeared in transit", + )); + } + Some(descriptions) + } else { + None }; - match transferred { + let installed_managed_fd = managed_transfer + .as_ref() + .map(|transfer| { + kernel + .fd_install_transfer( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + transfer, + close_on_exec, + ) + .map_err(kernel_error) + }) + .transpose()?; + let mut installed_managed_route = None; + let install_result = (|| -> Result<(), SidecarError> { + match transferred { TransferredHostNetSocket::Tcp { mut socket, metadata, @@ -1807,13 +2762,18 @@ where socket.kernel_socket_id, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(identity), + Arc::clone(&process.process_event_notify), ); register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -1822,6 +2782,8 @@ where let local = socket.guest_local_addr; let remote = socket.guest_remote_addr; process.tcp_sockets.insert(socket_id.clone(), *socket); + installed_managed_route = + Some(ManagedHostNetRoute::TcpSocket(socket_id.clone())); rights.push(transferred_hostnet_value( "tcp", metadata, @@ -1850,13 +2812,18 @@ where register_kernel_readiness_target( &kernel_readiness, listener.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), None, process.capability_readiness_identity(&capability_key), listener_id.clone(), KernelSocketReadinessEvent::Accept, ); process.tcp_listeners.insert(listener_id.clone(), listener); + installed_managed_route = Some( + ManagedHostNetRoute::TcpListener(listener_id.clone()), + ); rights.push(transferred_hostnet_value( "listener", metadata, @@ -1883,19 +2850,26 @@ where socket.kernel_socket_id, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(identity), + Arc::clone(&process.process_event_notify), ); register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), KernelSocketReadinessEvent::Datagram, ); process.udp_sockets.insert(socket_id.clone(), socket); + installed_managed_route = + Some(ManagedHostNetRoute::UdpSocket(socket_id.clone())); rights.push(transferred_hostnet_value( "udp", metadata, @@ -1925,10 +2899,15 @@ where None, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(identity), + Arc::clone(&process.process_event_notify), ); process.unix_sockets.insert(socket_id.clone(), socket); + installed_managed_route = + Some(ManagedHostNetRoute::UnixSocket(socket_id.clone())); rights.push(transferred_hostnet_value( "unix", metadata, @@ -1954,10 +2933,20 @@ where None, )?; listener.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process.execution.execution_wake_handle( + process.kernel_handle.runtime_identity(), + ), Some(identity), + Arc::clone(&process.process_event_notify), ); process.unix_listeners.insert(listener_id.clone(), listener); + installed_managed_route = if metadata.listening { + Some(ManagedHostNetRoute::UnixListener(listener_id.clone())) + } else { + Some(ManagedHostNetRoute::UnixBound { + listener_id: listener_id.clone(), + }) + }; rights.push(transferred_hostnet_value( "unix-listener", metadata, @@ -1967,17 +2956,72 @@ where None, )); } - TransferredHostNetSocket::Pending { metadata, .. } => { + TransferredHostNetSocket::Pending { + metadata, + tcp_reservation, + .. + } => { + installed_managed_route = if let Some(reservation) = tcp_reservation { + let reservation_id = process.allocate_tcp_port_reservation_id(); + process.tcp_port_reservations.insert( + reservation_id.clone(), + reservation, + ); + Some(ManagedHostNetRoute::TcpBound { reservation_id }) + } else { + Some(ManagedHostNetRoute::Unbound) + }; rights.push(transferred_hostnet_value( "pending", metadata, None, None, None, None, )); } + } + Ok(()) + })(); + if let Err(error) = install_result { + if let Some(fd) = installed_managed_fd { + if let Err(close_error) = kernel.fd_close( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + ) { + eprintln!( + "[agentos] failed to roll back received managed host-network fd {fd}: {close_error}" + ); + } + } + return Err(error); + } + if let Some(fd) = installed_managed_fd { + let description_id = managed_description_id.expect( + "managed fd installation requires a canonical description", + ); + if let Some(route) = installed_managed_route { + managed_registry + .as_mut() + .expect("managed registry was prevalidated") + .get_mut(&description_id) + .expect("managed description remains locked") + .routes + .insert(process.kernel_pid, route); + } + let value = rights.last_mut().expect( + "host-network receive must append one bootstrap right", + ); + let object = value + .as_object_mut() + .expect("host-network receive metadata is constructed as an object"); + object.insert("fd".into(), Value::from(fd)); + object.insert( + "descriptionId".into(), + Value::String(description_id.to_string()), + ); } } } } json!({ - "data": javascript_sync_rpc_bytes_value(&message.payload), + "data": host_bytes_value(&message.payload), "rights": rights, "payloadTruncated": message.payload_truncated, "controlTruncated": message.control_truncated, @@ -1985,7 +3029,7 @@ where }) } else { json!({ - "data": javascript_sync_rpc_bytes_value(&[]), + "data": host_bytes_value(&[]), "rights": [], "payloadTruncated": false, "controlTruncated": false, @@ -2028,16 +3072,11 @@ where "unknown process pid {target_pid}" ))); } - if !matches!( - canonical_signal_name(parsed_signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) { - apply_active_process_default_signal(kernel, process, parsed_signal)?; - } - Ok(json!({ - "self": true, - "action": "default", - })) + kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) + .map_err(kernel_error)?; + process.apply_runtime_controls()?; + Ok(Value::Null.into()) } "process.umask" => { let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?; @@ -2046,6 +3085,52 @@ where .map(|mask| json!(mask)) .map_err(kernel_error) } + "process.getrlimit" => { + let resource = javascript_sync_rpc_arg_u32( + &request.args, + 0, + "process.getrlimit resource", + )?; + let kind = process_resource_limit_kind(resource)?; + kernel + .get_resource_limit(EXECUTION_DRIVER_NAME, process.kernel_pid, kind) + .map(|limit| { + json!({ + "soft": limit.soft.unwrap_or(u64::MAX).to_string(), + "hard": limit.hard.unwrap_or(u64::MAX).to_string(), + }) + }) + .map_err(kernel_error) + } + "process.setrlimit" => { + let resource = javascript_sync_rpc_arg_u32( + &request.args, + 0, + "process.setrlimit resource", + )?; + let soft = javascript_sync_rpc_arg_rlim( + &request.args, + 1, + "process.setrlimit soft value", + )?; + let hard = javascript_sync_rpc_arg_rlim( + &request.args, + 2, + "process.setrlimit hard value", + )?; + kernel + .set_resource_limit( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + process_resource_limit_kind(resource)?, + agentos_kernel::kernel::ProcessResourceLimit { + soft: (soft != u64::MAX).then_some(soft), + hard: (hard != u64::MAX).then_some(hard), + }, + ) + .map(|()| Value::Null) + .map_err(kernel_error) + } "process.getuid" => kernel .getuid(EXECUTION_DRIVER_NAME, process.kernel_pid) .map(|value| json!(value)) @@ -2077,42 +3162,42 @@ where "process.getpwuid" => { let uid = javascript_sync_rpc_arg_u32(&request.args, 0, "passwd uid")?; kernel - .getpwuid(uid) + .getpwuid_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, uid) .map(|entry| json!(entry)) .map_err(kernel_error) } "process.getpwnam" => { let name = javascript_sync_rpc_arg_str(&request.args, 0, "passwd name")?; kernel - .getpwnam(name) + .getpwnam_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, name) .map(|entry| json!(entry)) .map_err(kernel_error) } "process.getpwent" => { let index = javascript_sync_rpc_arg_u32(&request.args, 0, "passwd index")?; kernel - .getpwent(index as usize) + .getpwent_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, index as usize) .map(|entry| json!(entry)) .map_err(kernel_error) } "process.getgrgid" => { let gid = javascript_sync_rpc_arg_u32(&request.args, 0, "group gid")?; kernel - .getgrgid(gid) + .getgrgid_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, gid) .map(|entry| json!(entry)) .map_err(kernel_error) } "process.getgrnam" => { let name = javascript_sync_rpc_arg_str(&request.args, 0, "group name")?; kernel - .getgrnam(name) + .getgrnam_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, name) .map(|entry| json!(entry)) .map_err(kernel_error) } "process.getgrent" => { let index = javascript_sync_rpc_arg_u32(&request.args, 0, "group index")?; kernel - .getgrent(index as usize) + .getgrent_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, index as usize) .map(|entry| json!(entry)) .map_err(kernel_error) } @@ -2236,12 +3321,6 @@ where .map(|()| Value::Null) .map_err(kernel_error) } - "fs.chmodSync" | "fs.promises.chmod" => { - let response = - service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?; - mirror_process_chmod_to_host(process, request)?; - Ok(response) - } _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request), }?; Ok(response.into()) @@ -2249,7 +3328,7 @@ where fn service_javascript_internal_bridge_sync_rpc( process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { // Module resolution / loading / format now reads the kernel VFS via // `service_javascript_module_sync_rpc`. This host-context path only handles @@ -2270,6 +3349,7 @@ fn service_javascript_internal_bridge_sync_rpc( method, &request.args, ) + .map_err(SidecarError::Host)? .ok_or_else(|| { SidecarError::InvalidState(format!( "JavaScript internal bridge method {method} returned no value" @@ -2277,60 +3357,6 @@ fn service_javascript_internal_bridge_sync_rpc( }) } -fn mirror_process_chmod_to_host( - process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, -) -> Result<(), SidecarError> { - let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?; - let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777; - let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else { - return Ok(()); - }; - if !host_path.exists() { - return Ok(()); - } - fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror chmod to host path {}: {error}", - host_path.display() - )) - }) -} - -fn resolve_process_guest_path_to_host( - process: &ActiveProcess, - guest_path: &str, -) -> Option { - let normalized_guest_path = if guest_path.starts_with('/') { - normalize_path(guest_path) - } else { - normalize_path(&format!( - "{}/{}", - process.guest_cwd.trim_end_matches('/'), - guest_path - )) - }; - if let Some(host_path) = - host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path) - { - return Some(host_path); - } - let normalized_guest_cwd = normalize_path(&process.guest_cwd); - let mut host_root = normalize_host_path(&process.host_cwd); - for _ in normalized_guest_cwd - .trim_start_matches('/') - .split('/') - .filter(|segment| !segment.is_empty()) - { - host_root = host_root.parent()?.to_path_buf(); - } - if normalized_guest_path == "/" { - Some(host_root) - } else { - Some(host_root.join(normalized_guest_path.trim_start_matches('/'))) - } -} - const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50); pub(in crate::execution) const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2); @@ -2359,11 +3385,11 @@ fn service_javascript_tls_deferred_rpc( vm_id: &str, kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, capabilities: &CapabilityRegistry, -) -> Result, SidecarError> { +) -> Result, SidecarError> { let operation_deadline = reactor_io_limits(&process.limits).operation_deadline; - let deferred = |receiver| JavascriptSyncRpcServiceResponse::Deferred { + let deferred = |receiver| HostServiceResponse::Deferred { receiver, timeout: Some(operation_deadline), task_class: agentos_runtime::TaskClass::Tls, @@ -2374,7 +3400,7 @@ fn service_javascript_tls_deferred_rpc( javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?; let options_json = javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?; - let options: JavascriptTlsBridgeOptions = + let options: TlsBridgeOptions = serde_json::from_str(options_json).map_err(|error| { SidecarError::InvalidState(format!( "net.socket_upgrade_tls options must be valid JSON: {error}" @@ -2384,9 +3410,10 @@ fn service_javascript_tls_deferred_rpc( .capability_leases .contains_key(&NativeCapabilityKey::TlsSocket(socket_id.to_owned())) { - return Err(SidecarError::Execution(format!( - "EALREADY: TCP socket {socket_id} is already upgraded to TLS" - ))); + return Err(SidecarError::host( + "EALREADY", + format!("TCP socket {socket_id} is already upgraded to TLS"), + )); } let pending = reserve_capability(capabilities, CapabilityKind::TlsTransport)?; let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| { @@ -2394,7 +3421,7 @@ fn service_javascript_tls_deferred_rpc( "unknown TCP socket {socket_id} for TLS upgrade" )) })?; - let receiver = socket.upgrade_tls(vm_id, kernel, options)?; + let receiver = socket.upgrade_tls(vm_id, kernel, process.kernel_pid, options)?; let kernel_socket_id = socket.kernel_socket_id; commit_process_capability( process, @@ -2440,9 +3467,9 @@ fn service_javascript_tls_deferred_rpc( fn service_javascript_plain_socket_deferred_rpc( process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, -) -> Result, SidecarError> { - let deferred = |receiver| JavascriptSyncRpcServiceResponse::Deferred { + request: &HostRpcRequest, +) -> Result, SidecarError> { + let deferred = |receiver| HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Socket, @@ -2486,9 +3513,9 @@ fn service_javascript_plain_socket_deferred_rpc( } } -fn service_javascript_net_sync_rpc_response( - request: JavascriptNetSyncRpcServiceRequest<'_, B>, -) -> Result +pub(in crate::execution) fn service_javascript_net_sync_rpc_response( + request: NetServiceRequest<'_, B>, +) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, @@ -2508,7 +3535,7 @@ where request.kernel, &request.kernel_readiness, )?; - return Ok(JavascriptSyncRpcServiceResponse::Json(Value::Null)); + return Ok(HostServiceResponse::Json(Value::Null)); } let listener = request @@ -2520,7 +3547,7 @@ where })?; release_unix_listener_capability(request.process, &listener_id, &listener)?; if !listener.is_final_description_handle() { - return Ok(JavascriptSyncRpcServiceResponse::Json(Value::Null)); + return Ok(HostServiceResponse::Json(Value::Null)); } for socket in request .process @@ -2565,13 +3592,20 @@ where .process .runtime_context .spawn(agentos_runtime::TaskClass::Listener, async move { - let result = match tokio::time::timeout(operation_deadline, close_completion).await { + let result = match crate::execution::operation_deadline_timeout( + "JavaScript Unix listener close", + operation_deadline, + close_completion, + ) + .await + { Ok(Ok(())) => Ok(Value::Null), Ok(Err(_)) => Err(crate::state::DeferredRpcError { code: String::from("ERR_AGENTOS_LISTENER_CLOSE"), message: format!( "Unix listener {listener_id} close task ended without acknowledgement" ), + details: None, }), Err(_) => Err(crate::state::DeferredRpcError { code: String::from("ETIMEDOUT"), @@ -2579,6 +3613,7 @@ where "Unix listener {listener_id} close exceeded {}ms; raise limits.reactor.operationDeadlineMs", operation_deadline.as_millis() ), + details: None, }), }; if respond_to.send(result).is_err() { @@ -2588,7 +3623,7 @@ where } }) .map_err(SidecarError::from)?; - return Ok(JavascriptSyncRpcServiceResponse::Deferred { + return Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Listener, @@ -2706,12 +3741,12 @@ where })?; let host = payload.host.as_deref().unwrap_or("localhost"); let is_http_loopback_target = is_loopback_socket_host(host) - && [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6] + && [SocketFamily::Ipv4, SocketFamily::Ipv6] .iter() .any(|family| { let family_number = match family { - JavascriptSocketFamily::Ipv4 => 4, - JavascriptSocketFamily::Ipv6 => 6, + SocketFamily::Ipv4 => 4, + SocketFamily::Ipv6 => 6, }; if payload .family @@ -2793,17 +3828,91 @@ where }))); }) .map_err(SidecarError::from)?; - return Ok(JavascriptSyncRpcServiceResponse::Deferred { + return Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Listener, }); } + if request.sync_request.method == "net.poll" { + let NetServiceRequest { + kernel, + kernel_readiness, + process, + socket_paths, + sync_request: request, + .. + } = request; + let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?; + let wait_ms = javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")? + .unwrap_or_default(); + let trace_enabled = net_tcp_trace_enabled(&process.env); + let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) { + socket.set_application_read_interest(true)?; + socket.poll( + kernel, + process.kernel_pid, + clamp_javascript_net_poll_wait(wait_ms), + trace_enabled, + )? + } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) { + socket.set_application_read_interest(true)?; + socket.poll(clamp_javascript_net_poll_wait(wait_ms))? + } else { + return Err(SidecarError::host( + "EBADF", + format!("unknown net socket {socket_id}"), + )); + }; + return match event { + Some(TcpSocketEvent::Data { + bytes, + reservation, + mut source_reservations, + }) => { + source_reservations.push(reservation); + Ok(HostServiceResponse::SourceBackedJson { + value: json!({ + "type": "data", + "data": host_bytes_value(&bytes), + }), + source_reservations, + }) + } + Some(TcpSocketEvent::End) => Ok(json!({ "type": "end" }).into()), + Some(TcpSocketEvent::Error { code, message }) => Ok(json!({ + "type": "error", + "code": code, + "message": message, + }) + .into()), + Some(TcpSocketEvent::Close { had_error }) => { + if let Some(socket) = process.tcp_sockets.remove(socket_id) { + release_tcp_socket_handle( + process, + socket_id, + socket, + kernel, + &kernel_readiness, + ); + } else if let Some(socket) = process.unix_sockets.remove(socket_id) { + release_unix_socket_handle( + process, + socket_id, + socket, + &socket_paths.unix_bound_addresses, + ); + } + Ok(json!({ "type": "close", "hadError": had_error }).into()) + } + None => Ok(Value::Null.into()), + }; + } if request.sync_request.method != "net.socket_read" { return service_javascript_net_sync_rpc(request).map(Into::into); } - let JavascriptNetSyncRpcServiceRequest { + let NetServiceRequest { kernel, process, sync_request: request, @@ -2846,7 +3955,7 @@ where }; match event { - Some(JavascriptTcpSocketEvent::Data { + Some(TcpSocketEvent::Data { bytes, reservation, mut source_reservations, @@ -2856,21 +3965,21 @@ where // ownership live through handoff, but do not charge the response // bytes a second time. source_reservations.push(reservation); - Ok(JavascriptSyncRpcServiceResponse::SourceBackedRaw { + Ok(HostServiceResponse::SourceBackedRaw { payload: bytes, source_reservations, }) } - other => javascript_net_read_value(other).map(Into::into), + other => net_read_value(other).map(Into::into), } } async fn service_javascript_dgram_poll_response( - socket_paths: &JavascriptSocketPathContext, + socket_paths: &SocketPathContext, kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, -) -> Result { + request: &HostRpcRequest, +) -> Result { let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?; let wait_ms = javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")? .unwrap_or_default(); @@ -2882,7 +3991,7 @@ async fn service_javascript_dgram_poll_response( .await?; match event { - Some(JavascriptUdpSocketEvent::Message { + Some(DatagramEvent::Message { data, remote_addr, _byte_reservation, @@ -2890,7 +3999,7 @@ async fn service_javascript_dgram_poll_response( _udp_byte_reservation, _udp_datagram_reservation, }) => { - let family = JavascriptSocketFamily::from_ip(remote_addr.ip()); + let family = SocketFamily::from_ip(remote_addr.ip()); let guest_remote_port = if is_loopback_ip(remote_addr.ip()) { socket_paths .guest_udp_port_for_host_port(family, remote_addr.port()) @@ -2904,9 +4013,9 @@ async fn service_javascript_dgram_poll_response( let mut response = remote_endpoint_value(&remote_addr, guest_remote_port); if let Value::Object(fields) = &mut response { fields.insert(String::from("type"), Value::String(String::from("message"))); - fields.insert(String::from("data"), javascript_sync_rpc_bytes_value(&data)); + fields.insert(String::from("data"), host_bytes_value(&data)); } - Ok(JavascriptSyncRpcServiceResponse::SourceBackedJson { + Ok(HostServiceResponse::SourceBackedJson { value: response, source_reservations: vec![ _byte_reservation, @@ -2916,25 +4025,23 @@ async fn service_javascript_dgram_poll_response( ], }) } - Some(JavascriptUdpSocketEvent::Error { code, message }) => { - Ok(JavascriptSyncRpcServiceResponse::Json(json!({ - "type": "error", - "code": code, - "message": message, - }))) - } - None => Ok(JavascriptSyncRpcServiceResponse::Json(Value::Null)), + Some(DatagramEvent::Error { code, message }) => Ok(HostServiceResponse::Json(json!({ + "type": "error", + "code": code, + "message": message, + }))), + None => Ok(HostServiceResponse::Json(Value::Null)), } } pub(crate) fn service_javascript_net_sync_rpc( - request: JavascriptNetSyncRpcServiceRequest<'_, B>, + request: NetServiceRequest<'_, B>, ) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let JavascriptNetSyncRpcServiceRequest { + let NetServiceRequest { bridge, vm_id, dns, @@ -2971,12 +4078,8 @@ where &socket_paths.used_tcp_guest_ports, socket_paths.listen_policy, )?; - let mut listener = ActiveTcpListener::bind( - bind_host, - guest_host, - port, - Some(DEFAULT_JAVASCRIPT_NET_BACKLOG), - )?; + let mut listener = + ActiveTcpListener::bind(bind_host, guest_host, port, Some(DEFAULT_NET_BACKLOG))?; let guest_local_addr = listener.guest_local_addr(); commit_process_capability( process, @@ -3003,7 +4106,7 @@ where "address": socket_address_value(&guest_local_addr) })) .map(Value::String) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) + .map_err(|error| SidecarError::host("ERR_AGENTOS_NODE_SYNC_RPC", format!("{error}"))) } "net.http_close" => { let server_id = @@ -3116,16 +4219,19 @@ where &socket_paths.unix_bound_addresses, ®istry_binding_id, )?; - if guest_errno_code(&error.to_string()) != Some("EADDRINUSE") { + if guest_error_code(&error) != Some("EADDRINUSE") { return Err(error); } } } } bound.ok_or_else(|| { - SidecarError::Execution(String::from( - "EADDRINUSE: Linux AF_UNIX autobind namespace exhausted after 4096 attempts", - )) + SidecarError::host( + "EADDRINUSE", + String::from( + "Linux AF_UNIX autobind namespace exhausted after 4096 attempts", + ), + ) })? } else if let Some(hex) = payload.abstract_path_hex.as_deref() { let guest_name = decode_abstract_unix_name(hex)?; @@ -3244,8 +4350,11 @@ where .expect("committed Unix listener capability lease"), ); listener.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); process.unix_listeners.insert(listener_id.clone(), listener); Ok(json!({ @@ -3368,7 +4477,7 @@ where &binding_id, )?; if explicit_name.is_some() - || guest_errno_code(&error.to_string()) != Some("EADDRINUSE") + || guest_error_code(&error) != Some("EADDRINUSE") { return Err(error); } @@ -3503,8 +4612,8 @@ where "localAddress": guest_host, "localPort": port, "family": match family { - JavascriptSocketFamily::Ipv4 => "IPv4", - JavascriptSocketFamily::Ipv6 => "IPv6", + SocketFamily::Ipv4 => "IPv4", + SocketFamily::Ipv6 => "IPv6", }, })) } @@ -3557,8 +4666,11 @@ where None, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket .set_fairness_identity(process.capability_fairness_identity(&capability_key))?; @@ -3593,11 +4705,11 @@ where format_tcp_resource(host, port), )?; if is_loopback_socket_host(host) { - let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6]; + let families = [SocketFamily::Ipv4, SocketFamily::Ipv6]; if let Some((family, target)) = families.iter().find_map(|family| { let family_number = match family { - JavascriptSocketFamily::Ipv4 => 4, - JavascriptSocketFamily::Ipv6 => 6, + SocketFamily::Ipv4 => 4, + SocketFamily::Ipv6 => 6, }; if payload .family @@ -3615,8 +4727,8 @@ where .insert(reservation_id, reservation); } let remote_address = match family { - JavascriptSocketFamily::Ipv4 => "127.0.0.1", - JavascriptSocketFamily::Ipv6 => "::1", + SocketFamily::Ipv4 => "127.0.0.1", + SocketFamily::Ipv6 => "::1", }; return Ok(json!({ "loopbackHttpTarget": { @@ -3626,15 +4738,15 @@ where "port": port, }, "localAddress": match family { - JavascriptSocketFamily::Ipv4 => "127.0.0.1", - JavascriptSocketFamily::Ipv6 => "::1", + SocketFamily::Ipv4 => "127.0.0.1", + SocketFamily::Ipv6 => "::1", }, "localPort": payload.local_port.unwrap_or(0), "remoteAddress": remote_address, "remotePort": port, "remoteFamily": match family { - JavascriptSocketFamily::Ipv4 => "IPv4", - JavascriptSocketFamily::Ipv6 => "IPv6", + SocketFamily::Ipv4 => "IPv4", + SocketFamily::Ipv6 => "IPv6", }, })); } @@ -3680,13 +4792,20 @@ where ) { Ok(identity) => identity, Err(error) => { - let _ = socket.close(kernel, process.kernel_pid); + if let Err(close_error) = socket.close(kernel, process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_TCP_ROLLBACK: failed to close rejected TCP socket: {close_error}" + ); + } return Err(error); } }; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket .set_fairness_identity(process.capability_fairness_identity(&capability_key))?; @@ -3698,7 +4817,9 @@ where register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -3779,8 +4900,11 @@ where )) })?; listener.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); process .unix_listeners @@ -3849,16 +4973,19 @@ where &socket_paths.unix_bound_addresses, ®istry_binding_id, )?; - if guest_errno_code(&error.to_string()) != Some("EADDRINUSE") { + if guest_error_code(&error) != Some("EADDRINUSE") { return Err(error); } } } } bound.ok_or_else(|| { - SidecarError::Execution(String::from( - "EADDRINUSE: Linux AF_UNIX autobind namespace exhausted after 4096 attempts", - )) + SidecarError::host( + "EADDRINUSE", + String::from( + "Linux AF_UNIX autobind namespace exhausted after 4096 attempts", + ), + ) })? } else if let Some(hex) = payload.abstract_path_hex.as_deref() { bridge.require_network_access( @@ -3983,8 +5110,11 @@ where .expect("committed Unix listener capability lease"), ); listener.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); process.unix_listeners.insert(listener_id.clone(), listener); Ok(json!({ @@ -4054,7 +5184,11 @@ where ) { Ok(identity) => identity, Err(error) => { - let _ = listener.close(kernel, process.kernel_pid); + if let Err(close_error) = listener.close(kernel, process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_TCP_ROLLBACK: failed to close rejected TCP listener: {close_error}" + ); + } return Err(error); } }; @@ -4066,7 +5200,9 @@ where register_kernel_readiness_target( &kernel_readiness, listener.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), None, process.capability_readiness_identity(&capability_key), listener_id.clone(), @@ -4101,19 +5237,19 @@ where ))); }; match event { - Some(JavascriptTcpSocketEvent::Data { bytes: chunk, .. }) => Ok(json!({ + Some(TcpSocketEvent::Data { bytes: chunk, .. }) => Ok(json!({ "type": "data", - "data": javascript_sync_rpc_bytes_value(&chunk), + "data": host_bytes_value(&chunk), })), - Some(JavascriptTcpSocketEvent::End) => Ok(json!({ + Some(TcpSocketEvent::End) => Ok(json!({ "type": "end", })), - Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({ + Some(TcpSocketEvent::Error { code, message }) => Ok(json!({ "type": "error", "code": code, "message": message, })), - Some(JavascriptTcpSocketEvent::Close { had_error }) => { + Some(TcpSocketEvent::Close { had_error }) => { if let Some(socket) = process.tcp_sockets.remove(socket_id) { release_tcp_socket_handle( process, @@ -4142,12 +5278,12 @@ where let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?; if let Some(socket) = process.tcp_sockets.get(socket_id) { - javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect") + encode_net_json_string(socket.socket_info(), "net.socket_wait_connect") } else { let socket = process.unix_sockets.get(socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net socket {socket_id}")) })?; - javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect") + encode_net_json_string(socket.socket_info(), "net.socket_wait_connect") } } "net.socket_read" => { @@ -4163,7 +5299,7 @@ where } if let Some(socket) = process.tcp_sockets.get_mut(socket_id) { socket.set_application_read_interest(true)?; - javascript_net_read_value(socket.poll( + net_read_value(socket.poll( kernel, process.kernel_pid, Duration::ZERO, @@ -4171,7 +5307,7 @@ where )?) } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) { socket.set_application_read_interest(true)?; - javascript_net_read_value(socket.poll(Duration::ZERO)?) + net_read_value(socket.poll(Duration::ZERO)?) } else { // A data callback may synchronously destroy its socket while the // readiness-driven read pump still owns an admitted turn. Match @@ -4284,8 +5420,8 @@ where Err(error) => { return Ok(json!({ "type": "error", - "code": javascript_sync_rpc_error_code(&error), - "message": javascript_sync_rpc_error_message(&error), + "code": host_service_error_code(&error), + "message": host_service_error_message(&error), })); } } @@ -4305,7 +5441,7 @@ where if let Some(event) = tcp_event { return match event { - Some(JavascriptTcpListenerEvent::Connection(pending)) => { + Some(TcpListenerEvent::Connection(pending)) => { let PendingTcpSocket { stream, kernel_socket_id, @@ -4350,13 +5486,20 @@ where ) { Ok(identity) => identity, Err(error) => { - let _ = socket.close(kernel, process.kernel_pid); + if let Err(close_error) = socket.close(kernel, process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_TCP_ROLLBACK: failed to close rejected TCP socket: {close_error}" + ); + } return Err(error); } }; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket.set_fairness_identity( process.capability_fairness_identity(&capability_key), @@ -4369,7 +5512,9 @@ where register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -4392,7 +5537,7 @@ where "remoteFamily": socket_addr_family(&guest_remote_addr), })) } - Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({ + Some(TcpListenerEvent::Error { code, message }) => Ok(json!({ "type": "error", "code": code, "message": message, @@ -4409,7 +5554,7 @@ where }; match event { - Some(JavascriptUnixListenerEvent::Connection { + Some(UnixListenerEvent::Connection { socket: mut pending, capability: pending_capability, }) => { @@ -4445,8 +5590,11 @@ where None, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket.set_fairness_identity( process.capability_fairness_identity(&capability_key), @@ -4472,7 +5620,7 @@ where "remoteAbstractPathHex": pending.remote_abstract_path_hex, })) } - Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({ + Some(UnixListenerEvent::Error { code, message }) => Ok(json!({ "type": "error", "code": code, "message": message, @@ -4500,7 +5648,7 @@ where Duration::ZERO, trace_enabled, )? { - Some(JavascriptTcpListenerEvent::Connection(pending)) => { + Some(TcpListenerEvent::Connection(pending)) => { let PendingTcpSocket { stream, kernel_socket_id, @@ -4544,13 +5692,20 @@ where ) { Ok(identity) => identity, Err(error) => { - let _ = socket.close(kernel, process.kernel_pid); + if let Err(close_error) = socket.close(kernel, process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_TCP_ROLLBACK: failed to close rejected TCP socket: {close_error}" + ); + } return Err(error); } }; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); if let Value::Object(fields) = &mut info { fields.insert(String::from("capabilityId"), json!(identity.0)); @@ -4567,7 +5722,9 @@ where register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -4578,7 +5735,7 @@ where Some(listener.register_connection(&socket_id)); } process.tcp_sockets.insert(socket_id.clone(), socket); - javascript_net_json_string( + encode_net_json_string( json!({ "socketId": socket_id, "info": info, @@ -4586,11 +5743,11 @@ where "net.server_accept", ) } - Some(JavascriptTcpListenerEvent::Error { code, message }) => { - let detail = code.unwrap_or_else(|| String::from("server accept")); - Err(SidecarError::Execution(format!("{detail}: {message}"))) - } - None => Ok(javascript_net_timeout_value()), + Some(TcpListenerEvent::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message, + )), + None => Ok(net_timeout_value()), }; } @@ -4608,7 +5765,7 @@ where .expect("validated Unix listener remains registered") .poll(Duration::ZERO)?; match event { - Some(JavascriptUnixListenerEvent::Connection { + Some(UnixListenerEvent::Connection { socket: mut pending, capability: pending_capability, }) => { @@ -4643,8 +5800,11 @@ where None, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket.set_fairness_identity( process.capability_fairness_identity(&capability_key), @@ -4663,7 +5823,7 @@ where Some(listener.register_connection(&socket_id)); } process.unix_sockets.insert(socket_id.clone(), socket); - javascript_net_json_string( + encode_net_json_string( json!({ "socketId": socket_id, "info": info, @@ -4671,11 +5831,11 @@ where "net.server_accept", ) } - Some(JavascriptUnixListenerEvent::Error { code, message }) => { - let detail = code.unwrap_or_else(|| String::from("server accept")); - Err(SidecarError::Execution(format!("{detail}: {message}"))) - } - None => Ok(javascript_net_timeout_value()), + Some(UnixListenerEvent::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message, + )), + None => Ok(net_timeout_value()), } } "net.server_connections" => { @@ -4825,7 +5985,7 @@ where Ok(Value::Null) } } - "tls.get_ciphers" => javascript_net_json_string( + "tls.get_ciphers" => encode_net_json_string( Value::Array( tls_provider() .cipher_suites @@ -4847,7 +6007,7 @@ where } } -fn resolve_guest_unix_path( +pub(in crate::execution) fn resolve_guest_unix_path( process: &ActiveProcess, path: &str, ) -> Result<(String, String), SidecarError> { @@ -4880,8 +6040,8 @@ fn host_mount_read_only_for_guest_path( .map(|mount| mount.read_only) } -fn reject_host_mounted_unix_socket_path( - context: &JavascriptSocketPathContext, +pub(in crate::execution) fn reject_host_mounted_unix_socket_path( + context: &SocketPathContext, guest_path: &str, ) -> Result<(), SidecarError> { if let Some(read_only) = host_mount_read_only_for_guest_path(&context.mounts, guest_path) { @@ -4895,8 +6055,8 @@ fn reject_host_mounted_unix_socket_path( Ok(()) } -fn allocate_guest_socket_host_path( - context: &JavascriptSocketPathContext, +pub(in crate::execution) fn allocate_guest_socket_host_path( + context: &SocketPathContext, kernel_pid: u32, listener_id: &str, guest_path: &str, @@ -4911,7 +6071,7 @@ fn allocate_guest_socket_host_path( context.unix_socket_host_dir.join(leaf) } -fn format_unix_socket_resource( +pub(in crate::execution) fn format_unix_socket_resource( path: Option<&str>, abstract_path_hex: Option<&str>, autobind: bool, @@ -4927,9 +6087,10 @@ fn format_unix_socket_resource( } } -pub(crate) fn error_code(error: &SidecarError) -> &'static str { +pub(crate) fn error_code(error: &SidecarError) -> &str { match error { SidecarError::ResourceLimit(_) => "ERR_AGENTOS_RESOURCE_LIMIT", + SidecarError::Host(error) => &error.code, SidecarError::InvalidState(_) => "invalid_state", SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch", SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch", @@ -4940,95 +6101,69 @@ pub(crate) fn error_code(error: &SidecarError) -> &'static str { SidecarError::Kernel(_) => "kernel_error", SidecarError::Plugin(_) => "plugin_error", SidecarError::Execution(_) => "execution_error", + SidecarError::ExecutionEventChannelClosed { .. } => "execution_event_channel_closed", SidecarError::Bridge(_) => "bridge_error", SidecarError::Io(_) => "io_error", } } -pub(in crate::execution) fn guest_errno_code(message: &str) -> Option<&str> { - const TRUSTED_PREFIXES: &[&str] = &[ - "ERR_AGENTOS_NODE_SYNC_RPC", - "ERR_AGENTOS_PYTHON_VFS_RPC", - "ERR_AGENTOS_BRIDGE", - ]; - - let mut segments = message.split(':').map(str::trim); - let first = segments.next()?; - if is_guest_errno_segment(first) { - return Some(first); - } - - if TRUSTED_PREFIXES.contains(&first) { - let second = segments.next()?; - if is_guest_errno_segment(second) { - return Some(second); - } - } - - None +pub(in crate::execution) fn guest_error_code(error: &SidecarError) -> Option<&str> { + error.code() } -fn is_guest_errno_segment(segment: &str) -> bool { - segment.len() >= 2 - && segment.starts_with('E') - && !segment.starts_with("ERR_") - && segment[1..] - .bytes() - .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') +pub(crate) fn host_service_error_code(error: &SidecarError) -> String { + error + .code() + .unwrap_or("ERR_AGENTOS_NODE_SYNC_RPC") + .to_owned() } -pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String { - let message = error.to_string(); - for code in [ - "ERR_SOCKET_BAD_PORT", - "ERR_SOCKET_DGRAM_IS_CONNECTED", - "ERR_SOCKET_DGRAM_NOT_CONNECTED", - "ERR_SOCKET_DGRAM_NOT_RUNNING", - ] { - if message - .strip_prefix(code) - .is_some_and(|suffix| suffix.starts_with(':')) - { - return code.to_owned(); - } - } - if let Some(code) = guest_errno_code(&message) { - return code.to_owned(); - } - if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") { - return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED"); - } - - let lower = message.to_ascii_lowercase(); - if lower.contains("no such file or directory") - || lower.contains("entry not found") - || lower.contains("not found") - { - return String::from("ENOENT"); - } - if lower.contains("permission denied") { - return String::from("EACCES"); - } - if lower.contains("already exists") - || lower.contains("already registered") - || lower.contains("file exists") - { - return String::from("EEXIST"); - } - if lower.contains("invalid argument") { - return String::from("EINVAL"); +pub(in crate::execution) fn host_service_error_message(error: &SidecarError) -> String { + match error { + SidecarError::ResourceLimit(limit) => crate::state::guest_limit_message(limit), + SidecarError::Host(error) => error.message.clone(), + _ => error.to_string(), } - - String::from("ERR_AGENTOS_NODE_SYNC_RPC") } -pub(in crate::execution) fn javascript_sync_rpc_error_message(error: &SidecarError) -> String { +pub(crate) fn host_service_error( + error: &SidecarError, +) -> agentos_execution::backend::HostServiceError { + use agentos_execution::backend::HostServiceError; + match error { - SidecarError::ResourceLimit(limit) => crate::state::guest_limit_message(limit), - _ => error.to_string(), + SidecarError::Host(error) => error.clone(), + SidecarError::ResourceLimit(limit) => { + let guest_scope = if limit.scope.starts_with("vm=") { + "vm" + } else { + "process" + }; + let mut details = serde_json::json!({ + "limitName": limit.resource.name(), + "limit": limit.limit, + "requested": limit.requested, + "configPath": limit.config_path, + "scope": guest_scope, + }); + if guest_scope == "vm" { + details["used"] = serde_json::json!(limit.used); + details["observed"] = serde_json::json!(limit.used.saturating_add(limit.requested)); + } + HostServiceError::new( + "ERR_AGENTOS_RESOURCE_LIMIT", + crate::state::guest_limit_message(limit), + ) + .with_details(details) + } + _ => HostServiceError::new( + host_service_error_code(error), + host_service_error_message(error), + ), } } +#[cfg(test)] pub(crate) fn ignore_stale_javascript_sync_rpc_response( error: SidecarError, ) -> Result<(), SidecarError> { @@ -5063,79 +6198,94 @@ pub(crate) fn ignore_stale_javascript_sync_rpc_response( #[cfg(test)] mod error_code_tests { use super::{ - guest_errno_code, ignore_stale_javascript_sync_rpc_response, - javascript_sync_rpc_error_code, javascript_sync_rpc_error_message, SidecarError, + host_service_error_code, host_service_error_message, + ignore_stale_javascript_sync_rpc_response, javascript_sync_rpc_arg_rlim, + process_resource_limit_kind, SidecarError, }; + use agentos_kernel::kernel::ProcessResourceLimitKind; use agentos_runtime::accounting::{LimitError, ResourceClass}; + use serde_json::json; #[test] - fn guest_errno_code_rejects_guest_controlled_errno_segments() { - assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None); + fn wasm_resource_limit_numbers_cover_the_linux_surface() { + let expected = [ + ProcessResourceLimitKind::Cpu, + ProcessResourceLimitKind::FileSize, + ProcessResourceLimitKind::Data, + ProcessResourceLimitKind::Stack, + ProcessResourceLimitKind::Core, + ProcessResourceLimitKind::ResidentSet, + ProcessResourceLimitKind::Processes, + ProcessResourceLimitKind::OpenFiles, + ProcessResourceLimitKind::LockedMemory, + ProcessResourceLimitKind::AddressSpace, + ]; + for (resource, expected) in expected.into_iter().enumerate() { + assert_eq!( + process_resource_limit_kind(resource as u32).expect("known resource"), + expected + ); + } assert_eq!( - guest_errno_code("prefix: user said 'EPERM': more text"), - None + process_resource_limit_kind(10) + .expect_err("unknown resource") + .code(), + Some("EINVAL") ); - assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None); } #[test] - fn guest_errno_code_accepts_trusted_secure_exec_prefixes() { - assert_eq!( - guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"), - Some("EACCES") - ); + fn wasm_resource_limit_values_preserve_full_u64_precision() { assert_eq!( - guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"), - Some("ENOENT") + javascript_sync_rpc_arg_rlim(&[json!(u64::MAX.to_string())], 0, "rlim") + .expect("parse RLIM_INFINITY"), + u64::MAX ); - assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST")); } #[test] - fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() { + fn host_service_error_code_ignores_spoofed_errnos() { let error = SidecarError::Execution(String::from("user said 'EACCES: denied'")); - assert_eq!( - javascript_sync_rpc_error_code(&error), - "ERR_AGENTOS_NODE_SYNC_RPC" - ); + assert_eq!(host_service_error_code(&error), "ERR_AGENTOS_NODE_SYNC_RPC"); } #[test] - fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() { - let error = SidecarError::Execution(String::from( - "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo", - )); - assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES"); + fn host_service_error_code_preserves_real_sidecar_errnos() { + let error = SidecarError::host("EACCES", "permission denied on /foo"); + assert_eq!(host_service_error_code(&error), "EACCES"); } #[test] - fn javascript_sync_rpc_error_code_preserves_dgram_state_errors() { + fn host_service_error_code_preserves_dgram_state_errors() { for code in [ "ERR_SOCKET_BAD_PORT", "ERR_SOCKET_DGRAM_IS_CONNECTED", "ERR_SOCKET_DGRAM_NOT_CONNECTED", "ERR_SOCKET_DGRAM_NOT_RUNNING", ] { - let error = SidecarError::Execution(format!("{code}: dgram state error")); - assert_eq!(javascript_sync_rpc_error_code(&error), code); + let error = SidecarError::host(code, "dgram state error"); + assert_eq!(host_service_error_code(&error), code); } } #[test] - fn javascript_sync_rpc_error_code_maps_file_exists_messages() { + fn host_service_error_code_does_not_parse_diagnostic_messages() { let error = SidecarError::Io(String::from( "failed to create mapped guest directory /.next/server: File exists (os error 17)", )); - assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST"); + assert_eq!(host_service_error_code(&error), "ERR_AGENTOS_NODE_SYNC_RPC"); } #[test] - fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() { - let error = SidecarError::Execution(String::from( - "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM", - )); + fn host_service_error_code_preserves_native_binary_rejections() { + let error = SidecarError::host( + "ERR_NATIVE_BINARY_NOT_SUPPORTED", + String::from( + "refused to execute native ELF guest binary at /tmp/fake-rg inside the VM", + ), + ); assert_eq!( - javascript_sync_rpc_error_code(&error), + host_service_error_code(&error), "ERR_NATIVE_BINARY_NOT_SUPPORTED" ); } @@ -5150,7 +6300,7 @@ mod error_code_tests { limit: 65_536, config_path: String::from("runtime.resources.maxBridgeResponseBytes"), }); - let message = javascript_sync_rpc_error_message(&error); + let message = host_service_error_message(&error); assert!(!message.contains("used=65535")); assert!(message.contains("scope=process")); assert!(message.contains("requested=1 limit=65536")); @@ -5181,16 +6331,16 @@ mod error_code_tests { #[cfg(test)] mod wasm_sync_rpc_tests { use super::{ - deferred_child_kernel_wait_request, remap_wasm_process_sync_rpc, JavascriptSyncRpcRequest, - ALLOWED_WASM_PROCESS_SYNC_RPCS, + deferred_child_kernel_wait_request, javascript_sync_rpc_request_bytes_arg, + remap_wasm_process_sync_rpc, HostRpcRequest, ALLOWED_WASM_PROCESS_SYNC_RPCS, }; use serde_json::json; use std::collections::{BTreeSet, HashMap}; - fn emitted_wasm_process_sync_rpcs() -> BTreeSet<&'static str> { + fn emitted_wasm_wrapped_sync_rpcs() -> BTreeSet<&'static str> { let source = include_str!("../../../../execution/src/wasm.rs"); let start = source - .find("case \"process.getpgid\":") + .find("case \"process.exec_image_open\":") .expect("WASM process sync-RPC switch must exist"); let end = source[start..] .find("_processWasmSyncRpc.applySync") @@ -5202,23 +6352,21 @@ mod wasm_sync_rpc_tests { line.trim() .strip_prefix("case \"") .and_then(|line| line.strip_suffix("\":")) - .filter(|method| method.starts_with("process.")) }) .collect() } #[test] - fn every_emitted_wasm_process_rpc_is_unwrapped_to_the_direct_handler_shape() { - let emitted = emitted_wasm_process_sync_rpcs(); - assert!(!emitted.is_empty(), "expected WASM process RPC methods"); + fn every_emitted_wasm_wrapped_rpc_is_unwrapped_to_the_direct_handler_shape() { + let emitted = emitted_wasm_wrapped_sync_rpcs(); + assert!(!emitted.is_empty(), "expected wrapped WASM RPC methods"); let allowed = ALLOWED_WASM_PROCESS_SYNC_RPCS .iter() .copied() .collect::>(); - let missing = emitted.difference(&allowed).copied().collect::>(); - assert!( - missing.is_empty(), - "WASM emits process RPCs the sidecar wrapper does not allow: {missing:?}" + assert_eq!( + emitted, allowed, + "the generic WASM wrapper switch and sidecar allowlist must remain exact" ); let service_source = include_str!("rpc.rs"); @@ -5230,19 +6378,25 @@ mod wasm_sync_rpc_tests { .map(|offset| service_start + offset) .expect("sync RPC service end must exist"); let service_source = &service_source[service_start..service_end]; + let typed_dispatch_source = include_str!("../host_dispatch/mod.rs"); + let typed_filesystem_dispatch_source = include_str!("../host_dispatch/filesystem.rs"); for method in emitted { - assert!( - service_source.contains(&format!("\"{method}\"")), - "WASM emits {method}, but the direct sync RPC dispatcher has no handler" - ); - let direct = JavascriptSyncRpcRequest { + if !crate::execution::host_dispatch::is_wasm_adapter_only_rpc(method) { + assert!( + service_source.contains(&format!("\"{method}\"")) + || typed_dispatch_source.contains(&format!("\"{method}\"")) + || typed_filesystem_dispatch_source.contains(&format!("\"{method}\"")), + "WASM emits {method}, but the direct sync RPC dispatcher has no handler" + ); + } + let direct = HostRpcRequest { id: 17, method: method.to_owned(), args: vec![json!({ "marker": method })], raw_bytes_args: HashMap::from([(0, vec![1, 2, 3])]), }; - let wrapped = JavascriptSyncRpcRequest { + let wrapped = HostRpcRequest { id: direct.id, method: String::from("process.wasm_sync_rpc"), args: vec![json!(method), direct.args[0].clone()], @@ -5266,7 +6420,7 @@ mod wasm_sync_rpc_tests { #[test] fn wrapped_wasm_fd_read_is_normalized_for_descendant_deferral() { - let request = JavascriptSyncRpcRequest { + let request = HostRpcRequest { id: 41, method: String::from("process.wasm_sync_rpc"), args: vec![json!("process.fd_read"), json!(7), json!(4096), json!(5000)], @@ -5280,4 +6434,60 @@ mod wasm_sync_rpc_tests { assert_eq!(normalized.method, "process.fd_read"); assert_eq!(normalized.args, request.args[1..]); } + + #[test] + fn request_byte_decoder_prefers_lossless_cbor_lane_and_accepts_compat_shapes() { + let raw = HostRpcRequest { + id: 1, + method: String::from("process.fd_sendmsg_rights"), + args: vec![ + json!(4), + json!({ "__agentOSType": "bytes", "base64": "d3Jvbmc=" }), + ], + raw_bytes_args: HashMap::from([(1, vec![0, 255, 1, 128])]), + }; + assert_eq!( + javascript_sync_rpc_request_bytes_arg(&raw, 1, "payload") + .expect("decode raw CBOR byte lane"), + vec![0, 255, 1, 128] + ); + + for (value, expected) in [ + ( + json!({ "__agentOSType": "bytes", "base64": "AP8BgA==" }), + vec![0, 255, 1, 128], + ), + ( + json!({ "__type": "Buffer", "data": "AP8BgA==" }), + vec![0, 255, 1, 128], + ), + ( + json!({ "__type": "buffer", "value": "AP8BgA==" }), + vec![0, 255, 1, 128], + ), + (json!("text"), b"text".to_vec()), + ] { + let request = HostRpcRequest { + id: 2, + method: String::from("test"), + args: vec![value], + raw_bytes_args: HashMap::new(), + }; + assert_eq!( + javascript_sync_rpc_request_bytes_arg(&request, 0, "payload") + .expect("decode compatibility byte shape"), + expected + ); + } + + let invalid = HostRpcRequest { + id: 3, + method: String::from("test"), + args: vec![json!([0, 255, 1, 128])], + raw_bytes_args: HashMap::new(), + }; + let error = javascript_sync_rpc_request_bytes_arg(&invalid, 0, "payload") + .expect_err("numeric arrays are not a bridge byte encoding"); + assert!(error.to_string().contains("payload")); + } } diff --git a/crates/native-sidecar/src/execution/javascript/sqlite.rs b/crates/native-sidecar/src/execution/javascript/sqlite.rs index 68b8849d7f..43fa4e357f 100644 --- a/crates/native-sidecar/src/execution/javascript/sqlite.rs +++ b/crates/native-sidecar/src/execution/javascript/sqlite.rs @@ -5,7 +5,7 @@ const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991; pub(in crate::execution) fn service_javascript_sqlite_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { match request.method.as_str() { "sqlite.constants" => Ok(json!({})), @@ -124,7 +124,7 @@ pub(in crate::execution) fn service_javascript_sqlite_sync_rpc( fn sqlite_open_database( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?; let path = request.args.first().and_then(Value::as_str); @@ -213,7 +213,9 @@ fn sqlite_open_database( .map_err(sqlite_error)?; } if host_path.is_some() && !read_only { - let _ = connection.pragma_update(None, "journal_mode", "WAL"); + connection + .pragma_update(None, "journal_mode", "WAL") + .map_err(sqlite_error)?; } process.sqlite_databases.insert( @@ -233,7 +235,7 @@ fn sqlite_open_database( fn sqlite_exec_database( _kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?; let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?; @@ -253,7 +255,7 @@ fn sqlite_exec_database( fn sqlite_query_database( _kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?; let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?; @@ -277,12 +279,12 @@ fn sqlite_query_database( fn sqlite_prepare_statement( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?; let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?; let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?; - let _ = sqlite_database(process, database_id)?; + sqlite_database(process, database_id)?; process.next_sqlite_statement_id += 1; let statement_id = process.next_sqlite_statement_id; process.sqlite_statements.insert( @@ -302,7 +304,7 @@ fn sqlite_prepare_statement( fn sqlite_run_statement( _kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let statement_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?; @@ -336,7 +338,7 @@ fn sqlite_run_statement( fn sqlite_get_statement( _kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let statement_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?; @@ -362,7 +364,7 @@ fn sqlite_get_statement( fn sqlite_all_statement( _kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let statement_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?; @@ -384,7 +386,7 @@ fn sqlite_all_statement( fn sqlite_statement_columns( process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let statement_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?; @@ -722,9 +724,20 @@ fn sqlite_sync_database( return Ok(()); } + database + .connection + .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)") + .map_err(sqlite_error)?; let host_path = database.host_path.as_ref().expect("sqlite host path"); - if !host_path.exists() { - return Ok(()); + match fs::metadata(host_path) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect sqlite temp database {}: {error}", + host_path.display() + ))) + } } // The main file alone is not a consistent snapshot when the guest selected // WAL mode. A checkpoint can remain busy while OpenCode keeps prepared read @@ -789,17 +802,26 @@ fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), Sidecar let parent = host_path.parent().map(PathBuf::from); for suffix in ["", "-wal", "-shm", ".snapshot"] { let path = PathBuf::from(format!("{}{}", host_path.display(), suffix)); - if path.exists() { - fs::remove_file(&path).map_err(|error| { - SidecarError::Io(format!( + match fs::remove_file(&path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(SidecarError::Io(format!( "failed to remove sqlite temp artifact {}: {error}", path.display() - )) - })?; + ))); + } } } if let Some(parent) = parent { - let _ = fs::remove_dir_all(parent); + if let Err(error) = fs::remove_dir_all(&parent) { + if error.kind() != std::io::ErrorKind::NotFound { + return Err(SidecarError::Io(format!( + "failed to remove sqlite temp directory {}: {error}", + parent.display() + ))); + } + } } Ok(()) } diff --git a/crates/native-sidecar/src/execution/launch.rs b/crates/native-sidecar/src/execution/launch.rs index ea0a66557a..1bbad50bea 100644 --- a/crates/native-sidecar/src/execution/launch.rs +++ b/crates/native-sidecar/src/execution/launch.rs @@ -30,7 +30,7 @@ const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[ const EXECUTION_REQUEST_TTY_ENV: &str = "AGENTOS_EXEC_TTY"; fn resolve_execute_request( - vm: &VmState, + vm: &mut VmState, payload: &ExecuteRequest, ) -> Result { let payload_env: BTreeMap = payload @@ -80,6 +80,11 @@ fn resolve_execute_request( .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint)); prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + let adapter_policy = match runtime { + GuestRuntimeKind::WebAssembly => ExecutionAdapterPolicy::KERNEL_HOST_CALL_POSIX, + GuestRuntimeKind::JavaScript => ExecutionAdapterPolicy::DIRECT_RUNTIME, + GuestRuntimeKind::Python => ExecutionAdapterPolicy::DIRECT_PYTHON_RUNTIME, + }; Ok(ResolvedChildProcessExecution { command: match runtime { GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND), @@ -99,11 +104,12 @@ fn resolve_execute_request( host_cwd, wasm_permission_tier: payload.wasm_permission_tier, binding_command: false, + adapter_policy, }) } fn resolve_command_execution( - vm: &VmState, + vm: &mut VmState, command: &str, args: &[String], extra_env: &BTreeMap, @@ -131,6 +137,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: true, + adapter_policy: ExecutionAdapterPolicy::BINDING, }); } @@ -168,6 +175,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -186,6 +194,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -207,6 +216,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -238,7 +248,7 @@ fn resolve_command_execution( guest_entrypoint.as_ref().map_or_else( || entrypoint_specifier.clone(), |guest_entrypoint| { - resolve_vm_guest_path_to_host(vm, guest_entrypoint) + runtime_launch_path_for_guest(vm, guest_entrypoint) .to_string_lossy() .into_owned() }, @@ -268,6 +278,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -292,7 +303,7 @@ fn resolve_command_execution( guest_entrypoint.as_ref().map_or_else( || command.to_owned(), |guest_entrypoint| { - resolve_vm_guest_path_to_host(vm, guest_entrypoint) + runtime_launch_path_for_guest(vm, guest_entrypoint) .to_string_lossy() .into_owned() }, @@ -315,6 +326,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } @@ -338,7 +350,10 @@ fn resolve_command_execution( .and_then(|name| vm.command_permissions.get(name).copied()) }); - let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); + // Resolution is authoritative in the kernel VFS. The compatibility + // engines receive only a VM-private snapshot path, populated after this + // live lookup has completed. + let host_entrypoint = runtime_asset_path_for_guest(vm, &guest_entrypoint); if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) { @@ -363,6 +378,7 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_RUNTIME, }); } prepare_guest_runtime_env( @@ -386,39 +402,32 @@ fn resolve_command_execution( host_cwd, wasm_permission_tier, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::KERNEL_HOST_CALL_POSIX, }) } const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4; pub(super) fn resolve_javascript_command_entrypoint( - vm: &VmState, + vm: &mut VmState, guest_entrypoint: &str, - host_entrypoint: &Path, + _host_entrypoint: &Path, ) -> Option<(String, PathBuf)> { - // agentOS package content is served guest-native (tar + single-symlink - // mounts) and is never materialized on the host, so the shebang-reading - // fallback below (which reads the host path) cannot classify these - // entrypoints. Within the package mount the only runtimes are WebAssembly - // (`*.wasm`) and JavaScript, and `bin/` launchers are frequently - // extensionless — so classify by extension here: `.wasm` is WASM (fall - // through), everything else in the mount is JavaScript. - if guest_path_is_within_agentos_package_mount(vm, guest_entrypoint) { - let extension = Path::new(guest_entrypoint) - .extension() - .and_then(|extension| extension.to_str()); - if extension != Some("wasm") { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); - } - return None; - } - - resolve_javascript_command_entrypoint_inner( - vm, + let package_mount_roots = vm + .configuration + .mounts + .iter() + .filter(|mount| mount.plugin.id == "agentos_packages") + .map(|mount| normalize_path(&mount.guest_path)) + .collect::>(); + let resolved_guest_entrypoint = resolve_javascript_command_entrypoint_inner( + &mut vm.kernel, guest_entrypoint, - host_entrypoint, + &package_mount_roots, MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, - ) + )?; + let launch_asset = runtime_asset_path_for_guest(vm, &resolved_guest_entrypoint); + Some((resolved_guest_entrypoint, launch_asset)) } /// Resolve the main module filename the same way Node does by default. @@ -438,80 +447,87 @@ pub(super) fn resolve_javascript_main_entrypoint( .realpath(guest_entrypoint) .map(|path| normalize_path(&path)) .unwrap_or_else(|_| normalize_path(guest_entrypoint)); - let resolved_host_entrypoint = resolve_vm_guest_path_to_host(vm, &resolved_guest_entrypoint); + let resolved_host_entrypoint = runtime_asset_path_for_guest(vm, &resolved_guest_entrypoint); (resolved_guest_entrypoint, resolved_host_entrypoint) } fn resolve_javascript_command_entrypoint_inner( - vm: &VmState, + kernel: &mut SidecarKernel, guest_entrypoint: &str, - host_entrypoint: &Path, + package_mount_roots: &[String], redirects_remaining: usize, -) -> Option<(String, PathBuf)> { - if redirects_remaining > 0 { - let symlink_target = fs::symlink_metadata(host_entrypoint) - .ok() - .filter(|metadata| metadata.file_type().is_symlink()) - .and_then(|_| fs::read_link(host_entrypoint).ok()); - if let Some(symlink_target) = symlink_target { - let guest_parent = Path::new(guest_entrypoint) - .parent() - .and_then(|path| path.to_str()) - .unwrap_or("/"); - let symlink_guest_entrypoint = if symlink_target.is_absolute() { - normalize_path(&symlink_target.to_string_lossy()) - } else { - normalize_path(&format!( - "{guest_parent}/{}", - symlink_target.to_string_lossy().replace('\\', "/") - )) - }; - let symlink_host_entrypoint = - resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint); - return resolve_javascript_command_entrypoint_inner( - vm, - &symlink_guest_entrypoint, - &symlink_host_entrypoint, - redirects_remaining - 1, - ); - } +) -> Option { + // Resolve and inspect the selected inode through the live kernel. Host + // projections and previously materialized scratch files are deliberately + // not consulted: once a kernel file is deleted or replaced, it cannot be + // resurrected by stale engine-launch state. + let initial_stat = kernel.lstat(guest_entrypoint).ok()?; + if initial_stat.is_directory { + return None; + } + let canonical_guest_entrypoint = normalize_path(&kernel.realpath(guest_entrypoint).ok()?); + let canonical_stat = kernel.lstat(&canonical_guest_entrypoint).ok()?; + if canonical_stat.is_directory || canonical_stat.is_symbolic_link { + return None; + } + let script = load_executable_script_preview(kernel, &canonical_guest_entrypoint)?; + if script.as_bytes().starts_with(b"\0asm") { + return None; } - - let script = load_executable_script_preview(host_entrypoint)?; let interpreter = parse_script_interpreter_name(&script); + let is_package_entrypoint = + guest_path_is_within_roots(&canonical_guest_entrypoint, package_mount_roots); - if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + if interpreter.is_none() + && (is_package_entrypoint + || is_probable_javascript_entrypoint(Path::new(&canonical_guest_entrypoint), &script)) + { + return Some(canonical_guest_entrypoint); } let interpreter = interpreter?; if interpreter == "node" { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + return Some(canonical_guest_entrypoint); } - if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") { - return None; + if redirects_remaining > 0 && matches!(interpreter.as_str(), "sh" | "bash" | "dash") { + if let Some(shim_target) = parse_node_shell_shim_target(&script) { + let guest_parent = Path::new(&canonical_guest_entrypoint) + .parent() + .and_then(|path| path.to_str()) + .unwrap_or("/"); + let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}")); + return resolve_javascript_command_entrypoint_inner( + kernel, + &shim_guest_entrypoint, + package_mount_roots, + redirects_remaining - 1, + ); + } } - let shim_target = parse_node_shell_shim_target(&script)?; - let guest_parent = Path::new(guest_entrypoint) - .parent() - .and_then(|path| path.to_str()) - .unwrap_or("/"); - let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}")); - let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint); - resolve_javascript_command_entrypoint_inner( - vm, - &shim_guest_entrypoint, - &shim_host_entrypoint, - redirects_remaining - 1, - ) + // Preserve the package-driver contract for non-WASM package launchers. + // Unlike the old extension-only decision, this fallback happens only after + // the selected live regular file was read and ruled out as WebAssembly. + is_package_entrypoint.then_some(canonical_guest_entrypoint) +} + +fn load_executable_script_preview(kernel: &mut SidecarKernel, guest_path: &str) -> Option { + const MAX_SCRIPT_PREVIEW_BYTES: usize = 16 * 1024; + let preview_limit = kernel + .resource_limits() + .max_pread_bytes + .unwrap_or(MAX_SCRIPT_PREVIEW_BYTES) + .min(MAX_SCRIPT_PREVIEW_BYTES); + let bytes = kernel.pread_file(guest_path, 0, preview_limit).ok()?; + Some(String::from_utf8_lossy(&bytes).into_owned()) } -fn load_executable_script_preview(path: &Path) -> Option { - let bytes = fs::read(path).ok()?; - let preview_len = bytes.len().min(16 * 1024); - Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned()) +fn guest_path_is_within_roots(guest_path: &str, roots: &[String]) -> bool { + let normalized = normalize_path(guest_path); + roots + .iter() + .any(|root| normalized == *root || normalized.starts_with(&format!("{root}/"))) } fn parse_script_interpreter_name(script: &str) -> Option { @@ -619,23 +635,32 @@ fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf let host_cwd = if value.is_none() { vm.host_cwd.clone() } else { - resolve_vm_guest_path_to_host(vm, &guest_cwd) + runtime_launch_path_for_guest(vm, &guest_cwd) }; (guest_cwd, host_cwd, value.is_none()) } -pub(super) fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf { +pub(super) fn runtime_launch_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf { host_mount_path_for_guest_path(vm, guest_path) - .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path)) + .unwrap_or_else(|| runtime_asset_path_for_guest(vm, guest_path)) } -pub(super) fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf { +pub(super) fn runtime_asset_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf { let normalized = normalize_path(guest_path); let relative = normalized.trim_start_matches('/'); if relative.is_empty() { - return vm.cwd.clone(); + return vm.runtime_scratch_root.clone(); } - vm.cwd.join(relative) + vm.runtime_scratch_root.join(relative) +} + +fn resolved_entrypoint_uses_kernel_launch_asset( + vm: &VmState, + resolved: &ResolvedChildProcessExecution, + guest_entrypoint: &str, +) -> bool { + normalize_host_path(Path::new(&resolved.entrypoint)) + == normalize_host_path(&runtime_asset_path_for_guest(vm, guest_entrypoint)) } pub(super) fn apply_shell_cwd_prefix( @@ -740,927 +765,149 @@ fn shell_single_quote(value: &str) -> String { format!("'{}'", value.replace('\'', "'\"'\"'")) } -pub(crate) fn sync_active_process_host_writes_to_kernel( - vm: &mut VmState, -) -> Result<(), SidecarError> { - if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { - sync_vm_shadow_root_to_kernel(vm)?; - } - - let normalized_vm_root = normalize_host_path(&vm.cwd); - let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root); - for (host_cwd, guest_cwd) in extra_roots { - sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?; +fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String { + if specifier.starts_with("file://") { + normalize_path(specifier.trim_start_matches("file://")) + } else if specifier.starts_with("file:") { + normalize_path(specifier.trim_start_matches("file:")) + } else if specifier.starts_with('/') { + normalize_path(specifier) + } else { + normalize_path(&format!("{cwd}/{specifier}")) } - - Ok(()) } -fn sync_vm_shadow_root_to_kernel(vm: &mut VmState) -> Result<(), SidecarError> { - let shadow_root = normalize_host_path(&vm.cwd); - if host_sync_root_is_filesystem_root(&shadow_root) { - tracing::warn!("skipping host shadow sync rooted at the host filesystem root"); - return Ok(()); - } - let mut snapshot = collect_shadow_sync_snapshot(&shadow_root)?; - snapshot - .entries - .retain(|path, _| !should_skip_shadow_sync_path(vm, path)); - - // An unreadable subtree is not an empty subtree. Preserve the previous - // inventory below it so a transient EACCES cannot be misinterpreted as a - // request to delete all of its kernel children. - for (path, entry) in &vm.shadow_sync_inventory { - if snapshot - .incomplete_subtrees - .iter() - .any(|prefix| guest_path_is_at_or_below(path, prefix)) - { - snapshot.entries.entry(path.clone()).or_insert(*entry); - } - } - - let failed_replacements = propagate_shadow_deletions_to_kernel(vm, &mut snapshot.entries); - let mut synced_file_times = BTreeMap::new(); - sync_host_directory_tree_to_kernel_inner( - vm, - &shadow_root, - &shadow_root, - "/", - &mut synced_file_times, - Some(&snapshot.entries), - Some(&failed_replacements), - )?; - vm.shadow_sync_inventory = snapshot.entries; - Ok(()) +fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option { + is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier)) } -#[derive(Default)] -struct ShadowSyncSnapshot { - entries: BTreeMap, - incomplete_subtrees: BTreeSet, +pub(super) fn is_node_runtime_command(command: &str) -> bool { + matches!(command, "node" | "npm" | "npx") + || Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| matches!(name, "node" | "npm" | "npx")) } -/// Capture the shadow root before any additive kernel writes. The separate -/// inventory pass lets deletion and type-replacement reconciliation happen -/// first, which is essential when a stale symlink or directory occupies the -/// pathname that is about to become a regular file. -pub(crate) fn initial_shadow_sync_inventory( - shadow_root: &Path, -) -> Result, SidecarError> { - Ok(collect_shadow_sync_snapshot(shadow_root)?.entries) +fn python_command_base_name(command: &str) -> &str { + Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(command) } -fn collect_shadow_sync_snapshot(shadow_root: &Path) -> Result { - let mut snapshot = ShadowSyncSnapshot::default(); - collect_shadow_sync_snapshot_inner(shadow_root, shadow_root, "/", &mut snapshot)?; - Ok(snapshot) +/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are +/// served by the embedded Pyodide runtime, mirroring how `node` is served by the +/// embedded V8 runtime. +pub(super) fn is_python_runtime_command(command: &str) -> bool { + matches!( + python_command_base_name(command), + "python" | "python3" | "pip" | "pip3" + ) } -fn collect_shadow_sync_snapshot_inner( - shadow_root: &Path, - current_host_dir: &Path, - current_guest_dir: &str, - snapshot: &mut ShadowSyncSnapshot, -) -> Result<(), SidecarError> { - let entries = match fs::read_dir(current_host_dir) { - Ok(entries) => entries, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) - if error.kind() == std::io::ErrorKind::PermissionDenied - || error.raw_os_error() == Some(libc::EPERM) => - { - let guest_path = normalize_path(current_guest_dir); - snapshot.incomplete_subtrees.insert(guest_path.clone()); - tracing::warn!( - path = %current_host_dir.display(), - guest_path = %guest_path, - "shadow inventory is incomplete because a host directory is unreadable" - ); - return Ok(()); - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inventory host shadow directory {}: {error}", - current_host_dir.display() - ))); - } - }; +/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the +/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` / +/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen +/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control +/// env, which the runner consumes and never exposes in the guest `os.environ`. +pub(super) fn resolve_python_command_execution( + vm: &VmState, + command: &str, + args: &[String], + mut env: BTreeMap, + guest_cwd: String, + host_cwd: PathBuf, +) -> Result { + let base_name = python_command_base_name(command); + let is_pip = matches!(base_name, "pip" | "pip3"); - for entry in entries { - let entry = match entry { - Ok(entry) => entry, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inventory host shadow entry in {}: {error}", - current_host_dir.display() - ))); - } - }; - let host_path = entry.path(); - let file_type = match entry.file_type() { - Ok(file_type) => file_type, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inventory host shadow entry {}: {error}", - host_path.display() - ))); - } - }; - let relative_path = host_path.strip_prefix(shadow_root).map_err(|error| { - SidecarError::InvalidState(format!( - "failed to relativize host shadow path {} against {}: {error}", - host_path.display(), - shadow_root.display() - )) - })?; - let guest_path = normalize_path(&format!( - "/{}", - relative_path.to_string_lossy().replace('\\', "/") - )); - let node_type = if file_type.is_dir() { - ShadowNodeType::Directory - } else if file_type.is_file() { - ShadowNodeType::File - } else if file_type.is_symlink() { - ShadowNodeType::Symlink - } else { - continue; - }; - snapshot.entries.insert( - guest_path.clone(), - ShadowSyncInventoryEntry::present(node_type), - ); - if node_type == ShadowNodeType::Directory { - collect_shadow_sync_snapshot_inner(shadow_root, &host_path, &guest_path, snapshot)?; - } - } - Ok(()) -} + let mut entrypoint = String::new(); + let mut argv: Vec = Vec::new(); + let mut module: Option = None; + let mut stdin_program = false; + let mut interactive = false; + let mut guest_entrypoint: Option = None; -/// Removes kernel paths whose shadow copy disappeared since the last walk. -/// -/// Best-effort by design: a failure to reconcile one stale path must not -/// poison the guest filesystem operation that triggered the sync, so failures -/// are surfaced as host-visible warnings instead of errors. Children sort -/// after their parents in the `BTreeSet`, so the reverse iteration removes -/// leaves before the directories that contain them. -fn propagate_shadow_deletions_to_kernel( - vm: &mut VmState, - current: &mut BTreeMap, -) -> BTreeSet { - let stale = vm - .shadow_sync_inventory - .iter() - .rev() - .filter_map(|(path, previous)| { - let replacement = current.get(path); - (previous.deletion_pending - || replacement.is_none() - || replacement.is_some_and(|entry| entry.node_type != previous.node_type)) - .then(|| (path.clone(), *previous)) - }) - .collect::>(); - let mut failed = BTreeSet::new(); - for (path, previous) in stale { - if path == "/" || should_skip_shadow_sync_path(vm, &path) || is_shadow_bootstrap_dir(&path) - { - continue; + if is_pip { + module = Some(String::from("pip")); + argv.push(String::from("pip")); + argv.extend(args.iter().cloned()); + } else { + // Skip the value-less interpreter flags we can safely ignore so they do + // not get mistaken for a script path. + let mut idx = 0; + while let Some(flag) = args.get(idx) { + match flag.as_str() { + "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b" + | "-d" | "-x" => idx += 1, + _ => break, + } } - let stat = match vm.kernel.lstat(&path) { - Ok(stat) => stat, - Err(error) if error.code() == "ENOENT" => continue, - Err(error) => { - tracing::warn!( - path = %path, - error = %error, - "failed to inspect a stale shadow path; deletion will be retried" - ); - retain_shadow_deletion_tombstone(current, &path, previous); - failed.insert(path); - continue; + let rest = &args[idx..]; + match rest.first().map(String::as_str) { + Some("-c") => { + entrypoint = rest.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from("argument expected for the -c option")) + })?; + argv.push(String::from("-c")); + argv.extend(rest.iter().skip(2).cloned()); } - }; - let result = if stat.is_directory && !stat.is_symbolic_link { - vm.kernel.remove_dir(&path) - } else { - vm.kernel.remove_file(&path) - }; - if let Err(error) = result { - if error.code() != "ENOENT" { - tracing::warn!( - path = %path, - error = %error, - "failed to propagate guest shadow deletion into the kernel VFS; deletion will be retried" - ); - retain_shadow_deletion_tombstone(current, &path, previous); - failed.insert(path); + Some("-m") => { + let name = rest.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from("argument expected for the -m option")) + })?; + module = Some(name); + argv.push(String::from("-m")); + argv.extend(rest.iter().skip(2).cloned()); + } + Some("-") => { + stdin_program = true; + argv.push(String::from("-")); + argv.extend(rest.iter().skip(1).cloned()); + } + Some(spec) if !spec.starts_with('-') => { + let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec) + .unwrap_or_else(|| spec.to_string()); + entrypoint = resolved_guest.clone(); + env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone()); + guest_entrypoint = Some(resolved_guest); + argv.push(spec.to_string()); + argv.extend(rest.iter().skip(1).cloned()); + } + Some(other) => { + return Err(SidecarError::InvalidState(format!( + "unsupported python option: {other}" + ))); + } + None => { + interactive = true; + argv.push(String::new()); } } } - failed -} - -fn retain_shadow_deletion_tombstone( - current: &mut BTreeMap, - path: &str, - previous: ShadowSyncInventoryEntry, -) { - current - .entry(path.to_owned()) - .and_modify(|entry| entry.deletion_pending = true) - .or_insert(ShadowSyncInventoryEntry { - node_type: previous.node_type, - deletion_pending: true, - }); -} -fn collect_active_process_host_sync_roots( - vm: &VmState, - normalized_vm_root: &Path, -) -> Vec<(PathBuf, String)> { - let mut roots = Vec::new(); - let mut seen = BTreeSet::new(); - - for process in vm.active_processes.values() { - collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots); + env.insert( + String::from("AGENTOS_PYTHON_ARGV"), + serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")), + ); + if let Some(module) = &module { + env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone()); + } + if stdin_program { + env.insert( + String::from("AGENTOS_PYTHON_STDIN_PROGRAM"), + String::from("1"), + ); + } + if interactive { + env.insert( + String::from("AGENTOS_PYTHON_INTERACTIVE"), + String::from("1"), + ); } - roots -} - -fn collect_process_host_sync_roots( - process: &ActiveProcess, - normalized_vm_root: &Path, - seen: &mut BTreeSet<(PathBuf, String)>, - roots: &mut Vec<(PathBuf, String)>, -) { - // Kernel-backed runtimes make clean writes observable immediately and keep - // their host trees only as mirrors. Importing every such mirror lets an - // inherited child's older snapshot overwrite a shared kernel file. Only a - // dirty or otherwise non-observable process root is authoritative input. - if process.host_write_dirty || !process.clean_host_writes_are_observable() { - let normalized_host_cwd = normalize_host_path(&process.host_cwd); - if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) { - let guest_cwd = normalize_path(&process.guest_cwd); - if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) { - roots.push((normalized_host_cwd, guest_cwd)); - } - } - } - - for child in process.child_processes.values() { - collect_process_host_sync_roots(child, normalized_vm_root, seen, roots); - } -} - -pub(crate) fn sync_process_host_writes_to_kernel( - vm: &mut VmState, - process: &ActiveProcess, -) -> Result<(), SidecarError> { - sync_process_host_roots_to_kernel( - vm, - &process.host_cwd, - &process.guest_cwd, - process.runtime != GuestRuntimeKind::JavaScript, - ) -} - -pub(super) fn sync_process_host_roots_to_kernel( - vm: &mut VmState, - process_host_cwd: &Path, - process_guest_cwd: &str, - sync_root_shadow: bool, -) -> Result<(), SidecarError> { - if sync_root_shadow && vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { - sync_vm_shadow_root_to_kernel(vm)?; - } - - if !path_is_within_root( - &normalize_host_path(process_host_cwd), - &normalize_host_path(&vm.cwd), - ) { - sync_host_directory_tree_to_kernel(vm, process_host_cwd, process_guest_cwd)?; - } - - Ok(()) -} - -fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool { - normalize_host_path(host_root) == Path::new("/") -} - -fn sync_host_directory_tree_to_kernel( - vm: &mut VmState, - host_root: &Path, - guest_root: &str, -) -> Result<(), SidecarError> { - let normalized_host_root = normalize_host_path(host_root); - let normalized_guest_root = normalize_path(guest_root); - if host_sync_root_is_filesystem_root(host_root) { - // A process tracked with host cwd "/" would pull the entire host - // filesystem into the kernel VFS (until the size/inode caps fire). - // No sanctioned flow shadows the host root wholesale; host access is - // scoped through mounts. - tracing::warn!("skipping host shadow sync rooted at the host filesystem root"); - return Ok(()); - } - let mut synced_file_times = BTreeMap::new(); - sync_host_directory_tree_to_kernel_inner( - vm, - &normalized_host_root, - &normalized_host_root, - &normalized_guest_root, - &mut synced_file_times, - None, - None, - ) -} - -fn sync_host_directory_tree_to_kernel_inner( - vm: &mut VmState, - host_root: &Path, - current_host_dir: &Path, - guest_root: &str, - synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>, - expected_inventory: Option<&BTreeMap>, - failed_replacements: Option<&BTreeSet>, -) -> Result<(), SidecarError> { - let entries = match fs::read_dir(current_host_dir) { - Ok(entries) => entries, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { - // Host dirs the sidecar user cannot read (e.g. root-owned - // /lost+found under a host-root mount) are skipped rather than - // failing the whole shadow sync; the guest just won't see them. - tracing::warn!( - path = %current_host_dir.display(), - "skipping unreadable host shadow directory" - ); - return Ok(()); - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow directory {}: {error}", - current_host_dir.display() - ))); - } - }; - - for entry in entries { - let entry = entry.map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow entry in {}: {error}", - current_host_dir.display() - )) - })?; - let host_path = entry.path(); - let file_type = entry.file_type().map_err(|error| { - SidecarError::Io(format!( - "failed to stat host shadow entry {}: {error}", - host_path.display() - )) - })?; - let relative_path = host_path - .strip_prefix(host_root) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to relativize host shadow path {} against {}: {error}", - host_path.display(), - host_root.display() - )) - })? - .to_string_lossy() - .replace('\\', "/"); - let guest_path = if guest_root == "/" { - normalize_path(&format!("/{relative_path}")) - } else { - normalize_path(&format!( - "{}/{}", - guest_root.trim_end_matches('/'), - relative_path - )) - }; - - if should_skip_shadow_sync_path(vm, &guest_path) { - continue; - } - if expected_inventory.is_some_and(|inventory| !inventory.contains_key(&guest_path)) { - // The entry appeared after the inventory pass. Pick it up on the - // next sync rather than mixing two different shadow snapshots. - continue; - } - if failed_replacements.is_some_and(|failed| failed.contains(&guest_path)) { - // Never write through a stale object whose removal failed. The - // retained tombstone will retry before a future additive write. - continue; - } - - if file_type.is_dir() { - ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Directory)?; - let metadata = entry.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow metadata {}: {error}", - host_path.display() - )) - })?; - if !is_shadow_bootstrap_dir(&guest_path) { - if !vm.kernel.exists(&guest_path).unwrap_or(false) { - vm.kernel.mkdir(&guest_path, true).map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow directory {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - } - vm.kernel - .chmod(&guest_path, host_shadow_mode(&metadata)) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow directory mode {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - } - sync_host_directory_tree_to_kernel_inner( - vm, - host_root, - &host_path, - guest_root, - synced_file_times, - expected_inventory, - failed_replacements, - )?; - continue; - } - - if file_type.is_file() { - ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::File)?; - let metadata = entry.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow metadata {}: {error}", - host_path.display() - )) - })?; - let timestamp_key = (metadata.dev(), metadata.ino()); - let (atime_ms, mtime_ms) = - *synced_file_times.entry(timestamp_key).or_insert_with(|| { - ( - metadata_time_ms(metadata.atime(), metadata.atime_nsec()), - metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()), - ) - }); - let desired_mode = host_shadow_mode(&metadata); - // Fast path: skip the expensive re-read + re-write when the kernel already - // holds a copy of this shadow file that matches on size, mode, and mtime. - // - // Every read-side fs op (exists/stat/readFile/...) triggers a full - // shadow-tree reconciliation walk. Without this skip the walk re-reads every - // file's bytes from the host and re-writes them into the kernel VFS on every - // op -- O(whole tree) per op, and super-linear as the VM's shadow grows, - // which is a dominant source of session-creation/runtime latency on - // populated VMs. - // - // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses - // by default. It needs no separate cache to invalidate -- it compares against - // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host - // change that moves size/mode/mtime forces a resync. Limitation: mtime is - // compared at the millisecond granularity the kernel stores (utimes truncates - // to ms), so a host-side rewrite that preserves byte length AND mode AND lands - // in the same wall-clock millisecond can be skipped and leave stale bytes. - // That window is sub-millisecond same-length edits; if it ever matters here, - // upgrade this to a content digest (or full-precision mtime) for files whose - // mtime is within the last few ms of `now`. - if let Ok(existing) = vm.kernel.lstat(&guest_path) { - if !existing.is_directory - && !existing.is_symbolic_link - && existing.size == metadata.len() - && (existing.mode & 0o7777) == (desired_mode & 0o7777) - && existing.mtime_ms == mtime_ms - { - continue; - } - } - let bytes = match read_host_shadow_file(&host_path, desired_mode) { - Ok(bytes) => bytes, - // The host entry vanished between the walk and the read - // (short-lived files churn constantly — editor swap files, - // temp files). Skipping matches native semantics; failing - // here would poison EVERY subsequent fs op on the VM. - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - // Same tolerance for entries the sidecar user cannot read - // (root-owned files under a host-root mount): skip them - // rather than poisoning the whole sync. - Err(error) - if error.kind() == std::io::ErrorKind::PermissionDenied - || error.raw_os_error() == Some(libc::EPERM) => - { - tracing::warn!( - path = %host_path.display(), - "skipping unreadable host shadow file" - ); - continue; - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow file {}: {error}", - host_path.display() - ))); - } - }; - match vm.kernel.write_file(&guest_path, bytes) { - Ok(()) => {} - // ENOENT here means the guest-side path cannot currently - // receive the write (e.g. it is a symlink whose target was - // just unlinked by the guest — vim's swap-file dance). The - // entry is mid-churn; skip it rather than failing the VM. - Err(error) if error.code() == "ENOENT" => continue, - Err(error) => { - return Err(SidecarError::InvalidState(format!( - "failed to sync host shadow file {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - ))); - } - } - vm.kernel - .chmod(&guest_path, desired_mode) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow file mode {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - vm.kernel - .utimes(&guest_path, atime_ms, mtime_ms) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow file times {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - continue; - } - - if file_type.is_symlink() { - ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Symlink)?; - let target = match fs::read_link(&host_path) { - Ok(target) => target, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow symlink {}: {error}", - host_path.display() - ))); - } - }; - replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?; - } - } - - Ok(()) -} - -fn ensure_kernel_shadow_node_type( - vm: &mut VmState, - guest_path: &str, - desired: ShadowNodeType, -) -> Result<(), SidecarError> { - let existing = match vm.kernel.lstat(guest_path) { - Ok(existing) => existing, - Err(error) if error.code() == "ENOENT" => return Ok(()), - Err(error) => return Err(kernel_error(error)), - }; - let existing_type = if existing.is_symbolic_link { - ShadowNodeType::Symlink - } else if existing.is_directory { - ShadowNodeType::Directory - } else { - ShadowNodeType::File - }; - if existing_type == desired { - return Ok(()); - } - - let result = if existing_type == ShadowNodeType::Directory { - vm.kernel.remove_dir(guest_path) - } else { - // `remove_file` unlinks the directory entry itself. It must run before - // `write_file`, otherwise that write would follow a stale symlink. - vm.kernel.remove_file(guest_path) - }; - result.map_err(|error| { - SidecarError::InvalidState(format!( - "failed to replace shadow path {guest_path} from {existing_type:?} to {desired:?}: {}", - kernel_error(error) - )) - }) -} - -fn replace_kernel_symlink( - vm: &mut VmState, - guest_path: &str, - target: &str, -) -> Result<(), SidecarError> { - if vm.kernel.symlink(target, guest_path).is_ok() { - return Ok(()); - } - - if let Ok(existing_target) = vm.kernel.read_link(guest_path) { - if existing_target == target { - return Ok(()); - } - } - - let _ = vm.kernel.remove_file(guest_path); - let _ = vm.kernel.remove_dir(guest_path); - vm.kernel - .symlink(target, guest_path) - .map_err(kernel_error)?; - Ok(()) -} - -fn host_shadow_mode(metadata: &fs::Metadata) -> u32 { - metadata.permissions().mode() & 0o7777 -} - -/// Reads a shadow-root file back into the kernel even when guest-visible mode -/// bits make it unreadable for the host user. The sidecar is the kernel for -/// this tree, so guest permission bits (for example a 0o200 write-only file -/// produced by `chmod` plus a shell append redirect) must not break the -/// exit-time shadow sync. The original mode is restored after the read. -fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result> { - match fs::read(host_path) { - Ok(bytes) => Ok(bytes), - Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { - fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?; - let result = fs::read(host_path); - fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?; - result - } - Err(error) => Err(error), - } -} - -fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 { - let seconds = seconds.max(0) as u64; - let nanos = nanos.max(0) as u64; - seconds - .saturating_mul(1_000) - .saturating_add(nanos / 1_000_000) -} - -fn is_shadow_bootstrap_dir(path: &str) -> bool { - matches!( - path, - "/dev" - | "/proc" - | "/tmp" - | "/bin" - | "/lib" - | "/sbin" - | "/boot" - | "/etc" - | "/root" - | "/run" - | "/srv" - | "/sys" - | "/opt" - | "/mnt" - | "/media" - | "/home" - | "/home/agentos" - | "/usr" - | "/usr/bin" - | "/usr/games" - | "/usr/include" - | "/usr/lib" - | "/usr/libexec" - | "/usr/man" - | "/usr/local" - | "/usr/local/bin" - | "/usr/sbin" - | "/usr/share" - | "/usr/share/man" - | "/var" - | "/var/cache" - | "/var/empty" - | "/var/lib" - | "/var/lock" - | "/var/log" - | "/var/run" - | "/var/spool" - | "/var/tmp" - | "/etc/agentos" - | "/workspace" - ) -} - -#[cfg(test)] -mod shadow_sync_tests { - use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir}; - - #[test] - fn shadow_bootstrap_sync_skips_virtual_home_tree() { - assert!(is_shadow_bootstrap_dir("/home")); - assert!(is_shadow_bootstrap_dir("/home/agentos")); - } - - #[test] - fn protected_agentos_paths_are_not_shadow_synced() { - assert!(is_protected_agentos_shadow_sync_path("/etc/agentos")); - assert!(is_protected_agentos_shadow_sync_path( - "/etc/agentos/instructions.md" - )); - assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy")); - assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md")); - } -} - -fn is_kernel_owned_shadow_sync_path(path: &str) -> bool { - matches!(path, "/dev" | "/proc" | "/sys") - || path.starts_with("/dev/") - || path.starts_with("/proc/") - || path.starts_with("/sys/") -} - -pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool { - path == "/etc/agentos" || path.starts_with("/etc/agentos/") -} - -fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool { - is_kernel_owned_shadow_sync_path(guest_path) - || is_protected_agentos_shadow_sync_path(guest_path) - // Every configured mount is kernel-owned at and below its normalized - // guest path. Shadow files are stale compatibility artifacts there; - // syncing them would overwrite memory/plugin state (or fail on a - // read-only mount) and deleting them must not unmount guest data. - || vm.configuration.mounts.iter().any(|mount| { - normalize_path(&mount.guest_path) != "/" - && guest_path_is_at_or_below(guest_path, &mount.guest_path) - }) -} - -fn guest_path_is_at_or_below(path: &str, prefix: &str) -> bool { - let path = normalize_path(path); - let prefix = normalize_path(prefix); - prefix == "/" || path == prefix || path.starts_with(&format!("{prefix}/")) -} - -fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String { - if specifier.starts_with("file://") { - normalize_path(specifier.trim_start_matches("file://")) - } else if specifier.starts_with("file:") { - normalize_path(specifier.trim_start_matches("file:")) - } else if specifier.starts_with('/') { - normalize_path(specifier) - } else { - normalize_path(&format!("{cwd}/{specifier}")) - } -} - -fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option { - is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier)) -} - -pub(super) fn is_node_runtime_command(command: &str) -> bool { - matches!(command, "node" | "npm" | "npx") - || Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| matches!(name, "node" | "npm" | "npx")) -} - -fn python_command_base_name(command: &str) -> &str { - Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(command) -} - -/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are -/// served by the embedded Pyodide runtime, mirroring how `node` is served by the -/// embedded V8 runtime. -pub(super) fn is_python_runtime_command(command: &str) -> bool { - matches!( - python_command_base_name(command), - "python" | "python3" | "pip" | "pip3" - ) -} - -/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the -/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` / -/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen -/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control -/// env, which the runner consumes and never exposes in the guest `os.environ`. -pub(super) fn resolve_python_command_execution( - vm: &VmState, - command: &str, - args: &[String], - mut env: BTreeMap, - guest_cwd: String, - host_cwd: PathBuf, -) -> Result { - let base_name = python_command_base_name(command); - let is_pip = matches!(base_name, "pip" | "pip3"); - - let mut entrypoint = String::new(); - let mut argv: Vec = Vec::new(); - let mut module: Option = None; - let mut stdin_program = false; - let mut interactive = false; - let mut guest_entrypoint: Option = None; - - if is_pip { - module = Some(String::from("pip")); - argv.push(String::from("pip")); - argv.extend(args.iter().cloned()); - } else { - // Skip the value-less interpreter flags we can safely ignore so they do - // not get mistaken for a script path. - let mut idx = 0; - while let Some(flag) = args.get(idx) { - match flag.as_str() { - "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b" - | "-d" | "-x" => idx += 1, - _ => break, - } - } - let rest = &args[idx..]; - match rest.first().map(String::as_str) { - Some("-c") => { - entrypoint = rest.get(1).cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from("argument expected for the -c option")) - })?; - argv.push(String::from("-c")); - argv.extend(rest.iter().skip(2).cloned()); - } - Some("-m") => { - let name = rest.get(1).cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from("argument expected for the -m option")) - })?; - module = Some(name); - argv.push(String::from("-m")); - argv.extend(rest.iter().skip(2).cloned()); - } - Some("-") => { - stdin_program = true; - argv.push(String::from("-")); - argv.extend(rest.iter().skip(1).cloned()); - } - Some(spec) if !spec.starts_with('-') => { - let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec) - .unwrap_or_else(|| spec.to_string()); - entrypoint = resolved_guest.clone(); - env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone()); - guest_entrypoint = Some(resolved_guest); - argv.push(spec.to_string()); - argv.extend(rest.iter().skip(1).cloned()); - } - Some(other) => { - return Err(SidecarError::InvalidState(format!( - "unsupported python option: {other}" - ))); - } - None => { - interactive = true; - argv.push(String::new()); - } - } - } - - env.insert( - String::from("AGENTOS_PYTHON_ARGV"), - serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")), - ); - if let Some(module) = &module { - env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone()); - } - if stdin_program { - env.insert( - String::from("AGENTOS_PYTHON_STDIN_PROGRAM"), - String::from("1"), - ); - } - if interactive { - env.insert( - String::from("AGENTOS_PYTHON_INTERACTIVE"), - String::from("1"), - ); - } - - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; Ok(ResolvedChildProcessExecution { command: String::from(PYTHON_COMMAND), @@ -1675,6 +922,7 @@ pub(super) fn resolve_python_command_execution( host_cwd, wasm_permission_tier: None, binding_command: false, + adapter_policy: ExecutionAdapterPolicy::DIRECT_PYTHON_RUNTIME, }) } @@ -2033,7 +1281,7 @@ pub(super) fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> S pub(super) fn rewrite_javascript_shebang_request( vm: &mut VmState, resolved: &ResolvedChildProcessExecution, - request: &mut JavascriptChildProcessSpawnRequest, + request: &mut ProcessLaunchRequest, ) -> Result { const MAX_SHEBANG_LINE_BYTES: usize = 256; @@ -2048,16 +1296,17 @@ pub(super) fn rewrite_javascript_shebang_request( else { return Ok(false); }; - let is_registered_command = vm - .command_guest_paths - .values() - .any(|path| normalize_path(path) == script_path); + let is_registered_command = registered_command_name_for_path(&vm.kernel, &resolved.command) + .is_some() + || (!is_path_like_specifier(&resolved.command) + && vm.kernel.commands().contains_key(&resolved.command)); if !is_registered_command { let stat = vm.kernel.stat(&script_path).map_err(kernel_error)?; if stat.is_directory || stat.mode & 0o111 == 0 { - return Err(SidecarError::Execution(format!( - "EACCES: permission denied, execute '{script_path}'" - ))); + return Err(SidecarError::host( + "EACCES", + format!("permission denied, execute '{script_path}'"), + )); } } let header = vm @@ -2088,15 +1337,17 @@ fn parse_javascript_shebang( let line_end = match header.iter().position(|byte| *byte == b'\n') { Some(index) if index > MAX_SHEBANG_LINE_BYTES => { - return Err(SidecarError::Execution(format!( - "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}"), + )); } Some(index) => index, None if header.len() > MAX_SHEBANG_LINE_BYTES => { - return Err(SidecarError::Execution(format!( - "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}"), + )); } None => header.len(), }; @@ -2104,7 +1355,7 @@ fn parse_javascript_shebang( .strip_suffix(b"\r") .unwrap_or(&header[2..line_end]); let text = std::str::from_utf8(line).map_err(|_| { - SidecarError::Execution(format!("ENOEXEC: invalid shebang line: {script_path}")) + SidecarError::host("ENOEXEC", format!("invalid shebang line: {script_path}")) })?; let text = text.trim_start_matches(|ch: char| ch.is_ascii_whitespace()); let (interpreter, optional_arg) = text @@ -2123,29 +1374,33 @@ fn parse_javascript_shebang( }) .unwrap_or((text, None)); if interpreter.is_empty() { - return Err(SidecarError::Execution(format!( - "ENOEXEC: invalid shebang line: {script_path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("invalid shebang line: {script_path}"), + )); } let (command, mut interpreter_args) = if matches!(interpreter, "/usr/bin/env" | "/bin/env") { let optional_arg = optional_arg.ok_or_else(|| { - SidecarError::Execution(format!( - "ENOENT: missing interpreter after {interpreter} in shebang: {script_path}" - )) + SidecarError::host( + "ENOENT", + format!("missing interpreter after {interpreter} in shebang: {script_path}"), + ) })?; if let Some(split_string) = optional_arg .strip_prefix("-S") .filter(|rest| rest.starts_with(|ch: char| ch.is_ascii_whitespace())) { let mut words = shlex::split(split_string.trim()).ok_or_else(|| { - SidecarError::Execution(format!( - "ENOEXEC: invalid /usr/bin/env -S quoting in shebang: {script_path}" - )) + SidecarError::host( + "ENOEXEC", + format!("invalid /usr/bin/env -S quoting in shebang: {script_path}"), + ) })?; if words.is_empty() { - return Err(SidecarError::Execution(format!( - "ENOENT: missing interpreter after /usr/bin/env -S in shebang: {script_path}" - ))); + return Err(SidecarError::host( + "ENOENT", + format!("missing interpreter after /usr/bin/env -S in shebang: {script_path}"), + )); } let command = words.remove(0); (command, words) @@ -2153,9 +1408,10 @@ fn parse_javascript_shebang( if optional_arg.starts_with('-') || optional_arg.chars().any(|ch| ch.is_ascii_whitespace()) { - return Err(SidecarError::Execution(format!( - "ENOEXEC: /usr/bin/env shebang arguments require -S: {script_path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("/usr/bin/env shebang arguments require -S: {script_path}"), + )); } (optional_arg.to_owned(), Vec::new()) } @@ -2254,19 +1510,17 @@ mod javascript_shebang_tests { } pub(super) fn resolve_guest_command_entrypoint( - vm: &VmState, + vm: &mut VmState, guest_cwd: &str, command: &str, path_env: Option<&str>, ) -> Option { if !is_path_like_specifier(command) { - if let Some(entrypoint) = vm.command_guest_paths.get(command) { - return Some(entrypoint.clone()); - } - for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) { let candidate = normalize_path(&format!("{search_dir}/{command}")); - if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) { + if let Some(entrypoint) = + resolve_guest_command_path_candidate(&mut vm.kernel, &candidate) + { return Some(entrypoint); } } @@ -2275,25 +1529,11 @@ pub(super) fn resolve_guest_command_entrypoint( } let normalized = resolve_path_like_guest_specifier(guest_cwd, command); - resolve_guest_command_path_candidate(vm, &normalized).or_else(|| { - // Some guest shells materialize PATH lookups into absolute candidate paths. - // If that path points into a searched directory but does not exist, fall - // back to the command basename so the sidecar can remap VM command packages. - let parent_dir = Path::new(&normalized).parent()?.to_str()?; - if !guest_command_search_dirs(vm, guest_cwd, path_env) - .iter() - .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir)) - { - return None; - } - - let file_name = Path::new(&normalized).file_name()?.to_str()?; - vm.command_guest_paths.get(file_name).cloned() - }) + resolve_guest_command_path_candidate(&mut vm.kernel, &normalized) } pub(super) fn resolve_exact_guest_command_entrypoint( - vm: &VmState, + vm: &mut VmState, guest_cwd: &str, command: &str, ) -> Option { @@ -2302,31 +1542,13 @@ pub(super) fn resolve_exact_guest_command_entrypoint( } let normalized = resolve_path_like_guest_specifier(guest_cwd, command); - if let Some(name) = registered_command_name_for_path(vm, &normalized) { - return vm.command_guest_paths.get(&name).cloned(); - } - if vm - .kernel - .exists(&normalized) - .ok() - .is_some_and(|exists| exists) - { - // execve follows the final symlink. Returning the real path also lets - // projected package commands select their actual JS/WASM entrypoint. - return vm - .kernel - .realpath(&normalized) - .ok() - .map(|path| normalize_path(&path)) - .or(Some(normalized)); - } - - resolve_vm_guest_path_to_host(vm, &normalized) - .is_file() - .then_some(normalized) + resolve_guest_command_path_candidate(&mut vm.kernel, &normalized) } -pub(super) fn registered_command_name_for_path(vm: &VmState, path: &str) -> Option { +pub(super) fn registered_command_name_for_path( + kernel: &SidecarKernel, + path: &str, +) -> Option { let normalized = normalize_path(path); let name = ["/bin/", "/usr/bin/", "/usr/local/bin/", "/opt/agentos/bin/"] .into_iter() @@ -2336,7 +1558,7 @@ pub(super) fn registered_command_name_for_path(vm: &VmState, path: &str) -> Opti .strip_prefix("/__secure_exec/commands/") .and_then(|suffix| suffix.rsplit('/').next()) })?; - (!name.is_empty() && !name.contains('/') && vm.kernel.commands().contains_key(name)) + (!name.is_empty() && !name.contains('/') && kernel.commands().contains_key(name)) .then(|| name.to_owned()) } @@ -2360,29 +1582,31 @@ fn parse_linux_shebang(header: &[u8], path: &str) -> Result .iter() .rposition(|byte| !matches!(*byte, b' ' | b'\t')) .map(|index| index + 1) - .ok_or_else(|| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + .ok_or_else(|| SidecarError::host("ENOEXEC", format!("invalid shebang line: {path}")))?; let line = &line[..line_end]; let interpreter_start = line .iter() .position(|byte| !matches!(*byte, b' ' | b'\t')) - .ok_or_else(|| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + .ok_or_else(|| SidecarError::host("ENOEXEC", format!("invalid shebang line: {path}")))?; let interpreter_tail = &line[interpreter_start..]; let separator = interpreter_tail .iter() .position(|byte| matches!(*byte, b' ' | b'\t')); if newline.is_none() && header.len() >= LINUX_BINPRM_BUF_SIZE && separator.is_none() { - return Err(SidecarError::Kernel(format!( - "ENOEXEC: shebang interpreter path exceeds the Linux header limit: {path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("shebang interpreter path exceeds the Linux header limit: {path}"), + )); } let interpreter_end = separator.unwrap_or(interpreter_tail.len()); let interpreter = std::str::from_utf8(&interpreter_tail[..interpreter_end]) - .map_err(|_| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + .map_err(|_| SidecarError::host("ENOEXEC", format!("invalid shebang line: {path}")))?; if interpreter.is_empty() { - return Err(SidecarError::Kernel(format!( - "ENOEXEC: invalid shebang line: {path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("invalid shebang line: {path}"), + )); } let optional_argument = separator .map(|index| &interpreter_tail[index..]) @@ -2402,7 +1626,7 @@ fn parse_linux_shebang(header: &[u8], path: &str) -> Result .map(|value| { std::str::from_utf8(value) .map(str::to_owned) - .map_err(|_| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}"))) + .map_err(|_| SidecarError::host("ENOEXEC", format!("invalid shebang line: {path}"))) }) .transpose()?; @@ -2417,10 +1641,7 @@ struct SpawnPathCandidate { script_argument: String, } -fn spawn_request_guest_cwd( - parent_guest_cwd: &str, - request: &JavascriptChildProcessSpawnRequest, -) -> String { +fn spawn_request_guest_cwd(parent_guest_cwd: &str, request: &ProcessLaunchRequest) -> String { request .options .cwd @@ -2449,9 +1670,10 @@ fn resolve_posix_spawn_path_candidate( search_path: &str, ) -> Result { if command.is_empty() { - return Err(SidecarError::Kernel(String::from( - "ENOENT: posix_spawnp command is empty", - ))); + return Err(SidecarError::host( + "ENOENT", + "posix_spawnp command is empty", + )); } let mut permission_error = None; @@ -2484,9 +1706,10 @@ fn resolve_posix_spawn_path_candidate( if let Some(error) = permission_error { Err(kernel_error(error)) } else { - Err(SidecarError::Kernel(format!( - "ENOENT: posix_spawnp command not found in PATH: {command}" - ))) + Err(SidecarError::host( + "ENOENT", + format!("posix_spawnp command not found in PATH: {command}"), + )) } } @@ -2496,7 +1719,7 @@ fn resolve_posix_spawn_path_candidate( pub(super) fn resolve_posix_spawn_program( vm: &mut VmState, parent_guest_cwd: &str, - request: &mut JavascriptChildProcessSpawnRequest, + request: &mut ProcessLaunchRequest, ) -> Result<(), SidecarError> { if request.options.spawn_exact_path { return resolve_spawn_shebang(vm, parent_guest_cwd, request, None); @@ -2533,7 +1756,7 @@ pub(super) fn resolve_posix_spawn_program( fn resolve_spawn_shebang( vm: &mut VmState, parent_guest_cwd: &str, - request: &mut JavascriptChildProcessSpawnRequest, + request: &mut ProcessLaunchRequest, mut initial_script_argument: Option, ) -> Result<(), SidecarError> { let guest_cwd = spawn_request_guest_cwd(parent_guest_cwd, request); @@ -2547,7 +1770,7 @@ fn resolve_spawn_shebang( .kernel .validate_executable_path(&request.command, &guest_cwd) .map_err(kernel_error)?; - if registered_command_name_for_path(vm, &resolved_path).is_some() { + if registered_command_name_for_path(&vm.kernel, &resolved_path).is_some() { return Ok(()); } @@ -2559,14 +1782,16 @@ fn resolve_spawn_shebang( return Ok(()); } let Some(shebang) = parse_linux_shebang(&header, &resolved_path)? else { - return Err(SidecarError::Kernel(format!( - "ENOEXEC: exec format error: {resolved_path}" - ))); + return Err(SidecarError::host( + "ENOEXEC", + format!("exec format error: {resolved_path}"), + )); }; if interpreter_depth >= LINUX_MAX_INTERPRETER_DEPTH { - return Err(SidecarError::Kernel(format!( - "ELOOP: interpreter recursion for {resolved_path} exceeds the Linux limit" - ))); + return Err(SidecarError::host( + "ELOOP", + format!("interpreter recursion for {resolved_path} exceeds the Linux limit"), + )); } interpreter_depth += 1; @@ -2621,9 +1846,10 @@ pub(super) fn validate_exact_exec_image_format( if valid { Ok(()) } else { - Err(SidecarError::InvalidState(format!( - "ENOEXEC: exec format error: {path}" - ))) + Err(SidecarError::host( + "ENOEXEC", + format!("exec format error: {path}"), + )) } } @@ -2668,41 +1894,229 @@ fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&st search_dirs } -fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option { - if candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) { - if let Ok(realpath) = vm.kernel.realpath(candidate) { - return Some(normalize_path(&realpath)); +fn resolve_guest_command_path_candidate( + kernel: &mut SidecarKernel, + candidate: &str, +) -> Option { + let normalized = normalize_path(candidate); + + // Standard command directories and `/opt/agentos/bin` contain + // kernel-created driver shims. Preserve their command identity, but select + // the executable image from the live package projection/legacy command + // mount rather than a configuration-time name -> path cache. + let registered_shim_name = ["/bin/", "/usr/bin/", "/usr/local/bin/", "/opt/agentos/bin/"] + .into_iter() + .find_map(|prefix| normalized.strip_prefix(prefix)) + .filter(|name| !name.is_empty() && !name.contains('/')) + .filter(|name| kernel.commands().contains_key(*name)) + .map(ToOwned::to_owned); + if let Some(name) = registered_shim_name { + if let Some(entrypoint) = resolve_live_registered_command_entrypoint(kernel, &name) { + return Some(entrypoint); } } - if candidate.starts_with("/bin/") - || candidate.starts_with("/usr/bin/") - || candidate.starts_with("/usr/local/bin/") - || candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) - || candidate.starts_with("/__secure_exec/commands/") - { - if let Some(file_name) = Path::new(candidate) - .file_name() - .and_then(|name| name.to_str()) - { - if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) { - return Some(guest_entrypoint.clone()); - } + resolve_live_kernel_file(kernel, &normalized) +} + +fn resolve_live_registered_command_entrypoint( + kernel: &mut SidecarKernel, + command: &str, +) -> Option { + // Match the existing command-discovery precedence: ordered legacy roots + // win, followed by the `/opt/agentos/bin` package projection. + let mut roots = kernel + .read_dir("/__secure_exec/commands") + .unwrap_or_default() + .into_iter() + .filter(|entry| !entry.is_empty() && entry.chars().all(|ch| ch.is_ascii_digit())) + .collect::>(); + roots.sort(); + for root in roots { + let candidate = normalize_path(&format!("/__secure_exec/commands/{root}/{command}")); + if let Some(entrypoint) = resolve_live_kernel_file(kernel, &candidate) { + return Some(entrypoint); } } - if vm - .kernel - .exists(candidate) - .ok() - .is_some_and(|exists| exists) - { - return Some(normalize_path(candidate)); + let projected = normalize_path(&format!( + "{}/{command}", + crate::package_projection::OPT_AGENTOS_BIN + )); + resolve_live_kernel_file(kernel, &projected) +} + +fn resolve_live_kernel_file(kernel: &SidecarKernel, candidate: &str) -> Option { + let canonical = normalize_path(&kernel.realpath(candidate).ok()?); + let stat = kernel.lstat(&canonical).ok()?; + (!stat.is_directory && !stat.is_symbolic_link).then_some(canonical) +} + +#[cfg(test)] +mod live_kernel_command_resolution_tests { + use super::*; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::KernelVmConfig; + use agentos_kernel::mount_table::{MountOptions, MountTable}; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::{MemoryFileSystem, VirtualFileSystem}; + + fn test_kernel(commands: impl IntoIterator) -> SidecarKernel { + let mut config = KernelVmConfig::new("vm-live-command-resolution"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, commands)) + .expect("register command-resolution test driver"); + kernel + } + + #[test] + fn registered_command_resolution_observes_late_mounts() { + let mut kernel = test_kernel(["late"]); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/late").as_deref(), + Some("/bin/late"), + "the live kernel driver shim remains the fallback before a package is mounted" + ); + + let mut package = MemoryFileSystem::new(); + package + .write_file("/bin/late", b"#!/usr/bin/env node\n".to_vec()) + .expect("seed late-mounted command"); + kernel + .mount_filesystem( + "/opt/agentos", + package, + MountOptions::new("late-command-test"), + ) + .expect("mount command package after kernel configuration"); + + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/late").as_deref(), + Some("/opt/agentos/bin/late") + ); + + kernel + .mkdir("/__secure_exec/commands/001", true) + .expect("create late legacy command root"); + kernel + .write_file( + "/__secure_exec/commands/001/late", + b"#!/usr/bin/env node\n".to_vec(), + ) + .expect("write late legacy command"); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/late").as_deref(), + Some("/__secure_exec/commands/001/late"), + "live legacy roots retain their established precedence" + ); + kernel + .remove_file("/__secure_exec/commands/001/late") + .expect("delete late legacy command"); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/late").as_deref(), + Some("/opt/agentos/bin/late"), + "deleting the preferred live entrypoint must reveal the live package projection" + ); + } + + #[test] + fn registered_command_resolution_does_not_reuse_deleted_backing_paths() { + let mut kernel = test_kernel(["legacy"]); + kernel + .mkdir("/__secure_exec/commands/001", true) + .expect("create legacy command root"); + kernel + .write_file( + "/__secure_exec/commands/001/legacy", + b"#!/usr/bin/env node\n".to_vec(), + ) + .expect("write legacy command"); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/legacy").as_deref(), + Some("/__secure_exec/commands/001/legacy") + ); + + kernel + .remove_file("/__secure_exec/commands/001/legacy") + .expect("delete legacy backing command"); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, "/bin/legacy").as_deref(), + Some("/bin/legacy"), + "deletion must expose only the live driver shim, never the stale backing image" + ); + } + + #[test] + fn javascript_classification_observes_live_symlink_targets_and_updates() { + let mut kernel = test_kernel([]); + kernel + .mkdir("/commands", true) + .expect("create command directory"); + kernel + .write_file("/commands/tool", b"\0asm\x01\0\0\0".to_vec()) + .expect("write initial WebAssembly command"); + kernel + .symlink("/commands/tool", "/tool") + .expect("create command symlink"); + + assert_eq!( + resolve_javascript_command_entrypoint_inner( + &mut kernel, + "/tool", + &[], + MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, + ), + None + ); + + kernel + .write_file( + "/commands/tool", + b"#!/usr/bin/env node\nconsole.log('updated');\n".to_vec(), + ) + .expect("replace command content after configuration"); + assert_eq!( + resolve_javascript_command_entrypoint_inner( + &mut kernel, + "/tool", + &[], + MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, + ) + .as_deref(), + Some("/commands/tool") + ); + + kernel + .remove_file("/commands/tool") + .expect("delete command target"); + assert_eq!( + resolve_javascript_command_entrypoint_inner( + &mut kernel, + "/tool", + &[], + MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, + ), + None + ); } - resolve_vm_guest_path_to_host(vm, candidate) - .is_file() - .then(|| normalize_path(candidate)) + #[test] + fn host_files_do_not_resurrect_kernel_command_misses() { + let mut kernel = test_kernel([]); + let host = tempfile::NamedTempFile::new().expect("create stale host launch asset"); + std::fs::write(host.path(), b"#!/usr/bin/env node\n") + .expect("seed stale host launch asset"); + let host_path = host.path().to_string_lossy(); + + assert!(host.path().is_file()); + assert_eq!( + resolve_guest_command_path_candidate(&mut kernel, &host_path), + None, + "host existence must not satisfy a kernel command lookup" + ); + } } fn resolve_host_entrypoint_within_vm_host_cwd( @@ -2740,29 +2154,19 @@ pub(super) fn prepare_guest_runtime_env( vm: &VmState, env: &mut BTreeMap, guest_cwd: &str, - host_cwd: &Path, + _host_cwd: &Path, guest_entrypoint: Option, ) -> Result<(), SidecarError> { let user = vm.kernel.user_profile(); let path_mappings = runtime_guest_path_mappings(vm); let read_paths = expand_host_access_paths( - std::iter::once(vm.cwd.clone()) - .chain( - path_mappings - .iter() - .map(|mapping| PathBuf::from(&mapping.host_path)), - ) - .chain(std::iter::once(host_cwd.to_path_buf())) - .collect::>() - .as_slice(), - ); - let write_paths = dedupe_host_paths( - std::iter::once(vm.cwd.clone()) - .chain(std::iter::once(host_cwd.to_path_buf())) - .chain(runtime_guest_writable_host_paths(vm)) + path_mappings + .iter() + .map(|mapping| PathBuf::from(&mapping.host_path)) .collect::>() .as_slice(), ); + let write_paths = dedupe_host_paths(&runtime_guest_writable_host_paths(vm)); let allowed_node_builtins = configured_allowed_node_builtins(vm); let loopback_exempt_ports = configured_loopback_exempt_ports(vm); @@ -2773,7 +2177,11 @@ pub(super) fn prepare_guest_runtime_env( })?, ); env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV)) - .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned()); + .or_insert_with(|| { + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned() + }); env.insert( String::from("AGENTOS_EXTRA_FS_READ_PATHS"), serde_json::to_string( @@ -2898,8 +2306,13 @@ pub(super) fn guest_runtime_identity( ) -> GuestRuntimeConfig { let user = vm.kernel.user_profile(); let resource_limits = vm.kernel.resource_limits(); - let mut identity = - shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid); + let mut identity = shared_guest_runtime_identity_with_system( + &user, + resource_limits, + vm.kernel.system_identity(), + virtual_pid, + virtual_ppid, + ); if let Some(pid) = virtual_pid.and_then(|pid| u32::try_from(pid).ok()) { if let Ok(process_identity) = vm.kernel.process_identity(EXECUTION_DRIVER_NAME, pid) { identity.virtual_uid = u64::from(process_identity.uid); @@ -2966,14 +2379,16 @@ pub(super) fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits { } } -/// Build the typed per-execution WebAssembly limits from the per-VM kernel -/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip; +/// Build the typed per-execution WebAssembly limits from normalized per-VM +/// limits and the kernel-owned resource caps. Replaces the old env round-trip; /// notably this is the path that finally enforces the stack cap that the /// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed. pub(super) fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits { let resource_limits = vm.kernel.resource_limits(); WasmExecutionLimits { - max_fuel: resource_limits.max_wasm_fuel, + active_cpu_time_limit_ms: Some(vm.limits.wasm.active_cpu_time_limit_ms), + wall_clock_limit_ms: vm.limits.wasm.wall_clock_limit_ms, + deterministic_fuel: vm.limits.wasm.deterministic_fuel, max_memory_bytes: resource_limits.max_wasm_memory_bytes, max_stack_bytes: resource_limits .max_wasm_stack_bytes @@ -2986,9 +2401,11 @@ pub(super) fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits { max_blocking_read_ms: resource_limits.max_blocking_read_ms, prewarm_timeout_ms: Some(vm.limits.wasm.prewarm_timeout_ms), runner_heap_limit_mb: Some(vm.limits.wasm.runner_heap_limit_mb), - runner_cpu_time_limit_ms: Some(vm.limits.wasm.runner_cpu_time_limit_ms), reactor_work_quantum: vm_reactor_work_quantum(&vm.limits), bridge_call_timeout_ms: Some(bridge_call_timeout_ms(&vm.limits)), + max_sync_rpc_response_line_bytes: Some(vm.limits.reactor.max_bridge_response_bytes as u64), + pending_event_count: Some(vm.limits.process.pending_event_count), + pending_event_bytes: Some(vm.limits.process.pending_event_bytes), } } @@ -3174,26 +2591,6 @@ fn runtime_guest_path_mappings(vm: &VmState) -> Vec { .flatten() }) .collect::>(); - let mut command_root_mappings = vm - .command_guest_paths - .values() - .filter_map(|guest_path| { - Path::new(guest_path) - .parent() - .and_then(|parent| parent.to_str()) - .map(normalize_path) - }) - .collect::>() - .into_iter() - .map(|guest_path| RuntimeGuestPathMapping { - host_path: resolve_vm_guest_path_to_host(vm, &guest_path) - .to_string_lossy() - .into_owned(), - guest_path, - read_only: false, - }) - .collect::>(); - mappings.append(&mut command_root_mappings); let mut extra_node_modules_roots = mappings .iter() .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/")) @@ -3208,11 +2605,6 @@ fn runtime_guest_path_mappings(vm: &VmState) -> Vec { }) .collect::>(); mappings.append(&mut extra_node_modules_roots); - mappings.push(RuntimeGuestPathMapping { - guest_path: String::from("/"), - host_path: vm.cwd.to_string_lossy().into_owned(), - read_only: false, - }); mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len())); mappings.dedup_by(|left, right| { left.guest_path == right.guest_path && left.host_path == right.host_path @@ -3220,138 +2612,6 @@ fn runtime_guest_path_mappings(vm: &VmState) -> Vec { mappings } -/// Build a `Send`-able, read-only VFS module reader over the VM's read-only -/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root -/// for nested mounts). When present, the V8 bridge thread resolves modules -/// inline against this reader — concurrently with the service loop — so a large -/// cold-start module graph never serializes behind / starves an in-flight ACP -/// `session/new` bootstrap on the single service-loop thread. The reader reads -/// the same mounted tree the guest sees (anchored resolve-beneath, escaping-symlink -/// refusal), never the host-direct path translator. Returns `None` when the VM -/// has no usable read-only mount, so resolution falls back to the service-loop -/// kernel reader. -pub(super) fn build_module_reader( - vm: &VmState, - resolved: &ResolvedChildProcessExecution, -) -> Option { - let mut pairs: Vec<(String, PathBuf)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.read_only) - .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) - .filter_map(|mount| { - mount_config_host_path(&mount.plugin.config) - .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path))) - }) - .collect(); - - // Packed package-version leaves: module resolution reads packed - // `node_modules` content straight from the `.aospkg` mount index (shared - // mmap cache; no kernel access), mirroring what the guest sees through the - // kernel tar mount. `(guest_path, aospkg_path, tar_root)` triples. - let mut package_tars: Vec<(String, String, String)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .filter_map(|mount| { - let config = serde_json::from_str::(&mount.plugin.config).ok()?; - if config.get("kind").and_then(Value::as_str) != Some("tar") { - return None; - } - let tar_path = config.get("tarPath").and_then(Value::as_str)?.to_owned(); - let root = config - .get("root") - .and_then(Value::as_str) - .unwrap_or("/") - .to_owned(); - Some((normalize_path(&mount.guest_path), tar_path, root)) - }) - .collect(); - // `/current -> ` symlink leaves: alias the current prefix to - // the same tar so modules that self-locate through `current` (rather than - // the realpathed version dir) still resolve. - let current_aliases: Vec<(String, String, String)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .filter_map(|mount| { - let config = serde_json::from_str::(&mount.plugin.config).ok()?; - if config.get("kind").and_then(Value::as_str) != Some("singleSymlink") { - return None; - } - let link_path = normalize_path(&mount.guest_path); - let target = config.get("target").and_then(Value::as_str)?; - let resolved_target = if target.starts_with('/') { - normalize_path(target) - } else { - let parent = Path::new(&link_path).parent()?.to_str()?; - normalize_path(&format!("{parent}/{target}")) - }; - package_tars - .iter() - .find(|(guest, _, _)| *guest == resolved_target) - .map(|(_, tar_path, root)| (link_path, tar_path.clone(), root.clone())) - }) - .collect(); - package_tars.extend(current_aliases); - - let guest_entrypoint = resolved - .env - .get("AGENTOS_GUEST_ENTRYPOINT") - .map(|path| normalize_path(path)); - if let Some(guest_entrypoint) = guest_entrypoint.as_deref() { - // Package entrypoints may still carry their pre-realpath launch path - // (`/opt/agentos/bin/` or `/current/...` symlink leaves), so - // gate on EVERY agentos_packages mount prefix, not just the tar leaves. - let package_mount_prefixes: Vec = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .map(|mount| normalize_path(&mount.guest_path)) - .collect(); - let entrypoint_in_read_only_mount = pairs - .iter() - .map(|(guest_path, _)| guest_path) - .chain(package_mount_prefixes.iter()) - .any(|guest_path| { - guest_entrypoint == guest_path - || guest_entrypoint.starts_with(&format!("{guest_path}/")) - }); - if !entrypoint_in_read_only_mount { - return None; - } - } - - // Mirror runtime_guest_path_mappings: a mount nested under - // `/root/node_modules/` implies a `/root/node_modules` root the resolver - // walks, so expose that root too (e.g. software-package mounts). - let extra_roots: Vec<(String, PathBuf)> = pairs - .iter() - .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/")) - .filter_map(|(_, host_path)| { - host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root)) - }) - .collect(); - pairs.extend(extra_roots); - - if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { - eprintln!( - "module-reader: entrypoint={:?} host_pairs={} package_tars={:?}", - resolved.env.get("AGENTOS_GUEST_ENTRYPOINT"), - pairs.len(), - package_tars - .iter() - .map(|(guest, _, _)| guest.as_str()) - .collect::>() - ); - } - crate::plugins::host_dir::HostDirModuleReader::from_mounts_and_package_tars(pairs, package_tars) -} - fn host_node_modules_root(path: &Path) -> Option { if let Some(root) = path .ancestors() @@ -3452,10 +2712,10 @@ mod runtime_guest_path_mapping_tests { #[cfg(test)] mod kernel_poll_sync_rpc_tests { use super::{ - parse_kernel_poll_args, parse_kernel_stdin_read_args, - service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveExecutionEvent, - ActiveProcess, BindingExecution, JavascriptSyncRpcRequest, KernelPollFdResponse, - SidecarKernel, EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND, + install_kernel_stdin_pipe, parse_kernel_poll_args, parse_kernel_stdin_read_args, + rollback_failed_top_level_process_start, service_javascript_kernel_poll_sync_rpc, + ActiveExecution, ActiveExecutionEvent, ActiveProcess, BindingExecution, HostRpcRequest, + KernelPollFdResponse, SidecarKernel, EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND, }; use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; @@ -3466,6 +2726,7 @@ mod kernel_poll_sync_rpc_tests { use serde_json::{json, Value}; use std::collections::HashMap; use std::future::Future; + use std::sync::atomic::Ordering; use std::sync::Arc; use std::task::{Context, Poll, Waker}; use tokio::sync::Notify; @@ -3478,7 +2739,7 @@ mod kernel_poll_sync_rpc_tests { #[test] fn explicit_null_kernel_wait_timeouts_mean_indefinite_readiness_waits() { - let stdin_request = JavascriptSyncRpcRequest { + let stdin_request = HostRpcRequest { id: 1, method: String::from("__kernel_stdin_read"), raw_bytes_args: HashMap::new(), @@ -3489,7 +2750,7 @@ mod kernel_poll_sync_rpc_tests { (4096, None) ); - let poll_request = JavascriptSyncRpcRequest { + let poll_request = HostRpcRequest { id: 2, method: String::from("__kernel_poll"), raw_bytes_args: HashMap::new(), @@ -3553,7 +2814,7 @@ mod kernel_poll_sync_rpc_tests { let response = service_javascript_kernel_poll_sync_rpc( &mut kernel, &process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: 1, method: String::from("__kernel_poll"), raw_bytes_args: HashMap::new(), @@ -3637,8 +2898,101 @@ mod kernel_poll_sync_rpc_tests { Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == b"echo" )); - process.kernel_handle.finish(0); - kernel.waitpid(pid).expect("wait javascript kernel process"); + process.kernel_handle.finish(0); + kernel.waitpid(pid).expect("wait javascript kernel process"); + } + + #[test] + fn top_level_setup_failure_reaps_process_pty_and_descriptors() { + let mut config = KernelVmConfig::new("vm-top-level-setup-rollback"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new( + EXECUTION_DRIVER_NAME, + [JAVASCRIPT_COMMAND], + )) + .expect("register execution driver"); + let baseline = kernel.resource_snapshot(); + let kernel_handle = kernel + .spawn_process( + JAVASCRIPT_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn startup process"); + let pid = kernel_handle.pid(); + let notify = Arc::new(Notify::new()); + let _runtime_control = + ActiveProcess::attach_runtime_control_before_start(&kernel_handle, notify) + .expect("attach startup endpoint"); + let (master_fd, slave_fd, _) = kernel + .open_pty(EXECUTION_DRIVER_NAME, pid) + .expect("allocate startup PTY"); + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, slave_fd, 0) + .expect("install startup PTY stdin"); + assert_ne!(kernel.resource_snapshot(), baseline); + assert!(kernel.list_processes().contains_key(&pid)); + + rollback_failed_top_level_process_start( + &mut kernel, + &kernel_handle, + None, + "test setup failure", + ); + + assert!(kernel.list_processes().is_empty()); + assert_eq!(kernel.resource_snapshot(), baseline); + let _ = master_fd; + } + + #[test] + fn top_level_engine_start_failure_reaps_process_and_stdin_pipe() { + let mut config = KernelVmConfig::new("vm-top-level-engine-start-rollback"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new( + EXECUTION_DRIVER_NAME, + [JAVASCRIPT_COMMAND], + )) + .expect("register execution driver"); + let baseline = kernel.resource_snapshot(); + let kernel_handle = kernel + .spawn_process( + JAVASCRIPT_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn startup process"); + let pid = kernel_handle.pid(); + let notify = Arc::new(Notify::new()); + let _runtime_control = + ActiveProcess::attach_runtime_control_before_start(&kernel_handle, notify) + .expect("attach startup endpoint"); + install_kernel_stdin_pipe(&mut kernel, pid).expect("install startup stdin pipe"); + let binding = BindingExecution::default(); + let cancelled = Arc::clone(&binding.cancelled); + let mut execution = ActiveExecution::Binding(binding); + assert_ne!(kernel.resource_snapshot(), baseline); + + rollback_failed_top_level_process_start( + &mut kernel, + &kernel_handle, + Some(&mut execution), + "test engine-start failure", + ); + + assert!(cancelled.load(Ordering::Acquire)); + assert!(kernel.list_processes().is_empty()); + assert_eq!(kernel.resource_snapshot(), baseline); } } @@ -3704,20 +3058,14 @@ fn expand_host_access_paths(paths: &[PathBuf]) -> Vec { expanded } -/// Package content is tar-mounted guest-native and never materialized on the -/// host, so command resolution classifies package-mount entrypoints by -/// extension only (`resolve_javascript_command_entrypoint`) and the resolved -/// host path for a WebAssembly module may not exist. Correct both here, where -/// the kernel is available: sniff the real entrypoint's magic through the -/// kernel VFS, flip misclassified extensionless WebAssembly binaries from -/// JavaScript to WebAssembly, and stage the module bytes into the VM shadow -/// tree so the wasm engine (which loads modules from a host path) can read -/// them. Staging is per-VM and write-once per resolved version path — package -/// versions are immutable — and only commands that actually execute are -/// materialized; filesystem reads stay on the zero-extraction tar mount. +/// Classify a package command from the authoritative kernel VFS. Resolution +/// follows symlinks with the launch authority and reads only the four-byte WASM +/// magic prefix. The sole full-module read remains +/// `stage_kernel_wasm_launch_asset`, where `maxModuleFileBytes` is enforced. pub(super) fn stage_agentos_package_command( vm: &mut VmState, resolved: &mut ResolvedChildProcessExecution, + authority: WasmLaunchAuthority, ) -> Result<(), SidecarError> { const WASM_MAGIC: &[u8] = b"\0asm"; if resolved.binding_command @@ -3739,75 +3087,188 @@ pub(super) fn stage_agentos_package_command( if !guest_path_is_within_agentos_package_mount(vm, &guest_entrypoint) { return Ok(()); } - let Ok(real_entrypoint) = vm.kernel.realpath(&guest_entrypoint) else { + // `node script.mjs` reads the script as interpreter input; it does not + // execute the script pathname. Classifying that input through the runtime + // image loader would incorrectly require an execute bit (and would make a + // normal 0644 JavaScript module fail with EACCES). Bare/path command + // launches still pass through the executable-image classifier below so a + // projected command containing WASM is selected without weakening Linux + // exec permission checks. + if resolved.runtime == GuestRuntimeKind::JavaScript + && is_node_runtime_command(&resolved.command) + { return Ok(()); - }; - let real_entrypoint = normalize_path(&real_entrypoint); - let Ok(magic) = vm.kernel.pread_file(&real_entrypoint, 0, WASM_MAGIC.len()) else { + } + let prefix = match authority { + WasmLaunchAuthority::TrustedInitialImage => vm + .kernel + .load_trusted_initial_runtime_image_prefix(&guest_entrypoint, WASM_MAGIC.len()), + WasmLaunchAuthority::GuestProcessImage { requester_pid } => { + vm.kernel.load_process_runtime_image_prefix( + EXECUTION_DRIVER_NAME, + requester_pid, + &guest_entrypoint, + WASM_MAGIC.len(), + ) + } + } + .map_err(kernel_error)?; + let real_entrypoint = normalize_path(&prefix.canonical_path); + if !guest_path_is_within_agentos_package_mount(vm, &real_entrypoint) { + return Err(SidecarError::host( + "EACCES", + format!( + "AgentOS package command resolved outside its package mount: {guest_entrypoint} -> {real_entrypoint}" + ), + )); + } + if prefix.bytes != WASM_MAGIC { return Ok(()); - }; - if magic != WASM_MAGIC { + } + resolved.runtime = GuestRuntimeKind::WebAssembly; + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum WasmLaunchAuthority { + /// A module selected directly by the trusted client Execute request may be + /// admitted once from that request's host source path. + TrustedInitialImage, + /// A guest spawn/exec replacement must already exist in the kernel VFS and + /// remains subject to the guest process's execute DAC checks. + GuestProcessImage { requester_pid: u32 }, +} + +/// Stage a WebAssembly module from the authoritative kernel filesystem into +/// the VM-private launch-asset tree consumed by the compatibility engine. +pub(super) fn stage_kernel_wasm_launch_asset( + vm: &mut VmState, + resolved: &mut ResolvedChildProcessExecution, + authority: WasmLaunchAuthority, +) -> Result<(), SidecarError> { + if resolved.binding_command || resolved.runtime != GuestRuntimeKind::WebAssembly { return Ok(()); } - let shadow_path = shadow_path_for_guest(vm, &real_entrypoint); - if !shadow_path.is_file() { - let bytes = vm + let Some(guest_entrypoint) = resolved + .env + .get("AGENTOS_GUEST_ENTRYPOINT") + .filter(|path| path.starts_with('/')) + .map(|path| normalize_path(path)) + else { + return Ok(()); + }; + let maximum_bytes = vm.limits.wasm.max_module_file_bytes; + let image = match authority { + WasmLaunchAuthority::TrustedInitialImage => vm .kernel - .read_file(&real_entrypoint) - .map_err(kernel_error)?; - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!("failed to create wasm shadow parent: {error}")) - })?; + .load_trusted_initial_runtime_image(&guest_entrypoint, maximum_bytes), + WasmLaunchAuthority::GuestProcessImage { requester_pid } => { + vm.kernel.load_process_runtime_image( + EXECUTION_DRIVER_NAME, + requester_pid, + &guest_entrypoint, + maximum_bytes, + ) } - fs::write(&shadow_path, &bytes).map_err(|error| { + } + .map_err(kernel_error)?; + let real_entrypoint = normalize_path(&image.canonical_path); + let asset_path = runtime_asset_path_for_guest(vm, &real_entrypoint); + if let Some(parent) = asset_path.parent() { + fs::create_dir_all(parent).map_err(|error| { SidecarError::Io(format!( - "failed to stage wasm module {}: {error}", - shadow_path.display() + "failed to create runtime launch-asset parent: {error}" )) })?; } - resolved.runtime = GuestRuntimeKind::WebAssembly; - resolved.entrypoint = shadow_path.to_string_lossy().into_owned(); + fs::write(&asset_path, image.bytes).map_err(|error| { + SidecarError::Io(format!("failed to stage runtime launch image: {error}")) + })?; + fs::set_permissions(&asset_path, fs::Permissions::from_mode(image.mode & 0o7777)).map_err( + |error| { + SidecarError::Io(format!( + "failed to set runtime launch image mode on {}: {error}", + asset_path.display() + )) + }, + )?; + resolved.entrypoint = asset_path.to_string_lossy().into_owned(); Ok(()) } -pub(super) fn enforce_resolved_wasm_execute_dac( +async fn admit_trusted_initial_wasm_source_if_missing( vm: &mut VmState, - parent_kernel_pid: u32, resolved: &ResolvedChildProcessExecution, ) -> Result<(), SidecarError> { if resolved.binding_command || resolved.runtime != GuestRuntimeKind::WebAssembly { return Ok(()); } - let Some(guest_entrypoint) = resolved.env.get("AGENTOS_GUEST_ENTRYPOINT") else { + let Some(guest_entrypoint) = resolved + .env + .get("AGENTOS_GUEST_ENTRYPOINT") + .filter(|path| path.starts_with('/')) + .map(|path| normalize_path(path)) + else { return Ok(()); }; - let guest_entrypoint = normalize_path(guest_entrypoint); - if vm - .command_guest_paths - .values() - .any(|path| normalize_path(path) == guest_entrypoint) + match vm + .kernel + .load_trusted_initial_runtime_image(&guest_entrypoint, vm.limits.wasm.max_module_file_bytes) { - return Ok(()); + Ok(_) => return Ok(()), + Err(error) + if error.code() == "ENOENT" + && !resolved_entrypoint_uses_kernel_launch_asset( + vm, + resolved, + &guest_entrypoint, + ) => {} + Err(error) => return Err(kernel_error(error)), } + + // A low-level Execute request may name a trusted caller-supplied host + // module while selecting a guest cwd such as `/`. Open and read that exact + // source on the fixed blocking executor, then admit it once. The opened + // handle pins the selected inode across metadata validation and the + // bounded read; after admission no guest operation consults the host path. + let host_entrypoint = { + let candidate = Path::new(&resolved.entrypoint); + if candidate.is_absolute() { + candidate.to_path_buf() + } else { + resolved.host_cwd.join(candidate) + } + }; + let source = read_bounded_host_launch_source_async( + vm, + host_entrypoint, + vm.limits.wasm.max_module_file_bytes, + ) + .await?; vm.kernel - .check_execute_for_process(EXECUTION_DRIVER_NAME, parent_kernel_pid, &guest_entrypoint) + .admit_trusted_initial_runtime_image( + &guest_entrypoint, + source.bytes, + source.mode, + vm.limits.wasm.max_module_file_bytes, + ) .map_err(kernel_error) } -pub(super) fn prepare_javascript_shadow( +pub(super) fn prepare_javascript_launch_assets( vm: &mut VmState, resolved: &ResolvedChildProcessExecution, env: &BTreeMap, + authority: WasmLaunchAuthority, + prepared_source: Option<&str>, ) -> Result<(), SidecarError> { let guest_entrypoint = env .get("AGENTOS_GUEST_ENTRYPOINT") .cloned() // An absolute `entrypoint` may be a host path that lives inside the VM's // host cwd (callers can pass a fully-qualified host path). The guest sees - // it at its translated guest path (host_cwd -> guest_cwd), so the shadow - // must be keyed by that guest path rather than the raw host path. Falling + // it at its translated guest path (host_cwd -> guest_cwd), so the private + // launch asset must be keyed by that guest path rather than the raw host path. Falling // back to the host path here would materialize the file at the wrong guest // location and the runtime's `require()` would fail with "Cannot find // module". @@ -3824,10 +3285,19 @@ pub(super) fn prepare_javascript_shadow( let Some(guest_entrypoint) = guest_entrypoint else { return Ok(()); }; - if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() { - return Ok(()); - } - if vm.kernel.lstat(&guest_entrypoint).is_err() { + let initial_stat = match authority { + WasmLaunchAuthority::TrustedInitialImage => vm.kernel.lstat(&guest_entrypoint), + WasmLaunchAuthority::GuestProcessImage { requester_pid } => { + vm.kernel + .lstat_for_process(EXECUTION_DRIVER_NAME, requester_pid, &guest_entrypoint) + } + }; + if matches!( + initial_stat.as_ref().err().map(|error| error.code()), + Some("ENOENT" | "ENOTDIR") + ) && authority == WasmLaunchAuthority::TrustedInitialImage + && !resolved_entrypoint_uses_kernel_launch_asset(vm, resolved, &guest_entrypoint) + { let host_entrypoint = { let candidate = Path::new(&resolved.entrypoint); if candidate.is_absolute() { @@ -3836,40 +3306,64 @@ pub(super) fn prepare_javascript_shadow( resolved.host_cwd.join(candidate) } }; - if host_entrypoint.exists() { - materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?; - // The shadow write only stages the file on the host side; the runtime - // resolves modules against the kernel VFS, so the staged entrypoint - // must be synced into the kernel before execution starts (otherwise - // `require()` reports "Cannot find module"). - return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint); + match fs::metadata(&host_entrypoint) { + Ok(_) => { + import_host_entrypoint_to_kernel(vm, &guest_entrypoint, &host_entrypoint, None)?; + return materialize_guest_launch_asset( + vm, + &guest_entrypoint, + authority, + prepared_source, + ); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect trusted JavaScript entrypoint {}: {error}", + host_entrypoint.display() + ))); + } } } - materialize_guest_path_to_shadow(vm, &guest_entrypoint) + initial_stat.map_err(kernel_error)?; + materialize_guest_launch_asset(vm, &guest_entrypoint, authority, prepared_source) } pub(super) fn resolve_agentos_package_javascript_launch_entrypoint( vm: &mut VmState, + requester_pid: u32, env: &mut BTreeMap, -) -> Option { - let guest_entrypoint = env +) -> Result, SidecarError> { + let Some(guest_entrypoint) = env .get("AGENTOS_GUEST_ENTRYPOINT") .filter(|path| path.starts_with('/')) - .map(|path| normalize_path(path))?; + .map(|path| normalize_path(path)) + else { + return Ok(None); + }; if !guest_path_is_within_agentos_package_mount(vm, &guest_entrypoint) { - return None; + return Ok(None); } - let real_entrypoint = normalize_path(&vm.kernel.realpath(&guest_entrypoint).ok()?); + let real_entrypoint = normalize_path( + &vm.kernel + .realpath_for_process(EXECUTION_DRIVER_NAME, requester_pid, &guest_entrypoint) + .map_err(kernel_error)?, + ); if !guest_path_is_within_agentos_package_mount(vm, &real_entrypoint) { - return None; + return Err(SidecarError::host( + "EACCES", + format!( + "AgentOS package JavaScript entrypoint resolved outside its package mount: {guest_entrypoint} -> {real_entrypoint}" + ), + )); } env.insert( String::from("AGENTOS_GUEST_ENTRYPOINT"), real_entrypoint.clone(), ); - if guest_javascript_entrypoint_uses_module_mode(vm, &real_entrypoint) { + if guest_javascript_entrypoint_uses_module_mode(vm, requester_pid, &real_entrypoint)? { env.insert( String::from("AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE"), String::from("1"), @@ -3877,7 +3371,7 @@ pub(super) fn resolve_agentos_package_javascript_launch_entrypoint( } else { env.remove("AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE"); } - Some(real_entrypoint) + Ok(Some(real_entrypoint)) } fn guest_path_is_within_agentos_package_mount(vm: &VmState, guest_path: &str) -> bool { @@ -3890,18 +3384,32 @@ fn guest_path_is_within_agentos_package_mount(vm: &VmState, guest_path: &str) -> }) } -fn guest_javascript_entrypoint_uses_module_mode(vm: &mut VmState, guest_path: &str) -> bool { +fn guest_javascript_entrypoint_uses_module_mode( + vm: &mut VmState, + requester_pid: u32, + guest_path: &str, +) -> Result { match Path::new(guest_path) .extension() .and_then(|ext| ext.to_str()) { - Some("mjs" | "mts") => true, - Some("js") => nearest_guest_package_json_type(vm, guest_path).as_deref() == Some("module"), - _ => false, + Some("mjs" | "mts") => Ok(true), + Some("js") => { + Ok( + nearest_guest_package_json_type(&mut vm.kernel, requester_pid, guest_path)? + .as_deref() + == Some("module"), + ) + } + _ => Ok(false), } } -fn nearest_guest_package_json_type(vm: &mut VmState, guest_path: &str) -> Option { +fn nearest_guest_package_json_type( + kernel: &mut SidecarKernel, + requester_pid: u32, + guest_path: &str, +) -> Result, SidecarError> { let mut dir = dirname(guest_path); loop { let package_json_path = if dir == "/" { @@ -3909,229 +3417,620 @@ fn nearest_guest_package_json_type(vm: &mut VmState, guest_path: &str) -> Option } else { normalize_path(&format!("{dir}/package.json")) }; - if let Ok(bytes) = vm.kernel.read_file(&package_json_path) { - if let Ok(value) = serde_json::from_slice::(&bytes) { - if let Some(package_type) = value.get("type").and_then(Value::as_str) { - return Some(package_type.to_owned()); - } + let bytes = match kernel.read_file_for_process( + EXECUTION_DRIVER_NAME, + requester_pid, + &package_json_path, + ) { + Ok(bytes) => Some(bytes), + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => None, + Err(error) => return Err(kernel_error(error)), + }; + if let Some(bytes) = bytes { + let contents = String::from_utf8(bytes).map_err(|error| { + SidecarError::host( + "EILSEQ", + format!( + "package configuration {package_json_path} is not valid UTF-8: {error}" + ), + ) + })?; + let value = serde_json::from_str::(&contents).map_err(|error| { + SidecarError::host( + "ERR_INVALID_PACKAGE_CONFIG", + format!("invalid package configuration {package_json_path}: {error}"), + ) + })?; + if let Some(package_type) = value.get("type").and_then(Value::as_str) { + return Ok(Some(package_type.to_owned())); } } if dir == "/" { - return None; + return Ok(None); } dir = dirname(&dir); } } -/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's -/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync -/// used by the broader shadow reconciliation, but scoped to the single -/// entrypoint we just materialized. -fn sync_shadow_entrypoint_into_kernel( - vm: &mut VmState, - guest_entrypoint: &str, -) -> Result<(), SidecarError> { - if vm.kernel.exists(guest_entrypoint).unwrap_or(false) { - return Ok(()); +#[cfg(test)] +mod package_json_launch_tests { + use super::*; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::resource_accounting::ResourceLimits; + use agentos_kernel::vfs::MemoryFileSystem; + + fn package_json_test_kernel(max_pread_bytes: usize) -> (SidecarKernel, u32) { + let mut config = KernelVmConfig::new("vm-package-json-launch"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_pread_bytes: Some(max_pread_bytes), + ..ResourceLimits::default() + }; + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new( + EXECUTION_DRIVER_NAME, + [JAVASCRIPT_COMMAND], + )) + .expect("register JavaScript test driver"); + let process = kernel + .spawn_process( + JAVASCRIPT_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn JavaScript test process"); + kernel + .mkdir("/pkg/sub", true) + .expect("create package test directory"); + (kernel, process.pid()) } - let shadow_path = shadow_path_for_guest(vm, guest_entrypoint); - let bytes = match fs::read(&shadow_path) { - Ok(bytes) => bytes, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read staged shadow entrypoint {}: {error}", - shadow_path.display() - ))); - } + + #[test] + fn package_json_type_read_accepts_exact_limit_and_rejects_plus_one() { + let exact = br#"{"type":"module"}"#; + let (mut kernel, pid) = package_json_test_kernel(exact.len()); + kernel + .write_file("/pkg/package.json", exact.to_vec()) + .expect("write exact package config"); + assert_eq!( + nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect("read exact package config") + .as_deref(), + Some("module") + ); + + let mut plus_one = exact.to_vec(); + plus_one.push(b' '); + kernel + .write_file("/pkg/package.json", plus_one) + .expect("write plus-one package config"); + let error = nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect_err("plus-one package config must exceed the bounded read"); + assert_eq!(error.code(), Some("EINVAL")); + assert!(error.to_string().contains("limits.resources.maxPreadBytes")); + } + + #[test] + fn package_json_type_read_preserves_typed_failures() { + let (mut kernel, pid) = package_json_test_kernel(128); + kernel + .write_file("/pkg/package.json", vec![0xff]) + .expect("write invalid UTF-8 package config"); + assert_eq!( + nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect_err("invalid UTF-8 must fail") + .code(), + Some("EILSEQ") + ); + + kernel + .write_file("/pkg/package.json", b"{".to_vec()) + .expect("write malformed package config"); + assert_eq!( + nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect_err("malformed JSON must fail") + .code(), + Some("ERR_INVALID_PACKAGE_CONFIG") + ); + + kernel + .write_file("/pkg/package.json", br#"{"type":"module"}"#.to_vec()) + .expect("restore package config"); + kernel + .chmod("/pkg/package.json", 0) + .expect("deny package config read"); + assert_eq!( + nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect_err("package config DAC failure must propagate") + .code(), + Some("EACCES") + ); + + kernel + .remove_file("/pkg/package.json") + .expect("remove package config"); + kernel + .symlink("/pkg/package-loop", "/pkg/package.json") + .expect("create first package config loop link"); + kernel + .symlink("/pkg/package.json", "/pkg/package-loop") + .expect("create second package config loop link"); + assert_eq!( + nearest_guest_package_json_type(&mut kernel, pid, "/pkg/sub/main.js") + .expect_err("package config symlink loop must propagate") + .code(), + Some("ELOOP") + ); + } +} + +/// Import a trusted caller-supplied host entrypoint once into the authoritative +/// kernel VFS. This is VM configuration/launch input, not a mutable host mount; +/// subsequent guest reads and writes never synchronize back to the host path. +#[derive(Debug)] +struct OpenHostLaunchSource { + file: fs::File, + path: PathBuf, + observed_bytes: u64, + mode: u32, +} + +#[derive(Debug)] +struct HostLaunchSource { + bytes: Vec, + mode: u32, +} + +fn host_launch_io_error(operation: &str, path: &Path, error: std::io::Error) -> SidecarError { + let code = match error.raw_os_error() { + Some(libc::EPERM) => "EPERM", + Some(libc::ENOENT) => "ENOENT", + Some(libc::EACCES) => "EACCES", + Some(libc::ENOTDIR) => "ENOTDIR", + Some(libc::EISDIR) => "EISDIR", + Some(libc::ELOOP) => "ELOOP", + _ => "EIO", }; - if let Some(parent) = guest_parent_path(guest_entrypoint) { - if !vm.kernel.exists(&parent).unwrap_or(false) { - vm.kernel.mkdir(&parent, true).map_err(kernel_error)?; - } + SidecarError::host( + code, + format!("{operation} host launch source {}: {error}", path.display()), + ) +} + +fn open_host_launch_source( + path: PathBuf, + maximum_bytes: u64, +) -> Result { + let file = fs::File::open(&path).map_err(|error| host_launch_io_error("open", &path, error))?; + let metadata = file + .metadata() + .map_err(|error| host_launch_io_error("stat", &path, error))?; + if !metadata.is_file() { + return Err(SidecarError::host( + "EINVAL", + format!( + "host launch source {} is not a regular file", + path.display() + ), + )); } - vm.kernel - .write_file(guest_entrypoint, bytes) - .map_err(kernel_error)?; - Ok(()) + admit_host_launch_source_bytes(&path, metadata.len(), Some(maximum_bytes))?; + Ok(OpenHostLaunchSource { + file, + path, + observed_bytes: metadata.len(), + mode: metadata.permissions().mode() & 0o7777, + }) } -fn guest_parent_path(guest_path: &str) -> Option { - let parent = Path::new(guest_path).parent()?; - let parent = parent.to_string_lossy(); - if parent.is_empty() || parent == "/" { - None - } else { - Some(parent.into_owned()) +fn host_launch_read_reservation(opened: &OpenHostLaunchSource) -> Result { + usize::try_from(opened.observed_bytes) + .ok() + .and_then(|observed| observed.checked_add(1)) + .ok_or_else(|| { + SidecarError::host( + "E2BIG", + format!( + "host launch source {} cannot fit in the host address space", + opened.path.display() + ), + ) + }) +} + +fn read_open_host_launch_source( + opened: OpenHostLaunchSource, + maximum_bytes: u64, +) -> Result { + let capacity = usize::try_from(opened.observed_bytes).map_err(|_| { + SidecarError::host( + "E2BIG", + format!( + "host launch source {} cannot fit in the host address space", + opened.path.display() + ), + ) + })?; + let mut bytes = Vec::with_capacity(capacity); + let mut bounded = opened.file.take(opened.observed_bytes.saturating_add(1)); + bounded + .read_to_end(&mut bytes) + .map_err(|error| host_launch_io_error("read", &opened.path, error))?; + if bytes.len() as u64 > opened.observed_bytes { + return Err(SidecarError::host( + "ESTALE", + format!( + "host launch source {} grew after admission metadata was captured; retry the launch", + opened.path.display() + ), + )); } + admit_host_launch_source_bytes(&opened.path, bytes.len() as u64, Some(maximum_bytes))?; + Ok(HostLaunchSource { + bytes, + mode: opened.mode, + }) } -fn materialize_host_path_to_shadow( +async fn read_bounded_host_launch_source_async( vm: &VmState, - guest_path: &str, - host_path: &Path, + path: PathBuf, + maximum_bytes: u64, +) -> Result { + let blocking = vm.runtime_context.blocking().clone(); + let path_reservation = path.to_string_lossy().len().saturating_add(1); + let opened = blocking + .run(path_reservation, move || { + open_host_launch_source(path, maximum_bytes) + }) + .await + .map_err(SidecarError::from)??; + let read_reservation = host_launch_read_reservation(&opened)?; + blocking + .run(read_reservation, move || { + read_open_host_launch_source(opened, maximum_bytes) + }) + .await + .map_err(SidecarError::from)? +} + +fn import_host_entrypoint_to_kernel( + vm: &mut VmState, + guest_entrypoint: &str, + host_entrypoint: &Path, + maximum_bytes: Option, ) -> Result<(), SidecarError> { - let shadow_path = shadow_path_for_guest(vm, guest_path); - let metadata = fs::symlink_metadata(host_path) - .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?; + // JavaScript guest-replacement paths still enter through synchronous + // compatibility RPCs. Keep their unavoidable host file I/O on the same + // fixed, bounded blocking executor; trusted initial WASM admission uses + // the async counterpart above and never blocks a Tokio worker. + let maximum_bytes = maximum_bytes.unwrap_or(vm.limits.wasm.max_module_file_bytes); + let blocking = vm.runtime_context.blocking().clone(); + let timeout = vm.runtime_context.blocking_job_timeout(); + let host_entrypoint = host_entrypoint.to_path_buf(); + let path_reservation = host_entrypoint.to_string_lossy().len().saturating_add(1); + let opened = blocking + .run_sync(path_reservation, timeout, move || { + open_host_launch_source(host_entrypoint, maximum_bytes) + }) + .map_err(SidecarError::from)??; + let read_reservation = host_launch_read_reservation(&opened)?; + let source = blocking + .run_sync(read_reservation, timeout, move || { + read_open_host_launch_source(opened, maximum_bytes) + }) + .map_err(SidecarError::from)??; + match vm.kernel.admit_trusted_initial_runtime_image( + guest_entrypoint, + source.bytes, + source.mode, + maximum_bytes, + ) { + Ok(()) => Ok(()), + // Kernel state wins if another trusted configuration path already + // admitted the same guest entrypoint. + Err(error) if error.code() == "EEXIST" => Ok(()), + Err(error) => Err(kernel_error(error)), + } +} - if metadata.file_type().is_symlink() { - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!("failed to create shadow symlink parent: {error}")) - })?; - } - let _ = fs::remove_file(&shadow_path); - let _ = fs::remove_dir_all(&shadow_path); - let target = fs::read_link(host_path) - .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?; - std::os::unix::fs::symlink(&target, &shadow_path) - .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?; +fn admit_host_launch_source_bytes( + host_entrypoint: &Path, + observed: u64, + maximum: Option, +) -> Result<(), SidecarError> { + let Some(maximum) = maximum else { return Ok(()); + }; + if observed > maximum { + return Err(SidecarError::Host( + HostServiceError::new( + "E2BIG", + format!( + "WASM launch source {} is {observed} bytes, exceeding limits.wasm.maxModuleFileBytes ({maximum})", + host_entrypoint.display(), + ), + ) + .with_details(json!({ + "limitName": "limits.wasm.maxModuleFileBytes", + "limit": maximum, + "requested": observed, + })), + )); + } + if observed >= maximum.saturating_mul(4) / 5 { + eprintln!( + "WARN_AGENTOS_WASM_LAUNCH_SOURCE_NEAR_LIMIT: path={} observed={} maximum={} limit=limits.wasm.maxModuleFileBytes", + host_entrypoint.display(), + observed, + maximum, + ); } + Ok(()) +} - if metadata.is_dir() { - fs::create_dir_all(&shadow_path).map_err(|error| { - SidecarError::Io(format!("failed to create shadow directory: {error}")) - })?; - fs::set_permissions( - &shadow_path, - fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777), - ) - .map_err(|error| { - SidecarError::Io(format!( - "failed to set shadow directory mode on {}: {error}", - shadow_path.display() - )) - })?; - return Ok(()); +#[cfg(test)] +mod bounded_host_launch_source_tests { + use super::{ + open_host_launch_source, read_open_host_launch_source, remove_existing_launch_asset, + }; + use std::fs::{self, OpenOptions}; + use std::io::Write; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_dir(label: &str) -> std::path::PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after Unix epoch") + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "agentos-native-sidecar-{label}-{}-{unique}", + std::process::id() + )); + fs::create_dir_all(&path).expect("create launch source test directory"); + path + } + + #[test] + fn opened_launch_source_pins_the_admitted_inode_across_path_replacement() { + let directory = temp_dir("launch-source-provenance"); + let source_path = directory.join("guest.wasm"); + let replacement_path = directory.join("replacement.wasm"); + fs::write(&source_path, b"original-image").expect("write original launch source"); + + let opened = + open_host_launch_source(source_path.clone(), 64).expect("open original launch source"); + fs::write(&replacement_path, b"replaced-image").expect("write replacement source"); + fs::rename(&replacement_path, &source_path).expect("replace launch source pathname"); + + let admitted = read_open_host_launch_source(opened, 64) + .expect("read the inode selected during admission"); + assert_eq!(admitted.bytes, b"original-image"); + assert_eq!( + fs::read(&source_path).expect("read replacement pathname"), + b"replaced-image" + ); + + fs::remove_dir_all(directory).expect("remove launch source test directory"); + } + + #[test] + fn launch_source_growth_is_typed_estale_and_oversize_is_rejected_before_read() { + let directory = temp_dir("launch-source-bounds"); + let source_path = directory.join("guest.wasm"); + fs::write(&source_path, b"abc").expect("write bounded launch source"); + let opened = + open_host_launch_source(source_path.clone(), 3).expect("open bounded launch source"); + OpenOptions::new() + .append(true) + .open(&source_path) + .expect("open launch source for growth") + .write_all(b"d") + .expect("grow launch source after metadata admission"); + let stale = read_open_host_launch_source(opened, 3) + .err() + .expect("growth after admission metadata must be rejected"); + assert_eq!(stale.code(), Some("ESTALE")); + + let oversize_path = directory.join("oversize.wasm"); + fs::write(&oversize_path, b"abcde").expect("write oversized launch source"); + let oversize = open_host_launch_source(oversize_path, 4) + .err() + .expect("oversized source must fail from handle metadata before reading"); + assert_eq!(oversize.code(), Some("E2BIG")); + + fs::remove_dir_all(directory).expect("remove launch source test directory"); } - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!("failed to create shadow parent: {error}")) - })?; + #[test] + fn replacing_stale_launch_asset_never_follows_its_final_symlink() { + let directory = temp_dir("launch-asset-replacement"); + let target = directory.join("target.js"); + let asset = directory.join("asset.js"); + fs::write(&target, b"preserve me").expect("write symlink target"); + std::os::unix::fs::symlink(&target, &asset).expect("create stale asset symlink"); + + remove_existing_launch_asset(&asset).expect("remove stale launch asset"); + assert!(fs::symlink_metadata(&asset).is_err()); + assert_eq!( + fs::read(&target).expect("read preserved target"), + b"preserve me" + ); + + fs::create_dir_all(asset.join("nested")).expect("create stale asset directory"); + fs::write(asset.join("nested/file"), b"stale").expect("write stale nested asset"); + remove_existing_launch_asset(&asset).expect("remove stale launch asset directory"); + assert!(!asset.exists()); + + fs::remove_dir_all(directory).expect("remove launch asset test directory"); } - let bytes = fs::read(host_path) - .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?; - fs::write(&shadow_path, bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror host file into shadow root: {error}" - )) - })?; - fs::set_permissions( - &shadow_path, - fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777), - ) - .map_err(|error| { - SidecarError::Io(format!( - "failed to set shadow file mode on {}: {error}", - shadow_path.display() - )) - })?; - Ok(()) } -fn materialize_guest_path_to_shadow( +fn materialize_guest_launch_asset( vm: &mut VmState, guest_path: &str, + authority: WasmLaunchAuthority, + prepared_source: Option<&str>, ) -> Result<(), SidecarError> { - let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?; - let shadow_path = shadow_path_for_guest(vm, guest_path); + let stat = match authority { + WasmLaunchAuthority::TrustedInitialImage => vm.kernel.lstat(guest_path), + WasmLaunchAuthority::GuestProcessImage { requester_pid } => { + vm.kernel + .lstat_for_process(EXECUTION_DRIVER_NAME, requester_pid, guest_path) + } + } + .map_err(kernel_error)?; + let asset_path = runtime_asset_path_for_guest(vm, guest_path); + remove_existing_launch_asset(&asset_path)?; if stat.is_symbolic_link { - if let Some(parent) = shadow_path.parent() { + if let Some(parent) = asset_path.parent() { fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!("failed to create shadow symlink parent: {error}")) + SidecarError::Io(format!( + "failed to create launch-asset symlink parent: {error}" + )) })?; } - let _ = fs::remove_file(&shadow_path); - let _ = fs::remove_dir_all(&shadow_path); - let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?; - std::os::unix::fs::symlink(&target, &shadow_path) - .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?; + let target = match authority { + WasmLaunchAuthority::TrustedInitialImage => vm.kernel.read_link(guest_path), + WasmLaunchAuthority::GuestProcessImage { requester_pid } => vm + .kernel + .read_link_for_process(EXECUTION_DRIVER_NAME, requester_pid, guest_path), + } + .map_err(kernel_error)?; + std::os::unix::fs::symlink(&target, &asset_path).map_err(|error| { + SidecarError::Io(format!("failed to stage launch symlink: {error}")) + })?; return Ok(()); } if stat.is_directory { - fs::create_dir_all(&shadow_path).map_err(|error| { - SidecarError::Io(format!("failed to create shadow directory: {error}")) + fs::create_dir_all(&asset_path).map_err(|error| { + SidecarError::Io(format!("failed to create launch-asset directory: {error}")) })?; - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( + fs::set_permissions(&asset_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( |error| { SidecarError::Io(format!( - "failed to set shadow directory mode on {}: {error}", - shadow_path.display() + "failed to set launch-asset directory mode on {}: {error}", + asset_path.display() )) }, )?; return Ok(()); } - if let Some(parent) = shadow_path.parent() { + if let Some(parent) = asset_path.parent() { fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!("failed to create shadow parent: {error}")) + SidecarError::Io(format!("failed to create launch-asset parent: {error}")) })?; } - let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?; - fs::write(&shadow_path, bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest file into shadow root: {error}" - )) + let owned_bytes; + let bytes = if let Some(source) = prepared_source { + source.as_bytes() + } else { + owned_bytes = match authority { + WasmLaunchAuthority::TrustedInitialImage => { + vm.kernel + .load_trusted_initial_runtime_image( + guest_path, + vm.limits.wasm.max_module_file_bytes, + ) + .map_err(kernel_error)? + .bytes + } + WasmLaunchAuthority::GuestProcessImage { requester_pid } => vm + .kernel + .read_file_for_process(EXECUTION_DRIVER_NAME, requester_pid, guest_path) + .map_err(kernel_error)?, + }; + owned_bytes.as_slice() + }; + fs::write(&asset_path, bytes).map_err(|error| { + SidecarError::Io(format!("failed to stage guest launch asset: {error}")) })?; - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( + fs::set_permissions(&asset_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( |error| { SidecarError::Io(format!( - "failed to set shadow file mode on {}: {error}", - shadow_path.display() + "failed to set launch-asset file mode on {}: {error}", + asset_path.display() )) }, )?; Ok(()) } +/// Remove an old projection without following its final symlink. A guest may +/// replace an entrypoint between launches (symlink -> file, file -> directory, +/// and so on); every new projection must replace the old inode before writing +/// so host `fs::write`/`create_dir_all` cannot follow stale projection state. +fn remove_existing_launch_asset(asset_path: &Path) -> Result<(), SidecarError> { + match fs::symlink_metadata(asset_path) { + Ok(metadata) if metadata.file_type().is_dir() => { + fs::remove_dir_all(asset_path).map_err(|error| { + SidecarError::Io(format!( + "failed to replace launch-asset directory {}: {error}", + asset_path.display() + )) + }) + } + Ok(_) => fs::remove_file(asset_path).map_err(|error| { + SidecarError::Io(format!( + "failed to replace launch-asset file {}: {error}", + asset_path.display() + )) + }), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(SidecarError::Io(format!( + "failed to inspect existing launch asset {}: {error}", + asset_path.display() + ))), + } +} + pub(super) fn load_javascript_entrypoint_source( vm: &mut VmState, - host_cwd: &Path, + kernel_pid: u32, + guest_cwd: &str, entrypoint: &str, env: &BTreeMap, -) -> Option { +) -> Result, SidecarError> { let mut read_guest_file = |path: &str| { - vm.kernel - .read_file(path) - .ok() - .and_then(|bytes| String::from_utf8(bytes).ok()) + let bytes = match vm + .kernel + .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) + { + Ok(bytes) => bytes, + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => return Ok(None), + Err(error) => return Err(kernel_error(error)), + }; + String::from_utf8(bytes).map(Some).map_err(|error| { + SidecarError::host( + "EILSEQ", + format!("JavaScript entrypoint {path} is not valid UTF-8: {error}"), + ) + }) }; - if let Some(source) = env + if let Some(path) = env .get("AGENTOS_GUEST_ENTRYPOINT") .filter(|path| path.starts_with('/')) - .and_then(|path| read_guest_file(path)) { - return Some(source); - } - - if entrypoint.starts_with('/') { - if let Some(source) = read_guest_file(entrypoint) { - return Some(source); + if let Some(source) = read_guest_file(path)? { + return Ok(Some(source)); } } - let host_entrypoint = if Path::new(entrypoint).is_absolute() { - PathBuf::from(entrypoint) - } else { - host_cwd.join(entrypoint) - }; - let normalized_entrypoint = normalize_host_path(&host_entrypoint); - let sandbox_root = normalize_host_path(&vm.cwd); - let host_cwd = normalize_host_path(&vm.host_cwd); - if !path_is_within_root(&normalized_entrypoint, &sandbox_root) - && !path_is_within_root(&normalized_entrypoint, &host_cwd) - { - return None; + if entrypoint.starts_with('/') { + return read_guest_file(entrypoint); } - - fs::read_to_string(&normalized_entrypoint).ok() + read_guest_file(&normalize_path(&format!("{guest_cwd}/{entrypoint}"))) } pub(super) fn python_file_entrypoint(entrypoint: &str) -> Option { @@ -4194,8 +4093,6 @@ pub(super) fn add_runtime_host_access_path( } } -// discover_command_guest_paths moved to crate::bootstrap - pub(super) fn is_path_like_specifier(specifier: &str) -> bool { specifier.starts_with('/') || specifier.starts_with("./") @@ -4203,32 +4100,24 @@ pub(super) fn is_path_like_specifier(specifier: &str) -> bool { || specifier.starts_with("file:") } -pub(super) fn execution_wasm_permission_tier( - tier: WasmPermissionTier, -) -> ExecutionWasmPermissionTier { +pub(super) fn kernel_process_permission_tier(tier: WasmPermissionTier) -> ProcessPermissionTier { match tier { - WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full, - WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite, - WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly, - WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated, + WasmPermissionTier::Full => ProcessPermissionTier::Full, + WasmPermissionTier::ReadWrite => ProcessPermissionTier::ReadWrite, + WasmPermissionTier::ReadOnly => ProcessPermissionTier::ReadOnly, + WasmPermissionTier::Isolated => ProcessPermissionTier::Isolated, } } -fn resolve_wasm_permission_tier( - vm: &VmState, - command_name: Option<&str>, - explicit_tier: Option, - entrypoint: &str, -) -> WasmPermissionTier { - explicit_tier - .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied())) - .or_else(|| { - Path::new(entrypoint) - .file_name() - .and_then(|name| name.to_str()) - .and_then(|command| vm.command_permissions.get(command).copied()) - }) - .unwrap_or(WasmPermissionTier::Full) +pub(super) fn execution_wasm_permission_tier( + tier: ProcessPermissionTier, +) -> ExecutionWasmPermissionTier { + match tier { + ProcessPermissionTier::Full => ExecutionWasmPermissionTier::Full, + ProcessPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite, + ProcessPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly, + ProcessPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated, + } } pub(super) fn tokenize_shell_free_command(command: &str) -> Vec { @@ -4448,101 +4337,6 @@ pub(crate) fn host_path_from_runtime_guest_mappings( None } -pub(super) fn guest_runtime_path_for_host_path( - runtime_env: &BTreeMap, - virtual_home: &str, - cwd: &Path, - host_path: &str, -) -> Option { - let resolved = if host_path.starts_with("file://") { - PathBuf::from(host_path.trim_start_matches("file://")) - } else if host_path.starts_with("file:") { - PathBuf::from(host_path.trim_start_matches("file:")) - } else { - let candidate = PathBuf::from(host_path); - if candidate.is_absolute() { - candidate - } else if host_path.starts_with("./") || host_path.starts_with("../") { - cwd.join(candidate) - } else { - return None; - } - }; - let normalized = normalize_host_path(&resolved); - - if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) { - return Some(path); - } - - let normalized_cwd = normalize_host_path(cwd); - if !path_is_within_root(&normalized, &normalized_cwd) { - return None; - } - - let virtual_home = if virtual_home.starts_with('/') { - virtual_home.to_string() - } else { - String::from("/root") - }; - let suffix = normalized - .strip_prefix(&normalized_cwd) - .ok()? - .to_string_lossy() - .replace('\\', "/") - .trim_start_matches('/') - .to_owned(); - - Some(if suffix.is_empty() { - virtual_home - } else { - normalize_path(&format!("{virtual_home}/{suffix}")) - }) -} - -fn guest_path_from_runtime_host_mappings( - runtime_env: &BTreeMap, - host_path: &Path, -) -> Option { - let mappings = runtime_env - .get("AGENTOS_GUEST_PATH_MAPPINGS") - .and_then(|value| serde_json::from_str::>(value).ok())?; - let normalized = normalize_host_path(host_path); - - let mut sorted_mappings = mappings - .into_iter() - .filter_map(|mapping| { - (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some(( - normalize_path(&mapping.guest_path), - normalize_host_path(Path::new(&mapping.host_path)), - )) - }) - .collect::>(); - sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len())); - - for (guest_root, host_root) in sorted_mappings { - if !path_is_within_root(&normalized, &host_root) { - continue; - } - let suffix = normalized - .strip_prefix(&host_root) - .ok()? - .to_string_lossy() - .replace('\\', "/") - .trim_start_matches('/') - .to_owned(); - - return Some(if suffix.is_empty() { - guest_root - } else if guest_root == "/" { - normalize_path(&format!("/{suffix}")) - } else { - normalize_path(&format!("{guest_root}/{suffix}")) - }); - } - - None -} - pub(super) fn host_mount_path_for_guest_path_from_mounts( mounts: &[crate::protocol::MountDescriptor], guest_path: &str, @@ -4638,7 +4432,7 @@ mod host_mount_path_for_guest_path_from_mounts_tests { } pub(super) fn resolve_guest_socket_host_path( - context: &JavascriptSocketPathContext, + context: &SocketPathContext, guest_path: &str, ) -> PathBuf { if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) { @@ -4654,7 +4448,8 @@ pub(super) fn resolve_guest_socket_host_path( host_path } -// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol +// ProcessLaunchOptions and ProcessLaunchRequest live in the runtime-neutral +// agentos-execution host contract. // ResolvedChildProcessExecution moved to crate::state pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env( @@ -4668,9 +4463,6 @@ pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env( "AGENTOS_VIRTUAL_PROCESS_UID", "AGENTOS_VIRTUAL_PROCESS_GID", "AGENTOS_VIRTUAL_PROCESS_VERSION", - "AGENTOS_WASM_INITIAL_SIGNAL_MASK", - "AGENTOS_WASM_INITIAL_SIGNAL_IGNORES", - "AGENTOS_WASM_INITIAL_PENDING_SIGNALS", ]; env.iter() @@ -4681,6 +4473,49 @@ pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env( .collect() } +fn rollback_failed_top_level_process_start( + kernel: &mut SidecarKernel, + kernel_handle: &agentos_kernel::kernel::KernelProcessHandle, + execution: Option<&mut ActiveExecution>, + context: &str, +) { + if let Some(execution) = execution { + if let Err(error) = execution.terminate() { + eprintln!( + "[agentos] failed to terminate rejected {context} runtime for PID {}: {error}", + kernel_handle.pid() + ); + } + } + kernel_handle.finish(127); + if let Err(error) = kernel.waitpid(kernel_handle.pid()) { + eprintln!( + "[agentos] failed to reap rejected {context} kernel PID {}: {error}", + kernel_handle.pid() + ); + } +} + +fn rollback_published_top_level_process_start(vm: &mut VmState, process_id: &str, context: &str) { + let Some(mut process) = vm.active_processes.remove(process_id) else { + eprintln!("[agentos] failed to find rejected {context} process {process_id} for rollback"); + return; + }; + let kernel_handle = process.kernel_handle.clone(); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + Some(&mut process.execution), + context, + ); +} + +enum StartedTopLevelAdapterContext { + Javascript(String), + Python(String), + WebAssembly(String), +} + // Network request types moved to crate::protocol // VmDnsConfig, DnsResolutionSource moved to crate::state @@ -4739,12 +4574,29 @@ where ) .map_err(kernel_error)?; let kernel_pid = kernel_handle.pid(); + let runtime_control = match ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + ) { + Ok(runtime_control) => runtime_control, + Err(error) => { + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + None, + "top-level binding runtime-control attachment", + ); + return Err(error); + } + }; let binding_execution = BindingExecution::with_event_notify( Arc::clone(&self.process_event_notify), process_event_capacity, ) .with_vm_pending_event_bytes_budget(Arc::clone(&vm_pending_event_bytes_budget)); let cancelled = binding_execution.cancelled.clone(); + let paused = Arc::clone(&binding_execution.paused); + let pause_notify = Arc::clone(&binding_execution.pause_notify); let pending_events = binding_execution.pending_events.clone(); let event_overflow_reason = binding_execution.event_overflow_reason.clone(); let pending_event_bytes = binding_execution.pending_event_bytes.clone(); @@ -4753,27 +4605,45 @@ where let binding_vm_pending_event_bytes_budget = binding_execution.vm_pending_event_bytes_budget.clone(); let event_notify = binding_execution.event_notify.clone(); - vm.active_processes.insert( - payload.process_id.clone(), - ActiveProcess::new( - kernel_pid, - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - process_event_capacity, - GuestRuntimeKind::JavaScript, - ActiveExecution::Binding(binding_execution), - ) - .with_event_notify(Arc::clone(&self.process_event_notify)) - .with_vm_pending_byte_budgets( - Arc::clone(&vm_pending_stdin_bytes_budget), - Arc::clone(&vm_pending_event_bytes_budget), - ) - .with_guest_cwd(guest_cwd.clone()) - .with_shadow_root(normalize_host_path(&vm.cwd)) - .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)), - ); - self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; + let host_cwd = runtime_launch_path_for_guest(vm, &guest_cwd); + let mut process = ActiveProcess::new_with_attached_runtime_control( + kernel_pid, + kernel_handle, + vm.runtime_context.clone(), + vm.limits.clone(), + process_event_capacity, + GuestRuntimeKind::JavaScript, + ActiveExecution::Binding(binding_execution), + runtime_control, + Arc::clone(&self.process_event_notify), + ) + .with_adapter_policy(ExecutionAdapterPolicy::BINDING) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_guest_cwd(guest_cwd.clone()) + .with_host_cwd(host_cwd); + if let Err(error) = process.apply_runtime_controls() { + let rollback_handle = process.kernel_handle.clone(); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &rollback_handle, + Some(&mut process.execution), + "top-level binding pending runtime control", + ); + return Err(error); + } + vm.active_processes + .insert(payload.process_id.clone(), process); + if let Err(error) = self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy) { + rollback_published_top_level_process_start( + vm, + &payload.process_id, + "top-level binding lifecycle publication", + ); + return Err(error); + } spawn_binding_process_events(BindingProcessEventRequest { runtime_context: vm.runtime_context.clone(), sidecar_requests: self.sidecar_requests.clone(), @@ -4782,6 +4652,8 @@ where vm_id: vm_id.clone(), binding_resolution, cancelled, + paused, + pause_notify, pending_events, event_overflow_reason, pending_event_bytes, @@ -4807,39 +4679,56 @@ where .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")); let phase_start = Instant::now(); let mut resolved = resolve_execute_request(vm, &payload)?; - stage_agentos_package_command(vm, &mut resolved)?; + stage_agentos_package_command(vm, &mut resolved, WasmLaunchAuthority::TrustedInitialImage)?; + admit_trusted_initial_wasm_source_if_missing(vm, &resolved).await?; + stage_kernel_wasm_launch_asset( + vm, + &mut resolved, + WasmLaunchAuthority::TrustedInitialImage, + )?; let resolved = resolved; record_execute_phase("resolve_execute_request", phase_start.elapsed()); let phase_start = Instant::now(); let mut env = resolved.env.clone(); env.remove(EXECUTION_REQUEST_TTY_ENV); - let sandbox_root = normalize_host_path(&vm.cwd); env.insert( String::from(EXECUTION_SANDBOX_ROOT_ENV), - sandbox_root.to_string_lossy().into_owned(), + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned(), ); - if resolved.runtime == GuestRuntimeKind::JavaScript { + if resolved.adapter_policy.forwards_kernel_stdin_rpc { env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - // A TTY guest-node process reads stdin through the kernel PTY: host - // input is written to the PTY master (write_kernel_process_stdin), - // line discipline runs (echo / VERASE / ICRNL / VEOF), and the - // sidecar drains the cooked bytes from the slave and forwards them - // to the isolate's stream-stdin dispatch - // (forward_tty_slave_input_to_javascript). The in-isolate - // `_kernelStdinRead` bridge stays local; no RPC forwarding is - // needed because the isolate never reads kernel fd 0 itself. - } else if resolved.runtime == GuestRuntimeKind::WebAssembly { + // Managed V8 reads fd 0 through the sidecar's kernel bridge. The + // execution crate keeps its local bridge only for standalone use. + env.insert( + String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"), + String::from("1"), + ); + } else if resolved.adapter_policy.encodes_inherited_fd_bootstrap { env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); } - let launch_entrypoint = if resolved.runtime == GuestRuntimeKind::JavaScript { - resolve_agentos_package_javascript_launch_entrypoint(vm, &mut env) + if resolved.adapter_policy.supports_prepared_in_place_exec { + env.insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); + } + let provisional_launch_entrypoint = if resolved + .adapter_policy + .uses_javascript_entrypoint_projection + { + env.get("AGENTOS_GUEST_ENTRYPOINT") + .filter(|path| path.starts_with('/')) + .map(|path| normalize_path(path)) .unwrap_or_else(|| resolved.entrypoint.clone()) } else { resolved.entrypoint.clone() }; - let argv = std::iter::once(launch_entrypoint.clone()) + let argv = std::iter::once(provisional_launch_entrypoint) .chain(resolved.execution_args.iter().cloned()) .collect::>(); + let requested_permission_tier = resolved + .wasm_permission_tier + .map(kernel_process_permission_tier) + .unwrap_or(ProcessPermissionTier::Full); record_execute_phase("env_argv_setup", phase_start.elapsed()); let phase_start = Instant::now(); let kernel_handle = vm @@ -4850,56 +4739,159 @@ where SpawnOptions { requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), cwd: Some(resolved.guest_cwd.clone()), + permission_tier: Some(requested_permission_tier), ..SpawnOptions::default() }, ) .map_err(kernel_error)?; let kernel_pid = kernel_handle.pid(); - if let Err(error) = enforce_resolved_wasm_execute_dac(vm, kernel_pid, &resolved) { - kernel_handle.finish(126); - return Err(error); - } record_execute_phase("kernel_spawn_process", phase_start.elapsed()); + + macro_rules! top_level_start_step { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(error) => { + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + None, + $context, + ); + return Err(error); + } + } + }; + } + + macro_rules! dispose_started_context { + ($context:expr) => { + match $context { + StartedTopLevelAdapterContext::Javascript(context_id) => { + self.javascript_engine.dispose_context(context_id); + } + StartedTopLevelAdapterContext::Python(context_id) => { + self.python_engine.dispose_context(context_id); + } + StartedTopLevelAdapterContext::WebAssembly(context_id) => { + self.wasm_engine.dispose_context(context_id); + } + } + }; + } + + let launch_entrypoint = if resolved + .adapter_policy + .uses_javascript_entrypoint_projection + { + top_level_start_step!( + resolve_agentos_package_javascript_launch_entrypoint(vm, kernel_pid, &mut env,), + "top-level JavaScript package entrypoint resolution" + ) + .unwrap_or_else(|| resolved.entrypoint.clone()) + } else { + resolved.entrypoint.clone() + }; + + // Attach before PTY setup, asset preparation, or engine start. Kernel + // signals arriving during any of those steps remain durable in this + // receiver; every failure below funnels through process reaping. + let runtime_control = top_level_start_step!( + ActiveProcess::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&self.process_event_notify), + ), + "top-level runtime-control attachment" + ); let tty_master_fd = if requested_tty { - let (master_fd, slave_fd, _) = vm - .kernel - .open_pty(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2) - .map_err(kernel_error)?; - vm.kernel - .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid) - .map_err(kernel_error)?; - if let Some((cols, rows)) = requested_pty_window_size(&env) { + let (master_fd, slave_fd, _) = top_level_start_step!( vm.kernel - .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows) - .map_err(kernel_error)?; + .open_pty(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error), + "top-level PTY allocation" + ); + top_level_start_step!( + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0) + .map_err(kernel_error), + "top-level PTY stdin installation" + ); + top_level_start_step!( + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1) + .map_err(kernel_error), + "top-level PTY stdout installation" + ); + top_level_start_step!( + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2) + .map_err(kernel_error), + "top-level PTY stderr installation" + ); + top_level_start_step!( + vm.kernel + .pty_set_foreground_pgid( + EXECUTION_DRIVER_NAME, + kernel_pid, + master_fd, + kernel_pid, + ) + .map_err(kernel_error), + "top-level PTY foreground-group setup" + ); + if let Some((cols, rows)) = requested_pty_window_size(&env) { + top_level_start_step!( + vm.kernel + .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows) + .map_err(kernel_error), + "top-level PTY resize" + ); } Some(master_fd) } else { None }; + let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd { + master_fd + } else { + top_level_start_step!( + install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid), + "top-level stdin pipe installation" + ) + }; - let (execution, process_env) = match resolved.runtime { + let (execution, process_env, started_context) = match resolved.runtime { GuestRuntimeKind::JavaScript => { let phase_start = Instant::now(); - let inline_code = load_javascript_entrypoint_source( - vm, - &resolved.host_cwd, - &launch_entrypoint, - &env, + top_level_start_step!( + prepare_javascript_launch_assets( + vm, + &resolved, + &env, + WasmLaunchAuthority::TrustedInitialImage, + None, + ), + "top-level JavaScript asset preparation" ); - record_execute_phase("js_load_entrypoint_source", phase_start.elapsed()); + record_execute_phase("js_prepare_launch_assets", phase_start.elapsed()); let phase_start = Instant::now(); - prepare_javascript_shadow(vm, &resolved, &env)?; - record_execute_phase("js_prepare_shadow", phase_start.elapsed()); + // A trusted initial request may name a host source that has not + // been admitted to the kernel VFS yet. Asset preparation above + // performs that one bounded admission. Load the executable + // source only after admission so the kernel remains the source + // of truth and the V8 import cache never falls back to the + // caller's ambient host pathname. + let inline_code = top_level_start_step!( + load_javascript_entrypoint_source( + vm, + kernel_pid, + &resolved.guest_cwd, + &launch_entrypoint, + &env, + ), + "top-level JavaScript entrypoint load" + ); + record_execute_phase("js_load_entrypoint_source", phase_start.elapsed()); let phase_start = Instant::now(); let context = @@ -4911,22 +4903,14 @@ where }); record_execute_phase("js_create_context", phase_start.elapsed()); let phase_start = Instant::now(); - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = - built_reader.map(|reader| Box::new(reader) as Box); - record_execute_phase("js_build_module_reader", phase_start.elapsed()); - let phase_start = Instant::now(); - let execution = self + let context_id = context.context_id; + let execution = match self .javascript_engine .start_execution_with_module_reader_and_runtime( StartJavascriptExecutionRequest { guest_runtime: guest_runtime_identity(vm, None, None), vm_id: vm_id.clone(), - context_id: context.context_id, + context_id: context_id.clone(), argv: std::iter::once(launch_entrypoint.clone()) .chain(resolved.execution_args.iter().cloned()) .collect(), @@ -4937,13 +4921,30 @@ where inline_code, wasm_module_bytes: None, }, - module_reader, - guest_reader, + None, + None, vm.runtime_context.clone(), ) - .map_err(javascript_error)?; + .map_err(javascript_error) + { + Ok(execution) => execution, + Err(error) => { + self.javascript_engine.dispose_context(&context_id); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + None, + "top-level JavaScript engine start", + ); + return Err(error); + } + }; record_execute_phase("js_start_execution", phase_start.elapsed()); - (ActiveExecution::Javascript(execution), env.clone()) + ( + ActiveExecution::Javascript(execution), + env.clone(), + StartedTopLevelAdapterContext::Javascript(context_id), + ) } GuestRuntimeKind::Python => { // The `python` command path (marked by AGENTOS_PYTHON_ARGV) is @@ -4955,11 +4956,13 @@ where } else { python_file_entrypoint(&resolved.entrypoint) }; - let pyodide_dist_path = self - .python_engine - .bundled_pyodide_dist_path_for_vm_async(&vm_id, &vm.runtime_context) - .await - .map_err(python_error)?; + let pyodide_dist_path = top_level_start_step!( + self.python_engine + .bundled_pyodide_dist_path_for_vm_async(&vm_id, &vm.runtime_context) + .await + .map_err(python_error), + "top-level Python asset preparation" + ); let pyodide_cache_path = pyodide_dist_path .parent() .and_then(Path::parent) @@ -4999,12 +5002,13 @@ where vm_id: vm_id.clone(), pyodide_dist_path, }); - let execution = self + let context_id = context.context_id; + let execution = match self .python_engine .start_execution_with_runtime_async( StartPythonExecutionRequest { vm_id: vm_id.clone(), - context_id: context.context_id, + context_id: context_id.clone(), code: resolved.entrypoint.clone(), file_path: python_file_path, env: env.clone(), @@ -5015,31 +5019,48 @@ where vm.runtime_context.clone(), ) .await - .map_err(python_error)?; - (ActiveExecution::Python(execution), env.clone()) + .map_err(python_error) + { + Ok(execution) => execution, + Err(error) => { + self.python_engine.dispose_context(&context_id); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + None, + "top-level Python engine start", + ); + return Err(error); + } + }; + ( + ActiveExecution::Python(execution), + env.clone(), + StartedTopLevelAdapterContext::Python(context_id), + ) } GuestRuntimeKind::WebAssembly => { let wasm_limits = wasm_execution_limits(vm); let wasm_guest_runtime = guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0)); - let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| { - resolve_wasm_permission_tier( - vm, - Some(&resolved.command), - None, - &resolved.entrypoint, - ) - }); + let wasm_permission_tier = top_level_start_step!( + vm.kernel + .process_permission_tier(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error), + "top-level compatibility-WASM permission lookup" + ); let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.clone(), module_path: Some(resolved.entrypoint.clone()), }); - let execution = self + let context_id = context.context_id; + let execution = match self .wasm_engine .start_execution_with_runtime_async( StartWasmExecutionRequest { vm_id: vm_id.clone(), - context_id: context.context_id, + context_id: context_id.clone(), + managed_kernel_host: true, argv: resolved.process_args.clone(), env: env.clone(), cwd: resolved.host_cwd.clone(), @@ -5050,41 +5071,69 @@ where vm.runtime_context.clone(), ) .await - .map_err(wasm_error)?; - (ActiveExecution::Wasm(Box::new(execution)), env) + .map_err(wasm_error) + { + Ok(execution) => execution, + Err(error) => { + self.wasm_engine.dispose_context(&context_id); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &kernel_handle, + None, + "top-level compatibility-WASM engine start", + ); + return Err(error); + } + }; + ( + ActiveExecution::Wasm(Box::new(execution)), + env, + StartedTopLevelAdapterContext::WebAssembly(context_id), + ) } }; - let child_pid = execution.child_pid(); + let reported_process_id = execution.native_process_id().unwrap_or(kernel_pid); let phase_start = Instant::now(); - let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd { - master_fd - } else { - install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)? - }; - vm.active_processes.insert( - payload.process_id.clone(), - ActiveProcess::new( - kernel_pid, - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - process_event_capacity, - resolved.runtime, - execution, - ) - .with_event_notify(Arc::clone(&self.process_event_notify)) - .with_vm_pending_byte_budgets( - vm_pending_stdin_bytes_budget, - vm_pending_event_bytes_budget, - ) - .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd) - .with_tty_master_fd(tty_master_fd) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(process_env) - .with_shadow_root(sandbox_root) - .with_host_cwd(resolved.host_cwd.clone()), - ); - self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; + let mut process = ActiveProcess::new_with_attached_runtime_control( + kernel_pid, + kernel_handle, + vm.runtime_context.clone(), + vm.limits.clone(), + process_event_capacity, + resolved.runtime, + execution, + runtime_control, + Arc::clone(&self.process_event_notify), + ) + .with_adapter_policy(resolved.adapter_policy) + .with_vm_pending_byte_budgets(vm_pending_stdin_bytes_budget, vm_pending_event_bytes_budget) + .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd) + .with_tty_master_fd(tty_master_fd) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(process_env) + .with_host_cwd(resolved.host_cwd.clone()); + if let Err(error) = process.apply_runtime_controls() { + let rollback_handle = process.kernel_handle.clone(); + rollback_failed_top_level_process_start( + &mut vm.kernel, + &rollback_handle, + Some(&mut process.execution), + "top-level pending runtime control", + ); + dispose_started_context!(&started_context); + return Err(error); + } + vm.active_processes + .insert(payload.process_id.clone(), process); + if let Err(error) = self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy) { + rollback_published_top_level_process_start( + vm, + &payload.process_id, + "top-level engine lifecycle publication", + ); + dispose_started_context!(&started_context); + return Err(error); + } mark_execute_response_ready(&vm_id, &payload.process_id); record_execute_phase("process_register_and_lifecycle", phase_start.elapsed()); record_execute_phase("execute_total", execute_total_start.elapsed()); @@ -5093,11 +5142,7 @@ where response: process_started_response( request, payload.process_id, - Some(if child_pid == 0 { - kernel_pid - } else { - child_pid - }), + Some(reported_process_id), ), events: Vec::new(), }) diff --git a/crates/native-sidecar/src/execution/mod.rs b/crates/native-sidecar/src/execution/mod.rs index 4526ddfc6e..380aeebb09 100644 --- a/crates/native-sidecar/src/execution/mod.rs +++ b/crates/native-sidecar/src/execution/mod.rs @@ -5,16 +5,14 @@ use self::child_process::*; mod coordinator; use self::coordinator::*; mod launch; +pub(crate) use self::launch::sanitize_javascript_child_process_internal_bootstrap_env; use self::launch::*; -pub(crate) use self::launch::{ - host_path_from_runtime_guest_mappings, initial_shadow_sync_inventory, - is_protected_agentos_shadow_sync_path, - sanitize_javascript_child_process_internal_bootstrap_env, - sync_active_process_host_writes_to_kernel, sync_process_host_writes_to_kernel, -}; +mod host_dispatch; +use self::host_dispatch::*; +pub(crate) use host_dispatch::checked_deferred_guest_wait_deadline; mod process; -pub(crate) use self::process::terminate_child_process_tree; use self::process::*; +pub(crate) use self::process::{settle_execution_host_call, terminate_child_process_tree}; mod process_events; #[cfg(test)] #[allow(unused_imports)] @@ -30,53 +28,48 @@ mod signals; pub(crate) use self::signals::runtime_child_is_alive; use self::signals::*; pub(crate) use self::signals::{ - apply_active_process_default_signal, canonical_signal_name, parse_signal, - signal_runtime_process, + apply_kernel_signal_registration, canonical_signal_name, parse_signal, + protocol_signal_registration, signal_runtime_process, }; mod stdio; -#[cfg(test)] -#[allow(unused_imports)] -pub(crate) use self::stdio::drain_tty_master_output; use self::stdio::*; pub(crate) use self::stdio::{ - close_kernel_process_stdin, flush_pending_kernel_stdin, install_kernel_stdin_pipe, + close_kernel_process_stdin, flush_pending_kernel_stdin, install_kernel_ignored_stdin, kernel_poll_response, kernel_stdin_read_response, parse_kernel_poll_args, parse_kernel_stdin_read_args, service_javascript_kernel_fd_write_sync_rpc, write_kernel_process_stdin, }; +#[cfg(test)] +#[allow(unused_imports)] +pub(crate) use self::stdio::{drain_tty_master_output, install_kernel_stdin_pipe}; mod network; #[cfg(test)] #[allow(unused_imports)] pub(crate) use self::network::reserve_udp_receive_buffer; use self::network::*; pub(crate) use self::network::{ - build_javascript_socket_path_context, finalize_javascript_net_connect, format_dns_resource, - reserve_tls_write_payload, + build_socket_path_context, finalize_net_connect, format_dns_resource, + reserve_tls_write_payload, HickoryDnsResolver, }; mod javascript; use self::javascript::*; #[cfg(test)] #[allow(unused_imports)] pub(crate) use self::javascript::{ - clamp_javascript_net_poll_wait, service_javascript_net_sync_rpc, - JavascriptNetSyncRpcServiceRequest, + clamp_javascript_net_poll_wait, service_javascript_net_sync_rpc, NetServiceRequest, }; pub(crate) use self::javascript::{ - deferred_kernel_wait_request_for_process, dispatch_loopback_http_request, - dispatch_loopback_http_request_deferred, ensure_vm_fetch_response_frame_within_limit, - error_code, ignore_stale_javascript_sync_rpc_response, javascript_sync_rpc_arg_bool, - javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, - javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, - javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg, - javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, javascript_sync_rpc_error_code, + deferred_kernel_wait_request_for_process, dispatch_loopback_http_request_deferred, + ensure_vm_fetch_response_frame_within_limit, error_code, host_bytes_value, + host_service_error_code, javascript_sync_rpc_arg_bool, javascript_sync_rpc_arg_i32, + javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, javascript_sync_rpc_arg_u32_optional, + javascript_sync_rpc_arg_u64, javascript_sync_rpc_arg_u64_optional, + javascript_sync_rpc_bytes_arg, javascript_sync_rpc_encoding, javascript_sync_rpc_may_make_fd_readable, javascript_sync_rpc_may_make_fd_writable, javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, - service_javascript_crypto_sync_rpc, service_javascript_sync_rpc, - JavascriptSyncRpcServiceRequest, JavascriptSyncRpcServiceResponse, KernelPollFdRequest, - LoopbackHttpDispatchRequest, + service_javascript_crypto_sync_rpc, service_javascript_sync_rpc, HostServiceResponse, + JavascriptSyncRpcServiceRequest, KernelPollFdRequest, LoopbackHttpDispatchRequest, }; -mod python; - use agentos_vm_config as vm_config; use crate::bindings::{ @@ -84,25 +77,22 @@ use crate::bindings::{ resolve_binding_command, BindingCommandResolution, }; use crate::filesystem::{ - handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request, service_javascript_fs_read_sync_rpc, service_javascript_fs_readdir_raw_sync_rpc, service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc, }; use crate::protocol::{ - CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest, FindBoundUdpRequest, + CloseStdinRequest, DgramBindOptions, DgramConnectOptions, DgramCreateSocketOptions, + DgramSendOptions, EventFrame, EventPayload, ExecuteRequest, FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetResourceSnapshotRequest, GetSignalStateRequest, GetZombieTimerCountRequest, GuestKernelCallRequest, - GuestKernelResultResponse, GuestRuntimeKind, JavascriptChildProcessSpawnOptions, - JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest, JavascriptDgramConnectRequest, - JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest, + GuestKernelResultResponse, GuestRuntimeKind, JavascriptDnsLookupRequest, JavascriptDnsResolveRequest, JavascriptNetBindConnectedUnixRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest, JavascriptNetReserveTcpPortRequest, - JavascriptPosixSpawnFileAction, JavascriptSpawnHostNetFd, KillProcessRequest, OwnershipScope, - ProcessExitedEvent, ProcessOutputEvent, ProcessSnapshotEntry, ProcessSnapshotStatus, - PtyResizedResponse, QueueSnapshotEntry, RequestFrame, ResizePtyRequest, - ResourceSnapshotResponse, ResponseFrame, ResponsePayload, SidecarRequestPayload, - SignalDispositionAction, SignalHandlerRegistration, SocketStateEntry, StreamChannel, - VmFetchRequest, VmFetchResponse, WasmPermissionTier, WriteStdinRequest, + KillProcessRequest, OwnershipScope, ProcessExitedEvent, ProcessOutputEvent, + ProcessSnapshotEntry, ProcessSnapshotStatus, PtyResizedResponse, QueueSnapshotEntry, + RequestFrame, ResizePtyRequest, ResourceSnapshotResponse, ResponseFrame, ResponsePayload, + SidecarRequestPayload, SignalDispositionAction, SignalHandlerRegistration, SocketStateEntry, + StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier, WriteStdinRequest, }; use crate::service::{ audit_fields, dirname, emit_security_audit_event, emit_structured_event_or_stderr, @@ -111,37 +101,35 @@ use crate::service::{ process_event_queue_overflow_error, python_error, wasm_error, }; use crate::state::{ - async_completion_channel, ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, - ActiveEcdhSession, ActiveExecution, ActiveExecutionEvent, ActiveHashSession, ActiveHttp2Server, - ActiveHttp2Session, ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, + async_completion_channel, tcp_socket_event_retained_bytes, unix_listener_event_retained_bytes, + ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession, + ActiveExecutableImage, ActiveExecution, ActiveExecutionEvent, ActiveHashSession, + ActiveHttp2Server, ActiveHttp2Session, ActiveHttp2Stream, ActiveHttpServer, ActiveProcess, ActiveRealIntervalTimer, ActiveSqliteDatabase, ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, - AsyncCompletionReceiver, AsyncCompletionSender, BindingExecution, BridgeError, - ExitedProcessSnapshot, GuestUnixAddress, GuestUnixAddressRegistry, - GuestUnixAddressRegistryEntry, GuestUnixConnectionState, HostNetTransferDescription, - HostNetTransferDescriptionRegistry, Http2BridgeEvent, Http2ResponseSender, - Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot, Http2SocketSnapshot, - JavascriptHttpLoopbackTarget, JavascriptSocketFamily, JavascriptSocketPathContext, - JavascriptTcpListenerEvent, JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions, - JavascriptTlsClientHello, JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily, - JavascriptUdpSocketEvent, JavascriptUnixListenerEvent, KernelSocketReadinessEvent, + AsyncCompletionReceiver, AsyncCompletionSender, BindingExecution, BridgeError, DatagramEvent, + DeferredGuestWait, DeferredGuestWaitKind, DeferredKernelPoll, DeferredKernelRead, + DeferredKernelReadResponse, ExecutionAdapterPolicy, ExecutionHostCall, ExitedProcessSnapshot, + GuestUnixAddress, GuestUnixAddressRegistry, GuestUnixAddressRegistryEntry, + GuestUnixConnectionState, HostNetTransferDescription, HostNetTransferDescriptionRegistry, + Http2BridgeEvent, Http2ResponseSender, Http2RuntimeSnapshot, Http2SessionCommand, + Http2SessionSnapshot, Http2SocketSnapshot, HttpLoopbackTarget, KernelSocketReadinessEvent, KernelSocketReadinessRegistry, KernelSocketReadinessTarget, ListenerConnectionRetirement, NativeCapabilityKey, NativePlainSocketCommand, NativeTlsCommand, NativeUdpCommand, NativeUdpSendPayload, NativeUdpSocketOption, NetworkResourceCounts, PendingChildProcessSync, - PendingChildProcessSyncCompletion, PendingHttpRequest, PendingJavascriptNetConnect, - PendingJavascriptNetConnectState, PendingKernelStdin, PendingPythonTcpConnect, - PendingTcpSocket, PendingUnixConnectionGuard, PendingUnixSocket, PlainSocketWritePayload, - ProcNetEntry, ProcessEventEnvelope, PythonHostSocket, PythonSocketConnectCompletion, - PythonTcpReadBuffer, QueuedHttp2Command, QueuedHttp2Event, ReactorIoLimits, - ResolvedChildProcessExecution, ResolvedTcpConnectAddr, ShadowNodeType, - ShadowSyncInventoryEntry, SharedBridge, SharedSidecarRequestClient, SidecarKernel, - SocketDescriptionLease, SocketQueryKind, SocketReadinessRegistration, - SocketReadinessSubscribers, TlsWritePayload, VmDnsConfig, VmFetchBodyMode, VmFetchStreamState, - VmListenPolicy, VmPendingByteBudget, VmState, BINDING_DRIVER_NAME, - DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, - JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, MAPPED_HOST_FD_START, PYTHON_COMMAND, - VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_EXEC_COMMIT_RPC_ENV, - WASM_STDIO_SYNC_RPC_ENV, + PendingChildProcessSyncCompletion, PendingHttpRequest, PendingKernelStdin, PendingNetConnect, + PendingNetConnectState, PendingTcpSocket, PendingUnixConnectionGuard, PendingUnixSocket, + PlainSocketWritePayload, ProcNetEntry, ProcessEventEnvelope, QueuedHttp2Command, + QueuedHttp2Event, ReactorIoLimits, ResolvedChildProcessExecution, ResolvedTcpConnectAddr, + SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketDescriptionLease, SocketFamily, + SocketPathContext, SocketQueryKind, SocketReadState, SocketReadTerminal, + SocketReadinessRegistration, SocketReadinessSubscribers, TcpListenerEvent, TcpSocketEvent, + TlsBridgeOptions, TlsClientHello, TlsDataValue, TlsMaterial, TlsWritePayload, UdpFamily, + UnixListenerEvent, VmDnsConfig, VmFetchBodyMode, VmFetchStreamState, VmListenPolicy, + VmPendingBudgetReservation, VmPendingByteBudget, VmState, BINDING_DRIVER_NAME, + DEFAULT_NET_BACKLOG, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, + LOOPBACK_EXEMPT_PORTS_ENV, PYTHON_COMMAND, VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, + WASM_COMMAND, WASM_EXEC_COMMIT_RPC_ENV, WASM_STDIO_SYNC_RPC_ENV, }; use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec}; use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError}; @@ -179,17 +167,26 @@ use pbkdf2::pbkdf2_hmac; use crate::crypto_cipher::{CipherError as AesCipherError, StreamCipherSession}; use agentos_bridge::{queue_tracker, LifecycleState}; -use agentos_execution::wasm::WasmExecutionError; +use agentos_execution::host::{ + ClockOperation, HostOperation, HostProcessContext, ProcessHostCapabilitySet, + ProcessLaunchOptions, ProcessLaunchRequest, ProcessOperation, ProcessSpawnFileAction, + ProcessSpawnHostNetworkDescriptor, +}; use agentos_execution::{ - javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle, - v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest, - CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent, - JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader, - NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent, - PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponder, - PythonVfsRpcResponsePayload, StartJavascriptExecutionRequest, StartPythonExecutionRequest, - StartWasmExecutionRequest, WasmExecutionEvent, WasmExecutionLimits, - WasmPermissionTier as ExecutionWasmPermissionTier, + backend::{ + bounded_execution_event_channel, DescendantOutputOwnership, DescendantWaitOwnership, + DirectHostReplyHandle, ExecutionBackend, ExecutionBackendKind, ExecutionEvent, + ExecutionExit, ExecutionWakeHandle, ExecutionWakeIdentity, HostCallIdentity, HostCallReply, + HostServiceError, PayloadLimit, PublishedSignalCheckpoint, ShutdownOutcome, ShutdownReason, + SignalCheckpointOutcome, SynchronousFdWritePolicy, + }, + javascript::handle_internal_bridge_call_from_host_context, + CreateJavascriptContextRequest, CreatePythonContextRequest, CreateWasmContextRequest, + ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration, GuestRuntimeConfig, + HostRpcRequest, JavascriptExecutionEvent, JavascriptExecutionLimits, + JavascriptSyncRpcResponder, PythonExecutionEvent, PythonExecutionLimits, PythonVfsRpcResponder, + StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest, + WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier, }; use agentos_kernel::dns::{ DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource, @@ -204,30 +201,33 @@ use agentos_kernel::network_policy::{ }; use agentos_kernel::permissions::NetworkOperation; use agentos_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN}; -use agentos_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM}; +use agentos_kernel::process_runtime::ProcessRuntimeIdentity; +use agentos_kernel::process_table::{ + ProcessPermissionTier, ProcessStatus, SigmaskHow, SignalSet, WaitPidFlags, SIGTERM, +}; use agentos_kernel::pty::MAX_PTY_BUFFER_BYTES; -use agentos_kernel::root_fs::RootFilesystemMode; use agentos_kernel::socket_table::{ reset_socket_read_trace, set_socket_read_trace_enabled, socket_read_trace_snapshot, InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec, SocketState, SocketType, }; +use agentos_kernel::system::KernelClockId; use agentos_native_sidecar_core::ca::CA_CERTIFICATES_GUEST_PATH; use agentos_native_sidecar_core::{ - apply_process_signal_state_update, bound_udp_snapshot_response, bridge_buffer_value, - decode_base64, decode_bridge_buffer_value, decode_encoded_bytes_value, encoded_bytes_value, + bound_udp_snapshot_response, bridge_buffer_value, decode_base64, decode_bridge_buffer_value, + decode_encoded_bytes_value, encoded_bytes_value, ensure_vm_fetch_raw_response_buffer_within_limit, ensure_vm_fetch_response_within_limit, listener_snapshot_response, local_endpoint_value, parse_kernel_http_fetch_response, parse_process_signal_state_request, process_killed_response, process_snapshot_entry_from_kernel, process_snapshot_response, process_started_response, - remote_endpoint_value, shared_guest_runtime_identity, signal_state_response, + remote_endpoint_value, shared_guest_runtime_identity_with_system, signal_state_response, socket_addr_family, socket_address_value, stdin_closed_response, stdin_written_response, tcp_socket_info_value, unix_socket_info_value, zombie_timer_count_response, SharedProcessSnapshotEntry, SharedProcessSnapshotStatus, SidecarCoreError, VM_FETCH_BUFFER_LIMIT_BYTES, }; use agentos_runtime::accounting::{ - Reservation, ResourceClass, ResourceLedger, ResourceLimit, SharedReservation, + LimitError, Reservation, ResourceClass, ResourceLedger, ResourceLimit, SharedReservation, }; use agentos_runtime::capability::{ CapabilityBackend, CapabilityKind, CapabilityRegistry, PendingCapability, @@ -259,7 +259,7 @@ use std::net::{ UdpSocket, }; use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; -use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::os::unix::fs::PermissionsExt; use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -277,7 +277,7 @@ use url::Url; const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024; const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100; -const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__agentos_net_timeout__"; +const NET_TIMEOUT_SENTINEL: &str = "__agentos_net_timeout__"; const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide"; const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache"; fn reactor_io_limits(limits: &crate::limits::VmLimits) -> ReactorIoLimits { @@ -292,6 +292,153 @@ fn reactor_io_limits(limits: &crate::limits::VmLimits) -> ReactorIoLimits { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DeadlineLimitWarning { + pub(crate) limit_name: &'static str, + pub(crate) operation: String, + pub(crate) observed_ms: u128, + pub(crate) limit_ms: u128, +} + +type DeadlineLimitWarningHandler = Arc; + +fn deadline_limit_warning_handler() -> &'static Mutex> { + static HANDLER: OnceLock>> = OnceLock::new(); + HANDLER.get_or_init(|| Mutex::new(None)) +} + +#[cfg(test)] +fn set_deadline_limit_warning_handler( + handler: impl Fn(&DeadlineLimitWarning) + Send + Sync + 'static, +) { + *deadline_limit_warning_handler() + .lock() + .expect("deadline warning handler") = Some(Arc::new(handler)); +} + +pub(crate) fn emit_deadline_limit_warning(operation: &str, observed: Duration, limit: Duration) { + let warning = DeadlineLimitWarning { + limit_name: "limits.reactor.operationDeadlineMs", + operation: operation.to_owned(), + observed_ms: observed.as_millis(), + limit_ms: limit.as_millis(), + }; + eprintln!( + "WARN_AGENTOS_DEADLINE_NEAR_LIMIT: operation={} observed_ms={} limit_ms={} config={}", + warning.operation, warning.observed_ms, warning.limit_ms, warning.limit_name + ); + let handler = match deadline_limit_warning_handler().lock() { + Ok(handler) => handler.clone(), + Err(_) => { + eprintln!( + "ERR_AGENTOS_DEADLINE_WARNING_HANDLER_POISONED: deadline warning handler was poisoned by a prior panic" + ); + None + } + }; + if let Some(handler) = handler { + handler(&warning); + } +} + +/// Synchronous state machine for one operation-deadline budget. It is usable +/// by both Tokio futures and poll/re-entry paths, and preserves the original +/// start plus the single warning edge when reconstructed from a parked RPC. +#[derive(Debug, Clone)] +pub(crate) struct OperationDeadlineTracker { + started: Instant, + warning_at: Instant, + deadline: Instant, + limit: Duration, + warning_emitted: bool, +} + +impl OperationDeadlineTracker { + pub(crate) fn new(limit: Duration) -> Self { + let started = Instant::now(); + Self { + started, + warning_at: started + limit.saturating_mul(4) / 5, + deadline: started + limit, + limit, + warning_emitted: false, + } + } + + pub(crate) fn from_deadline(deadline: Instant, limit: Duration, warning_emitted: bool) -> Self { + let started = deadline.checked_sub(limit).unwrap_or(deadline); + Self { + started, + warning_at: started + limit.saturating_mul(4) / 5, + deadline, + limit, + warning_emitted, + } + } + + pub(crate) fn observe(&mut self, operation: &str) { + let now = Instant::now(); + if !self.warning_emitted && now >= self.warning_at { + self.warning_emitted = true; + emit_deadline_limit_warning( + operation, + now.saturating_duration_since(self.started).min(self.limit), + self.limit, + ); + } + } + + pub(crate) fn next_edge(&self) -> Instant { + if self.warning_emitted { + self.deadline + } else { + self.warning_at + } + } + + pub(crate) fn remaining_until_next_edge(&self) -> Duration { + self.next_edge().saturating_duration_since(Instant::now()) + } + + pub(crate) fn remaining_until_deadline(&self) -> Duration { + self.deadline.saturating_duration_since(Instant::now()) + } + + pub(crate) fn expired(&self) -> bool { + Instant::now() >= self.deadline + } + + pub(crate) fn deadline(&self) -> Instant { + self.deadline + } + + pub(crate) fn warning_emitted(&self) -> bool { + self.warning_emitted + } +} + +/// Await one reactor operation with the configured hard deadline and a single +/// host-visible warning after 80% of that budget. The operation future remains +/// pinned across the warning edge; no work is restarted or duplicated. +pub(crate) async fn operation_deadline_timeout( + operation: &str, + limit: Duration, + future: F, +) -> Result +where + F: Future, +{ + let mut deadline = OperationDeadlineTracker::new(limit); + tokio::pin!(future); + match tokio::time::timeout_at(deadline.next_edge().into(), &mut future).await { + Ok(output) => Ok(output), + Err(_) => { + deadline.observe(operation); + tokio::time::timeout_at(deadline.deadline().into(), &mut future).await + } + } +} + fn socket_completion_capacity(limits: ReactorIoLimits) -> usize { debug_assert!( limits.max_async_completions > 0, @@ -301,7 +448,7 @@ fn socket_completion_capacity(limits: ReactorIoLimits) -> usize { } fn listener_accept_capacity(backlog: Option, limits: ReactorIoLimits) -> usize { - usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize") .max(1) .min(socket_completion_capacity(limits)) @@ -314,8 +461,36 @@ const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str = "AGENTOS_TEST_HTTP_LOOPBACK_R #[cfg(test)] mod configured_socket_capacity_tests { - use super::{listener_accept_capacity, reactor_io_limits, socket_completion_capacity}; + use super::{ + listener_accept_capacity, operation_deadline_timeout, reactor_io_limits, + set_deadline_limit_warning_handler, socket_completion_capacity, write_all_nonblocking, + }; use crate::limits::VmLimits; + use std::io::{self, Write}; + use std::os::fd::{AsFd, BorrowedFd}; + use std::os::unix::net::UnixStream; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + struct AlwaysWouldBlock { + fd: UnixStream, + } + + impl Write for AlwaysWouldBlock { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::from(io::ErrorKind::WouldBlock)) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl AsFd for AlwaysWouldBlock { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } + } #[test] fn socket_and_accept_queues_are_individually_bounded_by_vm_completion_limit() { @@ -327,4 +502,93 @@ mod configured_socket_capacity_tests { assert_eq!(listener_accept_capacity(Some(100), reactor), 3); assert_eq!(listener_accept_capacity(Some(2), reactor), 2); } + + #[tokio::test] + async fn operation_deadline_warns_at_eighty_percent_before_success_or_typed_expiry() { + let captured = Arc::new(Mutex::new(Vec::new())); + let sink = Arc::clone(&captured); + set_deadline_limit_warning_handler(move |warning| { + if warning.operation.starts_with("deadline-test-") + || warning.operation == "synchronous socket write" + { + sink.lock() + .expect("deadline warning sink") + .push(warning.clone()); + } + }); + + let completed = operation_deadline_timeout( + "deadline-test-completes-near-limit", + Duration::from_millis(50), + async { + tokio::time::sleep(Duration::from_millis(45)).await; + 7 + }, + ) + .await + .expect("operation may complete after the warning and before expiry"); + assert_eq!(completed, 7); + + operation_deadline_timeout( + "deadline-test-expires", + Duration::from_millis(50), + tokio::time::sleep(Duration::from_millis(100)), + ) + .await + .expect_err("operation must still expire at the hard deadline"); + + let warnings = captured.lock().expect("deadline warnings"); + assert_eq!(warnings.len(), 2); + for warning in warnings.iter() { + assert_eq!(warning.limit_name, "limits.reactor.operationDeadlineMs"); + assert_eq!(warning.limit_ms, 50); + assert!( + warning.observed_ms >= 40 && warning.observed_ms < 50, + "warning must precede the hard deadline: {warning:?}" + ); + } + drop(warnings); + + // The synchronous TCP/Unix write path uses the same warning state + // machine even though its readiness wait is `poll(2)`, not a Future. + let (fd, _peer) = UnixStream::pair().expect("create deadline test fd"); + let mut blocked = AlwaysWouldBlock { fd }; + let mut limits = VmLimits::default(); + limits.reactor.operation_deadline_ms = 25; + let error = write_all_nonblocking(&mut blocked, b"x", reactor_io_limits(&limits)) + .expect_err("permanently blocked synchronous write must expire"); + assert!(error.to_string().contains("ERR_AGENTOS_OPERATION_DEADLINE")); + + // A readiness wake may re-park the same RPC. Reconstructing from its + // absolute deadline and warning bit must neither reset the clock nor + // emit a duplicate warning. + let mut parked = super::OperationDeadlineTracker::new(Duration::from_millis(25)); + tokio::time::sleep(Duration::from_millis(21)).await; + parked.observe("deadline-test-repark"); + let mut reparked = super::OperationDeadlineTracker::from_deadline( + parked.deadline(), + Duration::from_millis(25), + parked.warning_emitted(), + ); + reparked.observe("deadline-test-repark"); + tokio::time::sleep(reparked.remaining_until_deadline()).await; + assert!(reparked.expired()); + + let warnings = captured.lock().expect("deadline warnings after sync paths"); + assert_eq!(warnings.len(), 4); + assert_eq!( + warnings + .iter() + .filter(|warning| warning.operation == "synchronous socket write") + .count(), + 1 + ); + assert_eq!( + warnings + .iter() + .filter(|warning| warning.operation == "deadline-test-repark") + .count(), + 1 + ); + } } diff --git a/crates/native-sidecar/src/execution/network/dns.rs b/crates/native-sidecar/src/execution/network/dns.rs index 6133600858..6cd9ba4f2d 100644 --- a/crates/native-sidecar/src/execution/network/dns.rs +++ b/crates/native-sidecar/src/execution/network/dns.rs @@ -1,4 +1,175 @@ use super::super::*; +use agentos_execution::backend::{HostCallReply, HostServiceError}; +use agentos_execution::host::{DnsAddressFamily, NetworkOperation}; + +fn enforce_dns_result_limit(maximum: usize, observed: usize) -> Result<(), SidecarError> { + if observed > maximum { + return Err(SidecarError::Host(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "runtime.network.maxDnsResults", + maximum as u64, + observed as u64, + ))); + } + Ok(()) +} + +/// Runtime-neutral DNS service used by every executor adapter after it has +/// copied and bounded its guest inputs. No adapter request or response type +/// crosses this boundary. +pub(in crate::execution) fn service_host_dns_operation( + bridge: SharedBridge, + kernel: &SidecarKernel, + vm_id: String, + dns: VmDnsConfig, + operation: NetworkOperation, +) -> std::pin::Pin< + Box> + Send + 'static>, +> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let future: std::pin::Pin< + Box> + Send>, + > = match operation { + NetworkOperation::ResolveDns { + host, + port: _, + family, + max_results, + } => { + let lookup = kernel.resolve_dns_async(host.as_str(), DnsLookupPolicy::CheckPermissions); + let host = host.into_string(); + let family = match family { + DnsAddressFamily::Any => None, + DnsAddressFamily::Inet4 => Some(4), + DnsAddressFamily::Inet6 => Some(6), + }; + Box::pin(async move { + let resolution = match lookup.await { + Ok(resolution) => resolution, + Err(error) => { + let sidecar_error = kernel_error(error.clone()); + if error.code() != "EACCES" { + emit_dns_resolution_failure_event( + &bridge, + &vm_id, + &host, + &dns, + &sidecar_error, + ); + } + return Err(sidecar_error); + } + }; + emit_dns_resolution_event( + &bridge, + &vm_id, + &host, + resolution.source(), + resolution.addresses(), + &dns, + ); + let addresses = filter_dns_safe_ip_addrs( + filter_dns_ip_addrs(resolution.addresses().to_vec(), family)?, + &host, + )?; + enforce_dns_result_limit(max_results.get(), addresses.len())?; + Ok(Value::Array( + addresses + .into_iter() + .map(|ip| { + json!({ + "address": ip.to_string(), + "family": if ip.is_ipv6() { 6 } else { 4 }, + }) + }) + .collect(), + )) + }) + } + NetworkOperation::ResolveDnsRecord { + host, + record_type, + raw, + max_results, + } => { + let host = host.into_string(); + let requested_type = record_type.into_string().to_ascii_uppercase(); + let record_type = match parse_dns_record_type(&requested_type) { + Ok(record_type) => record_type, + Err(error) => return Box::pin(async move { Err(host_service_error(&error)) }), + }; + if raw && !matches!(requested_type.as_str(), "PTR" | "SSHFP") { + return Box::pin(async move { + Err(HostServiceError::new( + "EINVAL", + format!("raw DNS RR bridge does not support {requested_type}"), + )) + }); + } + let lookup = kernel.resolve_dns_records_async( + &host, + record_type, + DnsLookupPolicy::CheckPermissions, + ); + Box::pin(async move { + let resolution = match lookup.await { + Ok(resolution) => resolution, + Err(error) if raw => { + if let Some(response) = dns_raw_rr_negative_response(error.code()) { + return Ok(response); + } + let sidecar_error = kernel_error(error.clone()); + if error.code() != "EACCES" { + emit_dns_resolution_failure_event( + &bridge, + &vm_id, + &host, + &dns, + &sidecar_error, + ); + } + return Err(sidecar_error); + } + Err(error) => { + let sidecar_error = kernel_error(error.clone()); + if error.code() != "EACCES" { + emit_dns_resolution_failure_event( + &bridge, + &vm_id, + &host, + &dns, + &sidecar_error, + ); + } + return Err(sidecar_error); + } + }; + emit_dns_record_resolution_event(&bridge, &vm_id, &host, &resolution, &dns); + enforce_dns_result_limit(max_results.get(), resolution.records().len())?; + if raw { + Ok(dns_raw_rr_response(&resolution, &requested_type)) + } else { + dns_resolution_to_node_value(&resolution, &requested_type) + } + }) + } + other => Box::pin(async move { + Err(SidecarError::host( + "ENOSYS", + format!("DNS service received non-DNS operation {other:?}"), + )) + }), + }; + Box::pin(async move { + future + .await + .map(HostCallReply::Json) + .map_err(|error| host_service_error(&error)) + }) +} pub(in crate::execution) fn emit_dns_resolution_event( bridge: &SharedBridge, @@ -137,9 +308,10 @@ fn parse_dns_record_type(rrtype: &str) -> Result { "NAPTR" => Ok(RecordType::NAPTR), "CAA" => Ok(RecordType::CAA), "ANY" => Ok(RecordType::ANY), - other => Err(SidecarError::Execution(format!( - "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge" - ))), + other => Err(SidecarError::host( + "ERR_NOT_IMPLEMENTED", + format!("dns rrtype {other} is not supported by the secure-exec dns bridge"), + )), } } @@ -323,9 +495,10 @@ fn dns_resolution_to_node_value( .filter_map(|record| dns_any_record_to_value(record, &safe_ips)) .collect(), )), - other => Err(SidecarError::Execution(format!( - "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge" - ))), + other => Err(SidecarError::host( + "ERR_NOT_IMPLEMENTED", + format!("dns rrtype {other} is not supported by the secure-exec dns bridge"), + )), } } @@ -530,47 +703,43 @@ pub(crate) fn format_dns_resource(hostname: &str) -> String { format!("dns://{hostname}") } -// --- Guest Python socket bridge helpers ------------------------------------ +pub(in crate::execution) enum DnsOperation { + Lookup { + hostname: String, + family: Option, + }, + Resolve { + hostname: String, + requested_type: String, + raw_record: bool, + }, +} -pub(in crate::execution) fn service_javascript_dns_sync_rpc( +pub(in crate::execution) fn service_dns_operation( bridge: &SharedBridge, kernel: &SidecarKernel, vm_id: &str, dns: &VmDnsConfig, - request: &JavascriptSyncRpcRequest, + operation: DnsOperation, ) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - match request.method.as_str() { - "dns.lookup" => { - let payload = request - .args - .first() - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dns.lookup requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err(|error| { - SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}")) - }) - })?; + match operation { + DnsOperation::Lookup { hostname, family } => { let addresses = filter_dns_ip_addrs( resolve_dns_ip_addrs( bridge, kernel, vm_id, dns, - &payload.hostname, + &hostname, DnsLookupPolicy::CheckPermissions, )?, - payload.family, + family, )?; - let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?; + let addresses = filter_dns_safe_ip_addrs(addresses, &hostname)?; Ok(Value::Array( addresses .into_iter() @@ -583,39 +752,21 @@ where .collect(), )) } - "dns.resolve" | "dns.resolve4" | "dns.resolve6" | "dns.resolveRawRr" => { - let payload = request - .args - .first() - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dns.resolve requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err(|error| { - SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}")) - }) - })?; - let requested_type = match request.method.as_str() { - "dns.resolve4" => String::from("A"), - "dns.resolve6" => String::from("AAAA"), - _ => payload - .rrtype - .as_deref() - .unwrap_or("A") - .to_ascii_uppercase(), - }; + DnsOperation::Resolve { + hostname, + requested_type, + raw_record, + } => { let record_type = parse_dns_record_type(&requested_type)?; - if request.method == "dns.resolveRawRr" { + if raw_record { if !matches!(requested_type.as_str(), "PTR" | "SSHFP") { - return Err(SidecarError::InvalidState(format!( - "EINVAL: raw DNS RR bridge does not support {requested_type}" - ))); + return Err(SidecarError::host( + "EINVAL", + format!("raw DNS RR bridge does not support {requested_type}"), + )); } let resolution = match kernel.resolve_dns_records( - &payload.hostname, + &hostname, record_type, DnsLookupPolicy::CheckPermissions, ) { @@ -623,7 +774,7 @@ where emit_dns_record_resolution_event( bridge, vm_id, - &payload.hostname, + &hostname, &resolution, dns, ); @@ -638,7 +789,7 @@ where emit_dns_resolution_failure_event( bridge, vm_id, - &payload.hostname, + &hostname, dns, &sidecar_error, ); @@ -653,16 +804,13 @@ where kernel, vm_id, dns, - &payload.hostname, + &hostname, record_type, DnsLookupPolicy::CheckPermissions, )?; dns_resolution_to_node_value(&resolution, &requested_type) } } - other => Err(SidecarError::InvalidState(format!( - "unsupported JavaScript dns sync RPC method {other}" - ))), } } @@ -675,6 +823,21 @@ mod raw_rr_tests { Name, }; + #[test] + fn dns_result_limit_rejects_instead_of_truncating() { + enforce_dns_result_limit(2, 2).expect("exact limit is admitted"); + let SidecarError::Host(error) = + enforce_dns_result_limit(2, 3).expect_err("limit plus one is rejected") + else { + panic!("expected typed host error"); + }; + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + let details = error.details.expect("typed limit details"); + assert_eq!(details["configPath"], "runtime.network.maxDnsResults"); + assert_eq!(details["limit"], 2); + assert_eq!(details["observed"], 3); + } + #[test] fn raw_rr_record_type_accepts_sshfp() { assert_eq!( diff --git a/crates/native-sidecar/src/execution/network/http2.rs b/crates/native-sidecar/src/execution/network/http2.rs index 9c23443e44..99f2b6b6b6 100644 --- a/crates/native-sidecar/src/execution/network/http2.rs +++ b/crates/native-sidecar/src/execution/network/http2.rs @@ -6,7 +6,7 @@ impl Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {} #[derive(Debug, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] -struct JavascriptHttp2ServerListenRequest { +struct Http2ServerListenOptions { server_id: u64, secure: bool, port: Option, @@ -14,41 +14,41 @@ struct JavascriptHttp2ServerListenRequest { backlog: Option, timeout: Option, settings: BTreeMap, - tls: Option, + tls: Option, } #[derive(Debug, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] -struct JavascriptHttp2SessionConnectRequest { +struct Http2SessionConnectOptions { authority: Option, protocol: Option, host: Option, port: Option, settings: BTreeMap, - tls: Option, + tls: Option, } #[derive(Debug, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] -struct JavascriptHttp2RequestOptions { +struct Http2RequestOptions { end_stream: bool, } #[derive(Debug, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] -struct JavascriptHttp2FileResponseOptions { +struct Http2FileResponseOptions { offset: Option, length: Option, } -pub(in crate::execution) struct JavascriptHttp2SyncRpcServiceRequest<'a, B> { +pub(in crate::execution) struct Http2ServiceRequest<'a, B> { pub(in crate::execution) bridge: &'a SharedBridge, pub(in crate::execution) kernel: &'a mut SidecarKernel, pub(in crate::execution) vm_id: &'a str, pub(in crate::execution) dns: &'a VmDnsConfig, - pub(in crate::execution) socket_paths: &'a JavascriptSocketPathContext, + pub(in crate::execution) socket_paths: &'a SocketPathContext, pub(in crate::execution) process: &'a mut ActiveProcess, - pub(in crate::execution) sync_request: &'a JavascriptSyncRpcRequest, + pub(in crate::execution) sync_request: &'a HostRpcRequest, pub(in crate::execution) capabilities: CapabilityRegistry, } @@ -128,9 +128,10 @@ fn reserve_http2_inbound_chunk( .as_ref() .cloned() .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: HTTP/2 read has no VM ResourceLedger", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("HTTP/2 read has no VM ResourceLedger"), + ) })?; reserve_http2_resources( &resources, @@ -231,9 +232,10 @@ fn reserve_http2_event( event: &Http2BridgeEvent, ) -> Result, SidecarError> { let resources = state.resources.as_ref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: HTTP/2 state has no VM ResourceLedger", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("HTTP/2 state has no VM ResourceLedger"), + ) })?; let event_bytes = http2_event_bytes(event); reserve_http2_resources( @@ -913,13 +915,13 @@ fn http2_runtime_snapshot() -> Http2RuntimeSnapshot { fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result { serde_json::to_string(snapshot) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) + .map_err(|error| SidecarError::host("ERR_AGENTOS_NODE_SYNC_RPC", format!("{error}"))) } fn http2_event_value(event: &Http2BridgeEvent) -> Result { serde_json::to_string(event) .map(Value::String) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) + .map_err(|error| SidecarError::host("ERR_AGENTOS_NODE_SYNC_RPC", format!("{error}"))) } fn push_http2_server_event( @@ -1097,7 +1099,7 @@ fn push_http2_data_event( } fn push_http2_retain_wake( - session: Option, + session: Option, identity: Option<( agentos_runtime::capability::CapabilityId, agentos_runtime::capability::CapabilityGeneration, @@ -1246,6 +1248,7 @@ async fn await_http2_event( let mut state = shared.lock().map_err(|_| crate::state::DeferredRpcError { code: String::from("ERR_AGENTOS_HTTP2_STATE_POISONED"), message: String::from("HTTP/2 event state lock poisoned"), + details: None, })?; let notified = Arc::clone(&state.ready).notified_owned(); let queue = if is_server { @@ -1290,36 +1293,47 @@ fn defer_http2_poll( id: u64, is_server: bool, wait_ms: u64, -) -> Result { +) -> Result { if let Some(event) = pop_http2_event_now(&process.http2.shared, id, is_server) { return http2_event_value(&event).map(Into::into); } if wait_ms == 0 { return Ok(Value::Null.into()); } - let wait = Duration::from_millis(wait_ms).min(Duration::from_millis( - process.limits.reactor.operation_deadline_ms, - )); + let operation_deadline = Duration::from_millis(process.limits.reactor.operation_deadline_ms); + let requested_wait = Duration::from_millis(wait_ms); + let wait = requested_wait.min(operation_deadline); + let warn_operation_deadline = requested_wait >= operation_deadline; let shared = Arc::clone(&process.http2.shared); let (respond_to, receiver) = tokio::sync::oneshot::channel(); process .runtime_context .spawn(agentos_runtime::TaskClass::Http2, async move { - let result = - match tokio::time::timeout(wait, await_http2_event(&shared, id, is_server)).await { - Ok(Ok(Some(event))) => { - http2_event_value(&event).map_err(|error| crate::state::DeferredRpcError { - code: String::from("ERR_AGENTOS_HTTP2_EVENT_SERIALIZE"), - message: error.to_string(), - }) - } - Ok(Ok(None)) | Err(_) => Ok(Value::Null), - Ok(Err(error)) => Err(error), - }; + let wait_result = if warn_operation_deadline { + crate::execution::operation_deadline_timeout( + "HTTP/2 event poll", + wait, + await_http2_event(&shared, id, is_server), + ) + .await + } else { + tokio::time::timeout(wait, await_http2_event(&shared, id, is_server)).await + }; + let result = match wait_result { + Ok(Ok(Some(event))) => { + http2_event_value(&event).map_err(|error| crate::state::DeferredRpcError { + code: String::from("ERR_AGENTOS_HTTP2_EVENT_SERIALIZE"), + message: error.to_string(), + details: None, + }) + } + Ok(Ok(None)) | Err(_) => Ok(Value::Null), + Ok(Err(error)) => Err(error), + }; respond_to.settle(result); }) .map_err(SidecarError::from)?; - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Http2, @@ -1330,13 +1344,14 @@ fn defer_http2_wait( process: &ActiveProcess, id: u64, is_server: bool, -) -> Result { +) -> Result { let shared = Arc::clone(&process.http2.shared); let event_session = shared .lock() .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))? .event_session .clone(); + let max_adapter_event_bytes = process.limits.js_runtime.event_payload_limit_bytes; let (respond_to, receiver) = tokio::sync::oneshot::channel(); process .runtime_context @@ -1359,23 +1374,21 @@ fn defer_http2_wait( break Err(crate::state::DeferredRpcError { code: String::from("ERR_AGENTOS_HTTP2_EVENT_SERIALIZE"), message: error.to_string(), + details: None, }); } }; if let Some(session) = &event_session { - let encoded = match v8_runtime::json_to_cbor_payload(&payload) { - Ok(encoded) => encoded, - Err(error) => { - break Err(crate::state::DeferredRpcError { - code: String::from("ERR_AGENTOS_HTTP2_EVENT_SERIALIZE"), - message: error.to_string(), - }); - } - }; - if let Err(error) = session.send_stream_event("http2", encoded) { + if let Err(error) = session.send_adapter_event( + "http2", + &payload, + "limits.jsRuntime.eventPayloadLimitBytes", + max_adapter_event_bytes, + ) { break Err(crate::state::DeferredRpcError { - code: String::from("ERR_AGENTOS_HTTP2_EVENT_DELIVERY"), - message: error.to_string(), + code: error.code().to_owned(), + message: error.message().to_owned(), + details: error.details().cloned(), }); } } @@ -1386,7 +1399,7 @@ fn defer_http2_wait( respond_to.settle(result); }) .map_err(SidecarError::from)?; - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Http2, @@ -1537,7 +1550,7 @@ fn serialize_http2_headers_map( } } serde_json::to_string(&serialized) - .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))) + .map_err(|error| SidecarError::host("ERR_AGENTOS_NODE_SYNC_RPC", format!("{error}"))) } fn serialize_http2_request_headers( @@ -1605,8 +1618,9 @@ fn track_http2_capability( entry.insert(lease); Ok(()) } - std::collections::btree_map::Entry::Occupied(_) => Err(SidecarError::InvalidState( - format!("ERR_AGENTOS_CAPABILITY_DUPLICATE: HTTP/2 state already owns {key:?}"), + std::collections::btree_map::Entry::Occupied(_) => Err(SidecarError::host( + "ERR_AGENTOS_CAPABILITY_DUPLICATE", + format!("HTTP/2 state already owns {key:?}"), )), } } @@ -1687,9 +1701,10 @@ fn admit_http2_session( lease, )?; let resources = state.resources.as_ref().cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: HTTP/2 session has no VM ResourceLedger", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("HTTP/2 session has no VM ResourceLedger"), + ) })?; let stream_resources = Arc::new(ResourceLedger::root( format!("http2-session={session_id}"), @@ -1735,9 +1750,10 @@ fn reserve_http2_connection( .as_ref() .cloned() .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: HTTP/2 connection has no VM ResourceLedger", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("HTTP/2 connection has no VM ResourceLedger"), + ) })?; reserve_http2_resources(&resources, &[(ResourceClass::Http2Connections, 1)]) } @@ -1860,7 +1876,7 @@ fn spawn_http2_client_session( shared: Arc>, session_id: u64, remote_addr: SocketAddr, - tls: Option, + tls: Option, default_ca_bundle: Vec, snapshot: Arc>, mut command_rx: TokioReceiver, @@ -2121,8 +2137,15 @@ fn spawn_http2_client_session( continue; } }; - let options: JavascriptHttp2RequestOptions = - serde_json::from_str(&options_json).unwrap_or_default(); + let options: Http2RequestOptions = match serde_json::from_str(&options_json) { + Ok(options) => options, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_INVALID_ARG_VALUE: invalid HTTP/2 request options: {error}" + ))); + continue; + } + }; let stream_id = match admit_http2_stream( &shared, pending_capability, @@ -2162,33 +2185,47 @@ fn spawn_http2_client_session( } } Http2SessionCommand::Settings { settings_json, respond_to } => { - let settings = serde_json::from_str::>(&settings_json) - .unwrap_or_default(); + let settings = match serde_json::from_str::>(&settings_json) { + Ok(settings) => settings, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_INVALID_ARG_VALUE: invalid HTTP/2 settings: {error}" + ))); + continue; + } + }; { let mut snapshot = snapshot.lock().expect("http2 snapshot lock"); snapshot.local_settings = http2_settings_from_value(&settings); } - if let Ok(headers_json) = serde_json::to_string(&settings) { - push_http2_session_event( - &shared, - session_id, - Http2BridgeEvent { - kind: String::from("sessionLocalSettings"), - id: session_id, - data: Some(headers_json.clone()), - ..Http2BridgeEvent::default() - }, - ); - push_http2_session_event( - &shared, - session_id, - Http2BridgeEvent { - kind: String::from("sessionSettingsAck"), - id: session_id, - ..Http2BridgeEvent::default() - }, - ); - } + let headers_json = match serde_json::to_string(&settings) { + Ok(headers_json) => headers_json, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_AGENTOS_SERIALIZATION: failed to encode HTTP/2 settings: {error}" + ))); + continue; + } + }; + push_http2_session_event( + &shared, + session_id, + Http2BridgeEvent { + kind: String::from("sessionLocalSettings"), + id: session_id, + data: Some(headers_json.clone()), + ..Http2BridgeEvent::default() + }, + ); + push_http2_session_event( + &shared, + session_id, + Http2BridgeEvent { + kind: String::from("sessionSettingsAck"), + id: session_id, + ..Http2BridgeEvent::default() + }, + ); respond_to.settle(Ok(Value::Null)); } Http2SessionCommand::SetLocalWindowSize { size, respond_to } => { @@ -2291,7 +2328,7 @@ fn spawn_http2_server_session( server_id: u64, session_id: u64, stream: tokio::net::TcpStream, - tls: Option, + tls: Option, snapshot: Arc>, mut command_rx: TokioReceiver, capabilities: CapabilityRegistry, @@ -2366,7 +2403,10 @@ fn spawn_http2_server_session( "serverConnection" }), id: server_id, - data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()), + data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_else(|error| { + eprintln!("ERR_AGENTOS_HTTP2_SERIALIZATION: failed to encode server socket snapshot: {error}"); + String::from("{}") + })), ..Http2BridgeEvent::default() }, ); @@ -2641,30 +2681,58 @@ fn spawn_http2_server_session( } = queued_command; match command { Http2SessionCommand::Settings { settings_json, respond_to } => { - let settings = serde_json::from_str::>(&settings_json) - .unwrap_or_default(); + let settings = match serde_json::from_str::>(&settings_json) { + Ok(settings) => settings, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_INVALID_ARG_VALUE: invalid HTTP/2 settings: {error}" + ))); + continue; + } + }; if let Some(initial_window_size) = settings .get("initialWindowSize") .and_then(Value::as_u64) { - let _ = connection.set_initial_window_size(initial_window_size as u32); + let initial_window_size = match u32::try_from(initial_window_size) { + Ok(initial_window_size) => initial_window_size, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_OUT_OF_RANGE: HTTP/2 initialWindowSize does not fit u32: {error}" + ))); + continue; + } + }; + if let Err(error) = connection.set_initial_window_size(initial_window_size) { + respond_to.settle(Err(format!( + "ERR_HTTP2_INVALID_SETTING_VALUE: failed to apply initialWindowSize: {error}" + ))); + continue; + } } { let mut snapshot = snapshot.lock().expect("http2 snapshot lock"); snapshot.local_settings = http2_settings_from_value(&settings); } - if let Ok(headers_json) = serde_json::to_string(&settings) { - push_http2_session_event( - &shared, - session_id, - Http2BridgeEvent { - kind: String::from("sessionLocalSettings"), - id: session_id, - data: Some(headers_json), - ..Http2BridgeEvent::default() - }, - ); - } + let headers_json = match serde_json::to_string(&settings) { + Ok(headers_json) => headers_json, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_AGENTOS_SERIALIZATION: failed to encode HTTP/2 settings: {error}" + ))); + continue; + } + }; + push_http2_session_event( + &shared, + session_id, + Http2BridgeEvent { + kind: String::from("sessionLocalSettings"), + id: session_id, + data: Some(headers_json), + ..Http2BridgeEvent::default() + }, + ); respond_to.settle(Ok(Value::Null)); } Http2SessionCommand::SetLocalWindowSize { size, respond_to } => { @@ -2848,8 +2916,15 @@ fn spawn_http2_server_session( respond_to.settle(Ok(Value::Null)); } Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => { - let options: JavascriptHttp2FileResponseOptions = - serde_json::from_str(&options_json).unwrap_or_default(); + let options: Http2FileResponseOptions = match serde_json::from_str(&options_json) { + Ok(options) => options, + Err(error) => { + respond_to.settle(Err(format!( + "ERR_INVALID_ARG_VALUE: invalid HTTP/2 file response options: {error}" + ))); + continue; + } + }; let response = match build_http2_response(&headers_json) { Ok(response) => response, Err(error) => { @@ -2857,12 +2932,26 @@ fn spawn_http2_server_session( continue; } }; - let offset = usize::try_from(options.offset.unwrap_or_default()) - .unwrap_or(0) - .min(body.len()); + let offset = match usize::try_from(options.offset.unwrap_or_default()) { + Ok(offset) => offset.min(body.len()), + Err(error) => { + respond_to.settle(Err(format!( + "ERR_OUT_OF_RANGE: HTTP/2 file response offset does not fit usize: {error}" + ))); + continue; + } + }; let available = body.len().saturating_sub(offset); let length = match options.length { - Some(length) if length >= 0 => available.min(length as usize), + Some(length) if length >= 0 => match usize::try_from(length) { + Ok(length) => available.min(length), + Err(error) => { + respond_to.settle(Err(format!( + "ERR_OUT_OF_RANGE: HTTP/2 file response length does not fit usize: {error}" + ))); + continue; + } + }, _ => available, }; let body = Bytes::from(body).slice(offset..offset.saturating_add(length)); @@ -3148,7 +3237,7 @@ fn send_http2_command( header_bytes: usize, data_bytes: usize, command: impl FnOnce(Http2ResponseSender) -> Result, -) -> Result { +) -> Result { let (respond_to, response_rx) = tokio::sync::oneshot::channel(); let respond_to = Http2ResponseSender::new(respond_to); let reservations = reserve_http2_resources( @@ -3172,25 +3261,47 @@ fn send_http2_command( reservations, }) .map_err(|error| match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::InvalidState( + tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::host( + "ERR_AGENTOS_HTTP2_COMMAND_LIMIT", String::from( - "ERR_AGENTOS_HTTP2_COMMAND_LIMIT: HTTP/2 session command queue is full; raise limits.http2.maxPendingCommands", + "HTTP/2 session command queue is full; raise limits.http2.maxPendingCommands", ), ), - tokio::sync::mpsc::error::TrySendError::Closed(_) => SidecarError::InvalidState( - String::from("HTTP/2 session command channel closed"), - ), + tokio::sync::mpsc::error::TrySendError::Closed(_) => { + SidecarError::InvalidState(String::from("HTTP/2 session command channel closed")) + } })?; - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver: response_rx, timeout: Some(session.command_timeout), task_class: agentos_runtime::TaskClass::Http2, }) } +fn read_http2_response_file_after_admission( + kernel: &mut SidecarKernel, + requester_pid: u32, + guest_path: &str, + admitted_bytes: usize, +) -> Result, SidecarError> { + let body = kernel + .read_file_for_process(EXECUTION_DRIVER_NAME, requester_pid, guest_path) + .map_err(kernel_error)?; + if body.len() != admitted_bytes { + return Err(SidecarError::host( + "ERR_AGENTOS_HTTP2_FILE_CHANGED", + format!( + "response file size changed after admission: {guest_path} (admitted {admitted_bytes} bytes, read {} bytes)", + body.len() + ), + )); + } + Ok(body) +} + fn parse_http2_server_listen_payload( - request: &JavascriptSyncRpcRequest, -) -> Result { + request: &HostRpcRequest, +) -> Result { let payload_json = javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?; serde_json::from_str(payload_json).map_err(|error| { @@ -3201,8 +3312,8 @@ fn parse_http2_server_listen_payload( } fn parse_http2_connect_payload( - request: &JavascriptSyncRpcRequest, -) -> Result { + request: &HostRpcRequest, +) -> Result { let payload_json = javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?; serde_json::from_str(payload_json).map_err(|error| { @@ -3245,13 +3356,13 @@ fn http2_stream_for_id( } pub(in crate::execution) fn service_javascript_http2_sync_rpc( - request: JavascriptHttp2SyncRpcServiceRequest<'_, B>, -) -> Result + request: Http2ServiceRequest<'_, B>, +) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let JavascriptHttp2SyncRpcServiceRequest { + let Http2ServiceRequest { bridge, kernel, vm_id, @@ -3321,7 +3432,9 @@ where }, ); if state.event_session.is_none() { - state.event_session = process.execution.javascript_v8_session_handle(); + state.event_session = process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()); } state.server_events.entry(payload.server_id).or_default(); } @@ -3337,7 +3450,7 @@ where close_notify, capabilities.clone(), ); - javascript_net_json_string( + encode_net_json_string( json!({ "address": socket_address_value(&guest_local_addr), "capabilityId": identity.0, @@ -3516,22 +3629,26 @@ where SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")) })?; if state.event_session.is_none() { - state.event_session = process.execution.javascript_v8_session_handle(); + state.event_session = process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()); } state.session_events.entry(session_id).or_default(); } let tls = if secure { - Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions { + Some(payload.tls.unwrap_or(TlsBridgeOptions { is_server: false, servername: Some(host.to_string()), alpn_protocols: Some(vec![String::from("h2")]), - ..JavascriptTlsBridgeOptions::default() + ..TlsBridgeOptions::default() })) } else { None }; let default_ca_bundle = match tls.as_ref() { - Some(options) => vm_default_ca_bundle_for_tls_options(kernel, options)?, + Some(options) => { + vm_default_ca_bundle_for_tls_options(kernel, process.kernel_pid, options)? + } None => Vec::new(), }; spawn_http2_client_session( @@ -3546,7 +3663,7 @@ where ); let snapshot_json = http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?; - javascript_net_json_string( + encode_net_json_string( json!({ "sessionId": session_id, "capabilityId": capability_id, @@ -3880,15 +3997,23 @@ where 3, "net.http2_stream_respond_with_file options", )?; + serde_json::from_str::(options_json).map_err(|error| { + SidecarError::host( + "ERR_INVALID_ARG_VALUE", + format!("invalid HTTP/2 file response options: {error}"), + ) + })?; let stream = http2_stream_for_id(process, stream_id)?; let session = http2_session_for_id(process, stream.session_id)?; let guest_path = resolve_http2_file_response_guest_path(process, path); - let file_bytes = usize::try_from(kernel.stat(&guest_path).map_err(kernel_error)?.size) - .map_err(|_| { - SidecarError::Execution(format!( - "EFBIG: HTTP/2 response file size does not fit usize: {guest_path}" - )) - })?; + let requester_pid = process.kernel_pid; + let file_bytes = kernel + .preflight_regular_file_read_for_process( + EXECUTION_DRIVER_NAME, + requester_pid, + &guest_path, + ) + .map_err(kernel_error)?; let command_bytes = file_bytes .saturating_add(headers_json.len()) .saturating_add(options_json.len()) @@ -3899,12 +4024,12 @@ where headers_json.len(), file_bytes, |respond_to| { - let body = kernel.read_file(&guest_path).map_err(kernel_error)?; - if body.len() > file_bytes { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_HTTP2_FILE_CHANGED: response file grew after admission: {guest_path}" - ))); - } + let body = read_http2_response_file_after_admission( + kernel, + requester_pid, + &guest_path, + file_bytes, + )?; Ok(Http2SessionCommand::StreamRespondWithFile { stream_id, body, @@ -3925,6 +4050,12 @@ where #[cfg(test)] mod http2_reactor_tests { use super::*; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::KernelVmConfig; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::resource_accounting::ResourceLimits; + use agentos_kernel::vfs::MemoryFileSystem; fn http2_ledger(event_limit: usize) -> Arc { Arc::new(ResourceLedger::root( @@ -3996,6 +4127,30 @@ mod http2_reactor_tests { (resources, registry) } + fn http2_file_test_kernel(max_pread_bytes: usize) -> (SidecarKernel, u32) { + let mut config = KernelVmConfig::new("vm-http2-file-response"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_pread_bytes: Some(max_pread_bytes), + ..ResourceLimits::default() + }; + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register HTTP/2 file test driver"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn HTTP/2 file test process"); + (kernel, process.pid()) + } + #[test] fn http2_multi_resource_admission_rolls_back_exactly() { let ledger = ResourceLedger::root( @@ -4139,7 +4294,7 @@ mod http2_reactor_tests { }) }) .expect("command admission"); - let JavascriptSyncRpcServiceResponse::Deferred { receiver, .. } = response else { + let HostServiceResponse::Deferred { receiver, .. } = response else { panic!("HTTP/2 command must return a deferred response"); }; let queued = command_rx.try_recv().expect("queued command"); @@ -4161,6 +4316,136 @@ mod http2_reactor_tests { assert!(resources.is_zero()); } + #[test] + fn http2_command_payload_failure_releases_every_reservation() { + let resources = http2_ledger(4); + let stream_resources = Arc::new(ResourceLedger::root( + "http2-session=payload-failure", + [( + ResourceClass::Http2Streams, + ResourceLimit::new(1, "limits.http2.maxStreamsPerConnection"), + )], + )); + let (command_tx, mut command_rx) = tokio_channel(1); + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("runtime") + .context(); + let session = ActiveHttp2Session { + command_tx, + capability_id: 1, + vm_generation: 1, + fairness: runtime.fairness().clone(), + command_timeout: Duration::from_secs(1), + close_requested: Arc::new(AtomicBool::new(false)), + close_abrupt: Arc::new(AtomicBool::new(false)), + close_notify: Arc::new(tokio::sync::Notify::new()), + _reservations: Vec::new(), + resources: Arc::clone(&resources), + stream_resources, + }; + + let error = send_http2_command(&session, 32, 4, 8, |_respond_to| { + Err(SidecarError::host( + "ERR_AGENTOS_HTTP2_FILE_CHANGED", + String::from("injected file race"), + )) + }) + .err() + .expect("payload construction failure must propagate"); + assert_eq!(error.code(), Some("ERR_AGENTOS_HTTP2_FILE_CHANGED")); + assert!(command_rx.try_recv().is_err()); + assert!( + resources.is_zero(), + "payload failure must release command/header/data reservations" + ); + } + + #[test] + fn http2_full_command_queue_releases_rejected_reservations() { + let resources = http2_ledger(4); + let stream_resources = Arc::new(ResourceLedger::root( + "http2-session=queue-full", + [( + ResourceClass::Http2Streams, + ResourceLimit::new(1, "limits.http2.maxStreamsPerConnection"), + )], + )); + let (command_tx, mut command_rx) = tokio_channel(1); + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("runtime") + .context(); + let session = ActiveHttp2Session { + command_tx, + capability_id: 1, + vm_generation: 1, + fairness: runtime.fairness().clone(), + command_timeout: Duration::from_secs(1), + close_requested: Arc::new(AtomicBool::new(false)), + close_abrupt: Arc::new(AtomicBool::new(false)), + close_notify: Arc::new(tokio::sync::Notify::new()), + _reservations: Vec::new(), + resources: Arc::clone(&resources), + stream_resources, + }; + + let first_response = send_http2_command(&session, 32, 4, 8, |respond_to| { + Ok(Http2SessionCommand::Settings { + settings_json: String::from("{}"), + respond_to, + }) + }) + .expect("fill command queue"); + let charged_classes = [ + ResourceClass::Http2Commands, + ResourceClass::Http2CommandBytes, + ResourceClass::Http2HeaderBytes, + ResourceClass::Http2DataBytes, + ResourceClass::Http2BufferedBytes, + ResourceClass::BufferedBytes, + ]; + let usage_before = charged_classes.map(|class| resources.usage(class).used); + + let error = send_http2_command(&session, 32, 4, 8, |respond_to| { + Ok(Http2SessionCommand::Settings { + settings_json: String::from("{}"), + respond_to, + }) + }) + .err() + .expect("full queue must reject the second command"); + assert_eq!(error.code(), Some("ERR_AGENTOS_HTTP2_COMMAND_LIMIT")); + assert_eq!( + charged_classes.map(|class| resources.usage(class).used), + usage_before, + "queue rejection must release only the rejected command reservations" + ); + + drop(command_rx.try_recv().expect("drain admitted command")); + drop(first_response); + assert!(resources.is_zero()); + } + + #[test] + fn http2_file_read_rejects_growth_after_admission() { + let (mut kernel, pid) = http2_file_test_kernel(16); + kernel + .write_file("/response", b"body".to_vec()) + .expect("write initial response"); + let admitted = kernel + .preflight_regular_file_read_for_process(EXECUTION_DRIVER_NAME, pid, "/response") + .expect("preflight response"); + kernel + .write_file("/response", b"grown".to_vec()) + .expect("grow response after admission"); + + let error = + read_http2_response_file_after_admission(&mut kernel, pid, "/response", admitted) + .expect_err("growth after admission must fail"); + assert_eq!(error.code(), Some("ERR_AGENTOS_HTTP2_FILE_CHANGED")); + } + #[test] fn http2_fair_turn_reports_actual_usage_before_next_capability_runs() { let runtime = diff --git a/crates/native-sidecar/src/execution/network/http_client.rs b/crates/native-sidecar/src/execution/network/http_client.rs new file mode 100644 index 0000000000..184176921c --- /dev/null +++ b/crates/native-sidecar/src/execution/network/http_client.rs @@ -0,0 +1,313 @@ +use super::super::*; +use agentos_execution::backend::{HostServiceError, PayloadLimit}; +use agentos_execution::host::HttpHeader; + +pub(in crate::execution) struct BoundedHttpRequest { + pub(in crate::execution) url: Url, + pub(in crate::execution) method: String, + pub(in crate::execution) headers: Vec, + pub(in crate::execution) body: Vec, + pub(in crate::execution) pinned_addresses: Vec, + pub(in crate::execution) default_ca_bundle: Vec, + pub(in crate::execution) max_response_bytes: usize, + pub(in crate::execution) max_header_bytes: usize, + pub(in crate::execution) max_body_bytes: usize, +} + +fn is_http_token(value: &str) -> bool { + !value.is_empty() + && value.bytes().all(|byte| { + byte.is_ascii_alphanumeric() + || matches!( + byte, + b'!' | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'.' + | b'^' + | b'_' + | b'`' + | b'|' + | b'~' + ) + }) +} + +pub(in crate::execution) fn validate_http_request_metadata( + method: &str, + headers: &[HttpHeader], +) -> Result<(), SidecarError> { + if !is_http_token(method) { + return Err(SidecarError::host( + "EINVAL", + "outbound HTTP method must be a valid HTTP token", + )); + } + for header in headers { + if !is_http_token(header.name.as_str()) { + return Err(SidecarError::host( + "EINVAL", + format!( + "outbound HTTP header name {:?} is not a valid HTTP token", + header.name.as_str() + ), + )); + } + if header.value.as_str().bytes().any(|byte| { + byte == b'\r' || byte == b'\n' || byte == 0x7f || (byte < 0x20 && byte != b'\t') + }) { + return Err(SidecarError::host( + "EINVAL", + format!( + "outbound HTTP header {:?} contains a forbidden control character", + header.name.as_str() + ), + )); + } + } + Ok(()) +} + +fn split_http_netloc(netloc: &str) -> Option<(&str, u16)> { + let (host, port) = netloc.rsplit_once(':')?; + let port: u16 = port.parse().ok()?; + let host = host + .strip_prefix('[') + .and_then(|rest| rest.strip_suffix(']')) + .unwrap_or(host); + Some((host, port)) +} + +fn http_limit(limit_name: &'static str, limit: usize, observed: usize) -> SidecarError { + SidecarError::Host(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + limit_name, + limit as u64, + observed as u64, + )) +} + +pub(in crate::execution) fn issue_bounded_http_request( + request: BoundedHttpRequest, +) -> Result { + validate_http_request_metadata(&request.method, &request.headers)?; + if request.pinned_addresses.is_empty() { + return Err(SidecarError::host( + "EACCES", + "no egress-vetted address available for outbound HTTP request", + )); + } + let pinned_host = request.url.host_str().map(str::to_owned); + let pinned_port = request.url.port_or_known_default(); + let pinned = request.pinned_addresses; + let resolver = move |netloc: &str| -> std::io::Result> { + let (host, port) = split_http_netloc(netloc).ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid network location: {netloc}"), + ) + })?; + if pinned_host.as_deref() != Some(host) || pinned_port != Some(port) { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + format!("EACCES: outbound HTTP resolver is not pinned for {host}:{port}"), + )); + } + Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect()) + }; + let mut agent_builder = ureq::AgentBuilder::new() + .resolver(resolver) + .redirects(0) + .timeout_connect(Duration::from_secs(5)) + .timeout_read(Duration::from_secs(15)) + .timeout_write(Duration::from_secs(15)); + if request.url.scheme() == "https" { + let tls_options = TlsBridgeOptions { + is_server: false, + servername: request.url.host_str().map(str::to_owned), + alpn_protocols: Some(vec![String::from("http/1.1")]), + ..TlsBridgeOptions::default() + }; + agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config( + &tls_options, + &request.default_ca_bundle, + )?)); + } + let agent = agent_builder.build(); + let mut outbound = agent.request_url(&request.method, &request.url); + for header in request.headers { + if header.name.as_str().eq_ignore_ascii_case("host") { + continue; + } + outbound = outbound.set(header.name.as_str(), header.value.as_str()); + } + let response = if request.body.is_empty() { + outbound.call() + } else { + outbound.send_bytes(&request.body) + }; + let response = match response { + Ok(response) | Err(ureq::Error::Status(_, response)) => response, + Err(ureq::Error::Transport(error)) => { + return Err(SidecarError::host( + "ERR_HTTP_REQUEST_FAILED", + error.to_string(), + )) + } + }; + + let status = response.status(); + let reason = response.status_text().to_owned(); + let mut header_bytes = 0usize; + let mut headers = BTreeMap::>::new(); + for raw_name in response.headers_names() { + for value in response.all(&raw_name) { + header_bytes = header_bytes + .saturating_add(raw_name.len()) + .saturating_add(value.len()); + if header_bytes > request.max_header_bytes { + return Err(http_limit( + "runtime.network.maxHttpHeaderBytes", + request.max_header_bytes, + header_bytes, + )); + } + headers + .entry(raw_name.to_ascii_lowercase()) + .or_default() + .push(value.to_owned()); + } + } + if let Some(content_length) = response + .header("content-length") + .and_then(|value| value.parse::().ok()) + { + if content_length > request.max_body_bytes { + return Err(http_limit( + "limits.http.maxFetchResponseBytes", + request.max_body_bytes, + content_length, + )); + } + } + let mut body = Vec::with_capacity(request.max_body_bytes.min(64 * 1024)); + response + .into_reader() + .take(request.max_body_bytes.saturating_add(1) as u64) + .read_to_end(&mut body) + .map_err(|error| { + SidecarError::host( + "ERR_HTTP_REQUEST_FAILED", + format!("failed to read HTTP response: {error}"), + ) + })?; + if body.len() > request.max_body_bytes { + return Err(http_limit( + "limits.http.maxFetchResponseBytes", + request.max_body_bytes, + body.len(), + )); + } + let result = json!({ + "status": status, + "reason": reason, + "url": request.url.as_str(), + "headers": headers, + "bodyBase64": base64::engine::general_purpose::STANDARD.encode(body), + }); + PayloadLimit::new( + "limits.reactor.maxBridgeResponseBytes", + request.max_response_bytes, + )? + .admit_json(&result) + .map_err(SidecarError::Host)?; + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Read as _, Write as _}; + use std::net::TcpListener; + + #[test] + fn split_http_netloc_handles_ipv4_names_and_ipv6() { + assert_eq!( + split_http_netloc("example.test:80"), + Some(("example.test", 80)) + ); + assert_eq!(split_http_netloc("[::1]:443"), Some(("::1", 443))); + assert_eq!(split_http_netloc("missing-port"), None); + } + + #[test] + fn typed_http_limit_names_the_public_configuration_path() { + let SidecarError::Host(error) = http_limit("limits.http.maxFetchResponseBytes", 8, 9) + else { + panic!("expected typed host error"); + }; + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + let details = error.details.expect("limit details"); + assert_eq!(details["configPath"], "limits.http.maxFetchResponseBytes"); + assert_eq!(details["limit"], 8); + assert_eq!(details["observed"], 9); + } + + fn header(name: &str, value: &str) -> HttpHeader { + let limit = PayloadLimit::new("test.http.metadata", 1024).expect("metadata limit"); + HttpHeader { + name: agentos_execution::host::BoundedString::try_new(name.to_owned(), &limit) + .expect("header name"), + value: agentos_execution::host::BoundedString::try_new(value.to_owned(), &limit) + .expect("header value"), + } + } + + #[test] + fn http_metadata_rejects_invalid_methods_names_and_values() { + validate_http_request_metadata("GET", &[header("x-test", "ok\tvalue")]) + .expect("valid metadata"); + assert!(validate_http_request_metadata("GET bad", &[]).is_err()); + assert!(validate_http_request_metadata("GET", &[header("bad name", "ok")]).is_err()); + assert!( + validate_http_request_metadata("GET", &[header("x-test", "bad\r\nvalue")]).is_err() + ); + } + + #[test] + fn bounded_http_client_does_not_follow_redirects() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let address = listener.local_addr().expect("test server address"); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept one request"); + let mut request = [0u8; 1024]; + let _ = stream.read(&mut request).expect("read request"); + stream + .write_all( + b"HTTP/1.1 302 Found\r\nLocation: /redirected\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .expect("write redirect"); + }); + let url = + Url::parse(&format!("http://127.0.0.1:{}/start", address.port())).expect("test URL"); + let result = issue_bounded_http_request(BoundedHttpRequest { + url, + method: String::from("GET"), + headers: Vec::new(), + body: Vec::new(), + pinned_addresses: vec![address.ip()], + default_ca_bundle: Vec::new(), + max_response_bytes: 4096, + max_header_bytes: 1024, + max_body_bytes: 1024, + }) + .expect("redirect response"); + server.join().expect("test server"); + assert_eq!(result["status"], 302); + } +} diff --git a/crates/native-sidecar/src/execution/network/managed.rs b/crates/native-sidecar/src/execution/network/managed.rs new file mode 100644 index 0000000000..f1151443f9 --- /dev/null +++ b/crates/native-sidecar/src/execution/network/managed.rs @@ -0,0 +1,665 @@ +use super::super::*; +use crate::state::DeferredRpcError; + +fn managed_would_block_value() -> Value { + json!({ "kind": "wouldBlock" }) +} + +pub(in crate::execution) struct ManagedNetworkServiceContext<'a> { + pub(in crate::execution) vm_id: &'a str, + pub(in crate::execution) socket_paths: &'a SocketPathContext, + pub(in crate::execution) kernel: &'a mut SidecarKernel, + pub(in crate::execution) kernel_readiness: KernelSocketReadinessRegistry, + pub(in crate::execution) process: &'a mut ActiveProcess, + pub(in crate::execution) capabilities: CapabilityRegistry, +} + +pub(in crate::execution) fn service_managed_network_operation( + context: ManagedNetworkServiceContext<'_>, + operation: agentos_execution::host::NetworkOperation, +) -> Result { + match operation { + agentos_execution::host::NetworkOperation::ManagedPoll { socket_id, wait_ms } => { + managed_socket_poll(context, socket_id.as_str(), wait_ms) + } + agentos_execution::host::NetworkOperation::ManagedRead { + socket_id, + max_bytes, + peek, + wait_ms, + } => managed_socket_read(context, socket_id.as_str(), max_bytes, peek, wait_ms), + agentos_execution::host::NetworkOperation::ManagedWaitConnect { socket_id } => { + let info = if let Some(socket) = context.process.tcp_sockets.get(socket_id.as_str()) { + socket.socket_info() + } else { + context + .process + .unix_sockets + .get(socket_id.as_str()) + .ok_or_else(|| { + SidecarError::host( + "EBADF", + format!("unknown net socket {}", socket_id.as_str()), + ) + })? + .socket_info() + }; + serde_json::to_string(&info) + .map(Value::String) + .map(HostServiceResponse::Json) + .map_err(|error| SidecarError::host("EIO", format!("encode socket info: {error}"))) + } + agentos_execution::host::NetworkOperation::ManagedWrite { socket_id, bytes } => { + managed_socket_write(context, socket_id.as_str(), bytes.as_slice()) + } + agentos_execution::host::NetworkOperation::ManagedDestroy { socket_id } => { + if let Some(socket) = context.process.tcp_sockets.remove(socket_id.as_str()) { + release_tcp_socket_handle( + context.process, + socket_id.as_str(), + socket, + context.kernel, + &context.kernel_readiness, + ); + } else if let Some(socket) = context.process.unix_sockets.remove(socket_id.as_str()) { + release_unix_socket_handle( + context.process, + socket_id.as_str(), + socket, + &context.socket_paths.unix_bound_addresses, + ); + } + Ok(Value::Null.into()) + } + agentos_execution::host::NetworkOperation::ManagedTlsUpgrade { + socket_id, + options_json, + } => managed_tls_upgrade(context, socket_id.as_str(), options_json.as_str()), + agentos_execution::host::NetworkOperation::ManagedCloseListener { listener_id } => { + managed_close_listener(context, listener_id.as_str()) + } + agentos_execution::host::NetworkOperation::ManagedAccept { listener_id } => { + managed_accept(context, listener_id.as_str()) + } + other => Err(SidecarError::host( + "EINVAL", + format!("managed reactor received unsupported operation: {other:?}"), + )), + } +} + +fn managed_socket_poll( + mut context: ManagedNetworkServiceContext<'_>, + socket_id: &str, + _wait_ms: u64, +) -> Result { + let read_state = prime_managed_socket_read_state(&mut context, socket_id)?; + let state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + if !state.bytes.is_empty() { + return Ok(json!({ "readable": true }).into()); + } + match state.terminal.as_ref() { + Some(SocketReadTerminal::End) => Ok(json!({ "type": "end" }).into()), + Some(SocketReadTerminal::Closed { had_error }) => Ok(json!({ + "type": "close", + "hadError": had_error, + }) + .into()), + Some(SocketReadTerminal::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message.clone(), + )), + None => Ok(Value::Null.into()), + } +} + +fn managed_socket_read( + mut context: ManagedNetworkServiceContext<'_>, + socket_id: &str, + max_bytes: u64, + peek: bool, + _wait_ms: u64, +) -> Result { + let maximum = usize::try_from(max_bytes).map_err(|_| { + SidecarError::host( + "EOVERFLOW", + format!("managed socket read length {max_bytes} exceeds usize"), + ) + })?; + if maximum == 0 { + return Ok(HostServiceResponse::Raw(Vec::new())); + } + + let read_state = prime_managed_socket_read_state(&mut context, socket_id)?; + let mut state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + if !state.bytes.is_empty() { + let count = maximum.min(state.bytes.len()); + let payload = state.bytes.iter().take(count).copied().collect::>(); + let source_reservations = state.source_reservations.clone(); + if !peek { + state.bytes.drain(..count); + if state.bytes.is_empty() { + state.source_reservations.clear(); + } + } + return Ok(HostServiceResponse::SourceBackedRaw { + payload, + source_reservations, + }); + } + match state.terminal.as_ref() { + Some(SocketReadTerminal::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message.clone(), + )), + Some(SocketReadTerminal::End | SocketReadTerminal::Closed { .. }) => Ok(Value::Null.into()), + None => Ok(HostServiceResponse::Json(managed_would_block_value())), + } +} + +fn prime_managed_socket_read_state( + context: &mut ManagedNetworkServiceContext<'_>, + socket_id: &str, +) -> Result>, SidecarError> { + let trace_enabled = net_tcp_trace_enabled(&context.process.env); + let (read_state, event) = if let Some(socket) = context.process.tcp_sockets.get_mut(socket_id) { + let read_state = Arc::clone(&socket.read_state); + let already_ready = { + let state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + !state.bytes.is_empty() || state.terminal.is_some() + }; + if already_ready { + return Ok(read_state); + } + socket.set_application_read_interest(true)?; + let event = socket.poll( + context.kernel, + context.process.kernel_pid, + Duration::ZERO, + trace_enabled, + )?; + (read_state, event) + } else if let Some(socket) = context.process.unix_sockets.get_mut(socket_id) { + let read_state = Arc::clone(&socket.read_state); + let already_ready = { + let state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + !state.bytes.is_empty() || state.terminal.is_some() + }; + if already_ready { + return Ok(read_state); + } + socket.set_application_read_interest(true)?; + let event = socket.poll(Duration::ZERO)?; + (read_state, event) + } else { + return Err(SidecarError::host( + "EBADF", + format!("unknown net socket {socket_id}"), + )); + }; + + let Some(event) = event else { + return Ok(read_state); + }; + let mut state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + match event { + TcpSocketEvent::Data { + bytes, + reservation, + mut source_reservations, + } => { + state.bytes.extend(bytes); + source_reservations.push(reservation); + state.source_reservations.extend(source_reservations); + } + TcpSocketEvent::End => state.terminal = Some(SocketReadTerminal::End), + TcpSocketEvent::Close { had_error } => { + state.terminal = Some(SocketReadTerminal::Closed { had_error }); + } + TcpSocketEvent::Error { code, message } => { + state.terminal = Some(SocketReadTerminal::Error { code, message }); + } + } + drop(state); + Ok(read_state) +} + +/// Re-probe one connected managed description without consuming guest-visible +/// bytes. Transport events are folded into the description-owned read state, +/// so a later read observes exactly the level that made poll return readable. +pub(in crate::execution) fn probe_managed_socket_readable( + context: &mut ManagedNetworkServiceContext<'_>, + socket_id: &str, +) -> Result { + let read_state = prime_managed_socket_read_state(context, socket_id)?; + let state = read_state.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_SOCKET_READ_STATE_POISONED", + format!("managed socket {socket_id} read state lock poisoned"), + ) + })?; + Ok(!state.bytes.is_empty() || state.terminal.is_some()) +} + +fn managed_socket_write( + context: ManagedNetworkServiceContext<'_>, + socket_id: &str, + bytes: &[u8], +) -> Result { + if let Some(socket) = context.process.tcp_sockets.get(socket_id) { + let receiver = if socket.tls_mode.load(Ordering::SeqCst) { + Some(( + socket.begin_tls_write(bytes)?, + agentos_runtime::TaskClass::Tls, + )) + } else if socket.kernel_socket_id.is_none() { + Some(( + socket.begin_plain_write(bytes)?, + agentos_runtime::TaskClass::Socket, + )) + } else { + None + }; + if let Some((receiver, task_class)) = receiver { + return Ok(HostServiceResponse::Deferred { + receiver, + timeout: (task_class == agentos_runtime::TaskClass::Tls) + .then_some(reactor_io_limits(&context.process.limits).operation_deadline), + task_class, + }); + } + return context + .process + .tcp_sockets + .get(socket_id) + .expect("validated TCP socket remains registered") + .write_all(context.kernel, context.process.kernel_pid, bytes) + .map(|written| json!(written).into()); + } + let socket = + context.process.unix_sockets.get(socket_id).ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown net socket {socket_id}")) + })?; + Ok(HostServiceResponse::Deferred { + receiver: socket.begin_plain_write(bytes)?, + timeout: None, + task_class: agentos_runtime::TaskClass::Socket, + }) +} + +fn managed_tls_upgrade( + context: ManagedNetworkServiceContext<'_>, + socket_id: &str, + options_json: &str, +) -> Result { + let options: TlsBridgeOptions = serde_json::from_str(options_json) + .map_err(|error| SidecarError::host("EINVAL", format!("invalid TLS options: {error}")))?; + if context + .process + .capability_leases + .contains_key(&NativeCapabilityKey::TlsSocket(socket_id.to_owned())) + { + return Err(SidecarError::host( + "EALREADY", + format!("TCP socket {socket_id} is already upgraded to TLS"), + )); + } + let pending = reserve_capability(&context.capabilities, CapabilityKind::TlsTransport)?; + let socket = + context.process.tcp_sockets.get(socket_id).ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown TCP socket {socket_id}")) + })?; + let receiver = socket.upgrade_tls( + context.vm_id, + context.kernel, + context.process.kernel_pid, + options, + )?; + let kernel_socket_id = socket.kernel_socket_id; + commit_process_capability( + context.process, + pending, + NativeCapabilityKey::TlsSocket(socket_id.to_owned()), + format!("tls-{socket_id}"), + kernel_socket_id, + )?; + Ok(HostServiceResponse::Deferred { + receiver, + timeout: Some(reactor_io_limits(&context.process.limits).operation_deadline), + task_class: agentos_runtime::TaskClass::Tls, + }) +} + +fn managed_close_listener( + context: ManagedNetworkServiceContext<'_>, + listener_id: &str, +) -> Result { + if let Some(listener) = context.process.tcp_listeners.remove(listener_id) { + release_tcp_listener_handle( + context.process, + listener_id, + listener, + context.kernel, + &context.kernel_readiness, + )?; + return Ok(Value::Null.into()); + } + let listener = context + .process + .unix_listeners + .remove(listener_id) + .ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown net listener {listener_id}")) + })?; + release_unix_listener_capability(context.process, listener_id, &listener)?; + if !listener.is_final_description_handle() { + return Ok(Value::Null.into()); + } + for socket in context + .process + .unix_sockets + .values_mut() + .filter(|socket| socket.listener_id.as_deref() == Some(listener_id)) + { + socket.cache_remote_peer_metadata(&context.socket_paths.unix_bound_addresses)?; + } + close_pending_guest_unix_connections( + &context.socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + )?; + release_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + )?; + purge_guest_unix_target( + &context.socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + )?; + let completion = listener.close(); + let deadline = reactor_io_limits(&context.process.limits).operation_deadline; + let listener_id = listener_id.to_owned(); + let (respond_to, receiver) = tokio::sync::oneshot::channel(); + context + .process + .runtime_context + .spawn(agentos_runtime::TaskClass::Listener, async move { + let result = match crate::execution::operation_deadline_timeout( + "Unix listener close", + deadline, + completion, + ) + .await + { + Ok(Ok(())) => Ok(Value::Null), + Ok(Err(_)) => Err(DeferredRpcError { + code: "ERR_AGENTOS_LISTENER_CLOSE".to_owned(), + message: format!( + "Unix listener {listener_id} close task ended without acknowledgement" + ), + details: None, + }), + Err(_) => Err(DeferredRpcError { + code: "ETIMEDOUT".to_owned(), + message: format!( + "Unix listener {listener_id} close exceeded {}ms; raise limits.reactor.operationDeadlineMs", + deadline.as_millis() + ), + details: None, + }), + }; + if respond_to.send(result).is_err() { + eprintln!( + "ERR_AGENTOS_LISTENER_CLOSE_COMPLETION_DROPPED: caller stopped waiting for Unix listener {listener_id}" + ); + } + }) + .map_err(SidecarError::from)?; + Ok(HostServiceResponse::Deferred { + receiver, + timeout: None, + task_class: agentos_runtime::TaskClass::Listener, + }) +} + +fn managed_accept( + context: ManagedNetworkServiceContext<'_>, + listener_id: &str, +) -> Result { + let trace_enabled = net_tcp_trace_enabled(&context.process.env); + if let Some(listener) = context.process.tcp_listeners.get_mut(listener_id) { + let pending_capability = + reserve_capability(&context.capabilities, CapabilityKind::TcpSocket)?; + return match listener.poll( + context.kernel, + context.process.kernel_pid, + Duration::ZERO, + trace_enabled, + )? { + Some(TcpListenerEvent::Connection(pending)) => { + let PendingTcpSocket { + stream, + kernel_socket_id, + guest_local_addr, + guest_remote_addr, + } = pending; + let mut info = tcp_socket_info_value(&guest_local_addr, &guest_remote_addr); + let mut socket = if let Some(stream) = stream { + ActiveTcpSocket::from_stream( + stream, + Some(listener_id.to_owned()), + guest_local_addr, + guest_remote_addr, + context.capabilities.resources(), + context.process.runtime_context.clone(), + reactor_io_limits(&context.process.limits), + )? + } else { + ActiveTcpSocket::from_kernel( + kernel_socket_id.ok_or_else(|| { + SidecarError::host("EIO", "kernel TCP accept missing socket id") + })?, + Some(listener_id.to_owned()), + guest_local_addr, + guest_remote_addr, + context.capabilities.resources(), + context.process.runtime_context.clone(), + reactor_io_limits(&context.process.limits), + ) + }; + let socket_id = context.process.allocate_tcp_socket_id(); + let capability_key = NativeCapabilityKey::TcpSocket(socket_id.clone()); + let identity = match commit_process_capability( + context.process, + pending_capability, + capability_key.clone(), + socket_id.clone(), + socket.kernel_socket_id, + ) { + Ok(identity) => identity, + Err(error) => { + if let Err(cleanup_error) = + socket.close(context.kernel, context.process.kernel_pid) + { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close accepted socket after capability commit failure: {cleanup_error}" + ); + } + return Err(error); + } + }; + socket.set_event_pusher( + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(identity), + Arc::clone(&context.process.process_event_notify), + ); + if let Value::Object(fields) = &mut info { + fields.insert("capabilityId".to_owned(), json!(identity.0)); + fields.insert("capabilityGeneration".to_owned(), json!(identity.1)); + } + socket.set_fairness_identity( + context + .process + .capability_fairness_identity(&capability_key), + )?; + socket.retain_description_lease( + context + .process + .shared_capability_lease(&capability_key) + .expect("committed TCP capability lease"), + ); + register_kernel_readiness_target( + &context.kernel_readiness, + socket.kernel_socket_id, + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(Arc::clone(&socket.read_event_notify)), + context + .process + .capability_readiness_identity(&capability_key), + socket_id.clone(), + KernelSocketReadinessEvent::Data, + ); + if let Some(listener) = context.process.tcp_listeners.get_mut(listener_id) { + socket.listener_connection_retirement = + Some(listener.register_connection(&socket_id)); + } + context + .process + .tcp_sockets + .insert(socket_id.clone(), socket); + encode_net_json_string( + json!({ "socketId": socket_id, "info": info }), + "net.server_accept", + ) + .map(Into::into) + } + Some(TcpListenerEvent::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message, + )), + None => Ok(HostServiceResponse::Json(managed_would_block_value())), + }; + } + + let target_binding_id = context + .process + .unix_listeners + .get(listener_id) + .ok_or_else(|| SidecarError::host("EBADF", format!("unknown net listener {listener_id}")))? + .registry_binding_id + .clone(); + let event = context + .process + .unix_listeners + .get_mut(listener_id) + .expect("validated Unix listener remains registered") + .poll(Duration::ZERO)?; + match event { + Some(UnixListenerEvent::Connection { + socket: mut pending, + capability: pending_capability, + }) => { + let mut info = json!({ + "localPath": pending.local_path.clone(), + "remotePath": pending.remote_path.clone(), + "localAbstractPathHex": pending.local_abstract_path_hex.clone(), + "remoteAbstractPathHex": pending.remote_abstract_path_hex.clone(), + }); + let mut socket = ActiveUnixSocket::from_stream_with_metadata( + pending.stream, + Some(listener_id.to_owned()), + pending.local_path, + pending.remote_path, + pending.local_abstract_path_hex, + pending.remote_abstract_path_hex, + None, + None, + context.capabilities.resources(), + context.process.runtime_context.clone(), + reactor_io_limits(&context.process.limits), + )?; + socket.connection_state = pending.connection_guard.state.take(); + socket.remote_registry_binding_id = Some(target_binding_id); + let socket_id = context.process.allocate_unix_socket_id(); + let capability_key = NativeCapabilityKey::UnixSocket(socket_id.clone()); + let identity = commit_process_capability( + context.process, + pending_capability, + capability_key.clone(), + socket_id.clone(), + None, + )?; + socket.set_event_pusher( + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(identity), + Arc::clone(&context.process.process_event_notify), + ); + socket.set_fairness_identity( + context + .process + .capability_fairness_identity(&capability_key), + )?; + socket.retain_description_lease( + context + .process + .shared_capability_lease(&capability_key) + .expect("committed Unix capability lease"), + ); + if let Value::Object(fields) = &mut info { + fields.insert("capabilityId".to_owned(), json!(identity.0)); + fields.insert("capabilityGeneration".to_owned(), json!(identity.1)); + } + if let Some(listener) = context.process.unix_listeners.get_mut(listener_id) { + socket.listener_connection_retirement = + Some(listener.register_connection(&socket_id)); + } + context + .process + .unix_sockets + .insert(socket_id.clone(), socket); + encode_net_json_string( + json!({ "socketId": socket_id, "info": info }), + "net.server_accept", + ) + .map(Into::into) + } + Some(UnixListenerEvent::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message, + )), + None => Ok(HostServiceResponse::Json(managed_would_block_value())), + } +} diff --git a/crates/native-sidecar/src/execution/network/managed_endpoint.rs b/crates/native-sidecar/src/execution/network/managed_endpoint.rs new file mode 100644 index 0000000000..03f50d71e2 --- /dev/null +++ b/crates/native-sidecar/src/execution/network/managed_endpoint.rs @@ -0,0 +1,1071 @@ +use super::super::*; +use agentos_execution::host::{ + ManagedTcpEndpoint, ManagedUnixAddress, NetworkOperation as HostNetworkOperation, +}; + +/// Runtime-neutral inputs required by the stateful managed endpoint lifecycle. +/// Executor adapters are responsible only for decoding compatibility payloads into +/// [`HostNetworkOperation`]; endpoint state and kernel/capability mutations +/// remain sidecar-owned here for every executor. +pub(in crate::execution) struct ManagedEndpointServiceContext<'a, B> { + pub(in crate::execution) bridge: &'a SharedBridge, + pub(in crate::execution) vm_id: &'a str, + pub(in crate::execution) dns: &'a VmDnsConfig, + pub(in crate::execution) socket_paths: &'a SocketPathContext, + pub(in crate::execution) kernel: &'a mut SidecarKernel, + pub(in crate::execution) kernel_readiness: KernelSocketReadinessRegistry, + pub(in crate::execution) process: &'a mut ActiveProcess, + pub(in crate::execution) capabilities: CapabilityRegistry, + pub(in crate::execution) call_id: u64, +} + +pub(in crate::execution) fn service_managed_endpoint_operation( + context: ManagedEndpointServiceContext<'_, B>, + operation: HostNetworkOperation, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let response = match operation { + HostNetworkOperation::ManagedBindUnix { address } => bind_unix_endpoint(context, address)?, + HostNetworkOperation::ManagedBindConnectedUnix { socket_id, address } => { + bind_connected_unix_endpoint(context, socket_id.as_str(), address)? + } + HostNetworkOperation::ManagedReserveTcpPort { host, port } => { + reserve_tcp_port(context, host.as_ref().map(|host| host.as_str()), port)? + } + HostNetworkOperation::ManagedReleaseTcpPort { reservation_id } => { + context + .process + .tcp_port_reservations + .remove(reservation_id.as_str()); + Value::Null + } + HostNetworkOperation::ManagedConnect { endpoint } => { + return connect_endpoint(context, endpoint); + } + HostNetworkOperation::ManagedListen { endpoint } => listen_endpoint(context, endpoint)?, + other => { + return Err(SidecarError::host( + "EINVAL", + format!("managed endpoint reactor received unsupported operation: {other:?}"), + )) + } + }; + Ok(HostServiceResponse::Json(response)) +} + +fn bind_unix_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + address: ManagedUnixAddress, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let (path, abstract_hex, autobind) = unix_address_parts(&address); + context.bridge.require_network_access( + context.vm_id, + agentos_kernel::permissions::NetworkOperation::Listen, + format_unix_socket_resource(path, abstract_hex, autobind), + )?; + let pending = reserve_capability(&context.capabilities, CapabilityKind::UnixListener)?; + let listener_id = context.process.allocate_unix_listener_id(); + let registry_binding_id = guest_unix_binding_id(context.process.kernel_pid, &listener_id); + let mut listener = match address { + ManagedUnixAddress::Autobind => { + let mut bound = None; + for nonce in 0..4096 { + let guest_name = + guest_autobind_unix_name(context.process.kernel_pid, &listener_id, nonce); + let host_name = host_abstract_unix_name(context.socket_paths, &guest_name); + register_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + &abstract_unix_host_address_key(&host_name), + GuestUnixAddress { + path: abstract_unix_node_path(&guest_name), + abstract_path_hex: Some(abstract_unix_name_hex(&guest_name)), + }, + None, + None, + )?; + match ActiveUnixListener::bind_abstract_unlistened( + &host_name, + &guest_name, + registry_binding_id.clone(), + context.process.runtime_context.clone(), + ) { + Ok(listener) => { + bound = Some(listener); + break; + } + Err(error) => { + rollback_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + )?; + if guest_error_code(&error) != Some("EADDRINUSE") { + return Err(error); + } + } + } + } + bound.ok_or_else(|| { + SidecarError::host( + "EADDRINUSE", + "Linux AF_UNIX autobind namespace exhausted after 4096 attempts", + ) + })? + } + ManagedUnixAddress::AbstractHex(hex) => { + let guest_name = decode_abstract_unix_name(hex.as_str())?; + let host_name = host_abstract_unix_name(context.socket_paths, &guest_name); + register_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + &abstract_unix_host_address_key(&host_name), + GuestUnixAddress { + path: abstract_unix_node_path(&guest_name), + abstract_path_hex: Some(abstract_unix_name_hex(&guest_name)), + }, + None, + None, + )?; + match ActiveUnixListener::bind_abstract_unlistened( + &host_name, + &guest_name, + registry_binding_id.clone(), + context.process.runtime_context.clone(), + ) { + Ok(listener) => listener, + Err(error) => { + rollback_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + )?; + return Err(error); + } + } + } + ManagedUnixAddress::Path(path) => { + let path = path.as_str(); + let (candidate_path, reported_path) = resolve_guest_unix_path(context.process, path)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &candidate_path)?; + let canonical_candidate = context + .kernel + .resolve_unix_socket_bind_target_for_process( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + &context.process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &canonical_candidate)?; + let node = context + .kernel + .bind_unix_socket_path_for_process( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + &context.process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + let guest_path = node.canonical_path; + let host_path = allocate_guest_socket_host_path( + context.socket_paths, + context.process.kernel_pid, + &listener_id, + &guest_path, + ); + if let Err(error) = register_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + &pathname_unix_host_address_key(&host_path), + GuestUnixAddress { + path: reported_path.clone(), + abstract_path_hex: None, + }, + Some((node.stat.dev, node.stat.ino)), + Some(host_path.clone()), + ) { + if let Err(rollback_error) = context.kernel.remove_file(&guest_path) { + return Err(SidecarError::Execution(format!( + "{error}; failed to roll back Unix socket node {guest_path}: {}", + kernel_error(rollback_error) + ))); + } + return Err(error); + } + match ActiveUnixListener::bind_unlistened( + &host_path, + &reported_path, + registry_binding_id.clone(), + context.process.runtime_context.clone(), + ) { + Ok(mut listener) => { + listener.guest_node_path = Some(guest_path); + listener + } + Err(error) => { + rollback_guest_unix_path_binding( + &context.socket_paths.unix_bound_addresses, + ®istry_binding_id, + context.kernel, + &guest_path, + &host_path, + )?; + return Err(error); + } + } + } + }; + listener + .registry_binding_id + .clone_from(®istry_binding_id); + let local_path = listener.path.clone(); + let local_abstract_path_hex = listener.abstract_path_hex.clone(); + let capability_key = NativeCapabilityKey::UnixListener(listener_id.clone()); + let identity = commit_process_capability( + context.process, + pending, + capability_key.clone(), + listener_id.clone(), + None, + )?; + listener.retain_description_lease( + context + .process + .shared_capability_lease(&capability_key) + .expect("committed Unix listener capability lease"), + ); + listener.set_event_pusher( + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(identity), + Arc::clone(&context.process.process_event_notify), + ); + context + .process + .unix_listeners + .insert(listener_id.clone(), listener); + Ok(json!({ + "serverId": listener_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "localPath": local_path, + "localAbstractPathHex": local_abstract_path_hex, + })) +} + +fn bind_connected_unix_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + socket_id: &str, + address: ManagedUnixAddress, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let (path, abstract_hex, autobind) = unix_address_parts(&address); + context.bridge.require_network_access( + context.vm_id, + agentos_kernel::permissions::NetworkOperation::Listen, + format_unix_socket_resource(path, abstract_hex, autobind), + )?; + let binding_id = guest_unix_binding_id( + context.process.kernel_pid, + &format!("connected:{socket_id}"), + ); + let socket = + context.process.unix_sockets.get(socket_id).ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown Unix socket {socket_id}")) + })?; + if socket.local_registry_binding_id.is_some() { + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::EINVAL, + ))); + } + let remote_registry_binding_id = socket.remote_registry_binding_id.clone(); + let peer_can_observe_late_bind = + guest_unix_connection_peer_open(socket.connection_state.as_ref()); + + match address { + ManagedUnixAddress::Autobind | ManagedUnixAddress::AbstractHex(_) => { + let explicit_name = match &address { + ManagedUnixAddress::AbstractHex(hex) => { + Some(decode_abstract_unix_name(hex.as_str())?) + } + ManagedUnixAddress::Autobind => None, + ManagedUnixAddress::Path(_) => unreachable!(), + }; + let attempts = if explicit_name.is_some() { 1 } else { 4096 }; + let mut bound_name = None; + for nonce in 0..attempts { + let guest_name = explicit_name.clone().unwrap_or_else(|| { + guest_autobind_unix_name(context.process.kernel_pid, &binding_id, nonce) + .to_vec() + }); + let host_name = host_abstract_unix_name(context.socket_paths, &guest_name); + register_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + &abstract_unix_host_address_key(&host_name), + GuestUnixAddress { + path: abstract_unix_node_path(&guest_name), + abstract_path_hex: Some(abstract_unix_name_hex(&guest_name)), + }, + None, + None, + )?; + if peer_can_observe_late_bind { + let target_binding_id = remote_registry_binding_id + .as_deref() + .expect("tracked Unix connection has a target binding"); + if let Err(error) = queue_guest_unix_peer( + &context.socket_paths.unix_bound_addresses, + &binding_id, + target_binding_id, + ) { + rollback_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + )?; + return Err(error); + } + } + let result = context + .process + .unix_sockets + .get_mut(socket_id) + .expect("validated Unix socket remains registered") + .bind_abstract(&host_name, &guest_name, &binding_id); + match result { + Ok(()) => { + bound_name = Some(guest_name); + break; + } + Err(error) => { + rollback_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + )?; + if explicit_name.is_some() || guest_error_code(&error) != Some("EADDRINUSE") + { + return Err(error); + } + } + } + } + let guest_name = bound_name.ok_or_else(|| { + sidecar_net_error(std::io::Error::from_raw_os_error(libc::EADDRINUSE)) + })?; + Ok(json!({ + "localPath": abstract_unix_node_path(&guest_name), + "localAbstractPathHex": abstract_unix_name_hex(&guest_name), + })) + } + ManagedUnixAddress::Path(path) => { + let path = path.as_str(); + let (candidate_path, reported_path) = resolve_guest_unix_path(context.process, path)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &candidate_path)?; + let canonical_candidate = context + .kernel + .resolve_unix_socket_bind_target_for_process( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + &context.process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &canonical_candidate)?; + let node = context + .kernel + .bind_unix_socket_path_for_process( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + &context.process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + let guest_path = node.canonical_path; + let host_path = allocate_guest_socket_host_path( + context.socket_paths, + context.process.kernel_pid, + &binding_id, + &guest_path, + ); + if let Err(error) = register_guest_unix_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + &pathname_unix_host_address_key(&host_path), + GuestUnixAddress { + path: reported_path.clone(), + abstract_path_hex: None, + }, + Some((node.stat.dev, node.stat.ino)), + Some(host_path.clone()), + ) { + if let Err(rollback_error) = context.kernel.remove_file(&guest_path) { + return Err(SidecarError::Execution(format!( + "{error}; failed to roll back Unix socket node {guest_path}: {}", + kernel_error(rollback_error) + ))); + } + return Err(error); + } + if peer_can_observe_late_bind { + let target_binding_id = remote_registry_binding_id + .as_deref() + .expect("tracked Unix connection has a target binding"); + if let Err(error) = queue_guest_unix_peer( + &context.socket_paths.unix_bound_addresses, + &binding_id, + target_binding_id, + ) { + rollback_guest_unix_path_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + context.kernel, + &guest_path, + &host_path, + )?; + return Err(error); + } + } + if let Err(error) = context + .process + .unix_sockets + .get_mut(socket_id) + .expect("validated Unix socket remains registered") + .bind_path(&host_path, &reported_path, &binding_id) + { + rollback_guest_unix_path_binding( + &context.socket_paths.unix_bound_addresses, + &binding_id, + context.kernel, + &guest_path, + &host_path, + )?; + return Err(error); + } + Ok(json!({ "localPath": reported_path })) + } + } +} + +fn reserve_tcp_port( + context: ManagedEndpointServiceContext<'_, B>, + host: Option<&str>, + requested_port: Option, +) -> Result { + let (family, _bind_host, guest_host) = normalize_tcp_listen_host(host)?; + let port = allocate_guest_listen_port( + requested_port.unwrap_or(0), + family, + &context.socket_paths.used_tcp_guest_ports, + context.socket_paths.listen_policy, + )?; + let reservation_id = context.process.allocate_tcp_port_reservation_id(); + context + .process + .tcp_port_reservations + .insert(reservation_id.clone(), (family, port)); + Ok(json!({ + "reservationId": reservation_id, + "localAddress": guest_host, + "localPort": port, + "family": match family { + SocketFamily::Ipv4 => "IPv4", + SocketFamily::Ipv6 => "IPv6", + }, + })) +} + +fn connect_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + endpoint: ManagedTcpEndpoint, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if let Some(address) = endpoint.unix { + return connect_unix_endpoint(context, address, endpoint.bound_server_id); + } + + let port = endpoint.port.ok_or_else(|| { + SidecarError::host( + "EINVAL", + "net.connect requires either a Unix address or port", + ) + })?; + let host = endpoint + .host + .as_ref() + .map_or("localhost", |host| host.as_str()); + let is_http_loopback_target = is_loopback_socket_host(host) + && [SocketFamily::Ipv4, SocketFamily::Ipv6] + .iter() + .any(|family| { + context + .socket_paths + .http_loopback_target(*family, port) + .is_some() + }); + if !is_http_loopback_target { + context.bridge.require_network_access( + context.vm_id, + agentos_kernel::permissions::NetworkOperation::Http, + format_tcp_resource(host, port), + )?; + let resolved = resolve_tcp_connect_addr( + context.bridge, + context.kernel, + context.vm_id, + context.dns, + host, + port, + None, + context.socket_paths, + )?; + if !resolved.use_kernel_loopback { + let pending = reserve_capability(&context.capabilities, CapabilityKind::TcpSocket)?; + return defer_native_tcp_connect( + context.process, + context.call_id, + pending, + resolved, + endpoint + .local_reservation + .map(|reservation| reservation.into_string()), + ); + } + } + + let pending = reserve_capability(&context.capabilities, CapabilityKind::TcpSocket)?; + let local_reservation_id = endpoint + .local_reservation + .as_ref() + .map(|reservation| reservation.as_str()); + let local_reservation = local_reservation_id.and_then(|id| { + context + .process + .tcp_port_reservations + .remove(id) + .map(|reservation| (id.to_owned(), reservation)) + }); + + if is_loopback_socket_host(host) { + let families = [SocketFamily::Ipv4, SocketFamily::Ipv6]; + if let Some((family, target)) = families.iter().find_map(|family| { + context + .socket_paths + .http_loopback_target(*family, port) + .map(|target| (*family, target)) + }) { + if let Some((reservation_id, reservation)) = local_reservation { + context + .process + .tcp_port_reservations + .insert(reservation_id, reservation); + } + drop(pending); + let remote_address = match family { + SocketFamily::Ipv4 => "127.0.0.1", + SocketFamily::Ipv6 => "::1", + }; + return Ok(json!({ + "loopbackHttpTarget": { + "processId": target.process_id.clone(), + "serverId": target.server_id, + "host": remote_address, + "port": port, + }, + "localAddress": remote_address, + "localPort": endpoint.local_port.unwrap_or(0), + "remoteAddress": remote_address, + "remotePort": port, + "remoteFamily": match family { + SocketFamily::Ipv4 => "IPv4", + SocketFamily::Ipv6 => "IPv6", + }, + }) + .into()); + } + } + + let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest { + bridge: context.bridge, + kernel: context.kernel, + kernel_pid: context.process.kernel_pid, + vm_id: context.vm_id, + dns: context.dns, + host, + port, + family: None, + local_address: endpoint + .local_address + .as_ref() + .map(|address| address.as_str()), + local_port: endpoint.local_port, + local_reservation: local_reservation + .as_ref() + .map(|(_, reservation)| *reservation), + context: context.socket_paths, + resources: context.capabilities.resources(), + runtime_context: context.process.runtime_context.clone(), + reactor_limits: reactor_io_limits(&context.process.limits), + }); + let socket = match connect_result { + Ok(socket) => socket, + Err(error) => { + if let Some((reservation_id, reservation)) = local_reservation { + context + .process + .tcp_port_reservations + .insert(reservation_id, reservation); + } + return Err(error); + } + }; + let socket_id = context.process.allocate_tcp_socket_id(); + let local_addr = socket.guest_local_addr; + let remote_addr = socket.guest_remote_addr; + let capability_key = NativeCapabilityKey::TcpSocket(socket_id.clone()); + let identity = match commit_process_capability( + context.process, + pending, + capability_key.clone(), + socket_id.clone(), + socket.kernel_socket_id, + ) { + Ok(identity) => identity, + Err(error) => { + if let Err(cleanup_error) = socket.close(context.kernel, context.process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close connected socket after capability commit failure: {cleanup_error}" + ); + } + return Err(error); + } + }; + socket.set_event_pusher( + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(identity), + Arc::clone(&context.process.process_event_notify), + ); + socket.set_fairness_identity( + context + .process + .capability_fairness_identity(&capability_key), + )?; + socket.retain_description_lease( + context + .process + .shared_capability_lease(&capability_key) + .expect("committed socket capability lease"), + ); + register_kernel_readiness_target( + &context.kernel_readiness, + socket.kernel_socket_id, + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(Arc::clone(&socket.read_event_notify)), + context + .process + .capability_readiness_identity(&capability_key), + socket_id.clone(), + KernelSocketReadinessEvent::Data, + ); + context + .process + .tcp_sockets + .insert(socket_id.clone(), socket); + Ok(json!({ + "socketId": socket_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "localAddress": local_addr.ip().to_string(), + "localPort": local_addr.port(), + "remoteAddress": remote_addr.ip().to_string(), + "remotePort": remote_addr.port(), + "remoteFamily": socket_addr_family(&remote_addr), + }) + .into()) +} + +fn connect_unix_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + address: ManagedUnixAddress, + bound_server_id: Option, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let (path, abstract_hex, autobind) = unix_address_parts(&address); + if autobind { + return Err(SidecarError::host( + "EINVAL", + "net.connect does not accept an autobind remote address", + )); + } + context.bridge.require_network_access( + context.vm_id, + agentos_kernel::permissions::NetworkOperation::Http, + format_unix_socket_resource(path, abstract_hex, false), + )?; + let (target, target_binding_id, remote_address) = if let Some(hex) = abstract_hex { + let guest_name = decode_abstract_unix_name(hex)?; + let host_name = host_abstract_unix_name(context.socket_paths, &guest_name); + let target = guest_unix_binding_for_host_key( + &context.socket_paths.unix_bound_addresses, + &abstract_unix_host_address_key(&host_name), + )? + .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::ECONNREFUSED)))?; + ( + NativeUnixConnectTarget::Abstract(host_name.to_vec()), + target.0, + target.1, + ) + } else { + let path = path.expect("validated Unix path"); + let (candidate_path, _) = resolve_guest_unix_path(context.process, path)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &candidate_path)?; + let node = context + .kernel + .resolve_unix_socket_connect_target_for_process( + EXECUTION_DRIVER_NAME, + context.process.kernel_pid, + &context.process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(context.socket_paths, &node.canonical_path)?; + let (host_path, binding_id, address) = + guest_unix_path_target(context.socket_paths, (node.stat.dev, node.stat.ino))? + .ok_or_else(|| { + sidecar_net_error(std::io::Error::from_raw_os_error(libc::ECONNREFUSED)) + })?; + ( + NativeUnixConnectTarget::Path(host_path), + binding_id, + address, + ) + }; + let pending = reserve_capability(&context.capabilities, CapabilityKind::UnixSocket)?; + let bound_listener = if let Some(listener_id) = bound_server_id { + let listener_id = listener_id.into_string(); + let listener = context + .process + .unix_listeners + .remove(&listener_id) + .ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown bound Unix socket {listener_id}")) + })?; + if listener.acceptor_started || listener.bound_socket.is_none() { + context.process.unix_listeners.insert(listener_id, listener); + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::EINVAL, + ))); + } + Some((listener_id, listener)) + } else { + None + }; + defer_native_unix_connect( + context.process, + context.call_id, + pending, + target, + remote_address, + Arc::clone(&context.socket_paths.unix_bound_addresses), + target_binding_id, + bound_listener, + ) +} + +fn listen_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + endpoint: ManagedTcpEndpoint, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if let Some(listener_id) = endpoint.bound_server_id.as_ref() { + if endpoint.unix.is_some() || endpoint.host.is_some() || endpoint.port.is_some() { + return Err(SidecarError::host( + "EINVAL", + "net.listen boundServerId cannot be combined with an address", + )); + } + return listen_bound_unix_endpoint(context, listener_id.as_str(), endpoint.backlog); + } + + if let Some(address) = endpoint.unix { + // The typed bind operation is the single implementation of Unix + // namespace registration. Promote its unlistened socket immediately; + // no executor-visible state exists between these two owner-thread + // mutations. + let bound = bind_unix_endpoint( + ManagedEndpointServiceContext { + bridge: context.bridge, + vm_id: context.vm_id, + dns: context.dns, + socket_paths: context.socket_paths, + kernel: &mut *context.kernel, + kernel_readiness: context.kernel_readiness.clone(), + process: &mut *context.process, + capabilities: context.capabilities.clone(), + call_id: context.call_id, + }, + address, + )?; + let listener_id = bound + .get("serverId") + .and_then(Value::as_str) + .ok_or_else(|| SidecarError::host("EIO", "Unix bind omitted serverId"))? + .to_owned(); + let mut listened = listen_bound_unix_endpoint(context, &listener_id, endpoint.backlog)?; + if let Some(fields) = listened.as_object_mut() { + if let Some(local_path) = fields.get("localPath").cloned() { + fields.insert("path".to_owned(), local_path); + } + } + return Ok(listened); + } + + let pending = reserve_capability(&context.capabilities, CapabilityKind::TcpListener)?; + let host = endpoint.host.as_ref().map(|host| host.as_str()); + let (family, bind_host, guest_host) = normalize_tcp_listen_host(host)?; + let requested_port = endpoint.port.unwrap_or(0); + context.bridge.require_network_access( + context.vm_id, + agentos_kernel::permissions::NetworkOperation::Listen, + format_tcp_resource(bind_host, requested_port), + )?; + let local_reservation_id = endpoint + .local_reservation + .as_ref() + .map(|reservation| reservation.as_str()); + let local_reservation = local_reservation_id.and_then(|id| { + context + .process + .tcp_port_reservations + .remove(id) + .map(|reservation| (id.to_owned(), reservation)) + }); + let port = if requested_port != 0 + && local_reservation + .as_ref() + .map(|(_, reservation)| *reservation) + == Some((family, requested_port)) + { + requested_port + } else { + allocate_guest_listen_port( + requested_port, + family, + &context.socket_paths.used_tcp_guest_ports, + context.socket_paths.listen_policy, + )? + }; + let listener = match ActiveTcpListener::bind_kernel( + context.kernel, + context.process.kernel_pid, + guest_host, + port, + endpoint.backlog, + ) { + Ok(listener) => listener, + Err(error) => { + if let Some((reservation_id, reservation)) = local_reservation { + context + .process + .tcp_port_reservations + .insert(reservation_id, reservation); + } + return Err(error); + } + }; + let listener_id = context.process.allocate_tcp_listener_id(); + let local_addr = listener.guest_local_addr(); + let capability_key = NativeCapabilityKey::TcpListener(listener_id.clone()); + let identity = match commit_process_capability( + context.process, + pending, + capability_key.clone(), + listener_id.clone(), + listener.kernel_socket_id, + ) { + Ok(identity) => identity, + Err(error) => { + if let Err(cleanup_error) = listener.close(context.kernel, context.process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_SOCKET_CLEANUP: failed to close listener after capability commit failure: {cleanup_error}" + ); + } + return Err(error); + } + }; + listener.retain_description_lease( + context + .process + .shared_capability_lease(&capability_key) + .expect("committed TCP listener capability lease"), + ); + register_kernel_readiness_target( + &context.kernel_readiness, + listener.kernel_socket_id, + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + None, + context + .process + .capability_readiness_identity(&capability_key), + listener_id.clone(), + KernelSocketReadinessEvent::Accept, + ); + context + .process + .tcp_listeners + .insert(listener_id.clone(), listener); + Ok(json!({ + "serverId": listener_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "localAddress": local_addr.ip().to_string(), + "localPort": local_addr.port(), + "family": socket_addr_family(&local_addr), + })) +} + +fn listen_bound_unix_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + listener_id: &str, + backlog: Option, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let listener = context + .process + .unix_listeners + .remove(listener_id) + .ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown bound Unix socket {listener_id}")) + })?; + let local_path = listener.path.clone(); + let local_abstract_path_hex = listener.abstract_path_hex.clone(); + let listener = match listener.listen_bound( + context.socket_paths.clone(), + backlog, + context.capabilities.clone(), + context.process.runtime_context.clone(), + reactor_io_limits(&context.process.limits), + ) { + Ok(listener) => listener, + Err(error) => { + context + .process + .release_capability_if_present(&NativeCapabilityKey::UnixListener( + listener_id.to_owned(), + )); + return Err(error); + } + }; + let capability_key = NativeCapabilityKey::UnixListener(listener_id.to_owned()); + let identity = context + .process + .capability_readiness_identity(&capability_key) + .ok_or_else(|| { + SidecarError::host( + "EIO", + format!("missing capability for bound Unix socket {listener_id}"), + ) + })?; + listener.set_event_pusher( + context + .process + .execution + .execution_wake_handle(context.process.kernel_handle.runtime_identity()), + Some(identity), + Arc::clone(&context.process.process_event_notify), + ); + context + .process + .unix_listeners + .insert(listener_id.to_owned(), listener); + Ok(json!({ + "serverId": listener_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "localPath": local_path, + "localAbstractPathHex": local_abstract_path_hex, + })) +} + +pub(in crate::execution) fn relisten_managed_unix_endpoint( + context: ManagedEndpointServiceContext<'_, B>, + listener_id: &str, + backlog: u32, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let reactor_limits = reactor_io_limits(&context.process.limits); + let (local_path, local_abstract_path_hex) = { + let listener = context + .process + .unix_listeners + .get_mut(listener_id) + .ok_or_else(|| { + SidecarError::host("EBADF", format!("unknown Unix listener {listener_id}")) + })?; + listener.relisten( + &context.socket_paths.unix_bound_addresses, + backlog, + reactor_limits, + )?; + (listener.path.clone(), listener.abstract_path_hex.clone()) + }; + let capability_key = NativeCapabilityKey::UnixListener(listener_id.to_owned()); + let identity = context + .process + .capability_readiness_identity(&capability_key) + .ok_or_else(|| { + SidecarError::host( + "EIO", + format!("missing capability for Unix listener {listener_id}"), + ) + })?; + Ok(HostServiceResponse::Json(json!({ + "serverId": listener_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "localPath": local_path, + "localAbstractPathHex": local_abstract_path_hex, + }))) +} + +fn unix_address_parts(address: &ManagedUnixAddress) -> (Option<&str>, Option<&str>, bool) { + match address { + ManagedUnixAddress::Path(path) => (Some(path.as_str()), None, false), + ManagedUnixAddress::AbstractHex(hex) => (None, Some(hex.as_str()), false), + ManagedUnixAddress::Autobind => (None, None, true), + } +} diff --git a/crates/native-sidecar/src/execution/network/mod.rs b/crates/native-sidecar/src/execution/network/mod.rs index 1f4191213c..bc32319906 100644 --- a/crates/native-sidecar/src/execution/network/mod.rs +++ b/crates/native-sidecar/src/execution/network/mod.rs @@ -1,8 +1,7 @@ mod tcp; pub(in crate::execution) use self::tcp::*; pub(crate) use self::tcp::{ - build_javascript_socket_path_context, finalize_javascript_net_connect, - restore_pending_bound_unix_connect, + build_socket_path_context, finalize_net_connect, restore_pending_bound_unix_connect, }; mod unix; pub(in crate::execution) use self::unix::*; @@ -19,3 +18,11 @@ pub(in crate::execution) use self::http2::*; mod dns; pub(crate) use self::dns::format_dns_resource; pub(in crate::execution) use self::dns::*; +mod resolver; +pub(crate) use self::resolver::HickoryDnsResolver; +mod managed; +pub(in crate::execution) use self::managed::*; +mod managed_endpoint; +pub(in crate::execution) use self::managed_endpoint::*; +mod http_client; +pub(in crate::execution) use self::http_client::*; diff --git a/crates/native-sidecar/src/execution/network/resolver.rs b/crates/native-sidecar/src/execution/network/resolver.rs new file mode 100644 index 0000000000..edd14f2fd1 --- /dev/null +++ b/crates/native-sidecar/src/execution/network/resolver.rs @@ -0,0 +1,302 @@ +//! Native DNS transport backed by Hickory and the process-owned Tokio runtime. + +use agentos_kernel::dns::{ + DnsLookupRequest, DnsRecordLookupRequest, DnsResolver, DnsResolverError, +}; +use agentos_runtime::{BlockingJobError, RuntimeContext}; +use hickory_resolver::config::{NameServerConfig, ResolverConfig}; +use hickory_resolver::net::runtime::TokioRuntimeProvider; +use hickory_resolver::proto::rr::{Record, RecordType}; +use hickory_resolver::TokioResolver; +use std::collections::BTreeSet; +use std::future::Future; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::pin::Pin; + +/// Native resolver implementation injected into each kernel VM by the sidecar. +/// +/// The kernel owns DNS policy, overrides, and result semantics. This adapter is +/// only the host transport: it creates Hickory resolvers on the one injected +/// process runtime and admits synchronous compatibility lookups through that +/// runtime's bounded blocking executor. +pub(crate) struct HickoryDnsResolver { + runtime: RuntimeContext, +} + +impl HickoryDnsResolver { + pub(crate) fn new(runtime: RuntimeContext) -> Self { + Self { runtime } + } + + fn send_lookup_ip( + &self, + hostname: String, + name_servers: Vec, + ) -> Result, DnsResolverError> { + let resolver = { + let _entered = self.runtime.handle().enter(); + resolver_for(&name_servers)? + }; + let reserved_bytes = dns_lookup_input_bytes(&hostname, &name_servers); + let handle = self.runtime.handle().clone(); + let timeout = self.runtime.blocking_job_timeout(); + self.runtime + .blocking() + .run_sync(reserved_bytes, timeout, move || { + handle.block_on(async move { + tokio::time::timeout(timeout, lookup_ip_with_resolver(resolver, hostname)) + .await + .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) + }) + }) + .map_err(map_blocking_lookup_error)? + } + + fn send_lookup_records( + &self, + hostname: String, + name_servers: Vec, + record_type: RecordType, + ) -> Result, DnsResolverError> { + let resolver = { + let _entered = self.runtime.handle().enter(); + resolver_for(&name_servers)? + }; + let reserved_bytes = dns_lookup_input_bytes(&hostname, &name_servers); + let handle = self.runtime.handle().clone(); + let timeout = self.runtime.blocking_job_timeout(); + self.runtime + .blocking() + .run_sync(reserved_bytes, timeout, move || { + handle.block_on(async move { + tokio::time::timeout( + timeout, + lookup_records_with_resolver(resolver, hostname, record_type), + ) + .await + .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) + }) + }) + .map_err(map_blocking_lookup_error)? + } +} + +impl DnsResolver for HickoryDnsResolver { + fn lookup_ip(&self, request: &DnsLookupRequest) -> Result, DnsResolverError> { + self.send_lookup_ip( + request.hostname().to_owned(), + request.name_servers().to_vec(), + ) + } + + fn lookup_records( + &self, + request: &DnsRecordLookupRequest, + ) -> Result, DnsResolverError> { + self.send_lookup_records( + request.hostname().to_owned(), + request.name_servers().to_vec(), + request.record_type(), + ) + } + + fn lookup_ip_async<'a>( + &'a self, + request: DnsLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { + let resolver = { + let _entered = self.runtime.handle().enter(); + resolver_for(request.name_servers())? + }; + let timeout = self.runtime.blocking_job_timeout(); + tokio::time::timeout( + timeout, + lookup_ip_with_resolver(resolver, request.hostname().to_owned()), + ) + .await + .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) + }) + } + + fn lookup_records_async<'a>( + &'a self, + request: DnsRecordLookupRequest, + ) -> Pin, DnsResolverError>> + Send + 'a>> { + Box::pin(async move { + let resolver = { + let _entered = self.runtime.handle().enter(); + resolver_for(request.name_servers())? + }; + let timeout = self.runtime.blocking_job_timeout(); + tokio::time::timeout( + timeout, + lookup_records_with_resolver( + resolver, + request.hostname().to_owned(), + request.record_type(), + ), + ) + .await + .unwrap_or_else(|_| Err(dns_lookup_timeout_error(timeout))) + }) + } +} + +fn resolver_for(name_servers: &[SocketAddr]) -> Result { + let resolver_config = resolver_config_from_name_servers(name_servers); + let builder = if let Some(config) = resolver_config { + TokioResolver::builder_with_config(config, TokioRuntimeProvider::default()) + } else { + TokioResolver::builder_tokio().map_err(|error| { + DnsResolverError::lookup_failed(format!( + "failed to initialize DNS resolver from system configuration: {error}" + )) + })? + }; + builder.build().map_err(|error| { + DnsResolverError::lookup_failed(format!("failed to build DNS resolver: {error}")) + }) +} + +fn dns_lookup_input_bytes(hostname: &str, name_servers: &[SocketAddr]) -> usize { + hostname.len().saturating_add( + name_servers + .len() + .saturating_mul(std::mem::size_of::()), + ) +} + +fn map_blocking_lookup_error(error: BlockingJobError) -> DnsResolverError { + DnsResolverError::lookup_failed(format!("ERR_AGENTOS_DNS_LOOKUP_EXECUTOR: {error}")) +} + +fn dns_lookup_timeout_error(timeout: std::time::Duration) -> DnsResolverError { + DnsResolverError::lookup_failed(format!( + "ERR_AGENTOS_DNS_LOOKUP_TIMEOUT: DNS lookup exceeded {}ms; raise runtime.blocking.jobTimeoutMs", + timeout.as_millis() + )) +} + +async fn lookup_ip_with_resolver( + resolver: TokioResolver, + hostname: String, +) -> Result, DnsResolverError> { + let lookup = resolver.lookup_ip(&hostname).await.map_err(|error| { + DnsResolverError::lookup_failed(format!( + "failed to resolve DNS address {hostname}: {error}" + )) + })?; + + let mut addresses = Vec::new(); + let mut seen = BTreeSet::new(); + for ip in lookup.iter() { + if seen.insert(ip) { + addresses.push(ip); + } + } + + if addresses.is_empty() { + return Err(DnsResolverError::lookup_failed(format!( + "failed to resolve DNS address {hostname}" + ))); + } + + Ok(addresses) +} + +async fn lookup_records_with_resolver( + resolver: TokioResolver, + hostname: String, + record_type: RecordType, +) -> Result, DnsResolverError> { + let lookup = resolver + .lookup(&hostname, record_type) + .await + .map_err(|error| { + let message = format!("failed to resolve DNS {record_type} record {hostname}: {error}"); + if error.is_nx_domain() { + DnsResolverError::nx_domain(message) + } else if error.is_no_records_found() { + DnsResolverError::no_data(message) + } else { + DnsResolverError::lookup_failed(message) + } + })?; + let records = lookup.answers().to_vec(); + if records.is_empty() { + return Err(DnsResolverError::no_data(format!( + "failed to resolve DNS {record_type} record {hostname}" + ))); + } + Ok(records) +} + +fn resolver_config_from_name_servers(name_servers: &[SocketAddr]) -> Option { + if name_servers.is_empty() { + return None; + } + + let name_servers = name_servers + .iter() + .map(|server| { + let mut config = NameServerConfig::udp_and_tcp(server.ip()); + for connection in &mut config.connections { + connection.port = server.port(); + connection.bind_addr = Some(SocketAddr::new( + if server.is_ipv6() { + IpAddr::V6(Ipv6Addr::UNSPECIFIED) + } else { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + }, + 0, + )); + } + config + }) + .collect(); + + Some(ResolverConfig::from_parts(None, vec![], name_servers)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_nameservers_preserve_family_port_and_unspecified_bind_address() { + let requested = [ + "203.0.113.53:5353".parse::().expect("IPv4 DNS"), + "[2001:db8::53]:5454" + .parse::() + .expect("IPv6 DNS"), + ]; + let config = resolver_config_from_name_servers(&requested).expect("explicit config"); + let configured = config.name_servers(); + + assert_eq!(configured.len(), requested.len()); + for (expected, server) in requested.iter().zip(configured.iter()) { + assert_eq!(server.ip, expected.ip()); + assert_eq!(server.connections.len(), 2, "UDP and TCP per nameserver"); + for connection in &server.connections { + assert_eq!(connection.port, expected.port()); + assert_eq!( + connection.bind_addr, + Some(SocketAddr::new( + if expected.is_ipv6() { + IpAddr::V6(Ipv6Addr::UNSPECIFIED) + } else { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + }, + 0 + )) + ); + } + } + } + + #[test] + fn empty_nameserver_list_defers_to_host_resolver_configuration() { + assert!(resolver_config_from_name_servers(&[]).is_none()); + } +} diff --git a/crates/native-sidecar/src/execution/network/tcp.rs b/crates/native-sidecar/src/execution/network/tcp.rs index b321a5b16b..ca18d4366c 100644 --- a/crates/native-sidecar/src/execution/network/tcp.rs +++ b/crates/native-sidecar/src/execution/network/tcp.rs @@ -87,8 +87,8 @@ pub(in crate::execution) struct ActiveTcpConnectRequest<'a, B> { pub(in crate::execution) family: Option, pub(in crate::execution) local_address: Option<&'a str>, pub(in crate::execution) local_port: Option, - pub(in crate::execution) local_reservation: Option<(JavascriptSocketFamily, u16)>, - pub(in crate::execution) context: &'a JavascriptSocketPathContext, + pub(in crate::execution) local_reservation: Option<(SocketFamily, u16)>, + pub(in crate::execution) context: &'a SocketPathContext, pub(in crate::execution) resources: Arc, pub(in crate::execution) runtime_context: agentos_runtime::RuntimeContext, pub(in crate::execution) reactor_limits: ReactorIoLimits, @@ -100,14 +100,16 @@ impl ActiveTcpSocket { identity: Option<(u64, u64)>, ) -> Result<(), SidecarError> { let identity = identity.ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_FAIRNESS_IDENTITY: TCP socket capability was committed outside a VM runtime scope", - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_IDENTITY", + String::from("TCP socket capability was committed outside a VM runtime scope"), + ) })?; self.fairness_identity.set(identity).map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_FAIRNESS_IDENTITY: TCP socket capability identity was committed more than once", - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_IDENTITY", + String::from("TCP socket capability identity was committed more than once"), + ) })?; self.fairness_identity_committed.notify_waiters(); Ok(()) @@ -154,19 +156,14 @@ impl ActiveTcpSocket { ); } - let stream = - TcpStream::connect_timeout(&resolved.actual_addr, reactor_limits.operation_deadline) - .map_err(sidecar_net_error)?; - let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?; - Self::from_stream( - stream, - None, - guest_local_addr, - resolved.guest_remote_addr, - resources, - runtime_context, - reactor_limits, - ) + // Native connects must have been converted to a reactor-owned deferred + // operation before entering this synchronous constructor. Keeping a + // blocking socket-connect fallback here would let a new adapter + // accidentally park a bounded VM-executor worker. + Err(SidecarError::host( + "EIO", + "native TCP connect reached the synchronous constructor without reactor deferral", + )) } #[allow(clippy::too_many_arguments)] @@ -176,14 +173,14 @@ impl ActiveTcpSocket { resolved: ResolvedTcpConnectAddr, local_address: Option<&str>, local_port: Option, - local_reservation: Option<(JavascriptSocketFamily, u16)>, - context: &JavascriptSocketPathContext, + local_reservation: Option<(SocketFamily, u16)>, + context: &SocketPathContext, resources: Arc, runtime_context: agentos_runtime::RuntimeContext, reactor_limits: ReactorIoLimits, ) -> Result { debug_assert!(resolved.use_kernel_loopback); - let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip()); + let family = SocketFamily::from_ip(resolved.guest_remote_addr.ip()); let requested_local_port = local_port.unwrap_or(0); let local_port = if requested_local_port != 0 && local_reservation == Some((family, requested_local_port)) @@ -198,31 +195,35 @@ impl ActiveTcpSocket { )? }; let local_ip = match (family, local_address) { - (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), - (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => { + (SocketFamily::Ipv4, Some("0.0.0.0")) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + (SocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => { IpAddr::V4(Ipv4Addr::LOCALHOST) } - (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), - (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => { + (SocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), + (SocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => { IpAddr::V6(Ipv6Addr::LOCALHOST) } - (JavascriptSocketFamily::Ipv4, Some(other)) => { - return Err(SidecarError::Execution(format!( - "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" - ))); + (SocketFamily::Ipv4, Some(other)) => { + return Err(SidecarError::host( + "EACCES", + format!( + "TCP sockets must bind to loopback or unspecified addresses, got {other}" + ), + )); } - (JavascriptSocketFamily::Ipv6, Some(other)) => { - return Err(SidecarError::Execution(format!( - "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" - ))); + (SocketFamily::Ipv6, Some(other)) => { + return Err(SidecarError::host( + "EACCES", + format!( + "TCP sockets must bind to loopback or unspecified addresses, got {other}" + ), + )); } }; let local_addr = SocketAddr::new(local_ip, local_port); let spec = match family { - JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(), - JavascriptSocketFamily::Ipv6 => { - SocketSpec::new(SocketDomain::Inet6, SocketType::Stream) - } + SocketFamily::Ipv4 => SocketSpec::tcp(), + SocketFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream), }; let socket_id = kernel .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) @@ -285,6 +286,7 @@ impl ActiveTcpSocket { let (sender, events) = async_completion_channel( runtime_context.clone(), socket_completion_capacity(reactor_limits), + tcp_socket_event_retained_bytes, ); let read_event_notify = Arc::new(tokio::sync::Notify::new()); let application_read_interest = Arc::new(AtomicBool::new(false)); @@ -333,7 +335,7 @@ impl ActiveTcpSocket { saw_remote_end, close_notified, pending_read_event: Arc::new(Mutex::new(None)), - read_buffer: Arc::new(Mutex::new(VecDeque::new())), + read_state: Arc::new(Mutex::new(SocketReadState::default())), description_handles: Arc::new(()), listener_connection_retirement: None, kernel_transfer_guard: None, @@ -353,6 +355,7 @@ impl ActiveTcpSocket { let (sender, events) = async_completion_channel( runtime_context.clone(), socket_completion_capacity(reactor_limits), + tcp_socket_event_retained_bytes, ); let fairness_identity = Arc::new(OnceLock::new()); let fairness_retirement = @@ -397,7 +400,7 @@ impl ActiveTcpSocket { saw_remote_end: Arc::new(AtomicBool::new(false)), close_notified: Arc::new(AtomicBool::new(false)), pending_read_event: Arc::new(Mutex::new(None)), - read_buffer: Arc::new(Mutex::new(VecDeque::new())), + read_state: Arc::new(Mutex::new(SocketReadState::default())), description_handles: Arc::new(()), listener_connection_retirement: None, kernel_transfer_guard: None, @@ -443,7 +446,7 @@ impl ActiveTcpSocket { saw_remote_end: Arc::clone(&self.saw_remote_end), close_notified: Arc::clone(&self.close_notified), pending_read_event: Arc::clone(&self.pending_read_event), - read_buffer: Arc::clone(&self.read_buffer), + read_state: Arc::clone(&self.read_state), description_handles: Arc::clone(&self.description_handles), listener_connection_retirement: self.listener_connection_retirement.clone(), kernel_transfer_guard: self.kernel_transfer_guard.clone(), @@ -464,11 +467,12 @@ impl ActiveTcpSocket { pub(in crate::execution) fn set_event_pusher( &self, - session: Option, + session: Option, identity: Option<( agentos_runtime::capability::CapabilityId, agentos_runtime::capability::CapabilityGeneration, )>, + owner_notify: Arc, ) { let (Some(session), Some((capability_id, capability_generation))) = (session, identity) else { @@ -477,6 +481,7 @@ impl ActiveTcpSocket { self.readiness_registration.register( Some(session), Some((capability_id, capability_generation)), + owner_notify, agentos_runtime::readiness::ReadyFlags::READABLE, ); } @@ -500,7 +505,7 @@ impl ActiveTcpSocket { kernel_pid: u32, _wait: Duration, trace_enabled: bool, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { if let Some(event) = self .pending_read_event .lock() @@ -583,7 +588,7 @@ impl ActiveTcpSocket { } let unused = READ_QUANTUM.saturating_sub(bytes.len()); drop(reservation.split(unused)); - Ok(Some(JavascriptTcpSocketEvent::Data { + Ok(Some(TcpSocketEvent::Data { bytes, reservation: SharedReservation::new(reservation), source_reservations: Vec::new(), @@ -596,7 +601,7 @@ impl ActiveTcpSocket { .fetch_add(1, Ordering::Relaxed); } drop(reservation.split(READ_QUANTUM)); - Ok(Some(JavascriptTcpSocketEvent::Data { + Ok(Some(TcpSocketEvent::Data { bytes: Vec::new(), reservation: SharedReservation::new(reservation), source_reservations: Vec::new(), @@ -609,7 +614,7 @@ impl ActiveTcpSocket { .fetch_add(1, Ordering::Relaxed); } self.saw_remote_end.store(true, Ordering::SeqCst); - Ok(Some(JavascriptTcpSocketEvent::End)) + Ok(Some(TcpSocketEvent::End)) } Err(error) if error.code() == "EAGAIN" => { if trace_enabled { @@ -625,7 +630,7 @@ impl ActiveTcpSocket { .socket_read_errors .fetch_add(1, Ordering::Relaxed); } - Ok(Some(JavascriptTcpSocketEvent::Error { + Ok(Some(TcpSocketEvent::Error { code: Some(error.code().to_string()), message: error.to_string(), })) @@ -634,10 +639,10 @@ impl ActiveTcpSocket { } if revents.intersects(POLLHUP) { self.saw_remote_end.store(true, Ordering::SeqCst); - return Ok(Some(JavascriptTcpSocketEvent::End)); + return Ok(Some(TcpSocketEvent::End)); } if revents.intersects(POLLERR) { - return Ok(Some(JavascriptTcpSocketEvent::Error { + return Ok(Some(TcpSocketEvent::Error { code: Some(String::from("EPIPE")), message: String::from("kernel TCP socket reported POLLERR"), })); @@ -768,20 +773,23 @@ impl ActiveTcpSocket { &self, vm_id: &str, kernel: &mut SidecarKernel, - options: JavascriptTlsBridgeOptions, + requester_pid: u32, + options: TlsBridgeOptions, ) -> Result< tokio::sync::oneshot::Receiver>, SidecarError, > { if self.tls_mode.load(Ordering::SeqCst) { - return Err(SidecarError::Execution(String::from( - "EALREADY: socket is already upgraded to TLS", - ))); + return Err(SidecarError::host( + "EALREADY", + String::from("socket is already upgraded to TLS"), + )); } let fairness_identity = self.fairness_identity.get().copied().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_FAIRNESS_IDENTITY: TLS transport has no committed TCP capability identity", - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_IDENTITY", + String::from("TLS transport has no committed TCP capability identity"), + ) })?; let client_hello = if options.is_server { @@ -806,7 +814,8 @@ impl ActiveTcpSocket { *state = Some(tls_state); } - let default_ca_bundle = vm_default_ca_bundle_for_tls_options(kernel, &options)?; + let default_ca_bundle = + vm_default_ca_bundle_for_tls_options(kernel, requester_pid, &options)?; if self.kernel_socket_id.is_none() { let role = native_tls_role(&options, &default_ca_bundle)?; self.tls_mode.store(true, Ordering::SeqCst); @@ -873,8 +882,7 @@ impl ActiveTcpSocket { .socket_get(socket_id) .and_then(|record| record.peer_socket_id()) .ok_or_else(|| { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_LOOPBACK_PEER_MISSING: kernel-backed loopback socket {socket_id} has no connected peer for TLS upgrade" + SidecarError::host("ERR_AGENTOS_LOOPBACK_PEER_MISSING", format!("kernel-backed loopback socket {socket_id} has no connected peer for TLS upgrade" )) })?; let endpoint = loopback_tls_endpoint( @@ -937,7 +945,7 @@ impl ActiveTcpSocket { &self, vm_id: &str, kernel: &SidecarKernel, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { if let Some(socket_id) = self.kernel_socket_id { let Some(peer_socket_id) = kernel .socket_get(socket_id) @@ -983,7 +991,7 @@ impl ActiveTcpSocket { .as_ref() .and_then(|state| state.client_hello.clone()) { - return javascript_net_json_string( + return encode_net_json_string( serde_json::to_value(client_hello).map_err(|error| { SidecarError::InvalidState(format!( "failed to serialize TLS client hello: {error}" @@ -993,7 +1001,7 @@ impl ActiveTcpSocket { ); } - javascript_net_json_string( + encode_net_json_string( serde_json::to_value( self.peek_tls_client_hello(vm_id, kernel)? .unwrap_or_default(), @@ -1059,7 +1067,7 @@ impl ActiveTcpSocket { ))); } }; - javascript_net_json_string(payload, "net.socket_tls_query") + encode_net_json_string(payload, "net.socket_tls_query") } pub(in crate::execution) fn begin_tls_write( @@ -1204,7 +1212,7 @@ impl ActiveTcpSocket { && !self.close_notified.swap(true, Ordering::SeqCst) && self.event_sender.as_ref().is_some_and(|sender| { sender - .try_send(JavascriptTcpSocketEvent::Close { had_error: false }) + .try_send(TcpSocketEvent::Close { had_error: false }) .is_ok() }) { @@ -1285,7 +1293,7 @@ impl ActiveTcpSocket { .ok_or_else(|| { SidecarError::InvalidState(String::from("TCP socket event sender missing")) })? - .try_send(JavascriptTcpSocketEvent::Close { had_error: false }) + .try_send(TcpSocketEvent::Close { had_error: false }) { eprintln!( "ERR_AGENTOS_SOCKET_EVENT_DROPPED: TCP close event was not admitted: {error}" @@ -1315,8 +1323,7 @@ impl ActiveTcpSocket { }) { Ok(()) => {} Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_TLS_COMMAND_LIMIT: TLS command queue exceeded {}; raise limits.reactor.maxHandleCommands", + return Err(SidecarError::host("ERR_AGENTOS_TLS_COMMAND_LIMIT", format!("TLS command queue exceeded {}; raise limits.reactor.maxHandleCommands", self.reactor_limits.max_handle_commands, ))); } @@ -1347,7 +1354,7 @@ impl ActiveTcpSocket { wait: Duration, trace_enabled: bool, max_bytes: usize, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { let pending = self .pending_read_event .lock() @@ -1370,13 +1377,10 @@ impl ActiveTcpSocket { } pub(in crate::execution) fn limit_tcp_socket_event( - event: Option, + event: Option, max_bytes: usize, -) -> ( - Option, - Option, -) { - let Some(JavascriptTcpSocketEvent::Data { +) -> (Option, Option) { + let Some(TcpSocketEvent::Data { mut bytes, reservation, source_reservations, @@ -1386,7 +1390,7 @@ pub(in crate::execution) fn limit_tcp_socket_event( }; if bytes.len() <= max_bytes { return ( - Some(JavascriptTcpSocketEvent::Data { + Some(TcpSocketEvent::Data { bytes, reservation, source_reservations, @@ -1396,13 +1400,13 @@ pub(in crate::execution) fn limit_tcp_socket_event( } let remainder_bytes = bytes.split_off(max_bytes); - let remainder = JavascriptTcpSocketEvent::Data { + let remainder = TcpSocketEvent::Data { bytes: remainder_bytes, reservation: reservation.clone(), source_reservations: source_reservations.clone(), }; ( - Some(JavascriptTcpSocketEvent::Data { + Some(TcpSocketEvent::Data { bytes, reservation, source_reservations, @@ -1426,7 +1430,7 @@ pub(in crate::execution) fn close_kernel_socket_idempotent( pub(in crate::execution) fn register_kernel_readiness_target( registry: &KernelSocketReadinessRegistry, kernel_socket_id: Option, - session: Option, + session: Option, notify: Option>, capability: Option<( agentos_runtime::capability::CapabilityId, @@ -1454,15 +1458,21 @@ pub(in crate::execution) fn register_kernel_readiness_target( capability_generation, target_id, event, + live: Arc::new(AtomicBool::new(true)), }; if let Err(error) = registry.register(kernel_socket_id, target.clone()) { eprintln!("{error}"); return; } - if let Some(notify) = &target.notify { - notify.notify_one(); + if target.live.load(Ordering::Acquire) { + if let Some(notify) = &target.notify { + notify.notify_one(); + } } - if let Some(session) = &target.session { + if target.live.load(Ordering::Acquire) { + let Some(session) = &target.session else { + return; + }; let flags = match target.event { KernelSocketReadinessEvent::Data => agentos_runtime::readiness::ReadyFlags::READABLE, KernelSocketReadinessEvent::Datagram => { @@ -1502,6 +1512,7 @@ pub(in crate::execution) fn release_tcp_socket_handle( kernel: &mut SidecarKernel, kernel_readiness: &KernelSocketReadinessRegistry, ) { + socket.readiness_registration.retire(); let identity = process .capability_readiness_identity(&NativeCapabilityKey::TcpSocket(socket_id.to_owned())); unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id, identity); @@ -1546,6 +1557,7 @@ pub(in crate::execution) fn release_unix_socket_handle( mut socket: ActiveUnixSocket, unix_bound_addresses: &GuestUnixAddressRegistry, ) { + socket.readiness_registration.retire(); if socket.is_final_description_handle() { if let Err(error) = socket.cache_remote_peer_metadata(unix_bound_addresses) { eprintln!("ERR_AGENTOS_UNIX_SOCKET_METADATA: {error}"); @@ -1575,10 +1587,7 @@ pub(in crate::execution) fn release_unix_socket_handle( pub(in crate::execution) fn deferred_connect_error( error: SidecarError, ) -> crate::state::DeferredRpcError { - crate::state::DeferredRpcError { - code: javascript_sync_rpc_error_code(&error), - message: javascript_sync_rpc_error_message(&error), - } + crate::state::DeferredRpcError::from(host_service_error(&error)) } pub(in crate::execution) fn defer_native_tcp_connect( @@ -1587,28 +1596,27 @@ pub(in crate::execution) fn defer_native_tcp_connect( pending_capability: PendingCapability, resolved: ResolvedTcpConnectAddr, local_reservation_id: Option, -) -> Result { +) -> Result { let socket_id = process.allocate_tcp_socket_id(); let runtime = process.runtime_context.clone(); let task_runtime = runtime.clone(); let resources = Arc::clone(process.runtime_context.resources()); let limits = reactor_io_limits(&process.limits); - let connected = Arc::new(Mutex::new(PendingJavascriptNetConnectState::default())); + let connected = Arc::new(Mutex::new(PendingNetConnectState::default())); let task_connected = Arc::clone(&connected); - if process - .pending_javascript_net_connects - .contains_key(&request_id) - { - return Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_SOCKET_CONNECT_STATE: request {request_id} already has a pending connect" - ))); + if process.pending_net_connects.contains_key(&request_id) { + return Err(SidecarError::host( + "ERR_AGENTOS_SOCKET_CONNECT_STATE", + format!("request {request_id} already has a pending connect"), + )); } process - .pending_javascript_net_connects + .pending_net_connects .insert(request_id, Arc::clone(&connected)); let (respond_to, receiver) = tokio::sync::oneshot::channel(); let spawn = runtime.spawn(agentos_runtime::TaskClass::Socket, async move { - let result = match tokio::time::timeout( + let result = match crate::execution::operation_deadline_timeout( + "TCP connect", limits.operation_deadline, tokio::net::TcpStream::connect(resolved.actual_addr), ) @@ -1639,7 +1647,7 @@ pub(in crate::execution) fn defer_native_tcp_connect( task_connected .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) - .connected = Some(PendingJavascriptNetConnect::Tcp { + .connected = Some(PendingNetConnect::Tcp { socket_id, socket: Box::new(socket), pending_capability, @@ -1657,6 +1665,7 @@ pub(in crate::execution) fn defer_native_tcp_connect( "TCP connect exceeded {}ms; raise limits.reactor.operationDeadlineMs", limits.operation_deadline.as_millis() ), + details: None, }), }; if respond_to.send(result).is_err() { @@ -1664,10 +1673,10 @@ pub(in crate::execution) fn defer_native_tcp_connect( } }); if let Err(error) = spawn { - process.pending_javascript_net_connects.remove(&request_id); + process.pending_net_connects.remove(&request_id); return Err(SidecarError::from(error)); } - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Socket, @@ -1691,12 +1700,13 @@ impl ActiveTcpListener { kernel_socket_id: None, local_addr: Some(local_addr), guest_local_addr: guest_addr, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize"), active_connection_ids: Arc::new(Mutex::new(BTreeSet::new())), description_handles: Arc::new(()), description_lease: Arc::new(SocketDescriptionLease::default()), kernel_transfer_guard: None, + pending_event: Arc::new(Mutex::new(None)), }) } @@ -1728,7 +1738,7 @@ impl ActiveTcpListener { EXECUTION_DRIVER_NAME, kernel_pid, socket_id, - usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize"), ) .map_err(kernel_error)?; @@ -1737,12 +1747,13 @@ impl ActiveTcpListener { kernel_socket_id: Some(socket_id), local_addr: Some(guest_addr), guest_local_addr: guest_addr, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize"), active_connection_ids: Arc::new(Mutex::new(BTreeSet::new())), description_handles: Arc::new(()), description_lease: Arc::new(SocketDescriptionLease::default()), kernel_transfer_guard: None, + pending_event: Arc::new(Mutex::new(None)), }) } @@ -1762,6 +1773,7 @@ impl ActiveTcpListener { description_handles: Arc::clone(&self.description_handles), description_lease: Arc::clone(&self.description_lease), kernel_transfer_guard: self.kernel_transfer_guard.clone(), + pending_event: Arc::clone(&self.pending_event), }) } @@ -1783,7 +1795,15 @@ impl ActiveTcpListener { kernel_pid: u32, wait: Duration, trace_enabled: bool, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { + if let Some(event) = self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "TCP listener pending event lock poisoned"))? + .take() + { + return Ok(Some(event)); + } if let Some(socket_id) = self.kernel_socket_id { let poll_started = Instant::now(); let result = kernel @@ -1821,7 +1841,7 @@ impl ActiveTcpListener { .server_accept_errors .fetch_add(1, Ordering::Relaxed); } - return Ok(Some(JavascriptTcpListenerEvent::Error { + return Ok(Some(TcpListenerEvent::Error { code: Some(error.code().to_string()), message: error.to_string(), })); @@ -1847,17 +1867,12 @@ impl ActiveTcpListener { .server_accept_connections .fetch_add(1, Ordering::Relaxed); } - return Ok(Some(JavascriptTcpListenerEvent::Connection( - PendingTcpSocket { - stream: None, - kernel_socket_id: Some(accepted_socket_id), - guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?, - guest_remote_addr: resolve_tcp_bind_addr( - remote_addr.host(), - remote_addr.port(), - )?, - }, - ))); + return Ok(Some(TcpListenerEvent::Connection(PendingTcpSocket { + stream: None, + kernel_socket_id: Some(accepted_socket_id), + guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?, + guest_remote_addr: resolve_tcp_bind_addr(remote_addr.host(), remote_addr.port())?, + }))); } let deadline = Instant::now() + wait; @@ -1878,20 +1893,22 @@ impl ActiveTcpListener { .len() >= self.backlog { - let _ = stream.shutdown(Shutdown::Both); + if let Err(error) = stream.shutdown(Shutdown::Both) { + eprintln!( + "ERR_AGENTOS_TCP_BACKLOG_CLEANUP: failed to shut down rejected connection: {error}" + ); + } if wait.is_zero() || Instant::now() >= deadline { return Ok(None); } continue; } - return Ok(Some(JavascriptTcpListenerEvent::Connection( - PendingTcpSocket { - stream: Some(stream), - kernel_socket_id: None, - guest_local_addr: self.guest_local_addr, - guest_remote_addr: remote_addr, - }, - ))); + return Ok(Some(TcpListenerEvent::Connection(PendingTcpSocket { + stream: Some(stream), + kernel_socket_id: None, + guest_local_addr: self.guest_local_addr, + guest_remote_addr: remote_addr, + }))); } Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { if wait.is_zero() || Instant::now() >= deadline { @@ -1908,7 +1925,7 @@ impl ActiveTcpListener { } } Err(error) => { - return Ok(Some(JavascriptTcpListenerEvent::Error { + return Ok(Some(TcpListenerEvent::Error { code: io_error_code(&error), message: error.to_string(), })); @@ -1917,6 +1934,30 @@ impl ActiveTcpListener { } } + /// Non-destructive listener readiness probe used by combined POSIX poll. + pub(in crate::execution) fn probe_readable( + &mut self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + trace_enabled: bool, + ) -> Result { + if self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "TCP listener pending event lock poisoned"))? + .is_some() + { + return Ok(true); + } + let event = self.poll(kernel, kernel_pid, Duration::ZERO, trace_enabled)?; + let mut pending = self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "TCP listener pending event lock poisoned"))?; + *pending = event; + Ok(pending.is_some()) + } + pub(in crate::execution) fn close( &self, kernel: &mut SidecarKernel, @@ -1956,9 +1997,7 @@ impl ActiveTcpListener { // UDP types moved to crate::state -pub(crate) fn build_javascript_socket_path_context( - vm: &VmState, -) -> Result { +pub(crate) fn build_socket_path_context(vm: &VmState) -> Result { let mut abstract_namespace_digest = Sha256::new(); abstract_namespace_digest.update(b"agentos-vm-unix-abstract-v1\0"); abstract_namespace_digest.update(vm.connection_id.as_bytes()); @@ -1974,7 +2013,7 @@ pub(crate) fn build_javascript_socket_path_context( let mut used_tcp_guest_ports = BTreeMap::new(); let mut used_udp_guest_ports = BTreeMap::new(); for (process_id, process) in &vm.active_processes { - collect_javascript_socket_port_state( + collect_socket_port_state( &vm.kernel, process_id, process, @@ -1986,8 +2025,8 @@ pub(crate) fn build_javascript_socket_path_context( &mut used_udp_guest_ports, ); } - Ok(JavascriptSocketPathContext { - sandbox_root: vm.cwd.clone(), + Ok(SocketPathContext { + sandbox_root: vm.runtime_scratch_root.clone(), unix_abstract_namespace, unix_socket_host_dir: vm.unix_socket_host_dir.clone(), unix_bound_addresses: Arc::clone(&vm.unix_address_registry), @@ -2004,25 +2043,27 @@ pub(crate) fn build_javascript_socket_path_context( }) } -pub(crate) fn finalize_javascript_net_connect( +pub(crate) fn finalize_net_connect( process: &mut ActiveProcess, kernel_readiness: &KernelSocketReadinessRegistry, - connected: Arc>, + connected: Arc>, ) -> Result { let mut state = connected.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_SOCKET_CONNECT_STATE: completion lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_SOCKET_CONNECT_STATE", + String::from("completion lock poisoned"), + ) })?; let connected = state.connected.take().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_SOCKET_CONNECT_STATE: successful connect had no socket", - )) + SidecarError::host( + "ERR_AGENTOS_SOCKET_CONNECT_STATE", + String::from("successful connect had no socket"), + ) })?; let bound_unix_listener = state.bound_unix_listener.take(); drop(state); match connected { - PendingJavascriptNetConnect::Tcp { + PendingNetConnect::Tcp { socket_id, socket, pending_capability, @@ -2042,8 +2083,11 @@ pub(crate) fn finalize_javascript_net_connect( process.tcp_port_reservations.remove(&reservation_id); } socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket.set_fairness_identity(process.capability_fairness_identity(&capability_key))?; socket.retain_description_lease( @@ -2054,7 +2098,9 @@ pub(crate) fn finalize_javascript_net_connect( register_kernel_readiness_target( kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -2072,7 +2118,7 @@ pub(crate) fn finalize_javascript_net_connect( "remoteFamily": socket_addr_family(&remote_addr), })) } - PendingJavascriptNetConnect::Unix { + PendingNetConnect::Unix { socket_id, socket, pending_capability, @@ -2096,8 +2142,11 @@ pub(crate) fn finalize_javascript_net_connect( None, )?; socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(identity), + Arc::clone(&process.process_event_notify), ); socket.set_fairness_identity(process.capability_fairness_identity(&capability_key))?; socket.retain_description_lease( @@ -2121,14 +2170,15 @@ pub(crate) fn finalize_javascript_net_connect( pub(crate) fn restore_pending_bound_unix_connect( process: &mut ActiveProcess, - pending: &Arc>, + pending: &Arc>, ) -> Result<(), SidecarError> { let bound = pending .lock() .map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_SOCKET_CONNECT_STATE: completion lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_SOCKET_CONNECT_STATE", + String::from("completion lock poisoned"), + ) })? .bound_unix_listener .take(); @@ -2140,50 +2190,51 @@ pub(crate) fn restore_pending_bound_unix_connect( pub(in crate::execution) fn normalize_tcp_listen_host( host: Option<&str>, -) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> { +) -> Result<(SocketFamily, &'static str, &'static str), SidecarError> { match host.unwrap_or("127.0.0.1") { - "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")), - "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")), - "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")), - "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")), - other => Err(SidecarError::Execution(format!( - "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}" - ))), + "127.0.0.1" | "localhost" => Ok((SocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")), + "::1" => Ok((SocketFamily::Ipv6, "::1", "::1")), + "0.0.0.0" => Ok((SocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")), + "::" => Ok((SocketFamily::Ipv6, "::1", "::")), + other => Err(SidecarError::host( + "EACCES", + format!("TCP listeners must bind to loopback or unspecified addresses, got {other}"), + )), } } pub(in crate::execution) fn normalize_udp_bind_host( host: Option<&str>, - family: JavascriptUdpFamily, -) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> { + family: UdpFamily, +) -> Result<(&'static str, &'static str, SocketFamily), SidecarError> { match (family, host) { - (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => { - Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4)) + (UdpFamily::Ipv4, None) | (UdpFamily::Ipv4, Some("0.0.0.0")) => { + Ok(("127.0.0.1", "0.0.0.0", SocketFamily::Ipv4)) } - (JavascriptUdpFamily::Ipv4, Some("127.0.0.1")) - | (JavascriptUdpFamily::Ipv4, Some("localhost")) => { - Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4)) + (UdpFamily::Ipv4, Some("127.0.0.1")) | (UdpFamily::Ipv4, Some("localhost")) => { + Ok(("127.0.0.1", "127.0.0.1", SocketFamily::Ipv4)) } - (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => { - Ok(("::1", "::", JavascriptSocketFamily::Ipv6)) + (UdpFamily::Ipv6, None) | (UdpFamily::Ipv6, Some("::")) => { + Ok(("::1", "::", SocketFamily::Ipv6)) } - (JavascriptUdpFamily::Ipv6, Some("::1")) - | (JavascriptUdpFamily::Ipv6, Some("localhost")) => { - Ok(("::1", "::1", JavascriptSocketFamily::Ipv6)) + (UdpFamily::Ipv6, Some("::1")) | (UdpFamily::Ipv6, Some("localhost")) => { + Ok(("::1", "::1", SocketFamily::Ipv6)) } - (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!( - "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}" - ))), - (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!( - "EACCES: udp6 sockets must bind to ::1 or ::, got {other}" - ))), + (UdpFamily::Ipv4, Some(other)) => Err(SidecarError::host( + "EACCES", + format!("udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"), + )), + (UdpFamily::Ipv6, Some(other)) => Err(SidecarError::host( + "EACCES", + format!("udp6 sockets must bind to ::1 or ::, got {other}"), + )), } } pub(in crate::execution) fn allocate_guest_listen_port( requested_port: u16, - family: JavascriptSocketFamily, - used_ports: &BTreeMap>, + family: SocketFamily, + used_ports: &BTreeMap>, policy: VmListenPolicy, ) -> Result { let is_allowed = |port: u16| { @@ -2323,12 +2374,13 @@ fn tls_command_admission_error( limit: usize, ) -> SidecarError { match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::Execution(format!( - "ERR_AGENTOS_TLS_COMMAND_LIMIT: TLS command queue exceeded {limit}; raise limits.reactor.maxHandleCommands" - )), - tokio::sync::mpsc::error::TrySendError::Closed(_) => SidecarError::Execution( - String::from("EPIPE: TLS transport task is closed"), + tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::host( + "ERR_AGENTOS_TLS_COMMAND_LIMIT", + format!("TLS command queue exceeded {limit}; raise limits.reactor.maxHandleCommands"), ), + tokio::sync::mpsc::error::TrySendError::Closed(_) => { + SidecarError::host("EPIPE", String::from("TLS transport task is closed")) + } } } @@ -2336,11 +2388,12 @@ pub(in crate::execution) fn plain_socket_command_admission_error( error: tokio::sync::mpsc::error::TrySendError, ) -> SidecarError { match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::Execution(String::from( - "ERR_AGENTOS_HANDLE_COMMAND_LIMIT: socket command queue is full; raise runtime.resources.maxHandleCommands", - )), + tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::host( + "ERR_AGENTOS_HANDLE_COMMAND_LIMIT", + String::from("socket command queue is full; raise runtime.resources.maxHandleCommands"), + ), tokio::sync::mpsc::error::TrySendError::Closed(_) => { - SidecarError::Execution(String::from("EPIPE: socket transport task is closed")) + SidecarError::host("EPIPE", "socket transport task is closed") } } } @@ -2470,52 +2523,57 @@ pub(in crate::execution) async fn run_plain_socket_transport( completion, } => { let written = payload.bytes.len(); - let result = match tokio::time::timeout(limits.operation_deadline, async { - let mut offset = 0; - while offset < payload.bytes.len() { - stream.writable().await?; - let (capability_id, vm_generation) = committed_socket_fairness_identity( - &fairness_identity, - &fairness_identity_committed, - ) - .await; - let turn = runtime - .fairness() - .acquire( - vm_generation, - capability_id, - FairBudget::new( - limits.operation_quantum.max(1), - limits.byte_quantum.max(1), - ), - ) - .await - .map_err(std::io::Error::other)?; - let chunk_len = turn - .allowance() - .bytes - .min(limits.byte_quantum.max(1)) - .min(payload.bytes.len() - offset) - .max(1); - match stream.try_write(&payload.bytes[offset..offset + chunk_len]) { - Ok(bytes) => { - turn.complete(FairBudget::new(1, bytes), false) - .map_err(std::io::Error::other)?; - offset += bytes; - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - turn.complete(FairBudget::new(1, 0), false) - .map_err(std::io::Error::other)?; - } - Err(error) => { - turn.complete(FairBudget::new(1, 0), false) - .map_err(std::io::Error::other)?; - return Err(error); + let result = match crate::execution::operation_deadline_timeout( + "TCP socket write", + limits.operation_deadline, + async { + let mut offset = 0; + while offset < payload.bytes.len() { + stream.writable().await?; + let (capability_id, vm_generation) = + committed_socket_fairness_identity( + &fairness_identity, + &fairness_identity_committed, + ) + .await; + let turn = runtime + .fairness() + .acquire( + vm_generation, + capability_id, + FairBudget::new( + limits.operation_quantum.max(1), + limits.byte_quantum.max(1), + ), + ) + .await + .map_err(std::io::Error::other)?; + let chunk_len = turn + .allowance() + .bytes + .min(limits.byte_quantum.max(1)) + .min(payload.bytes.len() - offset) + .max(1); + match stream.try_write(&payload.bytes[offset..offset + chunk_len]) { + Ok(bytes) => { + turn.complete(FairBudget::new(1, bytes), false) + .map_err(std::io::Error::other)?; + offset += bytes; + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + turn.complete(FairBudget::new(1, 0), false) + .map_err(std::io::Error::other)?; + } + Err(error) => { + turn.complete(FairBudget::new(1, 0), false) + .map_err(std::io::Error::other)?; + return Err(error); + } } } - } - Ok(()) - }) + Ok(()) + }, + ) .await { Ok(Ok(())) => Ok(json!(written)), @@ -2541,7 +2599,10 @@ pub(in crate::execution) async fn run_plain_socket_transport( _command_reservation: _, completion, } => { - let result = match tokio::time::timeout(limits.operation_deadline, async { + let result = match crate::execution::operation_deadline_timeout( + "TCP socket shutdown", + limits.operation_deadline, + async { run_plain_socket_fair_step( &runtime, limits, @@ -2584,9 +2645,10 @@ pub(in crate::execution) fn plain_socket_command_capacity( .limit .filter(|limit| *limit > 0) .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_HANDLE_COMMAND_UNBOUNDED: runtime.resources.maxHandleCommands must be non-zero", - )) + SidecarError::host( + "ERR_AGENTOS_HANDLE_COMMAND_UNBOUNDED", + String::from("runtime.resources.maxHandleCommands must be non-zero"), + ) }) } @@ -2636,6 +2698,7 @@ pub(in crate::execution) fn deferred_rpc_error( crate::state::DeferredRpcError { code: String::from(code), message: message.into(), + details: None, } } @@ -2657,14 +2720,12 @@ fn blocked_dns_resolution_error( cidr: &str, label: &str, ) -> SidecarError { - SidecarError::Execution(format!( - "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}" + SidecarError::host("EACCES", format!("blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}" )) } fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError { - SidecarError::Execution(format!( - "EACCES: blocked outbound network access to {resource}: {ip} is loopback ({}) and port {port} is not owned by this VM and is not listed in {LOOPBACK_EXEMPT_PORTS_ENV}", + SidecarError::host("EACCES", format!("blocked outbound network access to {resource}: {ip} is loopback ({}) and port {port} is not owned by this VM and is not listed in {LOOPBACK_EXEMPT_PORTS_ENV}", loopback_cidr(ip) )) } @@ -2698,7 +2759,7 @@ pub(in crate::execution) fn filter_dns_safe_ip_addrs( Ok(allowed) } -fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool { +fn loopback_connect_allowed(context: &SocketPathContext, port: u16) -> bool { context.loopback_port_allowed(port) } @@ -2706,7 +2767,7 @@ fn filter_tcp_connect_ip_addrs( addresses: Vec, host: &str, port: u16, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, ) -> Result, SidecarError> { let resource = format_tcp_resource(host, port); let mut allowed = Vec::new(); @@ -2746,7 +2807,7 @@ pub(in crate::execution) fn resolve_tcp_connect_addr( host: &str, port: u16, family: Option, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, ) -> Result where B: NativeSidecarBridge + Send + 'static, @@ -2781,7 +2842,7 @@ where .iter() .copied() .find(|candidate| { - let family = JavascriptSocketFamily::from_ip(*candidate); + let family = SocketFamily::from_ip(*candidate); context.translate_tcp_loopback_port(family, port).is_some() }) // We do not implement Happy Eyeballs yet, so prefer IPv4 over a @@ -2791,7 +2852,7 @@ where .ok_or_else(|| { SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}")) })?; - let family = JavascriptSocketFamily::from_ip(ip); + let family = SocketFamily::from_ip(ip); let translated_loopback_port = context.translate_tcp_loopback_port(family, port); let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port); let actual_port = if is_loopback_ip(ip) { @@ -2899,7 +2960,7 @@ pub(in crate::execution) fn filter_dns_ip_addrs( pub(in crate::execution) fn resolve_udp_bind_addr( host: &str, port: u16, - family: JavascriptUdpFamily, + family: UdpFamily, ) -> Result { (host, port) .to_socket_addrs() @@ -2955,7 +3016,7 @@ where allowed .into_iter() .map(|ip| { - let family_key = JavascriptSocketFamily::from_ip(ip); + let family_key = SocketFamily::from_ip(ip); let actual_port = if is_loopback_ip(ip) { context .translate_udp_loopback_port(family_key, port) @@ -2974,11 +3035,11 @@ where }) } -pub(in crate::execution) fn javascript_net_timeout_value() -> Value { - Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL)) +pub(in crate::execution) fn net_timeout_value() -> Value { + Value::String(String::from(NET_TIMEOUT_SENTINEL)) } -pub(in crate::execution) fn javascript_net_json_string( +pub(in crate::execution) fn encode_net_json_string( value: Value, label: &str, ) -> Result { @@ -2989,38 +3050,48 @@ pub(in crate::execution) fn javascript_net_json_string( }) } -pub(in crate::execution) fn javascript_net_read_value( - event: Option, +pub(in crate::execution) fn net_read_value( + event: Option, ) -> Result { match event { - Some(JavascriptTcpSocketEvent::Data { bytes, .. }) => Ok(Value::String( + Some(TcpSocketEvent::Data { bytes, .. }) => Ok(Value::String( base64::engine::general_purpose::STANDARD.encode(bytes), )), - Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => { - Ok(Value::Null) - } - Some(JavascriptTcpSocketEvent::Error { code, message }) => { - let detail = code.unwrap_or_else(|| String::from("socket read")); - Err(SidecarError::Execution(format!("{detail}: {message}"))) - } - None => Ok(javascript_net_timeout_value()), + Some(TcpSocketEvent::End | TcpSocketEvent::Close { .. }) => Ok(Value::Null), + Some(TcpSocketEvent::Error { code, message }) => Err(SidecarError::host( + code.as_deref().unwrap_or("EIO"), + message, + )), + None => Ok(net_timeout_value()), } } pub(in crate::execution) fn io_error_code(error: &std::io::Error) -> Option { match error.raw_os_error() { Some(libc::EACCES) => Some(String::from("EACCES")), + Some(libc::EAGAIN) => Some(String::from("EAGAIN")), + Some(libc::EALREADY) => Some(String::from("EALREADY")), Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")), Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")), Some(libc::EBADF) => Some(String::from("EBADF")), + Some(libc::ECONNABORTED) => Some(String::from("ECONNABORTED")), Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")), Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")), Some(libc::EDESTADDRREQ) => Some(String::from("EDESTADDRREQ")), + Some(libc::EEXIST) => Some(String::from("EEXIST")), + Some(libc::EINPROGRESS) => Some(String::from("EINPROGRESS")), Some(libc::EINVAL) => Some(String::from("EINVAL")), + Some(libc::EISCONN) => Some(String::from("EISCONN")), + Some(libc::EMFILE) => Some(String::from("EMFILE")), + Some(libc::ENETDOWN) => Some(String::from("ENETDOWN")), Some(libc::ENOPROTOOPT) => Some(String::from("ENOPROTOOPT")), + Some(libc::ENOSPC) => Some(String::from("ENOSPC")), Some(libc::ENOTCONN) => Some(String::from("ENOTCONN")), + Some(libc::ENOENT) => Some(String::from("ENOENT")), Some(libc::EOPNOTSUPP) => Some(String::from("EOPNOTSUPP")), Some(libc::EPIPE) => Some(String::from("EPIPE")), + Some(libc::EPROTONOSUPPORT) => Some(String::from("EPROTONOSUPPORT")), + Some(libc::ESRCH) => Some(String::from("ESRCH")), Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")), Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")), Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")), @@ -3029,11 +3100,11 @@ pub(in crate::execution) fn io_error_code(error: &std::io::Error) -> Option SidecarError { - let message = match io_error_code(&error) { - Some(code) => format!("{code}: {error}"), - None => error.to_string(), - }; - SidecarError::Execution(message) + let code = io_error_code(&error).unwrap_or_else(|| String::from("EIO")); + SidecarError::Host(agentos_execution::backend::HostServiceError::new( + code, + error.to_string(), + )) } struct PlainTcpReaderLease { @@ -3055,7 +3126,7 @@ impl Drop for PlainTcpReaderLease { fn spawn_tcp_socket_reader( runtime: agentos_runtime::RuntimeContext, stream: TcpStream, - sender: AsyncCompletionSender, + sender: AsyncCompletionSender, read_event_notify: Arc, event_pusher: Arc, application_read_interest: Arc, @@ -3185,7 +3256,7 @@ fn spawn_tcp_socket_reader( match read_result { Ok(0) => { saw_remote_end.store(true, Ordering::SeqCst); - if sender.send(JavascriptTcpSocketEvent::End).await.is_err() { + if sender.send(TcpSocketEvent::End).await.is_err() { break; } read_event_notify.notify_one(); @@ -3193,7 +3264,7 @@ fn spawn_tcp_socket_reader( if saw_local_shutdown.load(Ordering::SeqCst) && !close_notified.swap(true, Ordering::SeqCst) && sender - .send(JavascriptTcpSocketEvent::Close { had_error: false }) + .send(TcpSocketEvent::Close { had_error: false }) .await .is_ok() { @@ -3216,7 +3287,7 @@ fn spawn_tcp_socket_reader( break; }; if sender - .send(JavascriptTcpSocketEvent::Data { + .send(TcpSocketEvent::Data { bytes: buffer[..bytes_read].to_vec(), reservation: SharedReservation::new(reservation), source_reservations: Vec::new(), @@ -3258,7 +3329,7 @@ fn spawn_tcp_socket_reader( pub(in crate::execution) async fn reserve_socket_event_bytes_or_close( resources: &ResourceLedger, bytes: usize, - sender: &AsyncCompletionSender, + sender: &AsyncCompletionSender, event_pusher: &Arc, close_notified: &Arc, ) -> Option { @@ -3284,7 +3355,7 @@ pub(in crate::execution) async fn reserve_socket_event_bytes_or_close( pub(in crate::execution) async fn reserve_tls_event_bytes_or_close( resources: &ResourceLedger, bytes: usize, - sender: &AsyncCompletionSender, + sender: &AsyncCompletionSender, event_pusher: &Arc, close_notified: &Arc, ) -> Option<(Reservation, Reservation)> { @@ -3326,14 +3397,14 @@ pub(in crate::execution) async fn reserve_tls_event_bytes_or_close( } pub(in crate::execution) async fn send_async_socket_error_and_close( - sender: &AsyncCompletionSender, + sender: &AsyncCompletionSender, event_pusher: &Arc, close_notified: &Arc, code: Option, message: String, ) { if sender - .send(JavascriptTcpSocketEvent::Error { code, message }) + .send(TcpSocketEvent::Error { code, message }) .await .is_ok() { @@ -3341,7 +3412,7 @@ pub(in crate::execution) async fn send_async_socket_error_and_close( } if !close_notified.swap(true, Ordering::SeqCst) && sender - .send(JavascriptTcpSocketEvent::Close { had_error: true }) + .send(TcpSocketEvent::Close { had_error: true }) .await .is_ok() { @@ -3497,7 +3568,7 @@ fn record_net_tcp_kernel_poll( mod socket_read_limit_tests { use super::*; - fn data_event(bytes: &[u8]) -> JavascriptTcpSocketEvent { + fn data_event(bytes: &[u8]) -> TcpSocketEvent { let resources = ResourceLedger::root( "tcp-partial-read-test", [( @@ -3508,16 +3579,16 @@ mod socket_read_limit_tests { let reservation = resources .reserve(ResourceClass::BufferedBytes, bytes.len()) .expect("reserve test socket bytes"); - JavascriptTcpSocketEvent::Data { + TcpSocketEvent::Data { bytes: bytes.to_vec(), reservation: SharedReservation::new(reservation), source_reservations: Vec::new(), } } - fn event_bytes(event: Option) -> Vec { + fn event_bytes(event: Option) -> Vec { match event.expect("expected socket data event") { - JavascriptTcpSocketEvent::Data { bytes, .. } => bytes, + TcpSocketEvent::Data { bytes, .. } => bytes, other => panic!("expected socket data, got {other:?}"), } } @@ -3724,7 +3795,7 @@ mod transferred_alias_transport_tests { .try_recv() .expect("surviving alias reads queued data"); match event { - JavascriptTcpSocketEvent::Data { bytes, .. } => { + TcpSocketEvent::Data { bytes, .. } => { assert_eq!(bytes, b"host-to-survivor") } other => panic!("expected TCP data after alias close, got {other:?}"), @@ -3774,16 +3845,15 @@ mod ssrf_egress_classifier_tests { // deterministically. See FAILURES.md#F-005, #F-006, #F-007. use super::{ filter_dns_ip_addrs, filter_dns_safe_ip_addrs, filter_tcp_connect_ip_addrs, is_loopback_ip, - restricted_non_loopback_ip_range, JavascriptSocketFamily, JavascriptSocketPathContext, - SidecarError, VmListenPolicy, + restricted_non_loopback_ip_range, SocketFamily, SocketPathContext, VmListenPolicy, }; use std::collections::{BTreeMap, BTreeSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; - fn socket_policy_context() -> JavascriptSocketPathContext { - JavascriptSocketPathContext { + fn socket_policy_context() -> SocketPathContext { + SocketPathContext { sandbox_root: PathBuf::from("/tmp/agentos-egress-policy-test"), unix_abstract_namespace: [0; 32], unix_socket_host_dir: PathBuf::from("/tmp/agentos-egress-policy-test/unix"), @@ -3816,10 +3886,7 @@ mod ssrf_egress_classifier_tests { fn assert_dns_denied(ip: IpAddr, label: &str) { match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") { - Err(SidecarError::Execution(message)) => assert!( - message.starts_with("EACCES:"), - "{label}: egress filter must deny with EACCES, got: {message}" - ), + Err(error) if error.code() == Some("EACCES") => {} other => panic!("{label}: expected EACCES denial, got {other:?}"), } } @@ -3832,11 +3899,11 @@ mod ssrf_egress_classifier_tests { let mixed_error = filter_tcp_connect_ip_addrs(vec![private, public], "mixed.example", 53, &context) .expect_err("a mixed safe/blocked answer must fail closed as a unit"); - assert!(mixed_error.to_string().starts_with("EACCES:")); + assert_eq!(mixed_error.code(), Some("EACCES")); let error = filter_tcp_connect_ip_addrs(vec![private], "rebound.example", 53, &context) .expect_err("a DNS answer that rebinds entirely into private space is denied"); - assert!(error.to_string().starts_with("EACCES:")); + assert_eq!(error.code(), Some("EACCES")); } #[test] @@ -3860,7 +3927,7 @@ mod ssrf_egress_classifier_tests { let guest_port = 4242; context .udp_loopback_guest_to_host_ports - .insert((JavascriptSocketFamily::Ipv4, guest_port), guest_port); + .insert((SocketFamily::Ipv4, guest_port), guest_port); assert_eq!( filter_tcp_connect_ip_addrs( diff --git a/crates/native-sidecar/src/execution/network/tls.rs b/crates/native-sidecar/src/execution/network/tls.rs index 973039c5da..2a89047e46 100644 --- a/crates/native-sidecar/src/execution/network/tls.rs +++ b/crates/native-sidecar/src/execution/network/tls.rs @@ -176,7 +176,7 @@ impl crate::state::LoopbackTlsEndpoint { pub(in crate::execution) fn parse_tls_client_hello_from_bytes( buffer: &[u8], -) -> Result, SidecarError> { +) -> Result, SidecarError> { if buffer.is_empty() { return Ok(None); } @@ -196,7 +196,7 @@ pub(in crate::execution) fn parse_tls_client_hello_from_bytes( .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok()) .collect::>() }); - Ok(Some(JavascriptTlsClientHello { + Ok(Some(TlsClientHello { servername: client_hello.server_name().map(str::to_owned), alpn_protocols, })) @@ -206,7 +206,7 @@ pub(in crate::execution) fn peek_loopback_tls_client_hello( vm_id: &str, socket_id: SocketId, peer_socket_id: SocketId, -) -> Result, SidecarError> { +) -> Result, SidecarError> { let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id); let registry = loopback_tls_transport_registry(); let pair = registry @@ -591,7 +591,7 @@ pub(in crate::execution) fn tls_provider() -> Arc Result>, SidecarError> { let Some(certificates) = options.cert.as_ref() else { return Ok(Vec::new()); @@ -599,26 +599,26 @@ pub(in crate::execution) fn tls_local_certificates( tls_material_entries(certificates) } -fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result>, SidecarError> { +fn tls_material_entries(material: &TlsMaterial) -> Result>, SidecarError> { match material { - JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]), - JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(), + TlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]), + TlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(), } } -fn tls_data_value(value: &JavascriptTlsDataValue) -> Result, SidecarError> { +fn tls_data_value(value: &TlsDataValue) -> Result, SidecarError> { match value { - JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD + TlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD .decode(data) .map_err(|error| { SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}")) }), - JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()), + TlsDataValue::String { data } => Ok(data.as_bytes().to_vec()), } } fn tls_certificates_from_material( - material: &JavascriptTlsMaterial, + material: &TlsMaterial, ) -> Result>, SidecarError> { let mut certificates = Vec::new(); for entry in tls_material_entries(material)? { @@ -641,7 +641,7 @@ fn tls_certificates_from_material( } fn tls_private_key_from_material( - material: &JavascriptTlsMaterial, + material: &TlsMaterial, ) -> Result, SidecarError> { for entry in tls_material_entries(material)? { let mut reader = std::io::BufReader::new(Cursor::new(entry)); @@ -656,28 +656,119 @@ fn tls_private_key_from_material( pub(in crate::execution) fn vm_default_ca_bundle_for_tls_options( kernel: &mut SidecarKernel, - options: &JavascriptTlsBridgeOptions, + requester_pid: u32, + options: &TlsBridgeOptions, ) -> Result, SidecarError> { if options.is_server || options.reject_unauthorized == Some(false) || options.ca.is_some() { return Ok(Vec::new()); } - read_vm_default_ca_bundle(kernel) + read_vm_default_ca_bundle(kernel, requester_pid) } pub(in crate::execution) fn read_vm_default_ca_bundle( kernel: &mut SidecarKernel, + requester_pid: u32, ) -> Result, SidecarError> { kernel - .read_file(CA_CERTIFICATES_GUEST_PATH) - .map_err(|error| { - SidecarError::Execution(format!( - "failed to read VM TLS trust store {CA_CERTIFICATES_GUEST_PATH}: {error}" - )) - }) + .read_file_for_process( + EXECUTION_DRIVER_NAME, + requester_pid, + CA_CERTIFICATES_GUEST_PATH, + ) + .map_err(kernel_error) +} + +#[cfg(test)] +mod ca_bundle_tests { + use super::*; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::resource_accounting::ResourceLimits; + use agentos_kernel::vfs::MemoryFileSystem; + + fn ca_test_kernel() -> (SidecarKernel, u32) { + let mut config = KernelVmConfig::new("vm-ca-bundle-bounds"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_pread_bytes: Some(4), + ..ResourceLimits::default() + }; + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register CA test driver"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn CA test process"); + kernel + .mkdir("/etc/ssl/certs", true) + .expect("create CA directory"); + (kernel, process.pid()) + } + + #[test] + fn live_ca_bundle_read_is_process_authorized_and_bounded() { + let (mut kernel, pid) = ca_test_kernel(); + kernel + .write_file(CA_CERTIFICATES_GUEST_PATH, b"four".to_vec()) + .expect("write exact CA bundle"); + assert_eq!( + read_vm_default_ca_bundle(&mut kernel, pid).expect("read exact CA bundle"), + b"four" + ); + + kernel + .write_file(CA_CERTIFICATES_GUEST_PATH, b"five!".to_vec()) + .expect("grow CA bundle"); + let oversized = read_vm_default_ca_bundle(&mut kernel, pid) + .expect_err("oversized CA bundle must fail before allocation"); + assert_eq!(oversized.code(), Some("EINVAL")); + assert!(oversized + .to_string() + .contains("limits.resources.maxPreadBytes")); + + kernel + .write_file(CA_CERTIFICATES_GUEST_PATH, b"four".to_vec()) + .expect("restore CA bundle"); + kernel + .chmod(CA_CERTIFICATES_GUEST_PATH, 0) + .expect("deny CA bundle"); + assert_eq!( + read_vm_default_ca_bundle(&mut kernel, pid) + .expect_err("CA read must enforce process DAC") + .code(), + Some("EACCES") + ); + + kernel + .remove_file(CA_CERTIFICATES_GUEST_PATH) + .expect("remove CA bundle"); + kernel + .symlink("/etc/ssl/certs/ca-loop", CA_CERTIFICATES_GUEST_PATH) + .expect("first CA loop link"); + kernel + .symlink(CA_CERTIFICATES_GUEST_PATH, "/etc/ssl/certs/ca-loop") + .expect("second CA loop link"); + assert_eq!( + read_vm_default_ca_bundle(&mut kernel, pid) + .expect_err("CA symlink loop must stay typed") + .code(), + Some("ELOOP") + ); + } } fn tls_root_store( - options: &JavascriptTlsBridgeOptions, + options: &TlsBridgeOptions, default_ca_bundle: &[u8], ) -> Result { let mut roots = RootCertStore::empty(); @@ -710,7 +801,7 @@ fn tls_root_store( } pub(in crate::execution) fn build_client_tls_config( - options: &JavascriptTlsBridgeOptions, + options: &TlsBridgeOptions, default_ca_bundle: &[u8], ) -> Result { let provider = tls_provider(); @@ -747,7 +838,7 @@ pub(in crate::execution) fn build_client_tls_config( } pub(in crate::execution) fn build_server_tls_config( - options: &JavascriptTlsBridgeOptions, + options: &TlsBridgeOptions, ) -> Result { let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| { SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate")) @@ -952,7 +1043,7 @@ pub(in crate::execution) fn reserve_tls_command( } pub(in crate::execution) fn native_tls_role( - options: &JavascriptTlsBridgeOptions, + options: &TlsBridgeOptions, default_ca_bundle: &[u8], ) -> Result { if options.is_server { @@ -1016,7 +1107,7 @@ fn tls_transport_is_already_closed(error: &std::io::Error) -> bool { async fn run_native_tls_transport( mut stream: S, mut commands: TokioReceiver, - sender: AsyncCompletionSender, + sender: AsyncCompletionSender, event_pusher: Arc, application_read_interest: Arc, application_read_notify: Arc, @@ -1052,7 +1143,10 @@ async fn run_native_tls_transport( match command { NativeTlsCommand::Write { payload, completion } => { let payload_len = payload.bytes.len(); - let result = tokio::time::timeout(limits.operation_deadline, async { + let result = crate::execution::operation_deadline_timeout( + "TLS socket write", + limits.operation_deadline, + async { let mut offset = 0; while offset < payload.bytes.len() { let (capability_id, vm_generation) = fairness_identity; @@ -1111,7 +1205,8 @@ async fn run_native_tls_transport( _command_reservation: _, completion, } => { - let result = tokio::time::timeout( + let result = crate::execution::operation_deadline_timeout( + "TLS socket shutdown", limits.operation_deadline, async { let (capability_id, vm_generation) = fairness_identity; @@ -1155,7 +1250,8 @@ async fn run_native_tls_transport( // reacquire a retired capability; the bounded command // reservation and operation deadline already govern the // final transport shutdown. - let result = tokio::time::timeout( + let result = crate::execution::operation_deadline_timeout( + "TLS socket close", limits.operation_deadline, AsyncWriteExt::shutdown(&mut stream), ) @@ -1178,13 +1274,13 @@ async fn run_native_tls_transport( match read_result { Ok(0) => { saw_remote_end.store(true, Ordering::SeqCst); - if sender.send(JavascriptTcpSocketEvent::End).await.is_ok() { + if sender.send(TcpSocketEvent::End).await.is_ok() { push_socket_event(&event_pusher, "end"); } if saw_local_shutdown.load(Ordering::SeqCst) && !close_notified.swap(true, Ordering::SeqCst) && sender - .send(JavascriptTcpSocketEvent::Close { had_error: false }) + .send(TcpSocketEvent::Close { had_error: false }) .await .is_ok() { @@ -1246,7 +1342,7 @@ async fn run_native_tls_transport( .await; break; } - let event = JavascriptTcpSocketEvent::Data { + let event = TcpSocketEvent::Data { bytes: buffer[..bytes_read].to_vec(), reservation: SharedReservation::new(reservation), source_reservations: vec![SharedReservation::new(tls_reservation)], @@ -1273,13 +1369,13 @@ async fn run_native_tls_transport( } Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => { saw_remote_end.store(true, Ordering::SeqCst); - if sender.send(JavascriptTcpSocketEvent::End).await.is_ok() { + if sender.send(TcpSocketEvent::End).await.is_ok() { push_socket_event(&event_pusher, "end"); } if saw_local_shutdown.load(Ordering::SeqCst) && !close_notified.swap(true, Ordering::SeqCst) && sender - .send(JavascriptTcpSocketEvent::Close { had_error: false }) + .send(TcpSocketEvent::Close { had_error: false }) .await .is_ok() { @@ -1323,7 +1419,7 @@ pub(in crate::execution) fn spawn_native_tls_transport( stream: TcpStream, role: NativeTlsRole, tls_state: Arc>>, - sender: AsyncCompletionSender, + sender: AsyncCompletionSender, event_pusher: Arc, application_read_interest: Arc, application_read_notify: Arc, @@ -1400,7 +1496,8 @@ pub(in crate::execution) fn spawn_native_tls_transport( config, server_name, } => { - let handshake = tokio::time::timeout( + let handshake = crate::execution::operation_deadline_timeout( + "TLS client handshake", limits.operation_deadline, TlsConnector::from(config).connect(server_name, stream), ) @@ -1485,7 +1582,8 @@ pub(in crate::execution) fn spawn_native_tls_transport( .await; } NativeTlsRole::Server { config } => { - let handshake = tokio::time::timeout( + let handshake = crate::execution::operation_deadline_timeout( + "TLS server handshake", limits.operation_deadline, TlsAcceptor::from(config).accept(stream), ) @@ -1582,7 +1680,7 @@ pub(in crate::execution) fn spawn_loopback_tls_transport( endpoint: crate::state::LoopbackTlsEndpoint, role: NativeTlsRole, tls_state: Arc>>, - sender: AsyncCompletionSender, + sender: AsyncCompletionSender, event_pusher: Arc, application_read_interest: Arc, application_read_notify: Arc, @@ -1605,7 +1703,8 @@ pub(in crate::execution) fn spawn_loopback_tls_transport( config, server_name, } => { - let handshake = tokio::time::timeout( + let handshake = crate::execution::operation_deadline_timeout( + "TLS client handshake", limits.operation_deadline, TlsConnector::from(config).connect(server_name, endpoint), ) @@ -1690,7 +1789,8 @@ pub(in crate::execution) fn spawn_loopback_tls_transport( .await; } NativeTlsRole::Server { config } => { - let handshake = tokio::time::timeout( + let handshake = crate::execution::operation_deadline_timeout( + "TLS server handshake", limits.operation_deadline, TlsAcceptor::from(config).accept(endpoint), ) @@ -1786,7 +1886,7 @@ mod loopback_tls_registry_tests { loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key, native_tls_role, tls_transport_is_already_closed, NativeTlsRole, }; - use crate::state::JavascriptTlsBridgeOptions; + use crate::state::TlsBridgeOptions; use agentos_runtime::accounting::{ResourceClass, ResourceLedger, ResourceLimit}; use std::sync::Arc; @@ -1802,11 +1902,11 @@ mod loopback_tls_registry_tests { #[test] fn empty_servername_uses_ip_host_without_sni() { - let options = JavascriptTlsBridgeOptions { + let options = TlsBridgeOptions { host: Some(String::from("127.0.0.1")), servername: Some(String::new()), reject_unauthorized: Some(false), - ..JavascriptTlsBridgeOptions::default() + ..TlsBridgeOptions::default() }; let NativeTlsRole::Client { config, diff --git a/crates/native-sidecar/src/execution/network/udp.rs b/crates/native-sidecar/src/execution/network/udp.rs index 9a683b7945..c53eb6dae9 100644 --- a/crates/native-sidecar/src/execution/network/udp.rs +++ b/crates/native-sidecar/src/execution/network/udp.rs @@ -9,7 +9,7 @@ pub(in crate::execution) struct ActiveUdpSendToRequest<'a, B> { pub(in crate::execution) dns: &'a VmDnsConfig, pub(in crate::execution) host: &'a str, pub(in crate::execution) port: u16, - pub(in crate::execution) context: &'a JavascriptSocketPathContext, + pub(in crate::execution) context: &'a SocketPathContext, pub(in crate::execution) contents: &'a [u8], } @@ -21,7 +21,7 @@ pub(in crate::execution) struct ActiveUdpConnectRequest<'a, B> { dns: &'a VmDnsConfig, host: &'a str, port: u16, - context: &'a JavascriptSocketPathContext, + context: &'a SocketPathContext, } pub(in crate::execution) struct UdpRemoteAddrRequest<'a, B> { @@ -31,24 +31,153 @@ pub(in crate::execution) struct UdpRemoteAddrRequest<'a, B> { pub(in crate::execution) dns: &'a VmDnsConfig, pub(in crate::execution) host: &'a str, pub(in crate::execution) port: u16, - pub(in crate::execution) family: JavascriptUdpFamily, - pub(in crate::execution) context: &'a JavascriptSocketPathContext, + pub(in crate::execution) family: UdpFamily, + pub(in crate::execution) context: &'a SocketPathContext, } -pub(in crate::execution) struct JavascriptDgramSyncRpcServiceRequest<'a, B> { +pub(in crate::execution) struct DgramServiceRequest<'a, B> { pub(in crate::execution) bridge: &'a SharedBridge, pub(in crate::execution) kernel: &'a mut SidecarKernel, pub(in crate::execution) vm_id: &'a str, pub(in crate::execution) dns: &'a VmDnsConfig, - pub(in crate::execution) socket_paths: &'a JavascriptSocketPathContext, + pub(in crate::execution) socket_paths: &'a SocketPathContext, + pub(in crate::execution) process: &'a mut ActiveProcess, + pub(in crate::execution) kernel_readiness: KernelSocketReadinessRegistry, + pub(in crate::execution) operation: DgramOperation, + pub(in crate::execution) capabilities: CapabilityRegistry, +} + +pub(in crate::execution) enum DgramOperation { + Create { + family: UdpFamily, + }, + Bind { + socket_id: String, + address: Option, + port: u16, + }, + Send { + socket_id: String, + bytes: Vec, + address: Option, + port: Option, + }, + Connect { + socket_id: String, + address: Option, + port: u16, + }, + Disconnect { + socket_id: String, + }, + RemoteAddress { + socket_id: String, + }, + Close { + socket_id: String, + }, + Address { + socket_id: String, + }, + SetOption { + socket_id: String, + name: String, + payload: Value, + }, + SetBufferSize { + socket_id: String, + which: String, + size: usize, + }, + GetBufferSize { + socket_id: String, + which: String, + }, +} + +pub(in crate::execution) struct ManagedUdpServiceRequest<'a, B> { + pub(in crate::execution) bridge: &'a SharedBridge, + pub(in crate::execution) kernel: &'a mut SidecarKernel, + pub(in crate::execution) vm_id: &'a str, + pub(in crate::execution) dns: &'a VmDnsConfig, + pub(in crate::execution) socket_paths: &'a SocketPathContext, pub(in crate::execution) process: &'a mut ActiveProcess, pub(in crate::execution) kernel_readiness: KernelSocketReadinessRegistry, - pub(in crate::execution) sync_request: &'a JavascriptSyncRpcRequest, pub(in crate::execution) capabilities: CapabilityRegistry, } const UDP_MAX_DATAGRAM_BYTES: usize = 64 * 1024; +/// Cloneable poll-side capabilities only. This deliberately excludes socket +/// description handles, transfer guards, and capability leases: using a +/// descriptor clone as an async poll token would corrupt close/SCM_RIGHTS +/// lifetime accounting. +#[derive(Clone)] +pub(in crate::execution) struct ActiveUdpPollHandle { + pub(in crate::execution) native_commands: Option>, + resources: Arc, + runtime_context: agentos_runtime::RuntimeContext, + reactor_limits: ReactorIoLimits, + fairness_identity: Arc>, + fairness_identity_committed: Arc, + pub(in crate::execution) read_event_notify: Arc, + pub(in crate::execution) pending_datagram: Arc>>, +} + +impl ActiveUdpPollHandle { + pub(in crate::execution) fn operation_deadline(&self) -> Duration { + self.reactor_limits.operation_deadline + } + pub(in crate::execution) async fn acquire_fair_turn( + &self, + ) -> Result { + acquire_native_udp_fair_turn( + &self.runtime_context, + self.reactor_limits, + &self.fairness_identity, + &self.fairness_identity_committed, + ) + .await + } + + pub(in crate::execution) async fn poll_native_once( + &self, + ) -> Result, SidecarError> { + let Some(commands) = self.native_commands.as_ref() else { + return Ok(None); + }; + let (completion, receiver) = tokio::sync::oneshot::channel(); + commands + .try_send(NativeUdpCommand::Poll { + _command_reservation: reserve_udp_command(&self.resources)?, + completion, + }) + .map_err(|error| { + udp_command_admission_error(error, self.reactor_limits.max_handle_commands) + })?; + match crate::execution::operation_deadline_timeout( + "UDP poll completion", + self.reactor_limits.operation_deadline, + receiver, + ) + .await + { + Ok(Ok(result)) => result.map_err(SidecarError::from), + Ok(Err(_)) => Err(SidecarError::host( + "EPIPE", + "native UDP owner dropped poll completion", + )), + Err(_) => Err(SidecarError::host( + "ETIMEDOUT", + format!( + "UDP poll exceeded {}ms; raise limits.reactor.operationDeadlineMs", + self.reactor_limits.operation_deadline.as_millis() + ), + )), + } + } +} + fn udp_receive_capacity(resources: &ResourceLedger, limits: ReactorIoLimits) -> usize { let aggregate_limit = resources .usage(ResourceClass::BufferedBytes) @@ -109,10 +238,10 @@ pub(in crate::execution) enum ActiveUdpValueResult { fn udp_value_service_response( result: ActiveUdpValueResult, task_class: agentos_runtime::TaskClass, -) -> JavascriptSyncRpcServiceResponse { +) -> HostServiceResponse { match result { - ActiveUdpValueResult::Immediate(value) => JavascriptSyncRpcServiceResponse::Json(value), - ActiveUdpValueResult::Deferred(receiver) => JavascriptSyncRpcServiceResponse::Deferred { + ActiveUdpValueResult::Immediate(value) => HostServiceResponse::Json(value), + ActiveUdpValueResult::Deferred(receiver) => HostServiceResponse::Deferred { receiver, timeout: None, task_class, @@ -120,18 +249,18 @@ fn udp_value_service_response( } } -fn udp_send_service_response(result: ActiveUdpSendResult) -> JavascriptSyncRpcServiceResponse { +fn udp_send_service_response(result: ActiveUdpSendResult) -> HostServiceResponse { match result { ActiveUdpSendResult::Immediate { written, local_addr, - } => JavascriptSyncRpcServiceResponse::Json(json!({ + } => HostServiceResponse::Json(json!({ "bytes": written, "localAddress": local_addr.ip().to_string(), "localPort": local_addr.port(), "family": socket_addr_family(&local_addr), })), - ActiveUdpSendResult::Deferred { receiver } => JavascriptSyncRpcServiceResponse::Deferred { + ActiveUdpSendResult::Deferred { receiver } => HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Udp, @@ -139,49 +268,17 @@ fn udp_send_service_response(result: ActiveUdpSendResult) -> JavascriptSyncRpcSe } } -pub(in crate::execution) async fn await_udp_send_result( - result: ActiveUdpSendResult, -) -> Result { - match result { - ActiveUdpSendResult::Immediate { written, .. } => Ok(written), - ActiveUdpSendResult::Deferred { receiver } => { - let value = receiver.await.map_err(|_| { - SidecarError::Execution(String::from( - "EPIPE: native UDP owner dropped send completion", - )) - })?; - let value = value.map_err(|error| { - SidecarError::Execution(format!("{}: {}", error.code, error.message)) - })?; - value - .get("bytes") - .and_then(Value::as_u64) - .ok_or_else(|| { - SidecarError::Execution(String::from( - "ERR_AGENTOS_UDP_COMPLETION: native UDP send omitted byte count", - )) - }) - .and_then(|written| { - usize::try_from(written).map_err(|_| { - SidecarError::Execution(String::from( - "ERR_AGENTOS_UDP_COMPLETION: native UDP byte count overflow", - )) - }) - }) - } - } -} - fn udp_command_admission_error( error: tokio::sync::mpsc::error::TrySendError, limit: usize, ) -> SidecarError { match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::Execution(format!( - "ERR_AGENTOS_UDP_COMMAND_LIMIT: UDP command queue exceeded {limit}; raise limits.reactor.maxHandleCommands" - )), + tokio::sync::mpsc::error::TrySendError::Full(_) => SidecarError::host( + "ERR_AGENTOS_UDP_COMMAND_LIMIT", + format!("UDP command queue exceeded {limit}; raise limits.reactor.maxHandleCommands"), + ), tokio::sync::mpsc::error::TrySendError::Closed(_) => { - SidecarError::Execution(String::from("EBADF: native UDP owner task is closed")) + SidecarError::host("EBADF", "native UDP owner task is closed") } } } @@ -219,16 +316,14 @@ fn reserve_udp_send_payload( } fn udp_deferred_error(error: SidecarError) -> crate::state::DeferredRpcError { - crate::state::DeferredRpcError { - code: javascript_sync_rpc_error_code(&error), - message: javascript_sync_rpc_error_message(&error), - } + crate::state::DeferredRpcError::from(host_service_error(&error)) } fn udp_io_deferred_error(error: std::io::Error) -> crate::state::DeferredRpcError { crate::state::DeferredRpcError { code: io_error_code(&error).unwrap_or_else(|| String::from("ERR_AGENTOS_UDP_NATIVE")), message: error.to_string(), + details: None, } } @@ -252,12 +347,10 @@ fn ipv4_interface(interface: Option<&str>) -> Result { .map_err(|_| std::io::Error::from_raw_os_error(libc::EINVAL)) } -fn interface_name_to_index(interface: &str) -> Result { - nix::net::if_::if_nametoindex(interface) - .map_err(|error| std::io::Error::from_raw_os_error(error as i32)) -} - -fn ipv6_interface_index(interface: Option<&str>) -> Result { +fn ipv6_interface_index( + socket: &tokio::net::UdpSocket, + interface: Option<&str>, +) -> Result { let Some(interface) = interface.filter(|value| !value.is_empty()) else { return Ok(0); }; @@ -276,7 +369,8 @@ fn ipv6_interface_index(interface: Option<&str>) -> Result .map(|_| interface.interface_name) }) .ok_or_else(|| std::io::Error::from_raw_os_error(libc::EADDRNOTAVAIL))?; - return interface_name_to_index(interface_name.as_str()); + return rustix::net::netdevice::name_to_index(socket, interface_name.as_str()) + .map_err(|error| std::io::Error::from_raw_os_error(error.raw_os_error())); } let scope = interface .rsplit_once('%') @@ -284,22 +378,23 @@ fn ipv6_interface_index(interface: Option<&str>) -> Result if let Ok(index) = scope.parse::() { return Ok(index); } - interface_name_to_index(scope) + rustix::net::netdevice::name_to_index(socket, scope) + .map_err(|error| std::io::Error::from_raw_os_error(error.raw_os_error())) } fn set_udp_multicast_interface( socket: &tokio::net::UdpSocket, - family: JavascriptUdpFamily, + family: UdpFamily, interface: &str, ) -> Result<(), std::io::Error> { let socket_ref = SockRef::from(socket); match family { - JavascriptUdpFamily::Ipv4 => { + UdpFamily::Ipv4 => { let address = ipv4_interface(Some(interface))?; socket_ref.set_multicast_if_v4(&address) } - JavascriptUdpFamily::Ipv6 => { - let index = ipv6_interface_index(Some(interface))?; + UdpFamily::Ipv6 => { + let index = ipv6_interface_index(socket, Some(interface))?; socket_ref.set_multicast_if_v6(index) } } @@ -337,7 +432,7 @@ fn disconnect_native_udp(socket: &tokio::net::UdpSocket) -> Result<(), std::io:: fn apply_native_udp_option( socket: &tokio::net::UdpSocket, - family: JavascriptUdpFamily, + family: UdpFamily, option: NativeUdpSocketOption, ) -> Result { let socket_ref = SockRef::from(socket); @@ -348,13 +443,13 @@ fn apply_native_udp_option( } NativeUdpSocketOption::Ttl(ttl) => { match family { - JavascriptUdpFamily::Ipv4 => socket_ref.set_ttl_v4(ttl)?, - JavascriptUdpFamily::Ipv6 => socket_ref.set_unicast_hops_v6(ttl)?, + UdpFamily::Ipv4 => socket_ref.set_ttl_v4(ttl)?, + UdpFamily::Ipv6 => socket_ref.set_unicast_hops_v6(ttl)?, } Ok(json!(ttl)) } NativeUdpSocketOption::MulticastTtl(ttl) => { - if family != JavascriptUdpFamily::Ipv4 { + if family != UdpFamily::Ipv4 { return Err(std::io::Error::from_raw_os_error(libc::ENOPROTOOPT)); } socket_ref.set_multicast_ttl_v4(ttl)?; @@ -362,8 +457,8 @@ fn apply_native_udp_option( } NativeUdpSocketOption::MulticastLoopback(enabled) => { match family { - JavascriptUdpFamily::Ipv4 => socket_ref.set_multicast_loop_v4(enabled)?, - JavascriptUdpFamily::Ipv6 => socket_ref.set_multicast_loop_v6(enabled)?, + UdpFamily::Ipv4 => socket_ref.set_multicast_loop_v4(enabled)?, + UdpFamily::Ipv6 => socket_ref.set_multicast_loop_v6(enabled)?, } Ok(json!(if enabled { 1 } else { 0 })) } @@ -377,7 +472,7 @@ fn apply_native_udp_option( join, } => { match group { - IpAddr::V4(group) if family == JavascriptUdpFamily::Ipv4 => { + IpAddr::V4(group) if family == UdpFamily::Ipv4 => { let interface = ipv4_interface(interface.as_deref())?; if join { socket_ref.join_multicast_v4(&group, &interface)?; @@ -385,8 +480,8 @@ fn apply_native_udp_option( socket_ref.leave_multicast_v4(&group, &interface)?; } } - IpAddr::V6(group) if family == JavascriptUdpFamily::Ipv6 => { - let index = ipv6_interface_index(interface.as_deref())?; + IpAddr::V6(group) if family == UdpFamily::Ipv6 => { + let index = ipv6_interface_index(socket, interface.as_deref())?; if join { socket_ref.join_multicast_v6(&group, index)?; } else { @@ -453,8 +548,7 @@ where if payload_len > allowance.bytes { turn.complete(FairBudget::default(), false) .map_err(|error| SidecarError::Execution(error.to_string()))?; - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_FAIRNESS_BYTE_BUDGET: UDP datagram uses {payload_len} bytes, allowance {} bytes; raise limits.reactor.byteQuantum", + return Err(SidecarError::host("ERR_AGENTOS_FAIRNESS_BYTE_BUDGET", format!("UDP datagram uses {payload_len} bytes, allowance {} bytes; raise limits.reactor.byteQuantum", allowance.bytes ))); } @@ -559,7 +653,7 @@ async fn connect_native_udp_socket_fair( } struct NativeUdpOwnerRegistration { - family: JavascriptUdpFamily, + family: UdpFamily, resources: Arc, limits: ReactorIoLimits, fairness_identity: Arc>, @@ -643,9 +737,9 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { udp_datagram_reservation, )); let was_empty = receive_queue.is_empty(); - receive_queue.push_back(JavascriptUdpSocketEvent::Error { - code: Some(javascript_sync_rpc_error_code(&error)), - message: javascript_sync_rpc_error_message(&error), + receive_queue.push_back(DatagramEvent::Error { + code: Some(host_service_error_code(&error)), + message: host_service_error_message(&error), }); if was_empty { notify_native_udp_readable( @@ -671,7 +765,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { ); } let was_empty = receive_queue.is_empty(); - receive_queue.push_back(JavascriptUdpSocketEvent::Message { + receive_queue.push_back(DatagramEvent::Message { data: buffer, remote_addr, _byte_reservation: SharedReservation::new(byte_reservation), @@ -722,7 +816,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { ); } let was_empty = receive_queue.is_empty(); - receive_queue.push_back(JavascriptUdpSocketEvent::Error { + receive_queue.push_back(DatagramEvent::Error { code: io_error_code(&error), message: error.to_string(), }); @@ -774,6 +868,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { message: String::from( "Already connected: send() does not accept a destination", ), + details: None, })); continue; } @@ -783,10 +878,12 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { message: String::from( "Destination port is required for an unconnected UDP socket", ), + details: None, })); continue; } - let result = match tokio::time::timeout( + let result = match crate::execution::operation_deadline_timeout( + "UDP send", limits.operation_deadline, send_native_udp_datagram_fair( &socket, @@ -812,6 +909,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { "partial UDP datagram write: {written} of {} bytes", payload.bytes.len() ), + details: None, }), Ok(Ok(Err(error))) => Err(udp_io_deferred_error(error)), Ok(Err(error)) => Err(udp_deferred_error(error)), @@ -821,6 +919,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { "UDP send exceeded {}ms; raise limits.reactor.operationDeadlineMs", limits.operation_deadline.as_millis() ), + details: None, }), }; completion.settle(result); @@ -836,10 +935,12 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { completion.settle(Err(crate::state::DeferredRpcError { code: String::from("ERR_SOCKET_DGRAM_IS_CONNECTED"), message: String::from("Already connected"), + details: None, })); continue; } - let result = match tokio::time::timeout( + let result = match crate::execution::operation_deadline_timeout( + "UDP connect", limits.operation_deadline, connect_native_udp_socket_fair( &socket, @@ -871,6 +972,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { "UDP connect exceeded {}ms; raise limits.reactor.operationDeadlineMs", limits.operation_deadline.as_millis() ), + details: None, }), }; completion.settle(result); @@ -880,6 +982,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { completion.settle(Err(crate::state::DeferredRpcError { code: String::from("ERR_SOCKET_DGRAM_NOT_CONNECTED"), message: String::from("Not connected"), + details: None, })); continue; } @@ -901,6 +1004,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { .ok_or_else(|| crate::state::DeferredRpcError { code: String::from("ERR_SOCKET_DGRAM_NOT_CONNECTED"), message: String::from("Not connected"), + details: None, }); completion.settle(result); } @@ -963,7 +1067,7 @@ async fn run_native_udp_owner(task: NativeUdpOwnerTask) { Ok(()) => read_ready = true, Err(error) => { let was_empty = receive_queue.is_empty(); - receive_queue.push_back(JavascriptUdpSocketEvent::Error { + receive_queue.push_back(DatagramEvent::Error { code: io_error_code(&error), message: error.to_string(), }); @@ -1008,6 +1112,109 @@ fn spawn_native_udp_owner( } impl ActiveUdpSocket { + pub(in crate::execution) fn poll_handle(&self) -> ActiveUdpPollHandle { + ActiveUdpPollHandle { + native_commands: self.native_commands.clone(), + resources: Arc::clone(&self.resources), + runtime_context: self.runtime_context.clone(), + reactor_limits: self.reactor_limits, + fairness_identity: Arc::clone(&self.fairness_identity), + fairness_identity_committed: Arc::clone(&self.fairness_identity_committed), + read_event_notify: Arc::clone(&self.read_event_notify), + pending_datagram: Arc::clone(&self.pending_datagram), + } + } + + pub(in crate::execution) fn kernel_readable( + &self, + kernel: &SidecarKernel, + kernel_pid: u32, + ) -> Result { + let Some(socket_id) = self.kernel_socket_id else { + return Ok(false); + }; + let result = kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + kernel_pid, + vec![PollTargetEntry::socket(socket_id, POLLIN)], + 0, + ) + .map_err(kernel_error)?; + Ok(result + .targets + .first() + .is_some_and(|entry| !entry.revents.is_empty())) + } + + pub(in crate::execution) fn consume_kernel_datagram( + &self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + turn: FairWorkTurn, + ) -> Result, SidecarError> { + let Some(socket_id) = self.kernel_socket_id else { + turn.complete(FairBudget::default(), false) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + return Ok(None); + }; + let receive_capacity = udp_receive_capacity(&self.resources, self.reactor_limits) + .min(turn.allowance().bytes) + .max(1); + let (event, used_bytes) = match kernel.socket_recv_datagram_charged( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + receive_capacity, + ) { + Ok(Some(datagram)) => { + let (source_address, payload, reservations) = datagram.into_parts(); + let used_bytes = payload.len(); + let ( + byte_reservation, + datagram_reservation, + udp_byte_reservation, + udp_datagram_reservation, + ) = reservations.ok_or_else(|| { + SidecarError::host( + "ERR_AGENTOS_RESOURCE_ACCOUNTING_INVARIANT", + "kernel UDP handoff did not transfer its queue reservations", + ) + })?; + let remote_addr = source_address + .map(|source| resolve_udp_bind_addr(source.host(), source.port(), self.family)) + .transpose()? + .unwrap_or_else(|| match self.family { + UdpFamily::Ipv4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + UdpFamily::Ipv6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0), + }); + ( + Some(DatagramEvent::Message { + data: payload, + remote_addr, + _byte_reservation: SharedReservation::new(byte_reservation), + _datagram_reservation: SharedReservation::new(datagram_reservation), + _udp_byte_reservation: SharedReservation::new(udp_byte_reservation), + _udp_datagram_reservation: SharedReservation::new(udp_datagram_reservation), + }), + used_bytes, + ) + } + Ok(None) => (None, 0), + Err(error) if error.code() == "EAGAIN" => (None, 0), + Err(error) => ( + Some(DatagramEvent::Error { + code: Some(error.code().to_owned()), + message: error.to_string(), + }), + 0, + ), + }; + turn.complete(FairBudget::new(1, used_bytes), false) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + Ok(event) + } + pub(in crate::execution) fn set_fairness_identity(&mut self, identity: Option<(u64, u64)>) { let Some(identity) = identity else { return; @@ -1024,15 +1231,17 @@ impl ActiveUdpSocket { pub(in crate::execution) fn set_event_pusher( &self, - session: Option, + session: Option, identity: Option<( agentos_runtime::capability::CapabilityId, agentos_runtime::capability::CapabilityGeneration, )>, + owner_notify: Arc, ) { self.readiness_registration.register( session, identity, + owner_notify, agentos_runtime::readiness::ReadyFlags::DATAGRAM, ); } @@ -1050,14 +1259,14 @@ impl ActiveUdpSocket { pub(in crate::execution) fn new( kernel: &mut SidecarKernel, kernel_pid: u32, - family: JavascriptUdpFamily, + family: UdpFamily, resources: Arc, runtime_context: agentos_runtime::RuntimeContext, reactor_limits: ReactorIoLimits, ) -> Result { let spec = match family { - JavascriptUdpFamily::Ipv4 => SocketSpec::udp(), - JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram), + UdpFamily::Ipv4 => SocketSpec::udp(), + UdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram), }; let socket_id = kernel .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) @@ -1085,73 +1294,13 @@ impl ActiveUdpSocket { fairness_retirement, description_lease: Arc::new(SocketDescriptionLease::default()), read_event_notify: Arc::new(tokio::sync::Notify::new()), + pending_datagram: Arc::new(Mutex::new(None)), event_pusher: Arc::clone(&event_pusher), readiness_registration: SocketReadinessRegistration::new(event_pusher, None, None), native_read_wake_pending: Arc::new(AtomicBool::new(false)), }) } - /// Create a native-backed UDP capability without an adapter-owned task or - /// descriptor registry. The socket is bound lazily by the same `bind`, - /// `send_to`, and `poll` operations used by every native UDP consumer. - pub(in crate::execution) fn new_native( - family: JavascriptUdpFamily, - resources: Arc, - runtime_context: agentos_runtime::RuntimeContext, - reactor_limits: ReactorIoLimits, - ) -> Result { - let bind_addr = match family { - JavascriptUdpFamily::Ipv4 => "127.0.0.1:0", - JavascriptUdpFamily::Ipv6 => "[::1]:0", - }; - let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?; - let local_addr = socket.local_addr().map_err(sidecar_net_error)?; - let fairness_identity = Arc::new(OnceLock::new()); - let fairness_identity_committed = Arc::new(tokio::sync::Notify::new()); - let fairness_retirement = - SocketFairnessRetirement::new(Arc::clone(&fairness_identity), runtime_context.clone()); - let read_event_notify = Arc::new(tokio::sync::Notify::new()); - let event_pusher = SocketReadinessSubscribers::new(&resources); - let native_read_wake_pending = Arc::new(AtomicBool::new(false)); - let native_commands = spawn_native_udp_owner( - &runtime_context, - socket, - NativeUdpOwnerRegistration { - family, - resources: Arc::clone(&resources), - limits: reactor_limits, - fairness_identity: Arc::clone(&fairness_identity), - fairness_identity_committed: Arc::clone(&fairness_identity_committed), - event_pusher: Arc::clone(&event_pusher), - read_event_notify: Arc::clone(&read_event_notify), - wake_pending: Arc::clone(&native_read_wake_pending), - }, - )?; - Ok(Self { - family, - native_commands: Some(native_commands), - kernel_socket_id: None, - guest_local_addr: Some(local_addr), - native_local_addr: Some(local_addr), - kernel_connected_remote_addr: None, - recv_buffer_size: 0, - send_buffer_size: 0, - description_handles: Arc::new(()), - kernel_transfer_guard: None, - resources, - runtime_context, - reactor_limits, - fairness_identity, - fairness_identity_committed, - fairness_retirement, - description_lease: Arc::new(SocketDescriptionLease::default()), - read_event_notify, - event_pusher: Arc::clone(&event_pusher), - readiness_registration: SocketReadinessRegistration::new(event_pusher, None, None), - native_read_wake_pending, - }) - } - pub(in crate::execution) fn clone_for_fd_transfer(&self) -> Result { Ok(Self { family: self.family, @@ -1172,6 +1321,7 @@ impl ActiveUdpSocket { fairness_retirement: Arc::clone(&self.fairness_retirement), description_lease: Arc::clone(&self.description_lease), read_event_notify: Arc::clone(&self.read_event_notify), + pending_datagram: Arc::clone(&self.pending_datagram), event_pusher: Arc::clone(&self.event_pusher), readiness_registration: SocketReadinessRegistration::new( Arc::clone(&self.event_pusher), @@ -1203,12 +1353,13 @@ impl ActiveUdpSocket { kernel_pid: u32, host: Option<&str>, port: u16, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, ) -> Result { if self.native_commands.is_some() || self.guest_local_addr.is_some() { - return Err(SidecarError::Execution(String::from( - "EINVAL: secure-exec dgram socket is already bound", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("secure-exec dgram socket is already bound"), + )); } let (_bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?; @@ -1229,9 +1380,10 @@ impl ActiveUdpSocket { ) .map_err(kernel_error)?; } else { - return Err(SidecarError::Execution(String::from( - "EINVAL: native UDP socket is already bound", - ))); + return Err(SidecarError::host( + "EINVAL", + String::from("native UDP socket is already bound"), + )); } self.guest_local_addr = Some(local_addr); Ok(local_addr) @@ -1241,7 +1393,7 @@ impl ActiveUdpSocket { &mut self, kernel: &mut SidecarKernel, kernel_pid: u32, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, ) -> Result { if let Some(local_addr) = self.local_addr() { return Ok(local_addr); @@ -1253,9 +1405,10 @@ impl ActiveUdpSocket { fn ensure_native_owner(&mut self) -> Result<&TokioSender, SidecarError> { if self.native_commands.is_none() { let guest_addr = self.guest_local_addr.ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_UDP_NOT_BOUND: UDP socket must have a guest address before native activation", - )) + SidecarError::host( + "ERR_AGENTOS_UDP_NOT_BOUND", + String::from("UDP socket must have a guest address before native activation"), + ) })?; let socket = UdpSocket::bind(SocketAddr::new(guest_addr.ip(), 0)).map_err(sidecar_net_error)?; @@ -1289,9 +1442,9 @@ impl ActiveUdpSocket { self.native_commands = Some(commands); self.native_local_addr = Some(native_local_addr); } - self.native_commands.as_ref().ok_or_else(|| { - SidecarError::Execution(String::from("EBADF: native UDP owner is unavailable")) - }) + self.native_commands + .as_ref() + .ok_or_else(|| SidecarError::host("EBADF", "native UDP owner is unavailable")) } pub(in crate::execution) fn send_to( @@ -1303,9 +1456,10 @@ impl ActiveUdpSocket { BridgeError: fmt::Debug + Send + Sync + 'static, { if self.kernel_connected_remote_addr.is_some() { - return Err(SidecarError::Execution(String::from( - "ERR_SOCKET_DGRAM_IS_CONNECTED: send() does not accept a destination on a connected UDP socket", - ))); + return Err(SidecarError::host( + "ERR_SOCKET_DGRAM_IS_CONNECTED", + String::from("send() does not accept a destination on a connected UDP socket"), + )); } let ActiveUdpSendToRequest { bridge, @@ -1373,7 +1527,7 @@ impl ActiveUdpSocket { &mut self, kernel: &mut SidecarKernel, kernel_pid: u32, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, contents: &[u8], ) -> Result { let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?; @@ -1416,8 +1570,10 @@ impl ActiveUdpSocket { kernel: &mut SidecarKernel, kernel_pid: u32, wait: Duration, - ) -> Result, SidecarError> { - let wait = wait.min(self.reactor_limits.operation_deadline); + ) -> Result, SidecarError> { + let operation_deadline = self.reactor_limits.operation_deadline; + let warn_operation_deadline = wait >= operation_deadline; + let wait = wait.min(operation_deadline); let receive_capacity = udp_receive_capacity(&self.resources, self.reactor_limits); if let Some(socket_id) = self.kernel_socket_id { // A hybrid socket may be readable through either the VM-local @@ -1457,30 +1613,33 @@ impl ActiveUdpSocket { let (source_address, payload, reservations) = datagram.into_parts(); let used_bytes = payload.len(); let ( - byte_reservation, - datagram_reservation, - udp_byte_reservation, - udp_datagram_reservation, - ) = reservations.ok_or_else(|| { - SidecarError::Execution(String::from( - "ERR_AGENTOS_RESOURCE_ACCOUNTING_INVARIANT: kernel UDP handoff did not transfer its queue reservations", - )) - })?; + byte_reservation, + datagram_reservation, + udp_byte_reservation, + udp_datagram_reservation, + ) = reservations.ok_or_else(|| { + SidecarError::host( + "ERR_AGENTOS_RESOURCE_ACCOUNTING_INVARIANT", + String::from( + "kernel UDP handoff did not transfer its queue reservations", + ), + ) + })?; let remote_addr = source_address .map(|source| { resolve_udp_bind_addr(source.host(), source.port(), self.family) }) .transpose()? .unwrap_or_else(|| match self.family { - JavascriptUdpFamily::Ipv4 => { + UdpFamily::Ipv4 => { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0) } - JavascriptUdpFamily::Ipv6 => { + UdpFamily::Ipv6 => { SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0) } }); ( - Some(JavascriptUdpSocketEvent::Message { + Some(DatagramEvent::Message { data: payload, remote_addr, _byte_reservation: SharedReservation::new(byte_reservation), @@ -1496,7 +1655,7 @@ impl ActiveUdpSocket { Ok(None) => (None, 0), Err(error) if error.code() == "EAGAIN" => (None, 0), Err(error) => ( - Some(JavascriptUdpSocketEvent::Error { + Some(DatagramEvent::Error { code: Some(error.code().to_string()), message: error.to_string(), }), @@ -1522,17 +1681,25 @@ impl ActiveUdpSocket { commands.try_send(command).map_err(|error| { udp_command_admission_error(error, self.reactor_limits.max_handle_commands) })?; - match tokio::time::timeout(self.reactor_limits.operation_deadline, receiver).await { - Ok(Ok(result)) => result.map_err(|error| { - SidecarError::Execution(format!("{}: {}", error.code, error.message)) - }), - Ok(Err(_)) => Err(SidecarError::Execution(String::from( - "EPIPE: native UDP owner dropped poll completion", - ))), - Err(_) => Err(SidecarError::Execution(format!( - "ETIMEDOUT: UDP poll exceeded {}ms; raise limits.reactor.operationDeadlineMs", - self.reactor_limits.operation_deadline.as_millis() - ))), + match crate::execution::operation_deadline_timeout( + "UDP poll completion", + self.reactor_limits.operation_deadline, + receiver, + ) + .await + { + Ok(Ok(result)) => result.map_err(SidecarError::from), + Ok(Err(_)) => Err(SidecarError::host( + "EPIPE", + String::from("native UDP owner dropped poll completion"), + )), + Err(_) => Err(SidecarError::host( + "ETIMEDOUT", + format!( + "UDP poll exceeded {}ms; raise limits.reactor.operationDeadlineMs", + self.reactor_limits.operation_deadline.as_millis() + ), + )), } }; let event = poll_once().await?; @@ -1540,7 +1707,14 @@ impl ActiveUdpSocket { return Ok(event); } let notified = self.read_event_notify.notified(); - if tokio::time::timeout(wait, notified).await.is_err() { + let timed_out = if warn_operation_deadline { + crate::execution::operation_deadline_timeout("UDP poll wait", wait, notified) + .await + .is_err() + } else { + tokio::time::timeout(wait, notified).await.is_err() + }; + if timed_out { return Ok(None); } poll_once().await @@ -1572,9 +1746,10 @@ impl ActiveUdpSocket { BridgeError: fmt::Debug + Send + Sync + 'static, { if self.kernel_connected_remote_addr.is_some() { - return Err(SidecarError::Execution(String::from( - "ERR_SOCKET_DGRAM_IS_CONNECTED: Already connected", - ))); + return Err(SidecarError::host( + "ERR_SOCKET_DGRAM_IS_CONNECTED", + String::from("Already connected"), + )); } let ActiveUdpConnectRequest { bridge, @@ -1654,9 +1829,10 @@ impl ActiveUdpSocket { return Ok(ActiveUdpValueResult::Immediate(Value::Null)); } if self.native_commands.is_none() { - return Err(SidecarError::Execution(String::from( - "ERR_SOCKET_DGRAM_NOT_CONNECTED: Not connected", - ))); + return Err(SidecarError::host( + "ERR_SOCKET_DGRAM_NOT_CONNECTED", + String::from("Not connected"), + )); } self.submit_native_value_command(|command_reservation, completion| { NativeUdpCommand::Disconnect { @@ -1677,9 +1853,10 @@ impl ActiveUdpSocket { }))); } if self.native_commands.is_none() { - return Err(SidecarError::Execution(String::from( - "ERR_SOCKET_DGRAM_NOT_CONNECTED: Not connected", - ))); + return Err(SidecarError::host( + "ERR_SOCKET_DGRAM_NOT_CONNECTED", + String::from("Not connected"), + )); } self.submit_native_value_command(|command_reservation, completion| { NativeUdpCommand::RemoteAddress { @@ -1693,7 +1870,7 @@ impl ActiveUdpSocket { &mut self, kernel: &mut SidecarKernel, kernel_pid: u32, - context: &JavascriptSocketPathContext, + context: &SocketPathContext, option: NativeUdpSocketOption, ) -> Result { let permits_implicit_bind = matches!( @@ -1702,9 +1879,10 @@ impl ActiveUdpSocket { | NativeUdpSocketOption::SourceMembership { join: true, .. } ); if self.guest_local_addr.is_none() && !permits_implicit_bind { - return Err(SidecarError::Execution(String::from( - "EBADF: UDP socket option requires a bound socket", - ))); + return Err(SidecarError::host( + "EBADF", + String::from("UDP socket option requires a bound socket"), + )); } let guest_local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?; self.submit_native_value_command(|command_reservation, completion| { @@ -1721,7 +1899,11 @@ impl ActiveUdpSocket { self.native_read_wake_pending .store(false, Ordering::Release); if let Some(socket_id) = self.kernel_socket_id { - let _ = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id); + if let Err(error) = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id) { + eprintln!( + "ERR_AGENTOS_UDP_CLOSE: failed to close kernel socket {socket_id}: {error}" + ); + } } self.native_commands.take(); self.guest_local_addr = None; @@ -1792,9 +1974,9 @@ fn dgram_option_ip(payload: &Value, field: &str) -> Result let value = dgram_option_field(payload, field)? .as_str() .ok_or_else(|| SidecarError::InvalidState(format!("{field} must be an IP address")))?; - value.parse().map_err(|_| { - SidecarError::Execution(format!("EINVAL: invalid UDP {field} address {value}")) - }) + value + .parse() + .map_err(|_| SidecarError::host("EINVAL", format!("invalid UDP {field} address {value}"))) } fn dgram_option_interface(payload: &Value) -> Result, SidecarError> { @@ -1831,7 +2013,7 @@ fn parse_native_udp_option( )) })?, ) - .map_err(|_| SidecarError::Execution(String::from("EINVAL: UDP TTL overflow")))?, + .map_err(|_| SidecarError::host("EINVAL", "UDP TTL overflow"))?, )), "multicastTtl" => Ok(NativeUdpSocketOption::MulticastTtl( u32::try_from( @@ -1843,9 +2025,7 @@ fn parse_native_udp_option( )) })?, ) - .map_err(|_| { - SidecarError::Execution(String::from("EINVAL: UDP multicast TTL overflow")) - })?, + .map_err(|_| SidecarError::host("EINVAL", "UDP multicast TTL overflow"))?, )), "multicastLoopback" => Ok(NativeUdpSocketOption::MulticastLoopback( dgram_option_field(payload, "enabled")? @@ -1898,6 +2078,7 @@ pub(in crate::execution) fn release_udp_socket_handle( kernel: &mut SidecarKernel, kernel_readiness: &KernelSocketReadinessRegistry, ) -> Result<(), SidecarError> { + socket.readiness_registration.retire(); let identity = process .capability_readiness_identity(&NativeCapabilityKey::UdpSocket(socket_id.to_owned())); unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id, identity); @@ -1911,14 +2092,15 @@ pub(in crate::execution) fn release_udp_socket_handle( ) } -pub(in crate::execution) fn service_javascript_dgram_sync_rpc( - request: JavascriptDgramSyncRpcServiceRequest<'_, B>, -) -> Result +pub(in crate::execution) fn service_managed_udp_operation( + request: ManagedUdpServiceRequest<'_, B>, + operation: agentos_execution::host::NetworkOperation, +) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let JavascriptDgramSyncRpcServiceRequest { + let ManagedUdpServiceRequest { bridge, kernel, vm_id, @@ -1926,31 +2108,15 @@ where socket_paths, process, kernel_readiness, - sync_request: request, capabilities, } = request; - match request.method.as_str() { - "dgram.createSocket" => { + match operation { + agentos_execution::host::NetworkOperation::ManagedUdpCreate { family } => { let pending = reserve_capability(&capabilities, CapabilityKind::UdpSocket)?; - let payload = request - .args - .first() - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dgram.createSocket requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err( - |error| { - SidecarError::InvalidState(format!( - "invalid dgram.createSocket payload: {error}" - )) - }, - ) - })?; - let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?; + let family = match family { + agentos_execution::host::ManagedUdpFamily::Inet4 => UdpFamily::Ipv4, + agentos_execution::host::ManagedUdpFamily::Inet6 => UdpFamily::Ipv6, + }; let socket_id = process.allocate_udp_socket_id(); let mut socket = ActiveUdpSocket::new( kernel, @@ -1981,13 +2147,18 @@ where .expect("committed UDP capability lease"), ); socket.set_event_pusher( - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), process.capability_readiness_identity(&capability_key), + Arc::clone(&process.process_event_notify), ); register_kernel_readiness_target( &kernel_readiness, socket.kernel_socket_id, - process.execution.javascript_v8_session_handle(), + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), Some(Arc::clone(&socket.read_event_notify)), process.capability_readiness_identity(&capability_key), socket_id.clone(), @@ -2002,92 +2173,197 @@ where }) .into()) } - "dgram.bind" => { - let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?; - let payload = request - .args - .get(1) - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dgram.bind requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err(|error| { - SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}")) - }) - })?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + agentos_execution::host::NetworkOperation::ManagedUdpBind { + socket_id, + host, + port, + } => { + let socket_id = socket_id.into_string(); + let host = host.map(agentos_execution::host::BoundedString::into_string); + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; let local_addr = socket.bind( kernel, process.kernel_pid, - payload.address.as_deref(), - payload.port, + host.as_deref(), + port, socket_paths, )?; Ok(local_endpoint_value(&local_addr).into()) } - "dgram.send" => { - let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?; - let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?; - let payload = request - .args - .get(2) - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dgram.send requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err(|error| { - SidecarError::InvalidState(format!("invalid dgram.send payload: {error}")) - }) - })?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + agentos_execution::host::NetworkOperation::ManagedUdpSend { + socket_id, + bytes, + host, + port, + } => { + let socket_id = socket_id.into_string(); + let host = host.map(agentos_execution::host::BoundedString::into_string); + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; - let result = match payload.port { + let result = match port { Some(port) => socket.send_to(ActiveUdpSendToRequest { bridge, kernel, kernel_pid: process.kernel_pid, vm_id, dns, - host: payload.address.as_deref().unwrap_or("localhost"), + host: host.as_deref().unwrap_or("localhost"), port, context: socket_paths, - contents: &chunk, + contents: bytes.as_slice(), })?, - None => socket.send_connected(kernel, process.kernel_pid, socket_paths, &chunk)?, + None => socket.send_connected( + kernel, + process.kernel_pid, + socket_paths, + bytes.as_slice(), + )?, }; Ok(udp_send_service_response(result)) } - "dgram.connect" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.connect socket id")?; - let payload = request - .args - .get(1) - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dgram.connect requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err( - |error| { - SidecarError::InvalidState(format!( - "invalid dgram.connect payload: {error}" - )) - }, - ) - })?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + agentos_execution::host::NetworkOperation::ManagedUdpClose { socket_id } => { + let socket_id = socket_id.into_string(); + let socket = process.udp_sockets.remove(&socket_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) + })?; + release_udp_socket_handle(process, &socket_id, socket, kernel, &kernel_readiness)?; + Ok(Value::Null.into()) + } + other => Err(SidecarError::host( + "EINVAL", + format!("managed UDP executor received non-UDP operation: {other:?}"), + )), + } +} + +pub(in crate::execution) fn service_dgram_operation( + request: DgramServiceRequest<'_, B>, +) -> Result +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let DgramServiceRequest { + bridge, + kernel, + vm_id, + dns, + socket_paths, + process, + kernel_readiness, + operation, + capabilities, + } = request; + match operation { + DgramOperation::Create { family } => { + let pending = reserve_capability(&capabilities, CapabilityKind::UdpSocket)?; + let socket_id = process.allocate_udp_socket_id(); + let mut socket = ActiveUdpSocket::new( + kernel, + process.kernel_pid, + family, + capabilities.resources(), + process.runtime_context.clone(), + reactor_io_limits(&process.limits), + )?; + let capability_key = NativeCapabilityKey::UdpSocket(socket_id.clone()); + let identity = match commit_process_capability( + process, + pending, + capability_key.clone(), + socket_id.clone(), + socket.kernel_socket_id, + ) { + Ok(identity) => identity, + Err(error) => { + socket.close(kernel, process.kernel_pid); + return Err(error); + } + }; + socket.set_fairness_identity(process.capability_fairness_identity(&capability_key)); + socket.retain_description_lease( + process + .shared_capability_lease(&capability_key) + .expect("committed UDP capability lease"), + ); + socket.set_event_pusher( + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), + process.capability_readiness_identity(&capability_key), + Arc::clone(&process.process_event_notify), + ); + register_kernel_readiness_target( + &kernel_readiness, + socket.kernel_socket_id, + process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()), + Some(Arc::clone(&socket.read_event_notify)), + process.capability_readiness_identity(&capability_key), + socket_id.clone(), + KernelSocketReadinessEvent::Datagram, + ); + process.udp_sockets.insert(socket_id.clone(), socket); + Ok(json!({ + "socketId": socket_id, + "capabilityId": identity.0, + "capabilityGeneration": identity.1, + "type": family.socket_type(), + }) + .into()) + } + DgramOperation::Bind { + socket_id, + address, + port, + } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) + })?; + let local_addr = socket.bind( + kernel, + process.kernel_pid, + address.as_deref(), + port, + socket_paths, + )?; + Ok(local_endpoint_value(&local_addr).into()) + } + DgramOperation::Send { + socket_id, + bytes, + address, + port, + } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) + })?; + let result = match port { + Some(port) => socket.send_to(ActiveUdpSendToRequest { + bridge, + kernel, + kernel_pid: process.kernel_pid, + vm_id, + dns, + host: address.as_deref().unwrap_or("localhost"), + port, + context: socket_paths, + contents: &bytes, + })?, + None => socket.send_connected(kernel, process.kernel_pid, socket_paths, &bytes)?, + }; + Ok(udp_send_service_response(result)) + } + DgramOperation::Connect { + socket_id, + address, + port, + } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; let result = socket.connect(ActiveUdpConnectRequest { @@ -2096,8 +2372,8 @@ where kernel_pid: process.kernel_pid, vm_id, dns, - host: payload.address.as_deref().unwrap_or("localhost"), - port: payload.port, + host: address.as_deref().unwrap_or("localhost"), + port, context: socket_paths, })?; Ok(udp_value_service_response( @@ -2105,10 +2381,8 @@ where agentos_runtime::TaskClass::Udp, )) } - "dgram.disconnect" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.disconnect socket id")?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + DgramOperation::Disconnect { socket_id } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; let result = socket.disconnect(kernel, process.kernel_pid)?; @@ -2117,10 +2391,8 @@ where agentos_runtime::TaskClass::Udp, )) } - "dgram.remoteAddress" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.remoteAddress socket id")?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + DgramOperation::RemoteAddress { socket_id } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; let result = socket.remote_address()?; @@ -2129,24 +2401,21 @@ where agentos_runtime::TaskClass::Udp, )) } - "dgram.close" => { - let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?; - let socket = process.udp_sockets.remove(socket_id).ok_or_else(|| { + DgramOperation::Close { socket_id } => { + let socket = process.udp_sockets.remove(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; - release_udp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness)?; + release_udp_socket_handle(process, &socket_id, socket, kernel, &kernel_readiness)?; Ok(Value::Null.into()) } - "dgram.address" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?; - let socket = process.udp_sockets.get(socket_id).ok_or_else(|| { + DgramOperation::Address { socket_id } => { + let socket = process.udp_sockets.get(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; - let local_addr = socket.local_addr().ok_or_else(|| { - SidecarError::Execution(String::from("EBADF: bad file descriptor")) - })?; - javascript_net_json_string( + let local_addr = socket + .local_addr() + .ok_or_else(|| SidecarError::host("EBADF", "bad file descriptor"))?; + encode_net_json_string( json!({ "address": local_addr.ip().to_string(), "port": local_addr.port(), @@ -2156,18 +2425,13 @@ where ) .map(Into::into) } - "dgram.setOption" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setOption socket id")?; - let name = - javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setOption option name")?; - let payload = request.args.get(2).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "dgram.setOption requires an option payload", - )) - })?; - let option = parse_native_udp_option(name, payload)?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + DgramOperation::SetOption { + socket_id, + name, + payload, + } => { + let option = parse_native_udp_option(&name, &payload)?; + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; let result = socket.set_option(kernel, process.kernel_pid, socket_paths, option)?; @@ -2176,43 +2440,30 @@ where agentos_runtime::TaskClass::Udp, )) } - "dgram.setBufferSize" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?; - let which = - javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?; - let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?; - let size = usize::try_from(size).map_err(|_| { - SidecarError::InvalidState(String::from( - "dgram.setBufferSize size must fit within usize", - )) - })?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + DgramOperation::SetBufferSize { + socket_id, + which, + size, + } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; - let result = socket.set_buffer_size(which, size)?; + let result = socket.set_buffer_size(&which, size)?; Ok(udp_value_service_response( result, agentos_runtime::TaskClass::Udp, )) } - "dgram.getBufferSize" => { - let socket_id = - javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?; - let which = - javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?; - let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + DgramOperation::GetBufferSize { socket_id, which } => { + let socket = process.udp_sockets.get_mut(&socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; - let result = socket.get_buffer_size(which)?; + let result = socket.get_buffer_size(&which)?; Ok(udp_value_service_response( result, agentos_runtime::TaskClass::Udp, )) } - other => Err(SidecarError::InvalidState(format!( - "unsupported JavaScript dgram sync RPC method {other}" - ))), } } @@ -2425,6 +2676,7 @@ mod native_udp_owner_tests { fairness_identity .set((8001, 7001)) .expect("commit test fairness identity"); + let fairness_identity_committed = Arc::new(tokio::sync::Notify::new()); let limits = reactor_io_limits(&crate::limits::VmLimits::default()); let commands = { let _runtime_guard = runtime.handle().enter(); @@ -2432,11 +2684,11 @@ mod native_udp_owner_tests { &runtime, socket, NativeUdpOwnerRegistration { - family: JavascriptUdpFamily::Ipv4, + family: UdpFamily::Ipv4, resources: Arc::clone(&resources), limits, - fairness_identity, - fairness_identity_committed: Arc::new(tokio::sync::Notify::new()), + fairness_identity: Arc::clone(&fairness_identity), + fairness_identity_committed: Arc::clone(&fairness_identity_committed), event_pusher: SocketReadinessSubscribers::new(&resources), read_event_notify: Arc::clone(&read_event_notify), wake_pending: Arc::clone(&wake_pending), @@ -2444,6 +2696,16 @@ mod native_udp_owner_tests { ) .expect("spawn UDP owner") }; + let poll_handle = ActiveUdpPollHandle { + native_commands: Some(commands.clone()), + resources: Arc::clone(&resources), + runtime_context: runtime.clone(), + reactor_limits: limits, + fairness_identity, + fairness_identity_committed, + read_event_notify: Arc::clone(&read_event_notify), + pending_datagram: Arc::new(Mutex::new(None)), + }; let sender = UdpSocket::bind("127.0.0.1:0").expect("bind UDP sender"); runtime.handle().block_on(async { @@ -2458,20 +2720,12 @@ mod native_udp_owner_tests { .await .expect("first coalesced receive wake"); - let (first_completion, first_response) = tokio::sync::oneshot::channel(); - commands - .try_send(NativeUdpCommand::Poll { - _command_reservation: reserve_udp_command(&resources) - .expect("reserve first poll command"), - completion: first_completion, - }) - .expect("submit first poll"); - let first = first_response + let first = poll_handle + .poll_native_once() .await - .expect("first poll completion") - .expect("first poll success") + .expect("owned poll-handle completion") .expect("first queued datagram"); - let JavascriptUdpSocketEvent::Message { data, .. } = &first else { + let DatagramEvent::Message { data, .. } = &first else { panic!("first UDP event was not a datagram"); }; assert_eq!(data, b"first"); @@ -2511,7 +2765,7 @@ mod native_udp_owner_tests { .expect("second poll completion") .expect("second poll success") .expect("second queued datagram"); - let JavascriptUdpSocketEvent::Message { data, .. } = &second else { + let DatagramEvent::Message { data, .. } = &second else { panic!("second UDP event was not a datagram"); }; assert_eq!(data, b"second"); @@ -2519,6 +2773,7 @@ mod native_udp_owner_tests { drop(second); }); + drop(poll_handle); drop(commands); runtime.handle().block_on(async { tokio::time::timeout(Duration::from_secs(2), async { diff --git a/crates/native-sidecar/src/execution/network/unix.rs b/crates/native-sidecar/src/execution/network/unix.rs index 06520d4c53..e17e7c26f9 100644 --- a/crates/native-sidecar/src/execution/network/unix.rs +++ b/crates/native-sidecar/src/execution/network/unix.rs @@ -95,14 +95,35 @@ pub(in crate::execution) fn register_guest_unix_binding( generation: NEXT_GUEST_UNIX_BINDING_GENERATION.fetch_add(1, Ordering::Relaxed), active_bindings: 1, queued_by_target: BTreeMap::new(), + // A bound-but-not-yet-listening socket cannot accept peers. The + // listener path installs its configured bounded capacity before + // starting the acceptor. + pending_connection_limit: 1, pending_connections: VecDeque::new(), }, ); Ok(()) } +fn set_guest_unix_pending_connection_limit( + registry: &GuestUnixAddressRegistry, + binding_id: &str, + maximum: usize, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + let entry = registry.get_mut(binding_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "missing bound Unix address metadata for {binding_id}" + )) + })?; + entry.pending_connection_limit = maximum.max(1); + Ok(()) +} + pub(in crate::execution) fn guest_unix_path_target( - context: &JavascriptSocketPathContext, + context: &SocketPathContext, guest_device_inode: (u64, u64), ) -> Result, SidecarError> { let registry = context @@ -184,6 +205,15 @@ pub(in crate::execution) fn register_guest_unix_connection( "missing target Unix address metadata for {target_binding_id}" )) })?; + if target.pending_connections.len() >= target.pending_connection_limit { + return Err(SidecarError::host( + "EAGAIN", + format!( + "Unix listener pending connection limit is {}; raise the listen backlog or limits.reactor.maxAsyncCompletions", + target.pending_connection_limit + ), + )); + } let state = Arc::new(GuestUnixConnectionState { accepted_peer_open: AtomicBool::new(true), }); @@ -446,7 +476,7 @@ pub(in crate::execution) fn purge_guest_unix_target( } pub(in crate::execution) fn host_abstract_unix_name( - context: &JavascriptSocketPathContext, + context: &SocketPathContext, guest_name: &[u8], ) -> [u8; 32] { let mut digest = Sha256::new(); @@ -549,10 +579,12 @@ impl ActiveUnixSocket { let (sender, events) = async_completion_channel( runtime_context.clone(), socket_completion_capacity(reactor_limits), + tcp_socket_event_retained_bytes, ); let event_pusher = SocketReadinessSubscribers::new(&resources); let application_read_interest = Arc::new(AtomicBool::new(false)); let application_read_notify = Arc::new(tokio::sync::Notify::new()); + let read_event_notify = Arc::new(tokio::sync::Notify::new()); let saw_local_shutdown = Arc::new(AtomicBool::new(false)); let saw_remote_end = Arc::new(AtomicBool::new(false)); let close_notified = Arc::new(AtomicBool::new(false)); @@ -561,6 +593,7 @@ impl ActiveUnixSocket { read_stream, sender.clone(), Arc::clone(&event_pusher), + Arc::clone(&read_event_notify), Arc::clone(&application_read_interest), Arc::clone(&application_read_notify), Arc::clone(&saw_local_shutdown), @@ -582,6 +615,7 @@ impl ActiveUnixSocket { plain_commands, events: Arc::new(Mutex::new(events)), event_sender: sender, + read_event_notify, event_pusher: Arc::clone(&event_pusher), readiness_registration: SocketReadinessRegistration::new( event_pusher, @@ -603,7 +637,7 @@ impl ActiveUnixSocket { saw_remote_end, close_notified, pending_read_event: Arc::new(Mutex::new(None)), - read_buffer: Arc::new(Mutex::new(VecDeque::new())), + read_state: Arc::new(Mutex::new(SocketReadState::default())), description_handles: Arc::new(()), listener_connection_retirement: None, resources, @@ -621,6 +655,7 @@ impl ActiveUnixSocket { plain_commands: self.plain_commands.clone(), events: Arc::clone(&self.events), event_sender: self.event_sender.clone(), + read_event_notify: Arc::clone(&self.read_event_notify), event_pusher: Arc::clone(&self.event_pusher), readiness_registration: SocketReadinessRegistration::new( Arc::clone(&self.event_pusher), @@ -642,7 +677,7 @@ impl ActiveUnixSocket { saw_remote_end: Arc::clone(&self.saw_remote_end), close_notified: Arc::clone(&self.close_notified), pending_read_event: Arc::clone(&self.pending_read_event), - read_buffer: Arc::clone(&self.read_buffer), + read_state: Arc::clone(&self.read_state), description_handles: Arc::clone(&self.description_handles), listener_connection_retirement: self.listener_connection_retirement.clone(), resources: Arc::clone(&self.resources), @@ -662,11 +697,12 @@ impl ActiveUnixSocket { pub(in crate::execution) fn set_event_pusher( &self, - session: Option, + session: Option, identity: Option<( agentos_runtime::capability::CapabilityId, agentos_runtime::capability::CapabilityGeneration, )>, + owner_notify: Arc, ) { let (Some(session), Some((capability_id, capability_generation))) = (session, identity) else { @@ -675,6 +711,7 @@ impl ActiveUnixSocket { self.readiness_registration.register( Some(session), Some((capability_id, capability_generation)), + owner_notify, agentos_runtime::readiness::ReadyFlags::READABLE, ); } @@ -684,14 +721,16 @@ impl ActiveUnixSocket { identity: Option<(u64, u64)>, ) -> Result<(), SidecarError> { let identity = identity.ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_FAIRNESS_IDENTITY: Unix socket capability was committed outside a VM runtime scope", - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_IDENTITY", + String::from("Unix socket capability was committed outside a VM runtime scope"), + ) })?; self.fairness_identity.set(identity).map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_FAIRNESS_IDENTITY: Unix socket capability identity was committed more than once", - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_IDENTITY", + String::from("Unix socket capability identity was committed more than once"), + ) })?; self.fairness_identity_committed.notify_waiters(); Ok(()) @@ -709,7 +748,7 @@ impl ActiveUnixSocket { pub(in crate::execution) fn poll( &mut self, _wait: Duration, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { if let Some(event) = self .pending_read_event .lock() @@ -737,7 +776,7 @@ impl ActiveUnixSocket { &mut self, wait: Duration, max_bytes: usize, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { let pending = self .pending_read_event .lock() @@ -937,7 +976,7 @@ impl ActiveUnixSocket { && !self.close_notified.swap(true, Ordering::SeqCst) && self .event_sender - .try_send(JavascriptTcpSocketEvent::Close { had_error: false }) + .try_send(TcpSocketEvent::Close { had_error: false }) .is_ok() { push_socket_event(&self.event_pusher, "close"); @@ -959,7 +998,7 @@ impl ActiveUnixSocket { { if let Err(error) = self .event_sender - .try_send(JavascriptTcpSocketEvent::Close { had_error: false }) + .try_send(TcpSocketEvent::Close { had_error: false }) { eprintln!( "ERR_AGENTOS_SOCKET_EVENT_DROPPED: Unix socket close event was not admitted: {error}" @@ -1009,8 +1048,7 @@ async fn connect_native_unix_socket( NativeUnixConnectTarget::Abstract(_) => return Err(abstract_unix_unsupported()), }; if let Err(error) = connect_result { - let message = error.to_string(); - let code = guest_errno_code(&message); + let code = guest_error_code(&error); if !matches!(code, Some("EINPROGRESS" | "EALREADY" | "EAGAIN")) { return Err(error); } @@ -1036,7 +1074,7 @@ pub(in crate::execution) fn defer_native_unix_connect( unix_bound_addresses: GuestUnixAddressRegistry, target_binding_id: String, bound_listener: Option<(String, ActiveUnixListener)>, -) -> Result { +) -> Result { let socket_id = process.allocate_unix_socket_id(); let runtime = process.runtime_context.clone(); let task_runtime = runtime.clone(); @@ -1071,19 +1109,17 @@ pub(in crate::execution) fn defer_native_unix_connect( let private_host_path = bound_listener .as_ref() .and_then(|(_, listener)| listener.private_host_path.clone()); - let connected = Arc::new(Mutex::new(PendingJavascriptNetConnectState { + let connected = Arc::new(Mutex::new(PendingNetConnectState { connected: None, bound_unix_listener: bound_listener, })); let task_connected = Arc::clone(&connected); - if process - .pending_javascript_net_connects - .contains_key(&request_id) - { + if process.pending_net_connects.contains_key(&request_id) { restore_pending_bound_unix_connect(process, &connected)?; - return Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_SOCKET_CONNECT_STATE: request {request_id} already has a pending connect" - ))); + return Err(SidecarError::host( + "ERR_AGENTOS_SOCKET_CONNECT_STATE", + format!("request {request_id} already has a pending connect"), + )); } let connect_metadata = match PendingGuestUnixConnectMetadata::register( Arc::clone(&unix_bound_addresses), @@ -1097,11 +1133,12 @@ pub(in crate::execution) fn defer_native_unix_connect( } }; process - .pending_javascript_net_connects + .pending_net_connects .insert(request_id, Arc::clone(&connected)); let (respond_to, receiver) = tokio::sync::oneshot::channel(); let spawn = runtime.spawn(agentos_runtime::TaskClass::Socket, async move { - let result = match tokio::time::timeout( + let result = match crate::execution::operation_deadline_timeout( + "Unix socket connect", limits.operation_deadline, connect_native_unix_socket(bound_socket, &target), ) @@ -1130,7 +1167,7 @@ pub(in crate::execution) fn defer_native_unix_connect( task_connected .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) - .connected = Some(PendingJavascriptNetConnect::Unix { + .connected = Some(PendingNetConnect::Unix { socket_id, socket: Box::new(socket), pending_capability, @@ -1149,6 +1186,7 @@ pub(in crate::execution) fn defer_native_unix_connect( "Unix connect exceeded {}ms; raise limits.reactor.operationDeadlineMs", limits.operation_deadline.as_millis() ), + details: None, }), }; if respond_to.send(result).is_err() { @@ -1156,12 +1194,12 @@ pub(in crate::execution) fn defer_native_unix_connect( } }); if let Err(error) = spawn { - if let Some(pending) = process.pending_javascript_net_connects.remove(&request_id) { + if let Some(pending) = process.pending_net_connects.remove(&request_id) { restore_pending_bound_unix_connect(process, &pending)?; } return Err(SidecarError::from(error)); } - Ok(JavascriptSyncRpcServiceResponse::Deferred { + Ok(HostServiceResponse::Deferred { receiver, timeout: None, task_class: agentos_runtime::TaskClass::Socket, @@ -1183,7 +1221,8 @@ impl ActiveUnixListener { backlog: Option, ) -> Self { let event_pusher = SocketReadinessSubscribers::new(runtime_context.resources()); - let (sender, events) = async_completion_channel(runtime_context, 1); + let (sender, events) = + async_completion_channel(runtime_context, 1, unix_listener_event_retained_bytes); drop(sender); let (close_sender, close_completion) = tokio::sync::oneshot::channel(); drop(close_sender); @@ -1201,11 +1240,12 @@ impl ActiveUnixListener { registry_binding_id, private_host_path, guest_node_path, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize"), active_connection_ids: Arc::new(Mutex::new(BTreeSet::new())), description_handles: Arc::new(()), description_lease: Arc::new(SocketDescriptionLease::default()), + pending_event: Arc::new(Mutex::new(None)), } } @@ -1272,17 +1312,22 @@ impl ActiveUnixListener { #[allow(clippy::too_many_arguments)] pub(in crate::execution) fn listen_bound( mut self, - context: JavascriptSocketPathContext, + context: SocketPathContext, backlog: Option, capabilities: CapabilityRegistry, runtime_context: agentos_runtime::RuntimeContext, reactor_limits: ReactorIoLimits, ) -> Result { + set_guest_unix_pending_connection_limit( + &context.unix_bound_addresses, + &self.registry_binding_id, + listener_accept_capacity(backlog, reactor_limits), + )?; let socket = self .bound_socket .take() .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::EINVAL)))?; - let backlog_value = backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG); + let backlog_value = backlog.unwrap_or(DEFAULT_NET_BACKLOG); socket .listen(i32::try_from(backlog_value).unwrap_or(i32::MAX)) .map_err(sidecar_net_error)?; @@ -1312,12 +1357,34 @@ impl ActiveUnixListener { Ok(listened) } + pub(in crate::execution) fn relisten( + &mut self, + registry: &GuestUnixAddressRegistry, + backlog: u32, + reactor_limits: ReactorIoLimits, + ) -> Result<(), SidecarError> { + let listener = self + .listener + .as_ref() + .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::EINVAL)))?; + SockRef::from(listener) + .listen(i32::try_from(backlog).unwrap_or(i32::MAX)) + .map_err(sidecar_net_error)?; + set_guest_unix_pending_connection_limit( + registry, + &self.registry_binding_id, + listener_accept_capacity(Some(backlog), reactor_limits), + )?; + self.backlog = usize::try_from(backlog).unwrap_or(usize::MAX); + Ok(()) + } + #[allow(clippy::too_many_arguments)] pub(in crate::execution) fn bind( host_path: &Path, guest_path: &str, registry_binding_id: String, - context: JavascriptSocketPathContext, + context: SocketPathContext, backlog: Option, capabilities: CapabilityRegistry, runtime_context: agentos_runtime::RuntimeContext, @@ -1349,7 +1416,7 @@ impl ActiveUnixListener { host_name: &[u8], guest_name: &[u8], registry_binding_id: String, - context: JavascriptSocketPathContext, + context: SocketPathContext, backlog: Option, capabilities: CapabilityRegistry, runtime_context: agentos_runtime::RuntimeContext, @@ -1361,10 +1428,7 @@ impl ActiveUnixListener { bind_socket(socket.as_raw_fd(), &address) .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; socket - .listen( - i32::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) - .unwrap_or(i32::MAX), - ) + .listen(i32::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)).unwrap_or(i32::MAX)) .map_err(sidecar_net_error)?; socket.set_nonblocking(true).map_err(sidecar_net_error)?; Self::from_listener( @@ -1388,7 +1452,7 @@ impl ActiveUnixListener { _host_name: &[u8], _guest_name: &[u8], _registry_binding_id: String, - _context: JavascriptSocketPathContext, + _context: SocketPathContext, _backlog: Option, _capabilities: CapabilityRegistry, _runtime_context: agentos_runtime::RuntimeContext, @@ -1405,13 +1469,18 @@ impl ActiveUnixListener { registry_binding_id: String, private_host_path: Option, guest_node_path: Option, - context: JavascriptSocketPathContext, + context: SocketPathContext, backlog: Option, capabilities: CapabilityRegistry, runtime_context: agentos_runtime::RuntimeContext, reactor_limits: ReactorIoLimits, ) -> Result { let accept_capacity = listener_accept_capacity(backlog, reactor_limits); + set_guest_unix_pending_connection_limit( + &context.unix_bound_addresses, + ®istry_binding_id, + accept_capacity, + )?; let event_pusher = SocketReadinessSubscribers::new(capabilities.resources().as_ref()); let close_notify = Arc::new(tokio::sync::Notify::new()); let (close_complete, close_completion) = tokio::sync::oneshot::channel(); @@ -1443,11 +1512,12 @@ impl ActiveUnixListener { registry_binding_id, private_host_path, guest_node_path, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_NET_BACKLOG)) .expect("default backlog fits within usize"), active_connection_ids: Arc::new(Mutex::new(BTreeSet::new())), description_handles: Arc::new(()), description_lease: Arc::new(SocketDescriptionLease::default()), + pending_event: Arc::new(Mutex::new(None)), }) } @@ -1484,6 +1554,7 @@ impl ActiveUnixListener { active_connection_ids: Arc::clone(&self.active_connection_ids), description_handles: Arc::clone(&self.description_handles), description_lease: Arc::clone(&self.description_lease), + pending_event: Arc::clone(&self.pending_event), }) } @@ -1497,11 +1568,12 @@ impl ActiveUnixListener { pub(in crate::execution) fn set_event_pusher( &self, - session: Option, + session: Option, identity: Option<( agentos_runtime::capability::CapabilityId, agentos_runtime::capability::CapabilityGeneration, )>, + owner_notify: Arc, ) { let (Some(session), Some((capability_id, capability_generation))) = (session, identity) else { @@ -1510,6 +1582,7 @@ impl ActiveUnixListener { self.readiness_registration.register( Some(session), Some((capability_id, capability_generation)), + owner_notify, agentos_runtime::readiness::ReadyFlags::ACCEPT, ); } @@ -1517,7 +1590,15 @@ impl ActiveUnixListener { pub(in crate::execution) fn poll( &mut self, wait: Duration, - ) -> Result, SidecarError> { + ) -> Result, SidecarError> { + if let Some(event) = self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "Unix listener pending event lock poisoned"))? + .take() + { + return Ok(Some(event)); + } let _ = wait; match self .events @@ -1534,6 +1615,25 @@ impl ActiveUnixListener { } } + /// Non-destructive listener readiness probe used by combined POSIX poll. + pub(in crate::execution) fn probe_readable(&mut self) -> Result { + if self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "Unix listener pending event lock poisoned"))? + .is_some() + { + return Ok(true); + } + let event = self.poll(Duration::ZERO)?; + let mut pending = self + .pending_event + .lock() + .map_err(|_| SidecarError::host("EIO", "Unix listener pending event lock poisoned"))?; + *pending = event; + Ok(pending.is_some()) + } + pub(in crate::execution) fn close( self, ) -> Pin> + Send>> @@ -1604,6 +1704,7 @@ pub(in crate::execution) fn release_unix_listener_capability( listener_id: &str, listener: &ActiveUnixListener, ) -> Result<(), SidecarError> { + listener.readiness_registration.retire(); process.release_description_capability( &NativeCapabilityKey::UnixListener(listener_id.to_owned()), None, @@ -1701,6 +1802,33 @@ mod guest_unix_metadata_tests { .is_empty()); } + #[test] + fn pending_connection_metadata_is_bounded_by_listener_capacity() { + let registry = registry(); + register_abstract(®istry, "target", "abstract:target"); + set_guest_unix_pending_connection_limit(®istry, "target", 1) + .expect("set pending connection limit"); + + let first = register_guest_unix_connection(®istry, "target") + .expect("first pending connection fits"); + let error = register_guest_unix_connection(®istry, "target") + .expect_err("second pending connection exceeds listener capacity"); + + assert_eq!(guest_error_code(&error), Some("EAGAIN")); + assert!(error.to_string().contains("listen backlog")); + assert_eq!( + registry + .lock() + .expect("registry") + .get("target") + .expect("target") + .pending_connections + .len(), + 1 + ); + assert!(guest_unix_connection_peer_open(Some(&first))); + } + #[test] fn connector_metadata_is_visible_before_reactor_accept() { let registry = registry(); @@ -1829,7 +1957,7 @@ mod transferred_unix_alias_transport_tests { .expect("surviving Unix alias receives a transport wake") }); match event { - JavascriptTcpSocketEvent::Data { bytes, .. } => assert_eq!(bytes, b"host-to-unix"), + TcpSocketEvent::Data { bytes, .. } => assert_eq!(bytes, b"host-to-unix"), other => panic!("expected Unix data after alias close, got {other:?}"), } @@ -1917,8 +2045,12 @@ fn spawn_unix_listener_acceptor( accept_capacity: usize, capabilities: CapabilityRegistry, limits: ReactorIoLimits, -) -> Result, SidecarError> { - let (sender, receiver) = async_completion_channel(runtime.clone(), accept_capacity); +) -> Result, SidecarError> { + let (sender, receiver) = async_completion_channel( + runtime.clone(), + accept_capacity, + unix_listener_event_retained_bytes, + ); let completion = UnixListenerTaskCompletion(Some(close_complete)); runtime .spawn(agentos_runtime::TaskClass::Listener, async move { @@ -1927,7 +2059,7 @@ fn spawn_unix_listener_acceptor( Ok(listener) => listener, Err(error) => { if sender - .send(JavascriptUnixListenerEvent::Error { + .send(UnixListenerEvent::Error { code: io_error_code(&error), message: error.to_string(), }) @@ -1947,7 +2079,7 @@ fn spawn_unix_listener_acceptor( Ok(ready) => ready, Err(error) => { if sender - .send(JavascriptUnixListenerEvent::Error { + .send(UnixListenerEvent::Error { code: io_error_code(&error), message: error.to_string(), }) @@ -1967,7 +2099,7 @@ fn spawn_unix_listener_acceptor( match capability { Ok(capability) => capability, Err(error) => { - if sender.send(JavascriptUnixListenerEvent::Error { + if sender.send(UnixListenerEvent::Error { code: Some(String::from("ERR_AGENTOS_RESOURCE_LIMIT")), message: error.to_string(), }).await.is_ok() { @@ -1997,7 +2129,7 @@ fn spawn_unix_listener_acceptor( Ok::<_, SidecarError>((connection_state, remote)) })(); match metadata { - Ok((connection_state, remote)) => JavascriptUnixListenerEvent::Connection { + Ok((connection_state, remote)) => UnixListenerEvent::Connection { socket: PendingUnixSocket { stream, local_path: Some(guest_path.clone()), @@ -2011,15 +2143,19 @@ fn spawn_unix_listener_acceptor( capability, }, Err(error) => { - let _ = stream.shutdown(Shutdown::Both); - JavascriptUnixListenerEvent::Error { - code: Some(javascript_sync_rpc_error_code(&error)), + if let Err(shutdown_error) = stream.shutdown(Shutdown::Both) { + eprintln!( + "ERR_AGENTOS_UNIX_SOCKET_CLEANUP: failed to shut down rejected accepted socket: {shutdown_error}" + ); + } + UnixListenerEvent::Error { + code: Some(host_service_error_code(&error)), message: error.to_string(), } } } } - Ok(Err(error)) => JavascriptUnixListenerEvent::Error { + Ok(Err(error)) => UnixListenerEvent::Error { code: io_error_code(&error), message: error.to_string(), }, @@ -2057,11 +2193,8 @@ impl Drop for UnixListenerTaskCompletion { fn push_listener_event(event_pusher: &Arc) { for target in event_pusher.targets() { - if let Err(error) = target.session.publish_readiness( - target.capability_id, - target.capability_generation, - agentos_runtime::readiness::ReadyFlags::ACCEPT, - ) { + if let Err(error) = target.publish_readiness(agentos_runtime::readiness::ReadyFlags::ACCEPT) + { eprintln!("ERR_AGENTOS_NET_LISTENER_WAKE: failed to queue listener wake: {error}"); } } @@ -2095,16 +2228,13 @@ pub(in crate::execution) fn push_socket_event( } }; for target in targets { - match target.session.publish_readiness( - target.capability_id, - target.capability_generation, - flags, - ) { - Ok(()) => { + match target.publish_readiness(flags) { + Ok(true) => { NET_TCP_TRACE_COUNTERS .socket_read_push_sent .fetch_add(1, Ordering::Relaxed); } + Ok(false) => {} Err(error) => { NET_TCP_TRACE_COUNTERS .socket_read_push_errors @@ -2125,8 +2255,9 @@ pub(in crate::execution) fn push_socket_event( fn spawn_unix_socket_reader( runtime: agentos_runtime::RuntimeContext, stream: UnixStream, - sender: AsyncCompletionSender, + sender: AsyncCompletionSender, event_pusher: Arc, + read_event_notify: Arc, application_read_interest: Arc, application_read_notify: Arc, saw_local_shutdown: Arc, @@ -2153,6 +2284,7 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); return; } let stream = match tokio::net::UnixStream::from_std(stream) { @@ -2166,6 +2298,7 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); return; } }; @@ -2191,6 +2324,7 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); break; } let turn = match acquire_plain_socket_fair_turn( @@ -2211,6 +2345,7 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); break; } }; @@ -2226,23 +2361,26 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); break; } match read_result { Ok(0) => { saw_remote_end.store(true, Ordering::SeqCst); - if sender.send(JavascriptTcpSocketEvent::End).await.is_err() { + if sender.send(TcpSocketEvent::End).await.is_err() { break; } push_socket_event(&event_pusher, "end"); + read_event_notify.notify_one(); if saw_local_shutdown.load(Ordering::SeqCst) && !close_notified.swap(true, Ordering::SeqCst) && sender - .send(JavascriptTcpSocketEvent::Close { had_error: false }) + .send(TcpSocketEvent::Close { had_error: false }) .await .is_ok() { push_socket_event(&event_pusher, "close"); + read_event_notify.notify_one(); } break; } @@ -2256,10 +2394,11 @@ fn spawn_unix_socket_reader( ) .await else { + read_event_notify.notify_one(); break; }; if sender - .send(JavascriptTcpSocketEvent::Data { + .send(TcpSocketEvent::Data { bytes: buffer[..bytes_read].to_vec(), reservation: SharedReservation::new(reservation), source_reservations: Vec::new(), @@ -2270,6 +2409,7 @@ fn spawn_unix_socket_reader( break; } push_socket_event(&event_pusher, "data"); + read_event_notify.notify_one(); } Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => continue, Err(error) => { @@ -2282,6 +2422,7 @@ fn spawn_unix_socket_reader( error.to_string(), ) .await; + read_event_notify.notify_one(); break; } } diff --git a/crates/native-sidecar/src/execution/process.rs b/crates/native-sidecar/src/execution/process.rs index 05a0bcfab2..437e0fe66e 100644 --- a/crates/native-sidecar/src/execution/process.rs +++ b/crates/native-sidecar/src/execution/process.rs @@ -2,6 +2,30 @@ use super::*; static NEXT_SQLITE_HOST_NAMESPACE: AtomicU64 = AtomicU64::new(1); +pub(super) fn admit_one_slot_rpc( + pending_call_id: Option, + incoming_call_id: u64, + slot_name: &'static str, +) -> Result<(), HostServiceError> { + let Some(pending_call_id) = pending_call_id else { + return Ok(()); + }; + if pending_call_id == incoming_call_id { + return Ok(()); + } + Err(HostServiceError::new( + "EBUSY", + format!( + "{slot_name} already retains call {pending_call_id}; call {incoming_call_id} was not admitted" + ), + ) + .with_details(json!({ + "slotName": slot_name, + "pendingCallId": pending_call_id, + "incomingCallId": incoming_call_id, + }))) +} + /// Ownership of VM-wide retained-byte accounting for an event temporarily /// removed from a process queue. Keeping this reservation alive across a /// capacity check prevents a concurrent producer from consuming the bytes an @@ -48,6 +72,81 @@ impl PolledExecutionEvent { } impl ActiveProcess { + /// Attach the generation-bound kernel control receiver before any guest + /// engine is started. Controls requested while the adapter is being + /// constructed remain durable in this receiver and are applied before the + /// process is published in the sidecar's active-process map. + pub(crate) fn attach_runtime_control_before_start( + kernel_handle: &KernelProcessHandle, + event_notify: Arc, + ) -> Result { + kernel_handle + .attach_runtime_control(Arc::new(move || event_notify.notify_one())) + .map_err(|error| SidecarError::host(error.code(), error.message())) + } + + pub(crate) fn install_executable_image( + &mut self, + bytes: Vec, + retained_bytes: Reservation, + ) -> Result { + if self.executable_image.is_some() { + return Err(HostServiceError::new( + "EBUSY", + "this process already owns an executable-image snapshot", + )); + } + let handle = self.next_executable_image_handle; + self.next_executable_image_handle = handle.checked_add(1).ok_or_else(|| { + HostServiceError::new("EOVERFLOW", "executable-image handle space exhausted") + })?; + self.executable_image = Some(ActiveExecutableImage { + handle, + bytes, + _retained_bytes: retained_bytes, + }); + Ok(handle) + } + + pub(crate) fn read_executable_image( + &self, + handle: u64, + offset: u64, + maximum: usize, + ) -> Result<&[u8], HostServiceError> { + let image = self.executable_image.as_ref().ok_or_else(|| { + HostServiceError::new("EBADF", "no executable-image snapshot is open") + })?; + if image.handle != handle { + return Err(HostServiceError::new( + "ESTALE", + "executable-image handle does not name the active snapshot", + )); + } + let start = usize::try_from(offset) + .map_err(|_| HostServiceError::new("EOVERFLOW", "exec image offset exceeds usize"))?; + if start >= image.bytes.len() { + return Ok(&[]); + } + let end = start.saturating_add(maximum).min(image.bytes.len()); + Ok(&image.bytes[start..end]) + } + + pub(crate) fn close_executable_image(&mut self, handle: u64) -> Result<(), HostServiceError> { + let image = self.executable_image.as_ref().ok_or_else(|| { + HostServiceError::new("EBADF", "no executable-image snapshot is open") + })?; + if image.handle != handle { + return Err(HostServiceError::new( + "ESTALE", + "executable-image handle does not name the active snapshot", + )); + } + self.executable_image = None; + Ok(()) + } + + #[cfg(test)] pub(crate) fn new( kernel_pid: u32, kernel_handle: KernelProcessHandle, @@ -57,32 +156,95 @@ impl ActiveProcess { runtime: GuestRuntimeKind, execution: ActiveExecution, ) -> Self { + let process_event_notify = Arc::new(tokio::sync::Notify::new()); + let runtime_control = Self::attach_runtime_control_before_start( + &kernel_handle, + Arc::clone(&process_event_notify), + ) + .expect("a kernel process must attach exactly one runtime control receiver"); + Self::new_with_attached_runtime_control( + kernel_pid, + kernel_handle, + runtime_context, + limits, + process_event_capacity, + runtime, + execution, + runtime_control, + process_event_notify, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn new_with_attached_runtime_control( + kernel_pid: u32, + kernel_handle: KernelProcessHandle, + runtime_context: agentos_runtime::RuntimeContext, + limits: crate::limits::VmLimits, + process_event_capacity: usize, + runtime: GuestRuntimeKind, + mut execution: ActiveExecution, + runtime_control: agentos_kernel::process_runtime::RuntimeControlReceiver, + process_event_notify: Arc, + ) -> Self { + assert_eq!( + runtime_control.identity(), + kernel_handle.runtime_identity(), + "the attached runtime control receiver must match its kernel process" + ); let pending_event_count_limit = process_event_capacity.min(limits.process.pending_event_count); let pending_stdin_bytes_limit = limits.process.pending_stdin_bytes; let pending_event_bytes_limit = limits.process.pending_event_bytes; - if let ActiveExecution::Binding(binding) = &execution { - binding - .pending_event_count_limit - .store(pending_event_count_limit, Ordering::Release); - binding - .pending_event_bytes_limit - .store(pending_event_bytes_limit, Ordering::Release); - } + execution + .configure_adapter_event_limits(pending_event_count_limit, pending_event_bytes_limit); // Binding producers lease retained-byte reservations from their own // queue before an event can be moved into the ActiveProcess queue. // Both queues must therefore start with the same budget identity; a // signal-state drain may temporarily lease stdout/exit and requeue it. - let vm_pending_event_bytes_budget = match &execution { - ActiveExecution::Binding(binding) => Arc::clone(&binding.vm_pending_event_bytes_budget), - _ => VmPendingByteBudget::new( + let vm_pending_event_bytes_budget = + execution.adapter_event_bytes_budget().unwrap_or_else(|| { + VmPendingByteBudget::new( + pending_event_bytes_limit, + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ) + }); + let common_event_notify = Arc::new(Mutex::new(Arc::clone(&process_event_notify))); + let common_event_wake = Arc::clone(&common_event_notify); + let identity = kernel_handle.runtime_identity(); + let (event_submission, common_execution_events) = bounded_execution_event_channel( + HostProcessContext { + generation: identity.generation, + pid: identity.pid, + }, + pending_event_count_limit, + PayloadLimit::new( + "limits.process.pendingEventBytes", pending_event_bytes_limit, - queue_tracker::TrackedLimit::PendingExecutionEventBytes, - ), - }; + ) + .expect("an admitted process must have a nonzero common event byte limit"), + Arc::new(move || { + let notify = match common_event_wake.lock() { + Ok(notify) => Arc::clone(¬ify), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_EXECUTION_WAKE_LOCK_POISONED: recovering the execution wake target after a prior panic" + ); + Arc::clone(&poisoned.into_inner()) + } + }; + notify.notify_one(); + }), + ) + .expect("an admitted process must have a nonzero common event capacity"); + let host_capabilities = ProcessHostCapabilitySet::from_event_submission(event_submission); + ExecutionBackend::configure_host_services(&mut execution, host_capabilities.clone()); + let control_notify = Arc::clone(&process_event_notify); + runtime_control.set_wake(Arc::new(move || control_notify.notify_one())); Self { kernel_pid, kernel_handle, + runtime_control, runtime_context, limits, kernel_stdin_writer_fd: None, @@ -99,16 +261,18 @@ impl ActiveProcess { ), tty_master_fd: None, runtime, + adapter_policy: ExecutionAdapterPolicy::BINDING, detached: false, execution, guest_cwd: String::from("/"), env: BTreeMap::new(), host_cwd: PathBuf::from("/"), - shadow_root: None, - host_write_dirty: false, - mapped_host_fds: BTreeMap::new(), - next_mapped_host_fd: MAPPED_HOST_FD_START, - process_event_notify: Arc::new(tokio::sync::Notify::new()), + executable_image: None, + next_executable_image_handle: 1, + process_event_notify, + common_event_notify, + host_capabilities, + common_execution_events, process_event_capacity, wasm_flock_fds: BTreeMap::new(), pending_execution_events: VecDeque::new(), @@ -124,19 +288,16 @@ impl ActiveProcess { pending_event_bytes_limit, ), vm_pending_event_bytes_budget, - pending_javascript_net_connects: BTreeMap::new(), - pending_self_signal_exit: None, + pending_net_connects: BTreeMap::new(), + pending_managed_host_net_connects: BTreeMap::new(), + pending_runtime_exit: None, exit_signal: None, exit_core_dumped: false, - pending_wasm_signals: BTreeSet::new(), - pending_wasm_signals_gauge: queue_tracker::register_queue( - queue_tracker::TrackedLimit::PendingWasmSignals, - 64, - ), real_interval_timer: ActiveRealIntervalTimer::new(), child_processes: BTreeMap::new(), next_child_process_id: 0, pending_child_process_sync: BTreeMap::new(), + child_process_bridge_owns_output: false, http_servers: BTreeMap::new(), pending_http_requests: BTreeMap::new(), http2: Default::default(), @@ -153,8 +314,6 @@ impl ActiveProcess { next_unix_socket_id: 0, udp_sockets: BTreeMap::new(), next_udp_socket_id: 0, - python_sockets: BTreeMap::new(), - next_python_socket_id: 0, hash_sessions: BTreeMap::new(), next_hash_session_id: 0, cipher_sessions: BTreeMap::new(), @@ -173,18 +332,265 @@ impl ActiveProcess { tty_master_owner: None, tty_raw_mode_generation: None, deferred_kernel_wait_rpc: None, + deferred_kernel_wait_deadline_warned: false, deferred_child_write_timer: None, + deferred_guest_wait: None, + deferred_guest_wait_interrupted: false, + guest_signal_checkpoint_pending: false, + deferred_kernel_poll: None, + deferred_kernel_read: None, module_resolution_cache: agentos_execution::LocalModuleResolutionCache::default(), } } pub(crate) fn clear_deferred_kernel_wait_rpc(&mut self) { self.deferred_kernel_wait_rpc = None; + self.deferred_kernel_wait_deadline_warned = false; if let Some(timer) = self.deferred_child_write_timer.take() { timer.abort(); } } + pub(crate) fn clear_deferred_guest_wait(&mut self) -> Option { + self.deferred_guest_wait_interrupted = false; + let mut wait = self.deferred_guest_wait.take()?; + if let Some(task) = wait.wake_task.take() { + task.abort(); + } + Some(wait) + } + + pub(crate) fn clear_deferred_kernel_poll(&mut self) -> Option { + let mut poll = self.deferred_kernel_poll.take()?; + if let Some(task) = poll.wake_task.take() { + task.abort(); + } + Some(poll) + } + + pub(crate) fn clear_deferred_kernel_read(&mut self) -> Option { + let mut read = self.deferred_kernel_read.take()?; + if let Some(task) = read.wake_task.take() { + task.abort(); + } + Some(read) + } + + fn apply_backend_shutdown_outcome( + &mut self, + outcome: ShutdownOutcome, + ) -> Result<(), SidecarError> { + use agentos_kernel::process_runtime::ProcessExit; + + match outcome { + ShutdownOutcome::AwaitExit => Ok(()), + ShutdownOutcome::Exited(exit) => { + self.pending_runtime_exit.get_or_insert(match exit { + ExecutionExit::Exited(code) => ProcessExit::Exited(code), + ExecutionExit::Signaled { + signal, + core_dumped, + } => ProcessExit::Signaled { + signal, + core_dumped, + }, + }); + Ok(()) + } + ShutdownOutcome::ForwardSignal { process_id, signal } => { + signal_runtime_process(process_id, signal) + } + } + } + + pub(crate) fn apply_runtime_controls(&mut self) -> Result<(), SidecarError> { + use agentos_kernel::process_runtime::{ProcessCancellationReason, ProcessTermination}; + + let controls = self.runtime_control.pending(); + if controls.is_empty() { + return Ok(()); + } + + let result = (|| { + if let Some(stopped) = controls.stopped { + if stopped { + self.execution.pause()?; + } else { + self.execution.resume()?; + } + } + + let cancellation_shutdown_reason = controls.cancellation.map(|reason| match reason { + ProcessCancellationReason::VmTeardown => ShutdownReason::VmTeardown, + ProcessCancellationReason::Deadline => ShutdownReason::Deadline, + ProcessCancellationReason::HostRequest => ShutdownReason::HostRequest, + ProcessCancellationReason::RuntimeFault => ShutdownReason::RuntimeFault, + }); + if let Some(termination) = controls.termination { + let outcome = match termination { + ProcessTermination::Signal { signal, .. } => self + .execution + .begin_shutdown(ShutdownReason::Signal(signal))?, + ProcessTermination::RuntimeFault => self + .execution + .begin_shutdown(ShutdownReason::RuntimeFault)?, + }; + self.apply_backend_shutdown_outcome(outcome)?; + } else if let Some(reason) = cancellation_shutdown_reason { + let outcome = self.execution.begin_shutdown(reason)?; + self.apply_backend_shutdown_outcome(outcome)?; + } + + if controls.checkpoint { + // A combined ppoll owns its temporary mask. Select a signal + // while that mask is still active, but atomically restore the + // caller's mask before constructing the handler frame. This is + // what lets a signal unblocked only by ppoll interrupt the + // wait while nested handlers still observe the real mask. + let first_delivery = if let Some(token) = self + .deferred_kernel_poll + .as_mut() + .and_then(|poll| poll.temporary_signal_mask_token.take()) + { + match self + .kernel_handle + .end_temporary_signal_mask_and_begin_signal_delivery(token) + { + Ok(delivery) => delivery, + Err(error) => { + let failure = HostServiceError::new(error.code(), error.to_string()); + if let Some(poll) = self.clear_deferred_kernel_poll() { + poll.reply.fail(failure).map_err(SidecarError::from)?; + } + return Err(kernel_error(error)); + } + } + } else { + None + }; + for index in 0..64 { + let delivery = if index == 0 { + match first_delivery { + Some(delivery) => Some(delivery), + None => self + .kernel_handle + .begin_signal_delivery() + .map_err(kernel_error)?, + } + } else { + self.kernel_handle + .begin_signal_delivery() + .map_err(kernel_error)? + }; + let Some(delivery) = delivery else { break }; + let identity = self.kernel_handle.runtime_identity(); + let delivery_result = match self.execution.deliver_signal_checkpoint( + ExecutionWakeIdentity { + generation: identity.generation, + pid: identity.pid, + }, + delivery.signal, + delivery.token, + delivery.action.flags, + ) { + Ok(SignalCheckpointOutcome::Published) => { + // Every caught signal must release a parked guest syscall so + // its handler can run promptly. The guest adapter applies + // SA_RESTART after dispatching the handler and transparently + // reissues only the documented restartable operations. + if self.deferred_guest_wait.is_some() { + self.deferred_guest_wait_interrupted = true; + } + self.guest_signal_checkpoint_pending = true; + let interrupted = HostServiceError::new( + "EINTR", + "caught signal interrupted the pending guest host call", + ); + let mut interrupted_replies = Vec::new(); + if let Some(wait) = self.clear_deferred_guest_wait() { + interrupted_replies.push(wait.reply); + } + if let Some(poll) = self.clear_deferred_kernel_poll() { + interrupted_replies.push(poll.reply); + } + if let Some(read) = self.clear_deferred_kernel_read() { + interrupted_replies.push(read.reply); + } + if let Some((request, _)) = self.deferred_kernel_wait_rpc.clone() { + self.clear_deferred_kernel_wait_rpc(); + interrupted_replies.push(request.reply); + } + let mut settlement_error = None; + for reply in interrupted_replies { + if let Err(error) = reply.fail(interrupted.clone()) { + if settlement_error.is_none() { + settlement_error = Some(SidecarError::from(error)); + } + } + } + if let Some(error) = settlement_error { + return Err(error); + } + // The guest bridge owns exactly this one delivery + // until `signal_end`. Kernel delivery scopes are + // strict LIFO, so never preclaim a second token. + break; + } + Ok(SignalCheckpointOutcome::ForwardToProcess { process_id }) => { + signal_runtime_process(process_id, delivery.signal) + } + Ok(SignalCheckpointOutcome::Unsupported) => { + Err(SidecarError::InvalidState(format!( + "unsupported guest signal handler delivery for kernel pid {}", + self.kernel_pid + ))) + } + Err(error) => Err(error), + }; + let end_result = self + .kernel_handle + .end_signal_delivery(delivery.token) + .map_err(kernel_error); + delivery_result?; + end_result?; + } + } + Ok(()) + })(); + + match result { + Ok(()) => match self.runtime_control.acknowledge(controls) { + Ok(()) => Ok(()), + Err(error) => { + self.runtime_control.retry_pending(); + Err(SidecarError::host(error.code(), error.message())) + } + }, + Err(error) => { + self.runtime_control.retry_pending(); + Err(error) + } + } + } + + fn take_pending_runtime_exit_event(&mut self) -> Option { + use agentos_kernel::process_runtime::ProcessExit; + + self.pending_runtime_exit + .take() + .map(|termination| match termination { + ProcessExit::Exited(code) => ActiveExecutionEvent::Exited(code), + ProcessExit::Signaled { + signal, + core_dumped, + } => { + self.exit_signal = Some(signal); + self.exit_core_dumped = core_dumped; + ActiveExecutionEvent::Exited(termination.shell_status()) + } + }) + } + pub(crate) fn queue_pending_execution_event( &mut self, event: ActiveExecutionEvent, @@ -202,33 +608,51 @@ impl ActiveProcess { ) -> Result<(), (SidecarError, ActiveExecutionEvent)> { let event_bytes = event.retained_bytes(); if self.pending_execution_events.len() >= self.pending_execution_event_count_limit { + let limit = self.pending_execution_event_count_limit; return Err(( - SidecarError::InvalidState(format!( - "process execution event queue exceeded {} events (limits.process.pendingEventCount/runtime.protocol.maxProcessEvents); raise the limiting setting", - self.pending_execution_event_count_limit - )), + SidecarError::host_resource_limit( + "limits.process.pendingEventCount/runtime.protocol.maxProcessEvents", + limit, + self.pending_execution_events.len().saturating_add(1), + format!( + "process execution event queue exceeded {limit} events (limits.process.pendingEventCount/runtime.protocol.maxProcessEvents); raise the limiting setting" + ), + ), event, )); } - if self + let observed_process_bytes = self .pending_execution_event_bytes - .saturating_add(event_bytes) - > self.pending_execution_event_bytes_limit - { + .saturating_add(event_bytes); + if observed_process_bytes > self.pending_execution_event_bytes_limit { + let limit = self.pending_execution_event_bytes_limit; return Err(( - SidecarError::InvalidState(format!( - "process execution event queue exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", - self.pending_execution_event_bytes_limit - )), + SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limit, + observed_process_bytes, + format!( + "process execution event queue exceeded {limit} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), + ), event, )); } if !self.vm_pending_event_bytes_budget.try_reserve(event_bytes) { + let limit = self.vm_pending_event_bytes_budget.limit(); + let observed = self + .vm_pending_event_bytes_budget + .used() + .saturating_add(event_bytes); return Err(( - SidecarError::InvalidState(format!( - "VM process execution event queues exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", - self.vm_pending_event_bytes_budget.limit() - )), + SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limit, + observed, + format!( + "VM process execution event queues exceeded {limit} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), + ), event, )); } @@ -281,6 +705,12 @@ impl ActiveProcess { .observe_depth(self.pending_execution_events.len()); self.pending_execution_event_bytes_gauge .observe_depth(self.pending_execution_event_bytes); + // A parent queue may have backpressured an already-accounted child + // output event. Consuming one parent event creates capacity, so rearm + // the coalesced process pump when descendants exist. + if !self.child_processes.is_empty() { + self.process_event_notify.notify_one(); + } Some(PolledExecutionEvent { event, reservation: Some(PendingExecutionEventReservation { @@ -300,61 +730,166 @@ impl ActiveProcess { &mut self, polled: PolledExecutionEvent, ) -> Result<(), SidecarError> { - self.queue_polled_execution_event(polled, true) + self.queue_polled_execution_event(polled, true, true) } pub(super) fn queue_pending_polled_execution_event( &mut self, polled: PolledExecutionEvent, ) -> Result<(), SidecarError> { - self.queue_polled_execution_event(polled, false) + self.queue_polled_execution_event(polled, false, true) } - fn queue_polled_execution_event( + #[allow(clippy::result_large_err)] + pub(super) fn try_queue_pending_polled_execution_event( + &mut self, + polled: PolledExecutionEvent, + ) -> Result<(), (SidecarError, PolledExecutionEvent)> { + self.try_queue_polled_execution_event(polled, false, true) + } + + /// Return a public child event to the pull owner's durable queue without + /// waking the global pump that deliberately declined to consume it. The + /// parent's next `child_process.poll` HostCall supplies the next broker + /// edge; self-notifying here would spin on the same event indefinitely. + pub(super) fn queue_pull_owned_polled_execution_event( &mut self, polled: PolledExecutionEvent, - front: bool, ) -> Result<(), SidecarError> { - let PolledExecutionEvent { event, reservation } = polled; - let event_bytes = event.retained_bytes(); + self.queue_polled_execution_event(polled, false, false) + } + + pub(super) fn check_pending_polled_execution_event_admission( + &self, + polled: &PolledExecutionEvent, + ) -> Result<(), SidecarError> { + let event_bytes = polled.event.retained_bytes(); if self.pending_execution_events.len() >= self.pending_execution_event_count_limit { - return Err(SidecarError::InvalidState(format!( - "process execution event queue exceeded {} events (limits.process.pendingEventCount/runtime.protocol.maxProcessEvents); raise the limiting setting", - self.pending_execution_event_count_limit - ))); + let limit = self.pending_execution_event_count_limit; + return Err(SidecarError::host_resource_limit( + "limits.process.pendingEventCount/runtime.protocol.maxProcessEvents", + limit, + self.pending_execution_events.len().saturating_add(1), + format!( + "process execution event queue exceeded {limit} events (limits.process.pendingEventCount/runtime.protocol.maxProcessEvents); raise the limiting setting" + ), + )); } - if self + let observed_process_bytes = self .pending_execution_event_bytes + .saturating_add(event_bytes); + if observed_process_bytes > self.pending_execution_event_bytes_limit { + let limit = self.pending_execution_event_bytes_limit; + return Err(SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limit, + observed_process_bytes, + format!( + "process execution event queue exceeded {limit} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), + )); + } + if let Some(reservation) = polled.reservation.as_ref() { + if reservation.bytes != event_bytes + || !Arc::ptr_eq(&reservation.budget, &self.vm_pending_event_bytes_budget) + { + return Err(SidecarError::InvalidState(String::from( + "process execution event reservation no longer matches its VM queue; event requeue aborted", + ))); + } + } else if self + .vm_pending_event_bytes_budget + .used() .saturating_add(event_bytes) - > self.pending_execution_event_bytes_limit + > self.vm_pending_event_bytes_budget.limit() { - return Err(SidecarError::InvalidState(format!( - "process execution event queue exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", - self.pending_execution_event_bytes_limit - ))); + let limit = self.vm_pending_event_bytes_budget.limit(); + let observed = self + .vm_pending_event_bytes_budget + .used() + .saturating_add(event_bytes); + return Err(SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limit, + observed, + format!( + "VM process execution event queues exceeded {limit} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), + )); } + Ok(()) + } - let reservation = match reservation { - Some(reservation) => { - if reservation.bytes != event_bytes - || !Arc::ptr_eq(&reservation.budget, &self.vm_pending_event_bytes_budget) - { - return Err(SidecarError::InvalidState(String::from( - "process execution event reservation no longer matches its VM queue; event requeue aborted", - ))); - } - Some(reservation) - } - None => { - if !self.vm_pending_event_bytes_budget.try_reserve(event_bytes) { - return Err(SidecarError::InvalidState(format!( - "VM process execution event queues exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", - self.vm_pending_event_bytes_budget.limit() - ))); - } - None - } - }; + /// Collapse duplicate terminal events before ordering one authoritative + /// exit behind already queued output/internal events. Some runtimes expose + /// both their adapter exit and the kernel runtime-control exit; retaining + /// both makes two exits endlessly rotate ahead of one another. + pub(super) fn discard_pending_exit_events(&mut self) -> usize { + let previous_len = self.pending_execution_events.len(); + let previous_bytes = self.pending_execution_event_bytes; + self.pending_execution_events.retain(|event| { + !matches!( + event, + ActiveExecutionEvent::Exited(_) + | ActiveExecutionEvent::Common(ExecutionEvent::Exited(_)) + ) + }); + self.pending_execution_event_bytes = self + .pending_execution_events + .iter() + .map(ActiveExecutionEvent::retained_bytes) + .fold(0usize, usize::saturating_add); + self.vm_pending_event_bytes_budget + .release(previous_bytes.saturating_sub(self.pending_execution_event_bytes)); + self.pending_execution_event_count_gauge + .observe_depth(self.pending_execution_events.len()); + self.pending_execution_event_bytes_gauge + .observe_depth(self.pending_execution_event_bytes); + previous_len.saturating_sub(self.pending_execution_events.len()) + } + + fn queue_polled_execution_event( + &mut self, + polled: PolledExecutionEvent, + front: bool, + notify: bool, + ) -> Result<(), SidecarError> { + self.try_queue_polled_execution_event(polled, front, notify) + .map_err(|(error, _polled)| error) + } + + #[allow(clippy::result_large_err)] + fn try_queue_polled_execution_event( + &mut self, + polled: PolledExecutionEvent, + front: bool, + notify: bool, + ) -> Result<(), (SidecarError, PolledExecutionEvent)> { + if let Err(error) = self.check_pending_polled_execution_event_admission(&polled) { + return Err((error, polled)); + } + let event_bytes = polled.event.retained_bytes(); + if polled.reservation.is_none() + && !self.vm_pending_event_bytes_budget.try_reserve(event_bytes) + { + let limit = self.vm_pending_event_bytes_budget.limit(); + let observed = self + .vm_pending_event_bytes_budget + .used() + .saturating_add(event_bytes); + return Err(( + SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limit, + observed, + format!( + "VM process execution event queues exceeded {limit} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), + ), + polled, + )); + } + let PolledExecutionEvent { event, reservation } = polled; self.pending_execution_event_bytes = self .pending_execution_event_bytes @@ -371,7 +906,9 @@ impl ActiveProcess { if let Some(reservation) = reservation { reservation.transfer_to_queue(); } - self.process_event_notify.notify_one(); + if notify { + self.process_event_notify.notify_one(); + } Ok(()) } @@ -379,24 +916,91 @@ impl ActiveProcess { &mut self, timeout: Duration, ) -> Result, SidecarError> { - if let ActiveExecution::Binding(execution) = &mut self.execution { - return poll_binding_process_event_leased(execution); + self.apply_runtime_controls()?; + if let Some(event) = self.take_pending_runtime_exit_event() { + return Ok(Some(PolledExecutionEvent::unreserved(event))); + } + if let Some(event) = self.execution.poll_adapter_event_leased() { + return event; + } + if let Some(event) = self + .common_execution_events + .try_recv() + .map_err(SidecarError::from)? + { + return Ok(Some(PolledExecutionEvent::unreserved( + ActiveExecutionEvent::Common(event), + ))); + } + let host_capabilities = self.host_capabilities.clone(); + let event = self + .execution + .poll_event_with_host( + self.kernel_handle.runtime_identity(), + self.limits.reactor.max_bridge_response_bytes, + timeout, + &host_capabilities, + ) + .await + .map_err(|error| SidecarError::Execution(error.to_string()))?; + if let Some(event) = event { + return Ok(Some(PolledExecutionEvent::unreserved(event))); } - self.execution - .poll_event(timeout) + Ok(self + .common_execution_events + .try_recv() + .map_err(SidecarError::from)? + .map(ActiveExecutionEvent::Common) + .map(PolledExecutionEvent::unreserved)) + } + + #[cfg(test)] + pub(crate) async fn poll_execution_event_for_test( + &mut self, + timeout: Duration, + ) -> Result, SidecarError> { + self.poll_execution_event(timeout) .await - .map(|event| event.map(PolledExecutionEvent::unreserved)) + .map(|event| event.map(PolledExecutionEvent::into_event)) } pub(super) fn try_poll_execution_event( &mut self, ) -> Result, SidecarError> { - if let ActiveExecution::Binding(execution) = &mut self.execution { - return poll_binding_process_event_leased(execution); + self.apply_runtime_controls()?; + if let Some(event) = self.take_pending_runtime_exit_event() { + return Ok(Some(PolledExecutionEvent::unreserved(event))); + } + if let Some(event) = self.execution.poll_adapter_event_leased() { + return event; + } + if let Some(event) = self + .common_execution_events + .try_recv() + .map_err(SidecarError::from)? + { + return Ok(Some(PolledExecutionEvent::unreserved( + ActiveExecutionEvent::Common(event), + ))); + } + let host_capabilities = self.host_capabilities.clone(); + let event = self + .execution + .try_poll_event_with_host( + self.kernel_handle.runtime_identity(), + self.limits.reactor.max_bridge_response_bytes, + &host_capabilities, + ) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + if let Some(event) = event { + return Ok(Some(PolledExecutionEvent::unreserved(event))); } - self.execution - .try_poll_event() - .map(|event| event.map(PolledExecutionEvent::unreserved)) + Ok(self + .common_execution_events + .try_recv() + .map_err(SidecarError::from)? + .map(ActiveExecutionEvent::Common) + .map(PolledExecutionEvent::unreserved)) } pub(crate) fn with_process_event_limits( @@ -406,14 +1010,10 @@ impl ActiveProcess { self.pending_execution_event_count_limit = self.process_event_capacity.min(limits.pending_event_count); self.pending_execution_event_bytes_limit = limits.pending_event_bytes; - if let ActiveExecution::Binding(execution) = &self.execution { - execution - .pending_event_count_limit - .store(self.pending_execution_event_count_limit, Ordering::Release); - execution - .pending_event_bytes_limit - .store(limits.pending_event_bytes, Ordering::Release); - } + self.execution.configure_adapter_event_limits( + self.pending_execution_event_count_limit, + limits.pending_event_bytes, + ); self.pending_kernel_stdin_gauge = queue_tracker::register_queue( queue_tracker::TrackedLimit::PendingKernelStdinBytes, limits.pending_stdin_bytes, @@ -438,23 +1038,24 @@ impl ActiveProcess { debug_assert_eq!(self.pending_execution_event_bytes, 0); self.vm_pending_stdin_bytes_budget = stdin; self.vm_pending_event_bytes_budget = Arc::clone(&events); - if let ActiveExecution::Binding(execution) = &mut self.execution { - if !Arc::ptr_eq(&execution.vm_pending_event_bytes_budget, &events) { - debug_assert_eq!(execution.pending_event_bytes.load(Ordering::Acquire), 0); - execution.vm_pending_event_bytes_budget = events; - } - } + self.execution.bind_adapter_event_bytes_budget(events); self } - pub(crate) fn queue_pending_wasm_signal(&mut self, signal: i32) -> Result<(), SidecarError> { - self.pending_wasm_signals.insert(signal); - self.pending_wasm_signals_gauge - .observe_depth(self.pending_wasm_signals.len()); - Ok(()) - } - + #[cfg(test)] pub(crate) fn with_event_notify(mut self, event_notify: Arc) -> Self { + let control_notify = Arc::clone(&event_notify); + self.runtime_control + .set_wake(Arc::new(move || control_notify.notify_one())); + match self.common_event_notify.lock() { + Ok(mut notify) => *notify = Arc::clone(&event_notify), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_EXECUTION_WAKE_LOCK_POISONED: recovering the execution wake target after a prior panic" + ); + *poisoned.into_inner() = Arc::clone(&event_notify); + } + } self.process_event_notify = event_notify; self } @@ -464,38 +1065,6 @@ impl ActiveProcess { self } - pub(crate) fn with_shadow_root(mut self, shadow_root: PathBuf) -> Self { - self.shadow_root = Some(shadow_root); - self - } - - pub(crate) fn mark_host_write_dirty(&mut self) { - self.host_write_dirty = true; - } - - pub(crate) fn host_write_dirty_recursive(&self) -> bool { - self.host_write_dirty - || self - .child_processes - .values() - .any(ActiveProcess::host_write_dirty_recursive) - } - - pub(crate) fn clean_host_writes_are_observable(&self) -> bool { - matches!( - self.execution, - ActiveExecution::Javascript(_) | ActiveExecution::Python(_) | ActiveExecution::Wasm(_) - ) - } - - pub(crate) fn clean_host_writes_are_observable_recursive(&self) -> bool { - self.clean_host_writes_are_observable() - && self - .child_processes - .values() - .all(ActiveProcess::clean_host_writes_are_observable_recursive) - } - pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self { self.guest_cwd = guest_cwd; self @@ -521,26 +1090,9 @@ impl ActiveProcess { self } - pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 { - let handle = self.next_mapped_host_fd; - self.next_mapped_host_fd = self - .next_mapped_host_fd - .checked_add(1) - .unwrap_or(MAPPED_HOST_FD_START); - self.mapped_host_fds.insert(handle, fd); - handle - } - - pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> { - self.mapped_host_fds.get(&fd) - } - - pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> { - self.mapped_host_fds.get_mut(&fd) - } - - pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool { - self.mapped_host_fds.remove(&fd).is_some() + pub(crate) fn with_adapter_policy(mut self, adapter_policy: ExecutionAdapterPolicy) -> Self { + self.adapter_policy = adapter_policy; + self } pub(crate) fn allocate_child_process_id(&mut self) -> String { @@ -593,7 +1145,7 @@ impl ActiveProcess { descriptions: &mut BTreeMap, counts: &mut NetworkResourceCounts, ) { - counts.sockets += self.http_servers.len() + self.python_sockets.len(); + counts.sockets += self.http_servers.len(); let http2 = self .http2 .shared @@ -645,8 +1197,9 @@ impl ActiveProcess { entry.insert(Arc::new(lease)); Ok(()) } - std::collections::btree_map::Entry::Occupied(_) => Err(SidecarError::InvalidState( - format!("ERR_AGENTOS_CAPABILITY_DUPLICATE: process already owns {key:?}"), + std::collections::btree_map::Entry::Occupied(_) => Err(SidecarError::host( + "ERR_AGENTOS_CAPABILITY_DUPLICATE", + format!("process already owns {key:?}"), )), } } @@ -674,11 +1227,15 @@ impl ActiveProcess { preserved_identity: Option<(u64, u64)>, ) -> Result<(), SidecarError> { let lease = self.capability_leases.remove(key).ok_or_else(|| { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_CAPABILITY_MISSING: process does not own {key:?}" - )) + SidecarError::host( + "ERR_AGENTOS_CAPABILITY_MISSING", + format!("process does not own {key:?}"), + ) })?; - if let Some(session) = self.execution.javascript_v8_session_handle() { + if let Some(session) = self + .execution + .execution_wake_handle(self.kernel_handle.runtime_identity()) + { if let Err(error) = session.remove_readiness(lease.id(), lease.generation()) { eprintln!( "ERR_AGENTOS_READY_REMOVE: capability={} generation={}: {error}", @@ -710,14 +1267,18 @@ impl ActiveProcess { if description_lease.is_retained() { return Ok(()); } - Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_CAPABILITY_MISSING: process does not own {key:?} and the open description has no retained lease" - ))) + Err(SidecarError::host( + "ERR_AGENTOS_CAPABILITY_MISSING", + format!("process does not own {key:?} and the open description has no retained lease"), + )) } pub(super) fn release_capability_if_present(&mut self, key: &NativeCapabilityKey) { if let Some(lease) = self.capability_leases.remove(key) { - if let Some(session) = self.execution.javascript_v8_session_handle() { + if let Some(session) = self + .execution + .execution_wake_handle(self.kernel_handle.runtime_identity()) + { if let Err(error) = session.remove_readiness(lease.id(), lease.generation()) { eprintln!( "ERR_AGENTOS_READY_REMOVE: capability={} generation={}: {error}", @@ -773,14 +1334,16 @@ impl ActiveProcess { kind: CapabilityKind, ) -> Result<(), SidecarError> { let generation = self.runtime_context.vm_generation().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_CAPABILITY_SESSION: process runtime is not VM-generation scoped", - )) + SidecarError::host( + "ERR_AGENTOS_CAPABILITY_SESSION", + String::from("process runtime is not VM-generation scoped"), + ) })?; let lease = self.capability_leases.get(key).ok_or_else(|| { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_CAPABILITY_MISSING: process does not own {key:?}" - )) + SidecarError::host( + "ERR_AGENTOS_CAPABILITY_MISSING", + format!("process does not own {key:?}"), + ) })?; lease.validate(generation, kind).map_err(SidecarError::from) } @@ -815,6 +1378,8 @@ mod pending_event_reservation_tests { use agentos_kernel::mount_table::MountTable; use agentos_kernel::permissions::Permissions; use agentos_kernel::vfs::MemoryFileSystem; + use std::future::Future as _; + use std::task::{Context, Poll, Waker}; fn test_runtime_context() -> agentos_runtime::RuntimeContext { agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) @@ -822,6 +1387,185 @@ mod pending_event_reservation_tests { .context() } + fn take_notify_permit(notify: &tokio::sync::Notify) -> bool { + let mut notified = Box::pin(notify.notified()); + let mut context = Context::from_waker(Waker::noop()); + matches!(notified.as_mut().poll(&mut context), Poll::Ready(())) + } + + #[test] + fn startup_signal_is_durable_across_backend_construction() { + use agentos_kernel::process_runtime::ProcessExit; + + let mut config = KernelVmConfig::new("vm-startup-signal-endpoint"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let kernel_handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn kernel process"); + let pid = kernel_handle.pid(); + let notify = Arc::new(tokio::sync::Notify::new()); + let runtime_control = + ActiveProcess::attach_runtime_control_before_start(&kernel_handle, Arc::clone(¬ify)) + .expect("attach runtime endpoint before backend construction"); + + kernel + .kill_process(EXECUTION_DRIVER_NAME, pid, SIGTERM) + .expect("signal process during backend construction"); + assert!(take_notify_permit(¬ify)); + assert!(runtime_control.pending().termination.is_some()); + + let mut process = ActiveProcess::new_with_attached_runtime_control( + pid, + kernel_handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + super::GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + runtime_control, + Arc::clone(¬ify), + ); + process + .apply_runtime_controls() + .expect("apply durable startup signal to constructed backend"); + assert_eq!( + process.pending_runtime_exit, + Some(ProcessExit::Signaled { + signal: SIGTERM, + core_dumped: false, + }) + ); + + process.kernel_handle.finish_signaled(SIGTERM, false); + drop(process); + kernel.waitpid(pid).expect("reap signaled startup process"); + assert!(kernel.list_processes().is_empty()); + } + + #[test] + fn one_slot_rpc_admission_is_typed_and_never_overwrites() { + admit_one_slot_rpc(None, 8, "deferredKernelWaitRpc").expect("empty slot"); + admit_one_slot_rpc(Some(8), 8, "deferredKernelWaitRpc").expect("same call recheck"); + let error = admit_one_slot_rpc(Some(7), 8, "deferredKernelWaitRpc") + .expect_err("different call must not replace the retained waiter"); + assert_eq!(error.code, "EBUSY"); + assert_eq!( + error.details.as_ref().expect("slot details")["pendingCallId"], + 7 + ); + assert_eq!( + error.details.as_ref().expect("slot details")["incomingCallId"], + 8 + ); + } + + #[test] + fn executable_image_snapshot_is_single_bounded_and_releases_accounting() { + let mut config = KernelVmConfig::new("vm-executable-image-snapshot"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let pid = handle.pid(); + let runtime = test_runtime_context(); + let resources = Arc::clone(runtime.resources()); + let baseline = resources.usage(ResourceClass::ExecutorBytes).used; + let mut process = ActiveProcess::new( + pid, + handle, + runtime, + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ); + let retained = resources + .reserve(ResourceClass::ExecutorBytes, 4) + .expect("reserve first image"); + let image_handle = process + .install_executable_image(vec![1, 2, 3, 4], retained) + .expect("install first image"); + assert_eq!( + resources.usage(ResourceClass::ExecutorBytes).used, + baseline + 4 + ); + assert_eq!( + process + .read_executable_image(image_handle, 1, 2) + .expect("bounded image read"), + &[2, 3] + ); + assert_eq!( + process + .read_executable_image(image_handle, 99, usize::MAX) + .expect("read beyond EOF"), + &[] as &[u8] + ); + + let duplicate = resources + .reserve(ResourceClass::ExecutorBytes, 1) + .expect("reserve duplicate attempt"); + let error = process + .install_executable_image(vec![9], duplicate) + .expect_err("a second image must not replace the live snapshot"); + assert_eq!(error.code, "EBUSY"); + assert_eq!( + resources.usage(ResourceClass::ExecutorBytes).used, + baseline + 4, + "rejected image reservation must be released" + ); + let error = process + .close_executable_image(image_handle + 1) + .expect_err("a stale handle must not close the live snapshot"); + assert_eq!(error.code, "ESTALE"); + assert_eq!( + resources.usage(ResourceClass::ExecutorBytes).used, + baseline + 4 + ); + + process + .close_executable_image(image_handle) + .expect("close active image"); + assert_eq!(resources.usage(ResourceClass::ExecutorBytes).used, baseline); + + let retained = resources + .reserve(ResourceClass::ExecutorBytes, 3) + .expect("reserve teardown image"); + process + .install_executable_image(vec![5, 6, 7], retained) + .expect("install teardown image"); + process.kernel_handle.finish(0); + drop(process); + assert_eq!( + resources.usage(ResourceClass::ExecutorBytes).used, + baseline, + "process teardown must release the retained image" + ); + kernel.waitpid(pid).expect("reap process"); + } + #[test] fn checked_out_event_keeps_vm_bytes_reserved_until_requeue_or_consumption() { let event = ActiveExecutionEvent::Stdout(vec![0x5a; 32]); @@ -924,6 +1668,430 @@ mod pending_event_reservation_tests { kernel.waitpid(process.kernel_pid).expect("reap process"); } + #[test] + fn leased_event_transfers_at_exact_vm_cap_without_loss_or_double_charge() { + let event = ActiveExecutionEvent::Stdout(vec![0x51; 32]); + let event_bytes = event.retained_bytes(); + let budget = VmPendingByteBudget::new( + event_bytes, + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + let mut config = KernelVmConfig::new("vm-exact-cap-event-transfer"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let source_handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn source process"); + let target_handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn target process"); + let source_pid = source_handle.pid(); + let target_pid = target_handle.pid(); + let mut source = ActiveProcess::new( + source_pid, + source_handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ) + .with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + event_bytes, + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&budget), + ); + let mut target = ActiveProcess::new( + target_pid, + target_handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ) + .with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + event_bytes, + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&budget), + ); + + source + .queue_pending_execution_event(event) + .expect("source event exactly fills the VM cap"); + let leased = source + .lease_pending_execution_event() + .expect("lease source event"); + target + .try_queue_pending_polled_execution_event(leased) + .expect("the existing reservation transfers at the exact cap"); + assert_eq!( + budget.used(), + event_bytes, + "transfer must not double charge" + ); + assert!(source.pending_execution_events.is_empty()); + + let overflow = PolledExecutionEvent::unreserved(ActiveExecutionEvent::Exited(0)); + let (error, overflow) = target + .try_queue_pending_polled_execution_event(overflow) + .expect_err("one additional retained event must exceed the exact cap"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + assert!(matches!(overflow.event(), ActiveExecutionEvent::Exited(0))); + assert_eq!(budget.used(), event_bytes, "rejection must not leak bytes"); + + assert!(matches!( + target.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == vec![0x51; 32] + )); + assert_eq!( + budget.used(), + 0, + "consumption must release the one reservation" + ); + + let backpressure_budget = VmPendingByteBudget::new( + event_bytes.saturating_mul(2), + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + source = source.with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + event_bytes.saturating_mul(2), + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&backpressure_budget), + ); + target = target.with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + event_bytes.saturating_mul(2), + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&backpressure_budget), + ); + source.pending_execution_event_count_limit = 1; + target.pending_execution_event_count_limit = 1; + target + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![0x41; 32])) + .expect("fill the parent process queue"); + source + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![0x42; 32])) + .expect("fill the remaining VM aggregate bytes in the child"); + assert_eq!(backpressure_budget.used(), event_bytes * 2); + target.child_processes.insert(String::from("child"), source); + while take_notify_permit(&target.process_event_notify) {} + + let child_event = target + .child_processes + .get_mut("child") + .unwrap() + .lease_pending_execution_event() + .expect("lease child output for parent transfer"); + let (error, child_event) = target + .try_queue_pending_polled_execution_event(child_event) + .expect_err("a full parent process queue must backpressure the transfer"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + target + .child_processes + .get_mut("child") + .unwrap() + .queue_pull_owned_polled_execution_event(child_event) + .expect("return the same reservation to the child queue"); + assert_eq!( + backpressure_budget.used(), + event_bytes * 2, + "backpressure and requeue must neither lose nor double-charge bytes" + ); + + assert!(matches!( + target.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == vec![0x41; 32] + )); + assert!( + take_notify_permit(&target.process_event_notify), + "draining a parent with descendants must rearm the global pump" + ); + let child_event = target + .child_processes + .get_mut("child") + .unwrap() + .lease_pending_execution_event() + .expect("the pull-owned child event remains durable"); + target + .try_queue_pending_polled_execution_event(child_event) + .expect("the rearmed transfer succeeds after parent capacity is freed"); + assert_eq!(backpressure_budget.used(), event_bytes); + assert!(matches!( + target.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == vec![0x42; 32] + )); + assert_eq!(backpressure_budget.used(), 0); + source = target + .child_processes + .remove("child") + .expect("recover child for orderly teardown"); + + source.kernel_handle.finish(0); + target.kernel_handle.finish(0); + kernel.waitpid(source_pid).expect("reap source process"); + kernel.waitpid(target_pid).expect("reap target process"); + } + + #[test] + fn duplicate_exit_events_cannot_spin_ahead_of_trailing_output() { + let mut config = KernelVmConfig::new("duplicate-child-exit-ordering"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let mut process = ActiveProcess::new( + handle.pid(), + handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ); + + process + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"late\n".to_vec())) + .expect("queue trailing output"); + process + .queue_pending_execution_event(ActiveExecutionEvent::Exited(0)) + .expect("queue adapter exit"); + process + .queue_pending_execution_event(ActiveExecutionEvent::Common(ExecutionEvent::Exited( + agentos_execution::backend::ExecutionExit::Exited(0), + ))) + .expect("queue runtime-control exit"); + assert!(take_notify_permit(&process.process_event_notify)); + + assert_eq!(process.discard_pending_exit_events(), 2); + let trailing_output = process + .lease_pending_execution_event() + .expect("lease trailing output for its pull owner"); + process + .queue_pull_owned_polled_execution_event(trailing_output) + .expect("return pull-owned output without a global rearm"); + assert!( + !take_notify_permit(&process.process_event_notify), + "returning a pull-owned event must not self-rearm the global pump" + ); + process + .queue_pending_polled_execution_event(PolledExecutionEvent::unreserved( + ActiveExecutionEvent::Exited(0), + )) + .expect("queue one authoritative exit behind output"); + assert!(take_notify_permit(&process.process_event_notify)); + + assert!(matches!( + process.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == b"late\n" + )); + assert!(matches!( + process.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Exited(0)) + )); + assert!(process.pop_pending_execution_event().is_none()); + + process.kernel_handle.finish(0); + kernel.waitpid(process.kernel_pid).expect("reap process"); + } + + #[test] + fn synthetic_runtime_termination_preserves_exact_signal_until_event_emission() { + let mut config = KernelVmConfig::new("exact-synthetic-runtime-exit"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn binding process"); + let mut process = ActiveProcess::new( + handle.pid(), + handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::JavaScript, + ActiveExecution::Binding(BindingExecution::default()), + ); + + kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, SIGTERM) + .expect("request SIGTERM"); + assert_eq!(process.exit_signal, None, "a request is not an exit report"); + + let event = process + .try_poll_execution_event() + .expect("poll runtime control") + .expect("synthetic terminal event") + .into_event(); + assert!(matches!(event, ActiveExecutionEvent::Exited(143))); + assert_eq!(process.exit_signal, Some(SIGTERM)); + assert!(!process.exit_core_dumped); + + process.kernel_handle.finish_signaled(SIGTERM, false); + kernel.waitpid(process.kernel_pid).expect("reap process"); + } + + #[test] + fn stop_and_continue_wait_state_follow_runtime_control_acknowledgement() { + let mut config = KernelVmConfig::new("runtime-control-stop-ack"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn binding process"); + let mut process = ActiveProcess::new( + handle.pid(), + handle, + test_runtime_context(), + crate::limits::VmLimits::default(), + agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, + GuestRuntimeKind::JavaScript, + ActiveExecution::Binding(BindingExecution::default()), + ); + let ActiveExecution::Binding(binding) = &process.execution else { + unreachable!("test process must retain binding execution"); + }; + let paused = Arc::clone(&binding.paused); + let cancelled = Arc::clone(&binding.cancelled); + let pending_events = Arc::clone(&binding.pending_events); + let overflow_reason = Arc::clone(&binding.event_overflow_reason); + let pending_bytes = Arc::clone(&binding.pending_event_bytes); + let count_limit = Arc::clone(&binding.pending_event_count_limit); + let bytes_limit = Arc::clone(&binding.pending_event_bytes_limit); + let event_budget = Arc::clone(&binding.vm_pending_event_bytes_budget); + + kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, libc::SIGTSTP) + .expect("request stop"); + assert_eq!( + kernel + .list_processes() + .get(&process.kernel_pid) + .expect("kernel process") + .status, + agentos_kernel::process_table::ProcessStatus::Running + ); + process + .apply_runtime_controls() + .expect("apply and acknowledge stop"); + assert_eq!( + kernel + .list_processes() + .get(&process.kernel_pid) + .expect("kernel process") + .status, + agentos_kernel::process_table::ProcessStatus::Stopped + ); + assert!(process.runtime_control.pending().is_empty()); + assert!(paused.load(Ordering::Acquire)); + assert!(send_binding_process_event( + &cancelled, + &pending_events, + &overflow_reason, + &pending_bytes, + &count_limit, + &bytes_limit, + &event_budget, + ActiveExecutionEvent::Stdout(b"paused-output".to_vec()), + )); + assert!(process + .try_poll_execution_event() + .expect("poll stopped binding") + .is_none()); + + kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, libc::SIGCONT) + .expect("request continue"); + assert_eq!( + kernel + .list_processes() + .get(&process.kernel_pid) + .expect("kernel process") + .status, + agentos_kernel::process_table::ProcessStatus::Stopped + ); + process + .apply_runtime_controls() + .expect("apply and acknowledge continue"); + assert_eq!( + kernel + .list_processes() + .get(&process.kernel_pid) + .expect("kernel process") + .status, + agentos_kernel::process_table::ProcessStatus::Running + ); + assert!(process.runtime_control.pending().is_empty()); + assert!(!paused.load(Ordering::Acquire)); + let event = process + .try_poll_execution_event() + .expect("poll resumed binding") + .expect("queued binding event after resume") + .into_event(); + assert!(matches!( + event, + ActiveExecutionEvent::Stdout(bytes) if bytes == b"paused-output" + )); + + process.kernel_handle.finish(0); + kernel.waitpid(process.kernel_pid).expect("reap process"); + } + #[test] fn root_binding_signal_state_drain_preserves_output_and_exit_events() { let event_budget = VmPendingByteBudget::new( @@ -1179,11 +2347,31 @@ impl BindingExecution { debug_assert!(self .pending_events .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("binding pending-event queue") .is_empty()); self.vm_pending_event_bytes_budget = budget; self } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn with_descendant_wait_ownership( + mut self, + ownership: DescendantWaitOwnership, + ) -> Self { + self.descendant_wait_ownership = ownership; + self + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn with_descendant_output_ownership( + mut self, + ownership: DescendantOutputOwnership, + ) -> Self { + self.descendant_output_ownership = ownership; + self + } } impl Drop for BindingExecution { @@ -1192,10 +2380,16 @@ impl Drop for BindingExecution { // producer checks this flag while holding the same queue lock, so it // cannot enqueue after the retained-byte total is released here. self.cancelled.store(true, Ordering::Release); - let mut pending_events = self - .pending_events - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + self.pause_notify.notify_waiters(); + let mut pending_events = match self.pending_events.lock() { + Ok(pending_events) => pending_events, + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_QUEUE_POISONED: recovering the binding event queue while releasing reservations" + ); + poisoned.into_inner() + } + }; pending_events.clear(); let pending_bytes = self.pending_event_bytes.swap(0, Ordering::AcqRel); self.vm_pending_event_bytes_budget.release(pending_bytes); @@ -1217,7 +2411,15 @@ pub(super) fn add_live_host_net_transfer_descriptions( registry: &HostNetTransferDescriptionRegistry, descriptions: &mut BTreeMap, ) { - let mut transfers = registry.lock().unwrap_or_else(|error| error.into_inner()); + let mut transfers = match registry.lock() { + Ok(transfers) => transfers, + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_HOST_NET_TRANSFER_REGISTRY_POISONED: recovering the transfer registry during resource accounting" + ); + poisoned.into_inner() + } + }; transfers.retain(|description_id, transfer| { let alive = transfer.handles.upgrade().is_some(); if alive { @@ -1251,12 +2453,18 @@ pub(super) fn rebind_process_runtime_event_targets( process: &mut ActiveProcess, kernel_readiness: &KernelSocketReadinessRegistry, ) { - let session = process.execution.javascript_v8_session_handle(); + let session = process + .execution + .execution_wake_handle(process.kernel_handle.runtime_identity()); for (socket_id, socket) in &process.tcp_sockets { let key = NativeCapabilityKey::TcpSocket(socket_id.clone()); let identity = process.capability_readiness_identity(&key); - socket.set_event_pusher(session.clone(), identity); + socket.set_event_pusher( + session.clone(), + identity, + Arc::clone(&process.process_event_notify), + ); register_kernel_readiness_target( kernel_readiness, socket.kernel_socket_id, @@ -1269,7 +2477,11 @@ pub(super) fn rebind_process_runtime_event_targets( } for (socket_id, socket) in &process.unix_sockets { let key = NativeCapabilityKey::UnixSocket(socket_id.clone()); - socket.set_event_pusher(session.clone(), process.capability_readiness_identity(&key)); + socket.set_event_pusher( + session.clone(), + process.capability_readiness_identity(&key), + Arc::clone(&process.process_event_notify), + ); } for (listener_id, listener) in &process.tcp_listeners { let key = NativeCapabilityKey::TcpListener(listener_id.clone()); @@ -1285,12 +2497,20 @@ pub(super) fn rebind_process_runtime_event_targets( } for (listener_id, listener) in &process.unix_listeners { let key = NativeCapabilityKey::UnixListener(listener_id.clone()); - listener.set_event_pusher(session.clone(), process.capability_readiness_identity(&key)); + listener.set_event_pusher( + session.clone(), + process.capability_readiness_identity(&key), + Arc::clone(&process.process_event_notify), + ); } for (socket_id, socket) in &process.udp_sockets { let key = NativeCapabilityKey::UdpSocket(socket_id.clone()); let identity = process.capability_readiness_identity(&key); - socket.set_event_pusher(session.clone(), identity); + socket.set_event_pusher( + session.clone(), + identity, + Arc::clone(&process.process_event_notify), + ); register_kernel_readiness_target( kernel_readiness, socket.kernel_socket_id, @@ -1336,16 +2556,26 @@ pub(super) fn discard_replaced_image_pending_events(process: &mut ActiveProcess) impl ActiveExecutionEvent { pub(crate) fn retained_bytes(&self) -> usize { match self { + Self::Common(ExecutionEvent::Output { bytes, .. }) => { + std::mem::size_of::().saturating_add(bytes.len()) + } + Self::Common(ExecutionEvent::HostCall { .. }) + | Self::Common(ExecutionEvent::Exited(_)) => 4 * 1024, + Self::Common(ExecutionEvent::Warning(error)) + | Self::Common(ExecutionEvent::RuntimeFault(error)) => { + std::mem::size_of::().saturating_add(error.encoded_bytes()) + } + Self::Common(_) => 4 * 1024, Self::Stdout(bytes) | Self::Stderr(bytes) => { std::mem::size_of::().saturating_add(bytes.len()) } // Internal RPC events are serviced eagerly rather than retained; // account a conservative fixed envelope if briefly deferred. The // wire payload is independently frame-bounded. - Self::JavascriptSyncRpcRequest(_) - | Self::JavascriptSyncRpcCompletion(_) - | Self::PythonVfsRpcRequest(_) - | Self::PythonSocketConnectCompletion(_) => 4 * 1024, + Self::HostRpcRequest(_) + | Self::HostCallCompletion(_) + | Self::ManagedStreamReadRecheck(_) + | Self::ManagedUdpPollRecheck(_) => 4 * 1024, Self::SignalState { .. } | Self::Exited(_) => std::mem::size_of::(), } } @@ -1372,10 +2602,18 @@ fn poll_binding_process_event( fn poll_binding_process_event_leased( execution: &BindingExecution, ) -> Result, SidecarError> { + if execution.paused.load(Ordering::Acquire) && !execution.cancelled.load(Ordering::Acquire) { + return Ok(None); + } let event = execution .pending_events .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .map_err(|_| { + SidecarError::host( + "EIO", + "ERR_AGENTOS_BINDING_EVENT_QUEUE_POISONED: binding event queue was poisoned by a prior panic", + ) + })? .pop_front(); if let Some(event) = event { let event_bytes = event.retained_bytes(); @@ -1393,10 +2631,15 @@ fn poll_binding_process_event_leased( if let Some(reason) = execution .event_overflow_reason .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .map_err(|_| { + SidecarError::host( + "EIO", + "ERR_AGENTOS_BINDING_OVERFLOW_STATE_POISONED: binding overflow state was poisoned by a prior panic", + ) + })? .clone() { - return Err(SidecarError::InvalidState(reason)); + return Err(SidecarError::Host(reason)); } Ok(None) } @@ -1421,509 +2664,873 @@ pub(super) fn poll_child_execution_after_exit( ) -> Result, SidecarError> { match child.try_poll_execution_event() { Ok(event) => Ok(event), - Err(SidecarError::Execution(message)) - if child.runtime == GuestRuntimeKind::WebAssembly - && message == WasmExecutionError::EventChannelClosed.to_string() => - { - Ok(None) - } + Err(SidecarError::ExecutionEventChannelClosed { .. }) => Ok(None), Err(error) => Err(error), } } +impl ExecutionBackend for BindingExecution { + fn kind(&self) -> ExecutionBackendKind { + ExecutionBackendKind::Binding + } + + fn descendant_wait_ownership(&self) -> DescendantWaitOwnership { + self.descendant_wait_ownership + } + + fn descendant_output_ownership(&self) -> DescendantOutputOwnership { + self.descendant_output_ownership + } + + fn configure_host_services(&mut self, host: ProcessHostCapabilitySet) { + self.host_capabilities = Some(host); + } + + fn is_prepared_for_start(&self) -> bool { + false + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + Err(HostServiceError::new( + "ERR_AGENTOS_EXECUTION_NOT_PREPARED", + "binding execution cannot be a prepared execve image", + )) + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + self.cancelled.store(true, Ordering::Release); + self.pause_notify.notify_waiters(); + self.event_notify.notify_one(); + Ok(match reason { + ShutdownReason::Signal(signal) => ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal, + core_dumped: false, + }), + ShutdownReason::RuntimeFault => ShutdownOutcome::Exited(ExecutionExit::Exited(1)), + ShutdownReason::Deadline | ShutdownReason::VmTeardown | ShutdownReason::HostRequest => { + ShutdownOutcome::Exited(ExecutionExit::Exited(137)) + } + ShutdownReason::Completed => ShutdownOutcome::Exited(ExecutionExit::Exited(0)), + }) + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + self.paused.store(paused, Ordering::Release); + if !paused { + self.pause_notify.notify_waiters(); + self.event_notify.notify_one(); + } + Ok(()) + } + + fn write_stdin(&mut self, _bytes: &[u8]) -> Result<(), HostServiceError> { + Ok(()) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + Ok(()) + } + + fn deliver_signal_checkpoint( + &self, + _identity: ExecutionWakeIdentity, + _signal: i32, + _delivery_token: u64, + _flags: u32, + ) -> Result { + Ok(SignalCheckpointOutcome::Unsupported) + } +} + impl ActiveExecution { - pub(crate) fn is_prepared_for_start(&self) -> bool { + fn backend(&self) -> &dyn ExecutionBackend { match self { - Self::Javascript(execution) => execution.is_prepared_for_start(), - Self::Python(execution) => execution.is_prepared_for_start(), - Self::Wasm(execution) => execution.is_prepared_for_start(), - Self::Binding(_) => false, + Self::Javascript(execution) => execution, + Self::Python(execution) => execution, + Self::Wasm(execution) => execution.as_ref(), + Self::Binding(execution) => execution, } } - pub(crate) fn start_prepared(&mut self) -> Result<(), SidecarError> { + fn backend_mut(&mut self) -> &mut dyn ExecutionBackend { match self { - Self::Javascript(execution) => execution - .start_prepared() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .start_prepared() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .start_prepared() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Err(SidecarError::InvalidState(String::from( - "binding execution cannot be a prepared execve image", - ))), + Self::Javascript(execution) => execution, + Self::Python(execution) => execution, + Self::Wasm(execution) => execution.as_mut(), + Self::Binding(execution) => execution, } } - pub(crate) fn python_vfs_rpc_responder(&self) -> Result { + /// Poll an adapter-owned queue whose retained-byte reservation cannot be + /// represented by the generic backend event alone. `None` means the + /// adapter uses the common backend/event channel; `Some` owns the complete + /// poll result, including an empty queue. + fn poll_adapter_event_leased( + &mut self, + ) -> Option, SidecarError>> { match self { - Self::Python(execution) => Ok(execution.vfs_rpc_responder()), - _ => Err(SidecarError::InvalidState(String::from( - "only Python executions expose a Python VFS RPC responder", - ))), + Self::Binding(execution) => Some(poll_binding_process_event_leased(execution)), + Self::Javascript(_) | Self::Python(_) | Self::Wasm(_) => None, } } - pub(crate) fn claim_javascript_sync_rpc_response( - &mut self, - id: u64, - ) -> Result { + fn configure_adapter_event_limits(&self, count: usize, bytes: usize) { + if let Self::Binding(execution) = self { + execution + .pending_event_count_limit + .store(count, Ordering::Release); + execution + .pending_event_bytes_limit + .store(bytes, Ordering::Release); + } + } + + fn adapter_event_bytes_budget(&self) -> Option> { match self { - Self::Javascript(execution) => execution - .claim_sync_rpc_response(id) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .claim_javascript_sync_rpc_response(id) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .claim_sync_rpc_response(id) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Err(SidecarError::InvalidState(String::from( - "binding executions cannot claim JavaScript sync RPC responses", - ))), + Self::Binding(execution) => Some(Arc::clone(&execution.vm_pending_event_bytes_budget)), + Self::Javascript(_) | Self::Python(_) | Self::Wasm(_) => None, } } - pub(crate) fn respond_claimed_javascript_sync_rpc_success( - &mut self, - id: u64, - result: Value, + fn bind_adapter_event_bytes_budget(&mut self, budget: Arc) { + if let Self::Binding(execution) = self { + if !Arc::ptr_eq(&execution.vm_pending_event_bytes_budget, &budget) { + debug_assert_eq!(execution.pending_event_bytes.load(Ordering::Acquire), 0); + execution.vm_pending_event_bytes_budget = budget; + } + } + } + + pub(crate) fn is_prepared_for_start(&self) -> bool { + ExecutionBackend::is_prepared_for_start(self) + } + + pub(crate) fn start_prepared(&mut self) -> Result<(), SidecarError> { + ExecutionBackend::start_prepared(self).map_err(SidecarError::from) + } + + pub(crate) fn native_process_id(&self) -> Option { + ExecutionBackend::native_process_id(self) + } + + pub(crate) fn has_exited(&self) -> bool { + matches!(self, Self::Javascript(execution) if execution.has_exited()) + } + + pub(crate) fn send_javascript_stream_event( + &self, + event_type: &str, + payload: Value, ) -> Result<(), SidecarError> { match self { Self::Javascript(execution) => execution - .respond_claimed_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_claimed_javascript_sync_rpc_success(id, result) + .send_stream_event(event_type, payload) .map_err(|error| SidecarError::Execution(error.to_string())), Self::Wasm(execution) => execution - .respond_claimed_sync_rpc_success(id, result) + .send_stream_event(event_type, payload) .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Err(SidecarError::InvalidState(String::from( - "binding executions cannot service claimed JavaScript sync RPC responses", + _ => Err(SidecarError::InvalidState(String::from( + "only embedded V8 executions can receive JavaScript stream events", ))), } } - pub(crate) fn respond_claimed_javascript_sync_rpc_error( + pub(crate) fn execution_wake_handle( + &self, + identity: ProcessRuntimeIdentity, + ) -> Option { + ExecutionBackend::wake_handle( + self, + ExecutionWakeIdentity { + generation: identity.generation, + pid: identity.pid, + }, + ) + } + + pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> { + self.begin_shutdown(ShutdownReason::HostRequest).map(|_| ()) + } + + pub(crate) fn begin_shutdown( &mut self, - id: u64, - code: impl Into, - message: impl Into, - ) -> Result<(), SidecarError> { - let code = code.into(); - let message = message.into(); + reason: ShutdownReason, + ) -> Result { + ExecutionBackend::begin_shutdown(self, reason).map_err(SidecarError::from) + } + + pub(crate) fn pause(&self) -> Result<(), SidecarError> { + ExecutionBackend::set_paused(self, true).map_err(SidecarError::from) + } + + pub(crate) fn resume(&self) -> Result<(), SidecarError> { + ExecutionBackend::set_paused(self, false).map_err(SidecarError::from) + } + + pub(crate) fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result { + ExecutionBackend::deliver_signal_checkpoint(self, identity, signal, delivery_token, flags) + .map_err(SidecarError::from) + } + + // Source-included integration tests exercise the adapter without a host + // capability set. Production event pumps always use `poll_event_with_host`. + #[allow(dead_code)] + pub(crate) async fn poll_event( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + timeout: Duration, + ) -> Result, SidecarError> { + self.poll_event_inner(identity, max_reply_bytes, timeout, None) + .await + } + + pub(crate) async fn poll_event_with_host( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + timeout: Duration, + host: &ProcessHostCapabilitySet, + ) -> Result, SidecarError> { + self.poll_event_inner(identity, max_reply_bytes, timeout, Some(host)) + .await + } + + async fn poll_event_inner( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + timeout: Duration, + host: Option<&ProcessHostCapabilitySet>, + ) -> Result, SidecarError> { match self { - Self::Javascript(execution) => execution - .respond_claimed_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_claimed_javascript_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_claimed_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Err(SidecarError::InvalidState(String::from( - "binding executions cannot service claimed JavaScript sync RPC errors", - ))), + Self::Javascript(execution) => { + let responder = execution.sync_rpc_responder(); + let event = execution + .poll_event(timeout) + .await + .map_err(javascript_error)?; + match event { + Some(event) => map_javascript_execution_event_with_host( + event, + responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Python(execution) => { + let responder = execution.javascript_sync_rpc_responder(); + let python_responder = execution.vfs_rpc_responder(); + let event = execution.poll_event(timeout).await.map_err(python_error)?; + match event { + Some(event) => map_python_execution_event_with_host( + event, + responder, + python_responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Wasm(execution) => { + let responder = execution.sync_rpc_responder(); + let event = execution.poll_event(timeout).await.map_err(wasm_error)?; + match event { + Some(event) => map_wasm_execution_event_with_host( + event, + responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Binding(execution) => { + let _ = timeout; + poll_binding_process_event(execution) + } + } + } + + /// Probe the runtime event queue once without parking the sidecar thread or + /// registering a waker outside the coalesced process-event broker. + pub(crate) fn try_poll_event( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + ) -> Result, SidecarError> { + self.try_poll_event_inner(identity, max_reply_bytes, None) + } + + pub(crate) fn try_poll_event_with_host( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + host: &ProcessHostCapabilitySet, + ) -> Result, SidecarError> { + self.try_poll_event_inner(identity, max_reply_bytes, Some(host)) + } + + fn try_poll_event_inner( + &mut self, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + host: Option<&ProcessHostCapabilitySet>, + ) -> Result, SidecarError> { + match self { + Self::Javascript(execution) => { + let responder = execution.sync_rpc_responder(); + let event = execution.try_poll_event().map_err(javascript_error)?; + match event { + Some(event) => map_javascript_execution_event_with_host( + event, + responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Python(execution) => { + let responder = execution.javascript_sync_rpc_responder(); + let python_responder = execution.vfs_rpc_responder(); + let event = execution.try_poll_event().map_err(python_error)?; + match event { + Some(event) => map_python_execution_event_with_host( + event, + responder, + python_responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Wasm(execution) => { + let responder = execution.sync_rpc_responder(); + let event = execution.try_poll_event().map_err(wasm_error)?; + match event { + Some(event) => map_wasm_execution_event_with_host( + event, + responder, + identity, + max_reply_bytes, + host, + ), + None => Ok(None), + } + } + Self::Binding(execution) => poll_binding_process_event(execution), } } +} + +impl ExecutionBackend for ActiveExecution { + fn kind(&self) -> ExecutionBackendKind { + self.backend().kind() + } + + fn synchronous_fd_write_policy(&self) -> agentos_execution::backend::SynchronousFdWritePolicy { + self.backend().synchronous_fd_write_policy() + } + + fn descendant_wait_ownership(&self) -> DescendantWaitOwnership { + self.backend().descendant_wait_ownership() + } + + fn descendant_output_ownership(&self) -> DescendantOutputOwnership { + self.backend().descendant_output_ownership() + } + + fn native_process_id(&self) -> Option { + self.backend().native_process_id() + } + + fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { + self.backend().wake_handle(identity) + } + + fn configure_host_services(&mut self, host: ProcessHostCapabilitySet) { + self.backend_mut().configure_host_services(host) + } + + fn is_prepared_for_start(&self) -> bool { + self.backend().is_prepared_for_start() + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + self.backend_mut().start_prepared() + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + self.backend_mut().begin_shutdown(reason) + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + self.backend().set_paused(paused) + } + + fn write_stdin(&mut self, bytes: &[u8]) -> Result<(), HostServiceError> { + self.backend_mut().write_stdin(bytes) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + self.backend_mut().close_stdin() + } + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result { + self.backend() + .deliver_signal_checkpoint(identity, signal, delivery_token, flags) + } + + fn take_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + self.backend().take_signal_checkpoint(identity) + } + + fn discard_signal_checkpoints( + &self, + identity: ExecutionWakeIdentity, + ) -> Result<(), HostServiceError> { + self.backend().discard_signal_checkpoints(identity) + } +} + +pub(super) fn discard_exec_signal_state(process: &mut ActiveProcess) { + let identity = process.kernel_handle.runtime_identity(); + if let Err(error) = process + .execution + .discard_signal_checkpoints(ExecutionWakeIdentity { + generation: identity.generation, + pid: identity.pid, + }) + { + eprintln!("ERR_AGENTOS_EXEC_SIGNAL_CHECKPOINT_DISCARD: {error}"); + } + process.guest_signal_checkpoint_pending = false; +} + +#[cfg(test)] +mod execution_backend_lifecycle_tests { + use super::*; + + fn assert_execution_backend() {} + + #[test] + fn binding_and_active_execution_use_the_common_lifecycle_contract() { + assert_execution_backend::(); + assert_execution_backend::(); + + let binding = BindingExecution::default(); + let cancelled = Arc::clone(&binding.cancelled); + let mut execution = ActiveExecution::Binding(binding); + + let process = HostProcessContext { + generation: 11, + pid: 29, + }; + let (events, _receiver) = bounded_execution_event_channel( + process, + 1, + PayloadLimit::new("limits.process.pendingEventBytes", 1024).expect("byte limit"), + Arc::new(|| {}), + ) + .expect("host event lane"); + ExecutionBackend::configure_host_services( + &mut execution, + ProcessHostCapabilitySet::from_event_submission(events), + ); + let ActiveExecution::Binding(binding) = &execution else { + unreachable!("binding execution") + }; + assert_eq!( + binding + .host_capabilities + .as_ref() + .expect("backend received host services") + .process(), + process + ); - pub(crate) fn uses_shared_v8_runtime(&self) -> bool { - match self { - Self::Javascript(execution) => execution.uses_shared_v8_runtime(), - Self::Python(execution) => execution.uses_shared_v8_runtime(), - Self::Wasm(execution) => execution.uses_shared_v8_runtime(), - Self::Binding(_) => false, - } - } + assert_eq!( + ExecutionBackend::kind(&execution), + ExecutionBackendKind::Binding + ); + assert!(!execution.is_prepared_for_start()); - pub(crate) fn has_exited(&self) -> bool { - matches!(self, Self::Javascript(execution) if execution.has_exited()) - } + let error = ExecutionBackend::start_prepared(&mut execution) + .expect_err("binding adapters are never prepared exec images"); + assert_eq!(error.code, "ERR_AGENTOS_EXECUTION_NOT_PREPARED"); - pub(crate) fn child_pid(&self) -> u32 { - match self { - Self::Javascript(execution) => execution.child_pid(), - Self::Python(execution) => execution.child_pid(), - Self::Wasm(execution) => execution.child_pid(), - Self::Binding(_) => 0, - } - } + let outcome = ExecutionBackend::begin_shutdown(&mut execution, ShutdownReason::VmTeardown) + .expect("binding shutdown uses the shared lifecycle"); + assert_eq!(outcome, ShutdownOutcome::Exited(ExecutionExit::Exited(137))); + assert!(cancelled.load(Ordering::Acquire)); - pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .write_stdin(chunk) - .map_err(|error| SidecarError::Execution(error.to_string())), - // Sidecar Python and WASM read fd 0 from the sidecar kernel pipe. - // Their in-process stdin bridges are bypassed in this mode, so - // duplicating input into those bridges only fills an unread buffer. - Self::Python(_) | Self::Wasm(_) => Ok(()), - Self::Binding(_) => Ok(()), - } + let outcome = ExecutionBackend::begin_shutdown(&mut execution, ShutdownReason::Signal(15)) + .expect("binding signal shutdown uses the shared lifecycle"); + assert_eq!( + outcome, + ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal: 15, + core_dumped: false, + }) + ); } +} - pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Ok(()), - } - } +fn execution_host_call( + request: HostRpcRequest, + responder: JavascriptSyncRpcResponder, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, +) -> Result { + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: identity.generation, + pid: identity.pid, + call_id: request.id, + }, + Arc::new(responder), + max_reply_bytes, + ) + .map_err(|error| SidecarError::InvalidState(error.to_string()))?; + Ok(ExecutionHostCall { request, reply }) +} - pub(crate) fn respond_python_vfs_rpc_success( - &mut self, - id: u64, - payload: PythonVfsRpcResponsePayload, - ) -> Result<(), SidecarError> { - match self { - Self::Python(execution) => execution - .respond_vfs_rpc_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only Python executions can service Python VFS RPC responses", - ))), - } - } +pub(crate) fn settle_execution_host_call( + reply: &DirectHostReplyHandle, + response: Result, +) -> Result<(), SidecarError> { + let result = match response { + Ok(HostServiceResponse::Json(value)) => reply.succeed(HostCallReply::Json(value)), + Ok(HostServiceResponse::Raw(payload)) => reply.succeed(HostCallReply::Raw(payload)), + Ok(HostServiceResponse::SourceBackedJson { + value, + source_reservations, + }) => reply.succeed_retained(HostCallReply::Json(value), source_reservations), + Ok(HostServiceResponse::SourceBackedRaw { + payload, + source_reservations, + }) => reply.succeed_retained(HostCallReply::Raw(payload), source_reservations), + Ok(HostServiceResponse::Deferred { .. }) => Err(HostServiceError::new( + "EINVAL", + "deferred response must be awaited before direct settlement", + )), + Err(error) => reply.fail(host_service_error(&error)), + }; + result.map_err(SidecarError::from) +} - pub(crate) fn respond_python_vfs_rpc_error( - &mut self, - id: u64, - code: impl Into, - message: impl Into, - ) -> Result<(), SidecarError> { - match self { - Self::Python(execution) => execution - .respond_vfs_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only Python executions can service Python VFS RPC responses", - ))), - } - } +#[cfg(test)] +mod typed_direct_error_tests { + use super::*; + use agentos_execution::backend::{ + DirectHostReplyTarget, HostCallIdentity, HostCallReply, HostServiceError, + }; + use std::sync::{Arc, Mutex}; - pub(crate) fn send_javascript_stream_event( - &self, - event_type: &str, - payload: Value, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .send_stream_event(event_type, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .send_stream_event(event_type, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only embedded V8 executions can receive JavaScript stream events", - ))), - } + #[derive(Default)] + struct RecordingTarget { + replies: Mutex>>, } - pub(crate) fn javascript_v8_session_handle(&self) -> Option { - match self { - Self::Javascript(execution) => Some(execution.v8_session_handle()), - Self::Wasm(execution) => Some(execution.v8_session_handle()), - _ => None, + impl DirectHostReplyTarget for RecordingTarget { + fn claim(&self, _call_id: u64) -> Result { + Ok(true) } - } - pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .terminate() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .kill() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .terminate() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Ok(()), + fn respond( + &self, + _call_id: u64, + _claimed: bool, + result: Result, + ) -> Result<(), HostServiceError> { + self.replies.lock().expect("reply lock").push(result); + Ok(()) } } - pub(crate) fn pause(&self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .pause() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .pause() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .pause() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Ok(()), - } + fn reply(target: Arc, call_id: u64) -> DirectHostReplyHandle { + DirectHostReplyHandle::new( + HostCallIdentity { + generation: 3, + pid: 41, + call_id, + }, + target, + 64 * 1024, + ) + .expect("direct reply") } - pub(crate) fn resume(&self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .resume() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .resume() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .resume() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(_) => Ok(()), - } - } + #[test] + fn direct_settlement_preserves_limit_details_and_typed_errno() { + let target = Arc::new(RecordingTarget::default()); + let limit = SidecarError::ResourceLimit(agentos_runtime::accounting::LimitError { + scope: String::from("vm=vm-typed-errors"), + resource: agentos_runtime::accounting::ResourceClass::BridgeResponseBytes, + used: 60, + requested: 8, + limit: 64, + config_path: String::from("runtime.resources.maxBridgeResponseBytes"), + }); + let deferred = crate::state::DeferredRpcError::from(host_service_error(&limit)); + settle_execution_host_call( + &reply(Arc::clone(&target), 1), + Err(SidecarError::from(deferred)), + ) + .expect("settle deferred limit error"); - pub(crate) fn respond_javascript_sync_rpc_success( - &mut self, - id: u64, - result: Value, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_javascript_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", - ))), - } + let denied = HostServiceError::new( + "EACCES", + "permission denied; diagnostic mentions ENOENT but must not change the code", + ) + .with_details(serde_json::json!({ "path": "/private/config" })); + settle_execution_host_call( + &reply(Arc::clone(&target), 2), + Err(SidecarError::Host(denied)), + ) + .expect("settle permission error"); + + let replies = target.replies.lock().expect("reply lock"); + let limit = replies[0].as_ref().expect_err("limit error response"); + assert_eq!(limit.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + let details = limit.details.as_ref().expect("limit details"); + assert_eq!(details["limitName"], "bridgeResponseBytes"); + assert_eq!(details["limit"], 64); + assert_eq!(details["observed"], 68); + assert_eq!( + details["configPath"], + "runtime.resources.maxBridgeResponseBytes" + ); + + let denied = replies[1].as_ref().expect_err("permission error response"); + assert_eq!(denied.code, "EACCES"); + assert_eq!( + denied.details.as_ref().expect("errno details")["path"], + "/private/config" + ); } +} - pub(crate) fn respond_javascript_sync_rpc_raw_success( - &mut self, - id: u64, - payload: Vec, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_raw_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_raw_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only embedded V8 executions can service raw JavaScript sync RPC responses", - ))), +fn map_javascript_execution_event_with_host( + event: JavascriptExecutionEvent, + responder: JavascriptSyncRpcResponder, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + host: Option<&ProcessHostCapabilitySet>, +) -> Result, SidecarError> { + let event = match event { + JavascriptExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + JavascriptExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + JavascriptExecutionEvent::SyncRpcRequest(request) => { + return route_compatibility_host_call( + execution_host_call(request, responder, identity, max_reply_bytes)?, + false, + max_reply_bytes, + host, + ); } - } + JavascriptExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_execution_signal_registration(registration), + }, + JavascriptExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }; + Ok(Some(event)) +} - pub(crate) fn respond_javascript_sync_rpc_response( - &mut self, - id: u64, - response: JavascriptSyncRpcServiceResponse, - ) -> Result<(), SidecarError> { - match response { - JavascriptSyncRpcServiceResponse::Json(result) => { - self.respond_javascript_sync_rpc_success(id, result) - } - JavascriptSyncRpcServiceResponse::Raw(payload) => { - self.respond_javascript_sync_rpc_raw_success(id, payload) - } - JavascriptSyncRpcServiceResponse::Deferred { .. } => Err(SidecarError::InvalidState( - String::from("deferred response must be awaited by the sidecar dispatcher"), - )), - JavascriptSyncRpcServiceResponse::SourceBackedJson { - value, - source_reservations, - } => { - let result = self.respond_javascript_sync_rpc_success(id, value); - drop(source_reservations); - result - } - JavascriptSyncRpcServiceResponse::SourceBackedRaw { - payload, - source_reservations, - } => { - let result = self.respond_javascript_sync_rpc_raw_success(id, payload); - drop(source_reservations); - result +fn map_python_execution_event_with_host( + event: PythonExecutionEvent, + responder: JavascriptSyncRpcResponder, + python_responder: PythonVfsRpcResponder, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + host: Option<&ProcessHostCapabilitySet>, +) -> Result, SidecarError> { + let event = match event { + PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + PythonExecutionEvent::HostRpcRequest(request) => { + return route_compatibility_host_call( + execution_host_call(request, responder, identity, max_reply_bytes)?, + false, + max_reply_bytes, + host, + ); + } + PythonExecutionEvent::VfsRpcRequest(request) => { + let Some(host) = host else { + python_responder + .respond_host_error( + request.id, + HostServiceError::new( + "ENOTSUP", + "Python host capabilities are unavailable", + ) + .with_details(json!({ + "generation": identity.generation, + "pid": identity.pid, + })), + ) + .map_err(python_error)?; + return Ok(None); + }; + let admission = match host.admit_json_request(&*request, 0) { + Ok(admission) => admission, + Err(error) => { + python_responder + .respond_host_error(request.id, error) + .map_err(python_error)?; + return Ok(None); + } + }; + let call_id = request.id; + let Some(call) = (match python_responder.try_host_call( + *request, + HostCallIdentity { + generation: identity.generation, + pid: identity.pid, + call_id, + }, + max_reply_bytes, + max_reply_bytes, + ) { + Ok(call) => call, + Err(error) => { + python_responder + .respond_host_error(call_id, error) + .map_err(python_error)?; + return Ok(None); + } + }) else { + return Err(SidecarError::host( + "ENOSYS", + "Python request was not converted to a common host operation", + )); + }; + if let Err(error) = host.submit(call.operation, call.reply.clone(), admission) { + call.reply.fail(error).map_err(SidecarError::from)?; } + return Ok(None); } - } + PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }; + Ok(Some(event)) +} - pub(crate) fn respond_javascript_sync_rpc_error( - &mut self, - id: u64, - code: impl Into, - message: impl Into, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_javascript_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", - ))), +fn map_wasm_execution_event_with_host( + event: WasmExecutionEvent, + responder: JavascriptSyncRpcResponder, + identity: ProcessRuntimeIdentity, + max_reply_bytes: usize, + host: Option<&ProcessHostCapabilitySet>, +) -> Result, SidecarError> { + let event = match event { + WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + WasmExecutionEvent::SyncRpcRequest(request) => { + return route_compatibility_host_call( + execution_host_call(request, responder, identity, max_reply_bytes)?, + true, + max_reply_bytes, + host, + ); } - } + WasmExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_execution_signal_registration(registration), + }, + WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }; + Ok(Some(event)) +} - pub(crate) async fn poll_event( - &mut self, - timeout: Duration, - ) -> Result, SidecarError> { - match self { - Self::Javascript(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - JavascriptExecutionEvent::Stdout(chunk) => { - ActiveExecutionEvent::Stdout(chunk) - } - JavascriptExecutionEvent::Stderr(chunk) => { - ActiveExecutionEvent::Stderr(chunk) - } - JavascriptExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - JavascriptExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_node_signal_registration(registration), - }, - JavascriptExecutionEvent::Exited(code) => { - ActiveExecutionEvent::Exited(code) - } - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - PythonExecutionEvent::VfsRpcRequest(request) => { - ActiveExecutionEvent::PythonVfsRpcRequest(request) - } - PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - WasmExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - WasmExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_wasm_signal_registration(registration), - }, - WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(execution) => { - let _ = timeout; - poll_binding_process_event(execution) - } - } +fn route_compatibility_host_call( + call: ExecutionHostCall, + full_filesystem: bool, + max_reply_bytes: usize, + host: Option<&ProcessHostCapabilitySet>, +) -> Result, SidecarError> { + #[derive(Serialize)] + struct CompatibilityRequestCharge<'a> { + method: &'a str, + args: &'a [Value], } - /// Probe the runtime event queue once without parking the sidecar thread or - /// registering a waker outside the coalesced process-event broker. - pub(crate) fn try_poll_event(&mut self) -> Result, SidecarError> { - match self { - Self::Javascript(execution) => execution - .try_poll_event() - .map(|event| { - event.map(|event| match event { - JavascriptExecutionEvent::Stdout(chunk) => { - ActiveExecutionEvent::Stdout(chunk) - } - JavascriptExecutionEvent::Stderr(chunk) => { - ActiveExecutionEvent::Stderr(chunk) - } - JavascriptExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - JavascriptExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_node_signal_registration(registration), - }, - JavascriptExecutionEvent::Exited(code) => { - ActiveExecutionEvent::Exited(code) - } - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .try_poll_event() - .map(|event| { - event.map(|event| match event { - PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - PythonExecutionEvent::VfsRpcRequest(request) => { - ActiveExecutionEvent::PythonVfsRpcRequest(request) - } - PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .try_poll_event() - .map(|event| { - event.map(|event| match event { - WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - WasmExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - WasmExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_wasm_signal_registration(registration), - }, - WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Binding(execution) => poll_binding_process_event(execution), + let admission = host + .map(|host| { + let raw_bytes = call + .request + .raw_bytes_args + .values() + .try_fold(0usize, |total, bytes| total.checked_add(bytes.len())) + .ok_or_else(|| { + HostServiceError::new( + "EOVERFLOW", + "compatibility host-call raw payload size overflowed", + ) + })?; + host.admit_json_request( + &CompatibilityRequestCharge { + method: &call.request.method, + args: &call.request.args, + }, + raw_bytes, + ) + }) + .transpose() + .map_err(SidecarError::from)?; + let event = decode_compatibility_host_call(call, full_filesystem, max_reply_bytes)?; + let Some(host) = host else { + return Ok(Some(event)); + }; + match event { + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { operation, reply }) => { + host.submit( + operation, + reply, + admission.expect("host-backed routing always creates request admission"), + ) + .map_err(SidecarError::from)?; + Ok(None) } + other => Ok(Some(other)), } } @@ -2029,7 +3636,9 @@ pub(super) fn find_socket_state_entry( } } - let child_pid = process.execution.child_pid(); + let Some(child_pid) = process.execution.native_process_id() else { + continue; + }; let inodes = socket_inodes_for_pid(child_pid)?; if inodes.is_empty() { continue; @@ -2089,9 +3698,10 @@ where let reason = decision .and_then(|decision| decision.reason) .unwrap_or_else(|| format!("{capability} permission required")); - Err(SidecarError::Execution(format!( - "EACCES: permission denied, {resource}: {reason}" - ))) + Err(SidecarError::host( + "EACCES", + format!("permission denied, {resource}: {reason}"), + )) } pub(super) fn socket_query_resource( @@ -2292,7 +3902,12 @@ fn socket_inodes_for_pid(pid: u32) -> Result, SidecarError> { })?; let target = match fs::read_link(entry.path()) { Ok(target) => target, - Err(_) => continue, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect socket descriptor target for process {pid}: {error}" + ))); + } }; if let Some(inode) = parse_socket_inode(&target) { inodes.insert(inode); @@ -2409,26 +4024,23 @@ pub(super) fn vm_spawn_host_net_resource_counts(vm: &VmState) -> NetworkResource } #[allow(clippy::too_many_arguments)] -pub(super) fn collect_javascript_socket_port_state( +pub(super) fn collect_socket_port_state( kernel: &SidecarKernel, process_id: &str, process: &ActiveProcess, - tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>, - http_loopback_targets: &mut BTreeMap< - (JavascriptSocketFamily, u16), - JavascriptHttpLoopbackTarget, - >, - udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>, - udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>, - used_tcp_ports: &mut BTreeMap>, - used_udp_ports: &mut BTreeMap>, + tcp_guest_to_host: &mut BTreeMap<(SocketFamily, u16), u16>, + http_loopback_targets: &mut BTreeMap<(SocketFamily, u16), HttpLoopbackTarget>, + udp_guest_to_host: &mut BTreeMap<(SocketFamily, u16), u16>, + udp_host_to_guest: &mut BTreeMap<(SocketFamily, u16), u16>, + used_tcp_ports: &mut BTreeMap>, + used_udp_ports: &mut BTreeMap>, ) { for (family, port) in process.tcp_port_reservations.values() { used_tcp_ports.entry(*family).or_default().insert(*port); } let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| { - let family = JavascriptSocketFamily::from_ip(guest_addr.ip()); + let family = SocketFamily::from_ip(guest_addr.ip()); used_tcp_ports .entry(family) .or_default() @@ -2451,22 +4063,34 @@ pub(super) fn collect_javascript_socket_port_state( for (server_id, server) in &process.http_servers { let host_port = match server.listener.local_addr() { Ok(addr) => addr.port(), - Err(_) => continue, + Err(error) => { + eprintln!( + "ERR_AGENTOS_SOCKET_INVENTORY: failed to inspect HTTP listener {server_id} for process {process_id}: {error}" + ); + continue; + } }; record_tcp_listener(server.guest_local_addr, host_port); - let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip()); + let family = SocketFamily::from_ip(server.guest_local_addr.ip()); http_loopback_targets.insert( (family, server.guest_local_addr.port()), - JavascriptHttpLoopbackTarget { + HttpLoopbackTarget { process_id: process_id.to_owned(), server_id: *server_id, }, ); } - if let Ok(http2) = process.http2.shared.lock() { - for server in http2.servers.values() { - record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port()); + match process.http2.shared.lock() { + Ok(http2) => { + for server in http2.servers.values() { + record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port()); + } + } + Err(error) => { + eprintln!( + "ERR_AGENTOS_SOCKET_INVENTORY: failed to inspect HTTP/2 listeners for process {process_id}: {error}" + ); } } @@ -2477,7 +4101,7 @@ pub(super) fn collect_javascript_socket_port_state( .and_then(|record| record.local_address().cloned()) .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok()) .unwrap_or(socket.guest_local_addr); - let family = JavascriptSocketFamily::from_ip(guest_addr.ip()); + let family = SocketFamily::from_ip(guest_addr.ip()); used_tcp_ports .entry(family) .or_default() @@ -2496,7 +4120,7 @@ pub(super) fn collect_javascript_socket_port_state( let Some(guest_addr) = guest_addr else { continue; }; - let family = JavascriptSocketFamily::from_ip(guest_addr.ip()); + let family = SocketFamily::from_ip(guest_addr.ip()); used_udp_ports .entry(family) .or_default() @@ -2516,7 +4140,7 @@ pub(super) fn collect_javascript_socket_port_state( for (child_process_id, child) in &process.child_processes { let child_id = format!("{process_id}/{child_process_id}"); - collect_javascript_socket_port_state( + collect_socket_port_state( kernel, &child_id, child, @@ -2570,10 +4194,41 @@ pub(super) fn flush_parked_kernel_wait_rpc(process: &mut ActiveProcess) { .map(|(request, _)| request.clone()); process.clear_deferred_kernel_wait_rpc(); if let Some(request) = request { - let _ = process - .execution - .respond_javascript_sync_rpc_error(request.id, "EINTR", "process teardown") - .or_else(ignore_stale_javascript_sync_rpc_response); + if let Err(error) = request.reply.fail(HostServiceError::new( + "EINTR", + "process teardown interrupted the pending host call", + )) { + eprintln!("ERR_AGENTOS_PARKED_HOST_REPLY_TEARDOWN: {error}"); + } + } + if let Some(wait) = process.clear_deferred_guest_wait() { + if let Err(error) = wait.reply.fail(HostServiceError::new( + "EINTR", + "process teardown interrupted the pending wait", + )) { + eprintln!("ERR_AGENTOS_PARKED_GUEST_WAIT_TEARDOWN: {error}"); + } + } + if let Some(poll) = process.clear_deferred_kernel_poll() { + if let Some(token) = poll.temporary_signal_mask_token { + if let Err(error) = process.kernel_handle.end_temporary_signal_mask(token) { + eprintln!("ERR_AGENTOS_PPOLL_MASK_RESTORE_TEARDOWN: {error}"); + } + } + if let Err(error) = poll.reply.fail(HostServiceError::new( + "EINTR", + "process teardown interrupted the pending kernel poll", + )) { + eprintln!("ERR_AGENTOS_PARKED_KERNEL_POLL_TEARDOWN: {error}"); + } + } + if let Some(read) = process.clear_deferred_kernel_read() { + if let Err(error) = read.reply.fail(HostServiceError::new( + "EINTR", + "process teardown interrupted the pending descriptor read", + )) { + eprintln!("ERR_AGENTOS_PARKED_KERNEL_READ_TEARDOWN: {error}"); + } } } @@ -2588,7 +4243,7 @@ pub(crate) fn terminate_child_process_tree( for database_id in sqlite_database_ids { if let Err(error) = close_sqlite_database(kernel, process, database_id, true) { eprintln!( - "ERR_AGENTOS_SQLITE_CLOSE: pid={} database_id={database_id} error={error}", + "ERR_AGENTOS_SQLITE_TEARDOWN: failed to close database {database_id} while terminating process {}: {error}", process.kernel_pid ); } @@ -2674,20 +4329,31 @@ pub(crate) fn terminate_child_process_tree( } } - // Python handles are adapter references to the TCP/UDP capabilities closed - // above, not independent descriptors or leases. Dropping them also releases - // any charged partial-read view still retained by the adapter. - process.python_sockets.clear(); - let child_ids = process.child_processes.keys().cloned().collect::>(); for child_id in child_ids { let Some(mut child) = process.child_processes.remove(&child_id) else { continue; }; terminate_child_process_tree(kernel, &mut child, kernel_readiness, unix_address_registry); - let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM); - let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM); + if let Err(error) = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM) { + eprintln!( + "ERR_AGENTOS_CHILD_TEARDOWN_SIGNAL: failed to signal child kernel pid {}: {error}", + child.kernel_pid + ); + } + if let Some(native_process_id) = child.execution.native_process_id() { + if let Err(error) = signal_runtime_process(native_process_id, SIGTERM) { + eprintln!( + "ERR_AGENTOS_CHILD_TEARDOWN_SIGNAL: failed to signal native child pid {native_process_id}: {error}" + ); + } + } child.kernel_handle.finish(0); - let _ = kernel.wait_and_reap(child.kernel_pid); + if let Err(error) = kernel.wait_and_reap(child.kernel_pid) { + eprintln!( + "ERR_AGENTOS_CHILD_TEARDOWN_REAP: failed to reap child kernel pid {}: {error}", + child.kernel_pid + ); + } } } diff --git a/crates/native-sidecar/src/execution/process_events.rs b/crates/native-sidecar/src/execution/process_events.rs index 248a2360ad..e85224039f 100644 --- a/crates/native-sidecar/src/execution/process_events.rs +++ b/crates/native-sidecar/src/execution/process_events.rs @@ -1,4 +1,137 @@ use super::*; +use crate::state::{DeferredRpcError, ManagedHostNetDescriptionRegistry}; + +fn rollback_new_deferred_connect_resources( + process: &mut ActiveProcess, + kernel: &mut SidecarKernel, + kernel_readiness: &KernelSocketReadinessRegistry, + unix_addresses: &GuestUnixAddressRegistry, + previous_tcp_ids: &BTreeSet, + previous_unix_ids: &BTreeSet, +) { + let new_tcp_ids = process + .tcp_sockets + .keys() + .filter(|socket_id| !previous_tcp_ids.contains(*socket_id)) + .cloned() + .collect::>(); + for socket_id in new_tcp_ids { + if let Some(socket) = process.tcp_sockets.remove(&socket_id) { + release_tcp_socket_handle(process, &socket_id, socket, kernel, kernel_readiness); + } + } + let new_unix_ids = process + .unix_sockets + .keys() + .filter(|socket_id| !previous_unix_ids.contains(*socket_id)) + .cloned() + .collect::>(); + for socket_id in new_unix_ids { + if let Some(socket) = process.unix_sockets.remove(&socket_id) { + release_unix_socket_handle(process, &socket_id, socket, unix_addresses); + } + } +} + +pub(super) fn settle_host_call_completion_for_process( + kernel: &mut SidecarKernel, + kernel_readiness: &KernelSocketReadinessRegistry, + unix_addresses: &GuestUnixAddressRegistry, + managed_descriptions: &ManagedHostNetDescriptionRegistry, + process: &mut ActiveProcess, + completion: crate::state::HostCallCompletion, +) -> Result<(), SidecarError> { + let previous_tcp_ids = process.tcp_sockets.keys().cloned().collect::>(); + let previous_unix_ids = process + .unix_sockets + .keys() + .cloned() + .collect::>(); + let request_id = completion.reply.identity().call_id; + let connected = process.pending_net_connects.remove(&request_id); + let managed_description_id = process + .pending_managed_host_net_connects + .remove(&request_id); + let completion_result = match (completion.result, connected) { + (Ok(_), Some(connected)) => finalize_net_connect(process, kernel_readiness, connected) + .map_err(|error| crate::state::DeferredRpcError::from(host_service_error(&error))), + (result @ Err(_), Some(connected)) => { + match restore_pending_bound_unix_connect(process, &connected) { + Ok(()) => result, + Err(error) => Err(crate::state::DeferredRpcError::from(host_service_error( + &error, + ))), + } + } + (result, None) => result, + }; + let result = match completion_result { + Ok(value) => { + if let Some(description_id) = managed_description_id { + let update = (|| -> Result<(), SidecarError> { + let socket_id = value + .get("socketId") + .and_then(Value::as_str) + .ok_or_else(|| { + SidecarError::host( + "EIO", + "managed connect completion omitted socket id", + ) + })? + .to_owned(); + let local_address = + crate::execution::host_dispatch::managed_socket_address_from_info( + &value, false, + )?; + let peer_address = + crate::execution::host_dispatch::managed_socket_address_from_info( + &value, true, + )?; + let mut descriptions = managed_descriptions.lock().map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })?; + let description = descriptions.get_mut(&description_id).ok_or_else(|| { + SidecarError::host( + "ESTALE", + "managed connect description disappeared before completion", + ) + })?; + let route = if description.domain == agentos_execution::host::SocketDomain::Unix + { + crate::state::ManagedHostNetRoute::UnixSocket(socket_id) + } else { + crate::state::ManagedHostNetRoute::TcpSocket(socket_id) + }; + description.routes.insert(process.kernel_pid, route); + description.local_address = local_address; + description.peer_address = peer_address; + Ok(()) + })(); + if let Err(error) = update { + rollback_new_deferred_connect_resources( + process, + kernel, + kernel_readiness, + unix_addresses, + &previous_tcp_ids, + &previous_unix_ids, + ); + return completion + .reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from); + } + } + completion.reply.succeed(HostCallReply::Json(value)) + } + Err(error) => completion.reply.fail(HostServiceError { + code: error.code, + message: error.message, + details: error.details, + }), + }; + result.map_err(SidecarError::from) +} pub(super) struct BindingProcessEventRequest { pub(super) runtime_context: agentos_runtime::RuntimeContext, @@ -8,8 +141,10 @@ pub(super) struct BindingProcessEventRequest { pub(super) vm_id: String, pub(super) binding_resolution: BindingCommandResolution, pub(super) cancelled: Arc, + pub(super) paused: Arc, + pub(super) pause_notify: Arc, pub(super) pending_events: Arc>>, - pub(super) event_overflow_reason: Arc>>, + pub(super) event_overflow_reason: Arc>>, pub(super) pending_event_bytes: Arc, pub(super) pending_event_count_limit: Arc, pub(super) pending_event_bytes_limit: Arc, @@ -17,13 +152,104 @@ pub(super) struct BindingProcessEventRequest { pub(super) event_notify: Arc, } +#[allow(clippy::too_many_arguments)] +pub(in crate::execution) fn enqueue_deferred_host_service_completion( + sidecar: &NativeSidecar, + vm_id: &str, + process_id: &str, + runtime: agentos_runtime::RuntimeContext, + reply: DirectHostReplyHandle, + operation: &str, + receiver: tokio::sync::oneshot::Receiver>, + timeout: Option, + task_class: agentos_runtime::TaskClass, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + let task_reply = reply.clone(); + let method = operation.to_owned(); + let vm = sidecar + .vms + .get(vm_id) + .expect("validated deferred-service VM remains registered"); + let connection_id = vm.connection_id.clone(); + let session_id = vm.session_id.clone(); + let sender = sidecar.process_event_sender.clone(); + let event_notify = Arc::clone(&sidecar.process_event_notify); + let envelope_vm_id = vm_id.to_owned(); + let envelope_process_id = process_id.to_owned(); + if let Err(error) = runtime.spawn(task_class, async move { + let receive = async { + receiver.await.unwrap_or_else(|_| { + Err(DeferredRpcError { + code: "ERR_AGENTOS_DEFERRED_RPC_RESPONSE_CHANNEL_CLOSED".to_owned(), + message: format!("deferred host-service response channel closed for {method}"), + details: None, + }) + }) + }; + let result = match timeout { + Some(timeout) => { + match crate::execution::operation_deadline_timeout(&method, timeout, receive).await { + Ok(result) => result, + Err(_) => Err(DeferredRpcError { + code: "ETIMEDOUT".to_owned(), + message: format!( + "{method} exceeded limits.reactor.operationDeadlineMs ({} ms)", + timeout.as_millis() + ), + details: None, + }), + } + } + None => receive.await, + }; + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: envelope_vm_id, + process_id: envelope_process_id, + event: ActiveExecutionEvent::HostCallCompletion( + crate::state::HostCallCompletion { + reply: task_reply, + result, + }, + ), + }; + if let Err(error) = sender.send(envelope).await { + if let ActiveExecutionEvent::HostCallCompletion(completion) = error.0.event { + if let Err(reply_error) = completion.reply.fail(HostServiceError::new( + "ECANCELED", + "deferred host-service completion lane closed", + )) { + eprintln!( + "ERR_AGENTOS_HOST_REPLY_SETTLEMENT: failed to cancel deferred host-service completion after lane closure: {reply_error}" + ); + } + } + eprintln!( + "ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: deferred host-service completion could not be delivered" + ); + } else { + event_notify.notify_one(); + } + }) { + reply + .fail(host_service_error(&SidecarError::from(error))) + .map_err(SidecarError::from)?; + } + Ok(()) +} + // The producer owns these independent atomics/queues; keeping them explicit // avoids introducing another partially initialized shared-state wrapper. #[allow(clippy::too_many_arguments)] pub(crate) fn send_binding_process_event( cancelled: &AtomicBool, pending_events: &Arc>>, - event_overflow_reason: &Mutex>, + event_overflow_reason: &Mutex>, pending_event_bytes: &AtomicUsize, pending_event_count_limit: &AtomicUsize, pending_event_bytes_limit: &AtomicUsize, @@ -44,10 +270,18 @@ pub(crate) fn send_binding_process_event( .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); reason.get_or_insert_with(|| { - format!( - "process execution event queue exceeded {count_limit} events \ - (limits.process.pendingEventCount); raise limits.process.pendingEventCount" + HostServiceError::new( + "ERR_AGENTOS_RESOURCE_LIMIT", + format!( + "process execution event queue exceeded {count_limit} events \ + (limits.process.pendingEventCount); raise limits.process.pendingEventCount" + ), ) + .with_details(json!({ + "limitName": "limits.process.pendingEventCount", + "limit": count_limit, + "observed": pending_events.len().saturating_add(1), + })) }); return false; } @@ -57,23 +291,42 @@ pub(crate) fn send_binding_process_event( .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); reason.get_or_insert_with(|| { - format!( - "process execution event queue exceeded {byte_limit} bytes \ - (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + HostServiceError::new( + "ERR_AGENTOS_RESOURCE_LIMIT", + format!( + "process execution event queue exceeded {byte_limit} bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), ) + .with_details(json!({ + "limitName": "limits.process.pendingEventBytes", + "limit": byte_limit, + "observed": bytes.saturating_add(event_bytes), + })) }); return false; } if !vm_pending_event_bytes_budget.try_reserve(event_bytes) { + let limit = vm_pending_event_bytes_budget.limit(); + let observed = vm_pending_event_bytes_budget + .used() + .saturating_add(event_bytes); let mut reason = event_overflow_reason .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); reason.get_or_insert_with(|| { - format!( - "VM process execution event queues exceeded {} bytes \ - (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", - vm_pending_event_bytes_budget.limit() + HostServiceError::new( + "ERR_AGENTOS_RESOURCE_LIMIT", + format!( + "VM process execution event queues exceeded {limit} bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ), ) + .with_details(json!({ + "limitName": "limits.process.pendingEventBytes", + "limit": limit, + "observed": observed, + })) }); return false; } @@ -86,7 +339,7 @@ pub(crate) fn send_binding_process_event( fn send_binding_process_event_and_notify( cancelled: &AtomicBool, pending_events: &Arc>>, - event_overflow_reason: &Mutex>, + event_overflow_reason: &Mutex>, pending_event_bytes: &AtomicUsize, pending_event_count_limit: &AtomicUsize, pending_event_bytes_limit: &AtomicUsize, @@ -111,6 +364,43 @@ fn send_binding_process_event_and_notify( } pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) { + // A STOP acknowledged before producer admission must prevent the trusted + // callback from starting. Resume wakes this one bounded gate task. A + // callback already in flight may finish, but its events remain hidden by + // the paused adapter poll gate until CONT. + if request.paused.load(Ordering::Acquire) { + let runtime = request.runtime_context.clone(); + let paused = Arc::clone(&request.paused); + let pause_notify = Arc::clone(&request.pause_notify); + let cancelled = Arc::clone(&request.cancelled); + let failure_reason = Arc::clone(&request.event_overflow_reason); + let failure_notify = Arc::clone(&request.event_notify); + if let Err(error) = runtime.spawn(agentos_runtime::TaskClass::Vm, async move { + loop { + let notified = pause_notify.notified(); + if cancelled.load(Ordering::Acquire) { + return; + } + if !paused.load(Ordering::Acquire) { + break; + } + notified.await; + } + spawn_binding_process_events(request); + }) { + let mut reason = failure_reason + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + reason.get_or_insert_with(|| { + HostServiceError::new( + "ERR_AGENTOS_BINDING_PAUSE_GATE", + format!("failed to schedule paused binding producer gate: {error}"), + ) + }); + failure_notify.notify_one(); + } + return; + } let BindingProcessEventRequest { runtime_context, sidecar_requests, @@ -119,6 +409,8 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) vm_id, binding_resolution, cancelled, + paused: _, + pause_notify: _, pending_events, event_overflow_reason, pending_event_bytes, @@ -154,10 +446,23 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) }; match binding_resolution { BindingCommandResolution::Failure(message) => { - if enqueue(ActiveExecutionEvent::Stderr(format_binding_failure_output( + let output_enqueued = enqueue(ActiveExecutionEvent::Stderr( + format_binding_failure_output( &message, - ))) { - let _ = enqueue(ActiveExecutionEvent::Exited(1)); + ), + )); + if !output_enqueued && !cancelled.load(Ordering::Acquire) { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding failure output; queue limit state retains the typed failure" + ); + } else if output_enqueued { + if !enqueue(ActiveExecutionEvent::Exited(1)) + && !cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding failure exit event; queue limit state retains the typed failure" + ); + } } } BindingCommandResolution::Invoke { request, timeout } => { @@ -209,8 +514,19 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) } else { ActiveExecutionEvent::Stderr(output) }; - if enqueue(output_event) { - let _ = enqueue(ActiveExecutionEvent::Exited(exit_code)); + let output_enqueued = enqueue(output_event); + if !output_enqueued && !cancelled.load(Ordering::Acquire) { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding result output; queue limit state retains the typed failure" + ); + } else if output_enqueued { + if !enqueue(ActiveExecutionEvent::Exited(exit_code)) + && !cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding exit event; queue limit state retains the typed failure" + ); + } } } } @@ -229,10 +545,21 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) event, ) }; - if enqueue_failure(ActiveExecutionEvent::Stderr(format_binding_failure_output( - &error.to_string(), - ))) { - let _ = enqueue_failure(ActiveExecutionEvent::Exited(1)); + let output_enqueued = enqueue_failure(ActiveExecutionEvent::Stderr( + format_binding_failure_output(&error.to_string()), + )); + if !output_enqueued && !failure_cancelled.load(Ordering::Acquire) { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue blocking-admission failure output; queue limit state retains the typed failure" + ); + } else if output_enqueued { + if !enqueue_failure(ActiveExecutionEvent::Exited(1)) + && !failure_cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue blocking-admission exit event; queue limit state retains the typed failure" + ); + } } } } @@ -266,6 +593,7 @@ pub(crate) fn record_execute_phase(stage: &str, elapsed: Duration) { } let phases = EXECUTE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new())); let Ok(mut phases) = phases.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-phase statistics lock is poisoned"); return; }; let stats = phases.entry(stage.to_string()).or_default(); @@ -291,7 +619,12 @@ pub(crate) fn record_execute_phase(stage: &str, elapsed: Duration) { stats.calls )); } - let _ = fs::write(path, output); + if let Err(error) = fs::write(&path, output) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_WRITE: failed to write process execute-phase statistics to {}: {error}", + path.to_string_lossy() + ); + } } pub(super) fn mark_execute_response_ready(vm_id: &str, process_id: &str) { @@ -299,8 +632,13 @@ pub(super) fn mark_execute_response_ready(vm_id: &str, process_id: &str) { return; } let lifetimes = EXECUTE_LIFETIMES.get_or_init(|| Mutex::new(BTreeMap::new())); - if let Ok(mut lifetimes) = lifetimes.lock() { - lifetimes.insert(execute_phase_key(vm_id, process_id), Instant::now()); + match lifetimes.lock() { + Ok(mut lifetimes) => { + lifetimes.insert(execute_phase_key(vm_id, process_id), Instant::now()); + } + Err(_) => { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-lifetime lock is poisoned"); + } } } @@ -309,15 +647,20 @@ pub(crate) fn mark_execute_exit_event_queued(vm_id: &str, process_id: &str) { return; } let queued = EXECUTE_EXIT_EVENT_QUEUED.get_or_init(|| Mutex::new(BTreeMap::new())); - if let Ok(mut queued) = queued.lock() { - let key = execute_phase_key(vm_id, process_id); - if let std::collections::btree_map::Entry::Vacant(entry) = queued.entry(key) { - record_execute_response_to_exit_milestone( - "execute_response_to_exit_event_queued", - vm_id, - process_id, - ); - entry.insert(Instant::now()); + match queued.lock() { + Ok(mut queued) => { + let key = execute_phase_key(vm_id, process_id); + if let std::collections::btree_map::Entry::Vacant(entry) = queued.entry(key) { + record_execute_response_to_exit_milestone( + "execute_response_to_exit_event_queued", + vm_id, + process_id, + ); + entry.insert(Instant::now()); + } + } + Err(_) => { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-exit queue timing lock is poisoned"); } } } @@ -330,6 +673,7 @@ pub(crate) fn record_execute_exit_event_queue_wait(stage: &str, vm_id: &str, pro return; }; let Ok(mut queued) = queued.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-exit queue timing lock is poisoned"); return; }; if let Some(started) = queued.remove(&execute_phase_key(vm_id, process_id)) { @@ -349,6 +693,7 @@ pub(crate) fn record_execute_response_to_exit_milestone( return; }; let Ok(lifetimes) = lifetimes.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-lifetime lock is poisoned"); return; }; if let Some(started) = lifetimes.get(&execute_phase_key(vm_id, process_id)) { @@ -364,6 +709,7 @@ fn record_execute_response_to_exit(vm_id: &str, process_id: &str) { return; }; let Ok(mut lifetimes) = lifetimes.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: execute-lifetime lock is poisoned"); return; }; if let Some(started) = lifetimes.remove(&execute_phase_key(vm_id, process_id)) { @@ -379,6 +725,7 @@ pub(super) fn record_sync_rpc(method: &str) { let stats = SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); let Ok(mut map) = stats.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: sync-RPC statistics lock is poisoned"); return; }; *map.entry(method.to_string()).or_insert(0) += 1; @@ -465,6 +812,9 @@ where { continue; } + self.recheck_root_deferred_guest_wait(&vm_id, &process_id)?; + self.recheck_root_deferred_kernel_poll(&vm_id, &process_id)?; + self.recheck_root_deferred_kernel_read(&vm_id, &process_id)?; enum ProcessPollResult { Event(Box>), RecoverClosedChannel, @@ -481,14 +831,7 @@ where } else { match process.poll_execution_event(Duration::ZERO).await { Ok(event) => ProcessPollResult::Event(Box::new(event)), - Err(SidecarError::Execution(message)) - if (process.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { + Err(SidecarError::ExecutionEventChannelClosed { .. }) => { ProcessPollResult::RecoverClosedChannel } Err(other) => return Err(other), @@ -567,6 +910,128 @@ where Ok(emitted_any) } + fn recheck_root_deferred_guest_wait( + &mut self, + vm_id: &str, + process_id: &str, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.process_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(process) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if process.deferred_guest_wait.is_none() { + return Ok(()); + } + service_deferred_guest_wait( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + None, + ) + } + + fn recheck_root_deferred_kernel_poll( + &mut self, + vm_id: &str, + process_id: &str, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let socket_paths = self + .vms + .get(vm_id) + .map(build_socket_path_context) + .transpose()?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(process) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if process.deferred_kernel_poll.is_none() { + return Ok(()); + } + if process + .deferred_kernel_poll + .as_ref() + .is_some_and(|poll| poll.combined) + { + return host_dispatch::service_deferred_posix_poll( + generation, + &runtime, + wait_handle, + notify, + socket_paths + .as_ref() + .expect("registered VM has a socket path context"), + kernel_readiness, + capabilities, + managed_descriptions, + kernel, + process, + None, + ); + } + service_deferred_kernel_poll( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + None, + ) + } + + fn recheck_root_deferred_kernel_read( + &mut self, + vm_id: &str, + process_id: &str, + ) -> Result<(), SidecarError> { + let notify = Arc::clone(&self.process_event_notify); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let runtime = vm.runtime_context.clone(); + let wait_handle = vm.kernel.poll_wait_handle(); + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(process) = active_processes.get_mut(process_id) else { + return Ok(()); + }; + process.apply_runtime_controls()?; + if process.deferred_kernel_read.is_none() { + return Ok(()); + } + service_deferred_kernel_read( + generation, + &runtime, + wait_handle, + notify, + kernel, + process, + None, + ) + } + /// Arm exactly one sidecar task for the earliest zombie deadline across /// every VM. Kernel process tables remain runtime-neutral and are reaped on /// the next process-event turn after this coalesced wake. @@ -602,9 +1067,10 @@ where task.abort(); } let runtime = self.runtime_context.clone().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: kernel zombie reaper requires the process RuntimeContext", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("kernel zombie reaper requires the process RuntimeContext"), + ) })?; let notify = Arc::clone(&self.process_event_notify); let delay = next_deadline.saturating_duration_since(Instant::now()); @@ -623,9 +1089,11 @@ where fn internal_execution_event(event: &ActiveExecutionEvent) -> bool { matches!( event, - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { .. }) + | ActiveExecutionEvent::Common(ExecutionEvent::Warning(_)) + | ActiveExecutionEvent::Common(ExecutionEvent::RuntimeFault(_)) + | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } ) } @@ -641,19 +1109,9 @@ where let Some(process) = vm.active_processes.get_mut(process_id) else { return Ok(None); }; - if process.execution.uses_shared_v8_runtime() { - return Ok(None); - } - if process.runtime != GuestRuntimeKind::JavaScript - && process.runtime != GuestRuntimeKind::Python - && process.runtime != GuestRuntimeKind::WebAssembly - { + let Some(runtime_child_pid) = process.execution.native_process_id() else { return Ok(None); - } - let runtime_child_pid = process.execution.child_pid(); - if runtime_child_pid == 0 { - return Ok(None); - } + }; match runtime_child_exit_status(runtime_child_pid)? { RuntimeChildStatusObservation::Exited(status) => { process.exit_signal = status.signal; @@ -661,8 +1119,7 @@ where Ok(Some(ActiveExecutionEvent::Exited(status.status))) } RuntimeChildStatusObservation::Running => Ok(None), - RuntimeChildStatusObservation::NotWaitable => Err(SidecarError::Execution(format!( - "ECHILD: guest runtime process {runtime_child_pid} exited without an observable wait status" + RuntimeChildStatusObservation::NotWaitable => Err(SidecarError::host("ECHILD", format!("guest runtime process {runtime_child_pid} exited without an observable wait status" ))), } } @@ -719,15 +1176,6 @@ where None } - pub(super) fn descendant_parent_process<'a>( - vm: &'a VmState, - process_id: &str, - child_path: &[&str], - ) -> Option<&'a ActiveProcess> { - let root = vm.active_processes.get(process_id)?; - Self::active_process_by_path(root, child_path) - } - pub(super) fn descendant_parent_process_mut<'a>( vm: &'a mut VmState, process_id: &str, @@ -770,11 +1218,19 @@ where adopted } - pub(super) fn child_process_signal_key<'a>( - process_id: &'a str, - child_path: &[&'a str], - ) -> &'a str { - child_path.last().copied().unwrap_or(process_id) + pub(super) fn terminating_process_tree_kernel_pids(process: &ActiveProcess) -> Vec { + fn collect(process: &ActiveProcess, pids: &mut Vec) { + pids.push(process.kernel_pid); + for child in process.child_processes.values() { + if !child.detached { + collect(child, pids); + } + } + } + + let mut pids = Vec::new(); + collect(process, &mut pids); + pids } pub(super) fn resolve_detached_child_process_path( @@ -836,6 +1292,55 @@ where process_id: &str, event: ActiveExecutionEvent, ) -> Result, SidecarError> { + let event = match event { + ActiveExecutionEvent::Common(ExecutionEvent::RuntimeFault(fault)) => { + let fault = fault.into_error(); + let kernel_fault = agentos_kernel::process_runtime::ProcessRuntimeFault::try_new( + fault.code.clone(), + fault.message.clone(), + fault.details.clone(), + ) + .map_err(|error| SidecarError::host(error.code(), error.message()))?; + let Some(vm) = self.vms.get_mut(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "runtime fault dispatch", + ); + return Ok(None); + }; + let Some(process) = vm.active_processes.get_mut(process_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "runtime fault dispatch", + ); + return Ok(None); + }; + process.kernel_handle.finish_runtime_fault(kernel_fault); + tracing::error!( + vm_id, + process_id, + code = %fault.code, + message = %fault.message, + details = ?fault.details, + "executor reported a typed runtime fault" + ); + ActiveExecutionEvent::Exited(1) + } + ActiveExecutionEvent::Common(ExecutionEvent::Exited(exit)) => { + let exit_code = match exit { + agentos_execution::backend::ExecutionExit::Exited(code) => code, + agentos_execution::backend::ExecutionExit::Signaled { signal, .. } => { + 128_i32.saturating_add(signal) + } + }; + ActiveExecutionEvent::Exited(exit_code) + } + event => event, + }; let Some(vm) = self.vms.get(vm_id) else { log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch"); return Ok(None); @@ -852,6 +1357,71 @@ where } match event { + ActiveExecutionEvent::Common(ExecutionEvent::HostCall { operation, reply }) => { + let Some((operation, reply)) = + dispatch_context_host_operation(self, vm_id, process_id, operation, reply) + .await? + else { + return Ok(None); + }; + let Some(vm) = self.vms.get_mut(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "common host operation", + ); + return Ok(None); + }; + let generation = vm.generation; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let Some(process) = active_processes.get_mut(process_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "common host operation", + ); + return Ok(None); + }; + let effects = + dispatch_host_operation(generation, kernel, process, operation, reply)?; + if effects.may_make_fd_readable { + Self::wake_ready_deferred_fd_reads(vm)?; + } + if effects.may_make_fd_writable { + Self::wake_ready_deferred_fd_writes(vm)?; + } + Ok(None) + } + ActiveExecutionEvent::Common(ExecutionEvent::Output { stream, bytes }) => { + let channel = match stream { + agentos_execution::backend::OutputStream::Stdout => StreamChannel::Stdout, + agentos_execution::backend::OutputStream::Stderr => StreamChannel::Stderr, + }; + Ok(Some(EventFrame::new( + ownership, + EventPayload::ProcessOutput(ProcessOutputEvent { + process_id: process_id.to_owned(), + channel, + chunk: bytes.into_vec(), + }), + ))) + } + ActiveExecutionEvent::Common(ExecutionEvent::Warning(error)) => { + eprintln!("ERR_AGENTOS_EXECUTION_WARNING: {error}"); + Ok(None) + } + ActiveExecutionEvent::Common(ExecutionEvent::RuntimeFault(_)) => { + unreachable!("runtime fault events are normalized before dispatch") + } + ActiveExecutionEvent::Common(ExecutionEvent::Exited(_)) => { + unreachable!("common exit events are normalized before dispatch") + } + ActiveExecutionEvent::Common(_) => Err(SidecarError::host( + "ENOSYS", + "execution backend emitted an unsupported common event", + )), ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new( ownership, EventPayload::ProcessOutput(ProcessOutputEvent { @@ -868,22 +1438,21 @@ where chunk, }), ))), - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::HostRpcRequest(request) => { self.handle_javascript_sync_rpc_request(vm_id, process_id, request) .await?; Ok(None) } - ActiveExecutionEvent::JavascriptSyncRpcCompletion(completion) => { - self.handle_javascript_sync_rpc_completion(vm_id, process_id, completion)?; + ActiveExecutionEvent::HostCallCompletion(completion) => { + self.handle_host_call_completion(vm_id, process_id, completion)?; Ok(None) } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { - self.handle_python_vfs_rpc_request(vm_id, process_id, *request) - .await?; + ActiveExecutionEvent::ManagedStreamReadRecheck(pending) => { + dispatch_claimed_context_stream_read(self, vm_id, process_id, *pending)?; Ok(None) } - ActiveExecutionEvent::PythonSocketConnectCompletion(completion) => { - self.handle_python_socket_connect_completion(vm_id, process_id, *completion)?; + ActiveExecutionEvent::ManagedUdpPollRecheck(pending) => { + dispatch_claimed_context_udp_poll(self, vm_id, process_id, *pending)?; Ok(None) } ActiveExecutionEvent::SignalState { @@ -893,13 +1462,10 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(None); }; - if !vm.active_processes.contains_key(process_id) { + let Some(process) = vm.active_processes.get(process_id) else { return Ok(None); - } - vm.signal_states - .entry(process_id.to_owned()) - .or_default() - .insert(signal, registration); + }; + apply_kernel_signal_registration(process, signal, ®istration)?; Ok(None) } ActiveExecutionEvent::Exited(exit_code) => { @@ -932,144 +1498,42 @@ where } } - pub(super) fn handle_javascript_sync_rpc_completion( + pub(super) fn handle_host_call_completion( &mut self, vm_id: &str, process_id: &str, - completion: crate::state::JavascriptSyncRpcCompletion, + completion: crate::state::HostCallCompletion, ) -> Result<(), SidecarError> { let Some(vm) = self.vms.get_mut(vm_id) else { + completion + .reply + .fail(HostServiceError::new( + "ESTALE", + "deferred host-call VM no longer exists", + )) + .map_err(SidecarError::from)?; return Ok(()); }; let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let unix_addresses = Arc::clone(&vm.unix_address_registry); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); let Some(process) = vm.active_processes.get_mut(process_id) else { + completion + .reply + .fail(HostServiceError::new( + "ESTALE", + "deferred host-call process no longer exists", + )) + .map_err(SidecarError::from)?; return Ok(()); }; - let connected = process - .pending_javascript_net_connects - .remove(&completion.request_id); - let completion_result = match (completion.result, connected) { - (Ok(_), Some(connected)) => { - finalize_javascript_net_connect(process, &kernel_readiness, connected).map_err( - |error| crate::state::DeferredRpcError { - code: javascript_sync_rpc_error_code(&error), - message: javascript_sync_rpc_error_message(&error), - }, - ) - } - (result @ Err(_), Some(connected)) => { - restore_pending_bound_unix_connect(process, &connected)?; - result - } - (result, None) => result, - }; - let result = match completion_result { - Ok(value) => process - .execution - .respond_javascript_sync_rpc_success(completion.request_id, value), - Err(error) => process.execution.respond_javascript_sync_rpc_error( - completion.request_id, - error.code, - error.message, - ), - }; - result.or_else(ignore_stale_javascript_sync_rpc_response) - } - - pub(super) fn handle_python_socket_connect_completion( - &mut self, - vm_id: &str, - process_id: &str, - completion: PythonSocketConnectCompletion, - ) -> Result<(), SidecarError> { - let request_id = completion.request_id; - let connected = match completion.result { - Ok(connected) => connected, - Err(error) => { - return self.respond_python_rpc( - vm_id, - process_id, - request_id, - Err(SidecarError::Execution(format!( - "{}: {}", - error.code, error.message - ))), - ); - } - }; - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let Some(process) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let PendingPythonTcpConnect { - native_socket_id, - python_socket_id, - socket, - pending_capability, - } = connected; - let capability_key = NativeCapabilityKey::TcpSocket(native_socket_id.clone()); - if let Err(error) = commit_process_capability( - process, - pending_capability, - capability_key.clone(), - native_socket_id.clone(), - socket.kernel_socket_id, - ) { - if let Err(close_error) = socket.close(&mut vm.kernel, process.kernel_pid) { - eprintln!( - "ERR_AGENTOS_PYTHON_SOCKET_CLOSE: deferred TCP connect rollback failed: {close_error}" - ); - } - return self.respond_python_rpc(vm_id, process_id, request_id, Err(error)); - } - if let Err(error) = - socket.set_fairness_identity(process.capability_fairness_identity(&capability_key)) - { - if let Err(release_error) = process.release_capability(&capability_key) { - eprintln!( - "ERR_AGENTOS_CAPABILITY_RELEASE: deferred Python TCP rollback failed: {release_error}" - ); - } - if let Err(close_error) = socket.close(&mut vm.kernel, process.kernel_pid) { - eprintln!( - "ERR_AGENTOS_PYTHON_SOCKET_CLOSE: deferred TCP fairness rollback failed: {close_error}" - ); - } - return self.respond_python_rpc(vm_id, process_id, request_id, Err(error)); - } - socket.retain_description_lease( - process - .shared_capability_lease(&capability_key) - .expect("committed deferred Python TCP capability lease"), - ); - register_kernel_readiness_target( + settle_host_call_completion_for_process( + &mut vm.kernel, &kernel_readiness, - socket.kernel_socket_id, - None, - Some(Arc::clone(&socket.read_event_notify)), - process.capability_readiness_identity(&capability_key), - native_socket_id.clone(), - KernelSocketReadinessEvent::Data, - ); - process.tcp_sockets.insert(native_socket_id.clone(), socket); - process.python_sockets.insert( - python_socket_id, - PythonHostSocket::Tcp { - socket_id: native_socket_id, - pending_read: None, - }, - ); - debug_assert!(process.capability_leases.contains_key(&capability_key)); - self.respond_python_rpc( - vm_id, - process_id, - request_id, - Ok(PythonVfsRpcResponsePayload::SocketCreated { - socket_id: python_socket_id, - }), + &unix_addresses, + &managed_descriptions, + process, + completion, ) } @@ -1098,6 +1562,19 @@ where let process_table = vm.kernel.list_processes(); record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed()); let phase_start = Instant::now(); + let terminating_kernel_pids = Self::terminating_process_tree_kernel_pids( + vm.active_processes + .get(process_id) + .expect("validated exiting process remains registered"), + ); + for kernel_pid in terminating_kernel_pids { + retire_managed_process_routes(&self.bridge, vm_id, vm, kernel_pid)?; + } + record_execute_phase( + "process_exit_cleanup_managed_network_routes", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); let Some(mut process) = vm.active_processes.remove(process_id) else { return Ok(None); }; @@ -1119,22 +1596,6 @@ where let phase_start = Instant::now(); let detached_children = Self::adopt_detached_child_processes(process_id, &mut process); record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed()); - let phase_start = Instant::now(); - let should_sync_host_writes = process.host_write_dirty_recursive() - || !process.clean_host_writes_are_observable_recursive(); - let host_sync_result = if should_sync_host_writes { - sync_process_host_writes_to_kernel(vm, &process) - } else { - record_execute_phase( - "process_exit_cleanup_sync_host_writes_clean_skip", - Duration::ZERO, - ); - Ok(()) - }; - record_execute_phase( - "process_exit_cleanup_sync_host_writes", - phase_start.elapsed(), - ); let raw_mode_result = release_inherited_child_raw_mode(&mut vm.kernel, &process); let phase_start = Instant::now(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); @@ -1150,13 +1611,24 @@ where phase_start.elapsed(), ); let phase_start = Instant::now(); - process.kernel_handle.finish(exit_code); + if let Some(signal) = process.exit_signal { + process + .kernel_handle + .finish_signaled(signal, process.exit_core_dumped); + } else { + process.kernel_handle.finish(exit_code); + } record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed()); let phase_start = Instant::now(); - let _ = vm.kernel.wait_and_reap(process.kernel_pid); + if let Err(error) = vm.kernel.wait_and_reap(process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_PROCESS_REAP: failed to reap exited kernel pid {}: {error}", + process.kernel_pid + ); + } + retire_orphaned_managed_descriptions(vm)?; record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed()); let phase_start = Instant::now(); - vm.signal_states.remove(process_id); record_execute_phase( "process_exit_cleanup_signal_state_remove", phase_start.elapsed(), @@ -1180,10 +1652,8 @@ where record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed()); // The process was removed from active_processes before the fallible - // host/raw-mode cleanup. Surface those errors only after all process- - // owned resources (especially host-materialized SQLite state) have - // been copied back and finalized. - host_sync_result?; + // raw-mode cleanup. Surface the error only after all process-owned + // resources have been finalized. raw_mode_result?; Ok(Some(became_idle)) } diff --git a/crates/native-sidecar/src/execution/python/mod.rs b/crates/native-sidecar/src/execution/python/mod.rs deleted file mode 100644 index cc8600a35c..0000000000 --- a/crates/native-sidecar/src/execution/python/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod rpc; -mod sockets; -mod subprocess; diff --git a/crates/native-sidecar/src/execution/python/rpc.rs b/crates/native-sidecar/src/execution/python/rpc.rs deleted file mode 100644 index 2fcec12d8c..0000000000 --- a/crates/native-sidecar/src/execution/python/rpc.rs +++ /dev/null @@ -1,250 +0,0 @@ -use super::super::*; - -impl NativeSidecar -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - pub(crate) async fn handle_python_vfs_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - match request.method { - PythonVfsRpcMethod::Read - | PythonVfsRpcMethod::Write - | PythonVfsRpcMethod::Stat - | PythonVfsRpcMethod::Lstat - | PythonVfsRpcMethod::ReadDir - | PythonVfsRpcMethod::Mkdir - | PythonVfsRpcMethod::Unlink - | PythonVfsRpcMethod::Rmdir - | PythonVfsRpcMethod::Rename - | PythonVfsRpcMethod::Symlink - | PythonVfsRpcMethod::ReadLink - | PythonVfsRpcMethod::Setattr => { - filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request) - } - PythonVfsRpcMethod::HttpRequest => { - self.handle_python_http_rpc_request(vm_id, process_id, request) - } - PythonVfsRpcMethod::DnsLookup => { - self.handle_python_dns_rpc_request(vm_id, process_id, request) - } - PythonVfsRpcMethod::SubprocessRun => { - self.handle_python_subprocess_rpc_request(vm_id, process_id, request) - .await - } - PythonVfsRpcMethod::SocketConnect - | PythonVfsRpcMethod::SocketSend - | PythonVfsRpcMethod::SocketRecv - | PythonVfsRpcMethod::SocketClose - | PythonVfsRpcMethod::UdpCreate - | PythonVfsRpcMethod::UdpSendto - | PythonVfsRpcMethod::UdpRecvfrom => { - self.handle_python_socket_rpc_request(vm_id, process_id, request) - .await - } - } - } - - fn handle_python_http_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - if !vm.active_processes.contains_key(process_id) { - return Ok(()); - } - let response = (|| { - let url_text = request.url.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from("python httpRequest requires a url")) - })?; - let url = Url::parse(url_text) - .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?; - let host = url.host_str().ok_or_else(|| { - SidecarError::Execution(String::from("ERR_INVALID_URL: missing host")) - })?; - let port = url.port_or_known_default().ok_or_else(|| { - SidecarError::Execution(String::from("ERR_INVALID_URL: missing port")) - })?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(host, port), - )?; - // Pin the outbound connection to the IP addresses that pass the - // egress range guard at resolution time. A literal IP is validated - // directly; a hostname is resolved once here and the resulting - // address set is pinned into the HTTP client's resolver below so a - // rebinding DNS server cannot make the second (TLS/TCP) lookup land - // on a private/link-local/metadata IP that this check rejected. - let pinned_addresses = if let Ok(literal_ip) = host.parse::() { - filter_dns_safe_ip_addrs(vec![literal_ip], host)? - } else { - filter_dns_safe_ip_addrs( - resolve_dns_ip_addrs( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - host, - DnsLookupPolicy::SkipPermissions, - )?, - host, - )? - }; - self.bridge.require_resolved_network_access( - vm_id, - NetworkOperation::Http, - &format_tcp_resource(host, port), - &pinned_addresses - .iter() - .map(|ip| format_tcp_resource(&ip.to_string(), port)) - .collect::>(), - )?; - let mut headers = BTreeMap::new(); - for (name, value) in &request.headers { - headers.insert(name.clone(), Value::String(value.clone())); - } - let options = JavascriptHttpRequestOptions { - method: Some( - request - .http_method - .clone() - .unwrap_or_else(|| String::from("GET")), - ), - headers, - body: request.body_base64.as_deref().map(|body| { - String::from_utf8( - base64::engine::general_purpose::STANDARD - .decode(body) - .unwrap_or_default(), - ) - .unwrap_or_default() - }), - reject_unauthorized: None, - }; - let headers = - parse_http_header_collection(&options.headers, "python httpRequest headers")?; - let default_ca_bundle = if url.scheme() == "https" { - read_vm_default_ca_bundle(&mut vm.kernel)? - } else { - Vec::new() - }; - let response = issue_outbound_http_request( - &url, - &options, - &headers, - &pinned_addresses, - &default_ca_bundle, - )?; - let payload_json = response.as_str().ok_or_else(|| { - SidecarError::Execution(String::from( - "python httpRequest returned a non-string response payload", - )) - })?; - let payload: Value = serde_json::from_str(payload_json).map_err(|error| { - SidecarError::Execution(format!( - "python httpRequest response must be valid JSON: {error}" - )) - })?; - let header_map = payload - .get("headers") - .and_then(Value::as_array) - .map(|entries| { - let mut normalized = BTreeMap::>::new(); - for entry in entries { - let Some(pair) = entry.as_array() else { - continue; - }; - let Some(name) = pair.first().and_then(Value::as_str) else { - continue; - }; - let Some(value) = pair.get(1).and_then(Value::as_str) else { - continue; - }; - normalized - .entry(name.to_owned()) - .or_default() - .push(value.to_owned()); - } - normalized - }) - .unwrap_or_default(); - Ok(PythonVfsRpcResponsePayload::Http { - status: payload - .get("status") - .and_then(Value::as_u64) - .map(|value| value as u16) - .unwrap_or_default(), - reason: payload - .get("statusText") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - url: payload - .get("url") - .and_then(Value::as_str) - .unwrap_or(url_text) - .to_owned(), - headers: header_map, - body_base64: payload - .get("body") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - }) - })(); - - self.respond_python_rpc(vm_id, process_id, request.id, response) - } - - fn handle_python_dns_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); - }; - if !vm.active_processes.contains_key(process_id) { - return Ok(()); - } - let response = (|| { - let hostname = request.hostname.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from("python dnsLookup requires a hostname")) - })?; - let mut addresses = filter_dns_safe_ip_addrs( - resolve_dns_ip_addrs( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - hostname, - DnsLookupPolicy::CheckPermissions, - )?, - hostname, - )?; - if let Some(family) = request.family { - addresses.retain(|address| { - matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_))) - }); - } - Ok(PythonVfsRpcResponsePayload::DnsLookup { - addresses: addresses - .into_iter() - .map(|address| address.to_string()) - .collect(), - }) - })(); - - self.respond_python_rpc(vm_id, process_id, request.id, response) - } -} diff --git a/crates/native-sidecar/src/execution/python/sockets.rs b/crates/native-sidecar/src/execution/python/sockets.rs deleted file mode 100644 index 63c6997367..0000000000 --- a/crates/native-sidecar/src/execution/python/sockets.rs +++ /dev/null @@ -1,1072 +0,0 @@ -use super::super::*; - -const PYTHON_SOCKET_DEFAULT_RECV: usize = 65536; -const PYTHON_SOCKET_MAX_RECV: usize = 4 * 1024 * 1024; - -fn python_socket_host(request: &PythonVfsRpcRequest) -> Result { - request - .hostname - .clone() - .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a host"))) -} - -fn python_socket_port(request: &PythonVfsRpcRequest) -> Result { - request - .port - .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a port"))) -} - -#[derive(Debug)] -struct PythonSocketPayload { - bytes: Vec, - _reservation: Reservation, -} - -fn python_socket_payload( - request: &PythonVfsRpcRequest, - resources: &ResourceLedger, -) -> Result { - decode_python_socket_payload(request.body_base64.as_deref(), resources) -} - -fn decode_python_socket_payload( - body: Option<&str>, - resources: &ResourceLedger, -) -> Result { - let Some(body) = body else { - return Ok(PythonSocketPayload { - bytes: Vec::new(), - _reservation: resources - .reserve(ResourceClass::BufferedBytes, 0) - .map_err(SidecarError::from)?, - }); - }; - let padding = body - .as_bytes() - .iter() - .rev() - .take_while(|byte| **byte == b'=') - .take(2) - .count(); - let capacity = base64::decoded_len_estimate(body.len()).saturating_sub(padding); - let mut reservation = resources - .reserve(ResourceClass::BufferedBytes, capacity) - .map_err(SidecarError::from)?; - let bytes = base64::engine::general_purpose::STANDARD - .decode(body) - .map_err(|error| { - SidecarError::InvalidState(format!("invalid base64 python socket payload: {error}")) - })?; - if capacity > bytes.len() { - drop( - reservation - .split(capacity - bytes.len()) - .expect("decoded payload cannot exceed its reserved estimate"), - ); - } - Ok(PythonSocketPayload { - bytes, - _reservation: reservation, - }) -} - -fn python_socket_recv_len(request: &PythonVfsRpcRequest) -> usize { - request - .max_buffer - .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV) - .clamp(1, PYTHON_SOCKET_MAX_RECV) -} - -fn python_socket_wait_timeout(request: &PythonVfsRpcRequest, limits: ReactorIoLimits) -> Duration { - request - .timeout_ms - .map_or(limits.operation_deadline, |timeout_ms| { - Duration::from_millis(timeout_ms).min(limits.operation_deadline) - }) -} - -pub(in crate::execution) fn python_socket_id( - request: &PythonVfsRpcRequest, -) -> Result { - request.socket_id.ok_or_else(|| { - SidecarError::InvalidState(String::from("python socket op requires socketId")) - }) -} - -fn python_socket_missing_error(socket_id: u64) -> SidecarError { - SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}")) -} - -fn python_socket_backend_missing_error(socket_id: u64) -> SidecarError { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_CAPABILITY_BACKEND_MISSING: Python socket {socket_id} lost its shared backend" - )) -} - -fn consume_python_tcp_pending_read( - pending_read: &mut Option, - max: usize, - resources: &ResourceLedger, -) -> Result, SidecarError> { - let Some(pending) = pending_read.as_mut() else { - return Ok(None); - }; - let (data_base64, response_reservation, consumed_all) = { - let end = pending.offset.saturating_add(max).min(pending.data.len()); - let (data_base64, response_reservation) = - encode_python_socket_bytes(&pending.data[pending.offset..end], resources)?; - pending.offset = end; - (data_base64, response_reservation, end == pending.data.len()) - }; - if consumed_all { - *pending_read = None; - } - Ok(Some(PythonSocketImmediate { - payload: PythonVfsRpcResponsePayload::SocketReceived { - data_base64, - closed: false, - timed_out: false, - }, - _response_reservation: response_reservation, - })) -} - -fn python_tcp_event_response( - event: Option, - pending_read: &mut Option, - max: usize, - resources: &ResourceLedger, -) -> Result { - match event { - Some(JavascriptTcpSocketEvent::Data { - bytes, - reservation, - source_reservations, - }) => { - let end = max.min(bytes.len()); - let (data_base64, response_reservation) = - encode_python_socket_bytes(&bytes[..end], resources)?; - if end < bytes.len() { - *pending_read = Some(PythonTcpReadBuffer { - data: bytes, - offset: end, - _reservation: reservation, - _source_reservations: source_reservations, - }); - } - Ok(PythonSocketResponse::Charged(PythonSocketImmediate { - payload: PythonVfsRpcResponsePayload::SocketReceived { - data_base64, - closed: false, - timed_out: false, - }, - _response_reservation: response_reservation, - })) - } - Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => Ok( - PythonSocketResponse::Uncharged(PythonVfsRpcResponsePayload::SocketReceived { - data_base64: String::new(), - closed: true, - timed_out: false, - }), - ), - Some(JavascriptTcpSocketEvent::Error { code, message }) => { - let code = code.unwrap_or_else(|| String::from("EIO")); - Err(SidecarError::Execution(format!("{code}: {message}"))) - } - None => Ok(PythonSocketResponse::Uncharged( - PythonVfsRpcResponsePayload::SocketReceived { - data_base64: String::new(), - closed: false, - timed_out: true, - }, - )), - } -} - -fn python_udp_event_response( - event: Option, - max: usize, - resources: &ResourceLedger, -) -> Result { - match event { - Some(JavascriptUdpSocketEvent::Message { - data, remote_addr, .. - }) => { - let (data_base64, response_reservation) = - encode_python_socket_bytes(&data[..max.min(data.len())], resources)?; - Ok(PythonSocketResponse::Charged(PythonSocketImmediate { - payload: PythonVfsRpcResponsePayload::UdpReceived { - data_base64, - host: remote_addr.ip().to_string(), - port: remote_addr.port(), - timed_out: false, - }, - _response_reservation: response_reservation, - })) - } - Some(JavascriptUdpSocketEvent::Error { code, message }) => { - let code = code.unwrap_or_else(|| String::from("EIO")); - Err(SidecarError::Execution(format!("{code}: {message}"))) - } - None => Ok(PythonSocketResponse::Uncharged( - PythonVfsRpcResponsePayload::UdpReceived { - data_base64: String::new(), - host: String::new(), - port: 0, - timed_out: true, - }, - )), - } -} - -fn encode_python_socket_bytes( - bytes: &[u8], - resources: &ResourceLedger, -) -> Result<(String, Reservation), SidecarError> { - let encoded_len = base64::encoded_len(bytes.len(), true).ok_or_else(|| { - SidecarError::Execution(String::from( - "ERR_AGENTOS_RESOURCE_LIMIT: Python socket response length overflowed usize", - )) - })?; - let reservation = resources - .reserve(ResourceClass::BufferedBytes, encoded_len) - .map_err(SidecarError::from)?; - let encoded = base64::engine::general_purpose::STANDARD.encode(bytes); - debug_assert_eq!(encoded.len(), encoded_len); - Ok((encoded, reservation)) -} - -enum PythonSocketOp { - Immediate(PythonVfsRpcResponsePayload), - Charged(PythonSocketImmediate), - Deferred, - Wait(PythonSocketWait), -} - -struct PythonSocketImmediate { - payload: PythonVfsRpcResponsePayload, - _response_reservation: Reservation, -} - -enum PythonSocketResponse { - Uncharged(PythonVfsRpcResponsePayload), - Charged(PythonSocketImmediate), -} - -struct PythonSocketWait { - source: PythonSocketWaitSource, - timeout: Duration, - task_class: agentos_runtime::TaskClass, -} - -enum PythonSocketWaitSource { - Notify(Arc), -} - -fn python_socket_completion_dropped_error() -> SidecarError { - SidecarError::Execution(String::from( - "EPIPE: Python socket task stopped before command completion", - )) -} - -fn respond_python_socket_async( - responder: &PythonVfsRpcResponder, - request_id: u64, - response: Result, -) { - let result = match response { - Ok(payload) => responder.respond_success(request_id, payload), - Err(error) => { - responder.respond_error(request_id, "ERR_AGENTOS_PYTHON_VFS_RPC", error.to_string()) - } - }; - if let Err(error) = result { - eprintln!( - "ERR_AGENTOS_PYTHON_SOCKET_RESPONSE: async Python socket response {request_id} failed: {error}" - ); - } -} - -fn python_socket_kind_error(op: &str, expected: &str) -> SidecarError { - SidecarError::Execution(format!( - "EOPNOTSUPP: python socket {op} requires a {expected} socket" - )) -} - -impl NativeSidecar -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - pub(in crate::execution) async fn handle_python_socket_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - if !self.vms.contains_key(vm_id) { - return Ok(()); - } - match self.python_socket_op(vm_id, process_id, &request).await { - Ok(PythonSocketOp::Immediate(response)) => { - self.respond_python_rpc(vm_id, process_id, request.id, Ok(response)) - } - Ok(PythonSocketOp::Charged(response)) => { - self.respond_python_rpc(vm_id, process_id, request.id, Ok(response.payload)) - } - Ok(PythonSocketOp::Deferred) => Ok(()), - Ok(PythonSocketOp::Wait(wait)) => { - self.schedule_python_socket_wait(vm_id, process_id, request, wait) - } - Err(error) => self.respond_python_rpc(vm_id, process_id, request.id, Err(error)), - } - } - - async fn python_socket_op( - &mut self, - vm_id: &str, - process_id: &str, - request: &PythonVfsRpcRequest, - ) -> Result { - match request.method { - PythonVfsRpcMethod::SocketConnect => { - let host = python_socket_host(request)?; - let port = python_socket_port(request)?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(&host, port), - )?; - let socket_paths = build_javascript_socket_path_context( - self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?, - )?; - let resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - resolve_tcp_connect_addr( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - &host, - port, - None, - &socket_paths, - )? - }; - if !resolved.use_kernel_loopback { - return self - .defer_python_native_tcp_connect(vm_id, process_id, request.id, resolved); - } - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let pending = reserve_capability(&vm.capabilities, CapabilityKind::TcpSocket)?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let socket = ActiveTcpSocket::connect_kernel_loopback( - &mut vm.kernel, - process.kernel_pid, - resolved, - None, - None, - None, - &socket_paths, - vm.capabilities.resources(), - process.runtime_context.clone(), - reactor_io_limits(&process.limits), - )?; - let native_socket_id = process.allocate_tcp_socket_id(); - let capability_key = NativeCapabilityKey::TcpSocket(native_socket_id.clone()); - let identity = match commit_process_capability( - process, - pending, - capability_key.clone(), - native_socket_id.clone(), - socket.kernel_socket_id, - ) { - Ok(identity) => identity, - Err(error) => { - if let Err(close_error) = socket.close(&mut vm.kernel, process.kernel_pid) { - eprintln!( - "ERR_AGENTOS_PYTHON_SOCKET_CLOSE: TCP connect rollback failed: {close_error}" - ); - } - return Err(error); - } - }; - socket - .set_fairness_identity(process.capability_fairness_identity(&capability_key))?; - socket.retain_description_lease( - process - .shared_capability_lease(&capability_key) - .expect("committed Python TCP capability lease"), - ); - register_kernel_readiness_target( - &vm.kernel_socket_readiness, - socket.kernel_socket_id, - None, - Some(Arc::clone(&socket.read_event_notify)), - process.capability_readiness_identity(&capability_key), - native_socket_id.clone(), - KernelSocketReadinessEvent::Data, - ); - process.tcp_sockets.insert(native_socket_id.clone(), socket); - let python_socket_id = process.next_python_socket_id; - process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1); - process.python_sockets.insert( - python_socket_id, - PythonHostSocket::Tcp { - socket_id: native_socket_id, - pending_read: None, - }, - ); - debug_assert!(process.capability_leases.contains_key(&capability_key)); - let _ = identity; - Ok(PythonSocketOp::Immediate( - PythonVfsRpcResponsePayload::SocketCreated { - socket_id: python_socket_id, - }, - )) - } - PythonVfsRpcMethod::SocketSend => { - let python_socket_id = python_socket_id(request)?; - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let data = python_socket_payload(request, process.runtime_context.resources())?; - let native_socket_id = match process.python_sockets.get(&python_socket_id) { - Some(PythonHostSocket::Tcp { socket_id, .. }) => socket_id.clone(), - Some(PythonHostSocket::Udp { .. }) => { - return Err(python_socket_kind_error("send", "TCP")); - } - None => return Err(python_socket_missing_error(python_socket_id)), - }; - process.validate_capability_alias( - &NativeCapabilityKey::TcpSocket(native_socket_id.clone()), - CapabilityKind::TcpSocket, - )?; - let socket = process - .tcp_sockets - .get(&native_socket_id) - .ok_or_else(|| python_socket_backend_missing_error(python_socket_id))?; - if socket.kernel_socket_id.is_some() { - let bytes_sent = - socket.write_all(&mut vm.kernel, process.kernel_pid, &data.bytes)?; - return Ok(PythonSocketOp::Immediate( - PythonVfsRpcResponsePayload::SocketSent { bytes_sent }, - )); - } - let response = socket.begin_plain_write(&data.bytes)?; - let (runtime, responder) = self.python_socket_async_context(vm_id, process_id)?; - let request_id = request.id; - runtime - .spawn(agentos_runtime::TaskClass::Socket, async move { - let response = match response.await { - Ok(Ok(value)) => value - .as_u64() - .and_then(|value| usize::try_from(value).ok()) - .map(|bytes_sent| PythonVfsRpcResponsePayload::SocketSent { - bytes_sent, - }) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "plain TCP transport returned an invalid byte count", - )) - }), - Ok(Err(error)) => Err(SidecarError::Execution(format!( - "{}: {}", - error.code, error.message - ))), - Err(_) => Err(python_socket_completion_dropped_error()), - }; - respond_python_socket_async(&responder, request_id, response); - }) - .map_err(SidecarError::from)?; - Ok(PythonSocketOp::Deferred) - } - PythonVfsRpcMethod::SocketRecv => { - let max = python_socket_recv_len(request); - let python_socket_id = python_socket_id(request)?; - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let resources = Arc::clone(process.runtime_context.resources()); - let mut handle = process - .python_sockets - .remove(&python_socket_id) - .ok_or_else(|| python_socket_missing_error(python_socket_id))?; - let result = (|| { - let PythonHostSocket::Tcp { - socket_id, - pending_read, - } = &mut handle - else { - return Err(python_socket_kind_error("recv", "TCP")); - }; - process.validate_capability_alias( - &NativeCapabilityKey::TcpSocket(socket_id.clone()), - CapabilityKind::TcpSocket, - )?; - if let Some(response) = - consume_python_tcp_pending_read(pending_read, max, &resources)? - { - return Ok(PythonSocketOp::Charged(response)); - } - let socket = process - .tcp_sockets - .get_mut(socket_id) - .ok_or_else(|| python_socket_backend_missing_error(python_socket_id))?; - socket.set_application_read_interest(true)?; - let event = - socket.poll(&mut vm.kernel, process.kernel_pid, Duration::ZERO, false)?; - let wait_timeout = python_socket_wait_timeout(request, socket.reactor_limits); - if event.is_none() && !wait_timeout.is_zero() { - return Ok(PythonSocketOp::Wait(PythonSocketWait { - source: PythonSocketWaitSource::Notify(Arc::clone( - &socket.read_event_notify, - )), - timeout: wait_timeout, - task_class: agentos_runtime::TaskClass::Socket, - })); - } - python_tcp_event_response(event, pending_read, max, &resources).map( - |response| match response { - PythonSocketResponse::Uncharged(response) => { - PythonSocketOp::Immediate(response) - } - PythonSocketResponse::Charged(response) => { - PythonSocketOp::Charged(response) - } - }, - ) - })(); - process.python_sockets.insert(python_socket_id, handle); - result - } - PythonVfsRpcMethod::SocketClose => { - self.remove_python_socket(vm_id, process_id, request)?; - Ok(PythonSocketOp::Immediate( - PythonVfsRpcResponsePayload::Empty, - )) - } - PythonVfsRpcMethod::UdpCreate => { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let pending = reserve_capability(&vm.capabilities, CapabilityKind::UdpSocket)?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let mut socket = ActiveUdpSocket::new_native( - JavascriptUdpFamily::Ipv4, - vm.capabilities.resources(), - process.runtime_context.clone(), - reactor_io_limits(&process.limits), - )?; - let native_socket_id = process.allocate_udp_socket_id(); - let capability_key = NativeCapabilityKey::UdpSocket(native_socket_id.clone()); - commit_process_capability( - process, - pending, - capability_key.clone(), - native_socket_id.clone(), - None, - )?; - socket.set_fairness_identity(process.capability_fairness_identity(&capability_key)); - socket.retain_description_lease( - process - .shared_capability_lease(&capability_key) - .expect("committed Python UDP capability lease"), - ); - process.udp_sockets.insert(native_socket_id.clone(), socket); - let python_socket_id = process.next_python_socket_id; - process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1); - process.python_sockets.insert( - python_socket_id, - PythonHostSocket::Udp { - socket_id: native_socket_id, - }, - ); - Ok(PythonSocketOp::Immediate( - PythonVfsRpcResponsePayload::SocketCreated { - socket_id: python_socket_id, - }, - )) - } - PythonVfsRpcMethod::UdpSendto => { - let host = python_socket_host(request)?; - let port = python_socket_port(request)?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(&host, port), - )?; - let socket_paths = build_javascript_socket_path_context( - self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?, - )?; - let python_socket_id = python_socket_id(request)?; - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let data = python_socket_payload(request, process.runtime_context.resources())?; - let native_socket_id = match process.python_sockets.get(&python_socket_id) { - Some(PythonHostSocket::Udp { socket_id }) => socket_id.clone(), - Some(PythonHostSocket::Tcp { .. }) => { - return Err(python_socket_kind_error("sendto", "UDP")); - } - None => return Err(python_socket_missing_error(python_socket_id)), - }; - process.validate_capability_alias( - &NativeCapabilityKey::UdpSocket(native_socket_id.clone()), - CapabilityKind::UdpSocket, - )?; - let socket = process - .udp_sockets - .get_mut(&native_socket_id) - .ok_or_else(|| python_socket_backend_missing_error(python_socket_id))?; - let send = socket.send_to(ActiveUdpSendToRequest { - bridge: &self.bridge, - kernel: &mut vm.kernel, - kernel_pid: process.kernel_pid, - vm_id, - dns: &vm.dns, - host: &host, - port, - context: &socket_paths, - contents: &data.bytes, - })?; - let bytes_sent = await_udp_send_result(send).await?; - Ok(PythonSocketOp::Immediate( - PythonVfsRpcResponsePayload::SocketSent { bytes_sent }, - )) - } - PythonVfsRpcMethod::UdpRecvfrom => { - let max = python_socket_recv_len(request); - let python_socket_id = python_socket_id(request)?; - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket op for reaped vm/process", - )) - })?; - let resources = Arc::clone(process.runtime_context.resources()); - let native_socket_id = match process.python_sockets.get(&python_socket_id) { - Some(PythonHostSocket::Udp { socket_id }) => socket_id.clone(), - Some(PythonHostSocket::Tcp { .. }) => { - return Err(python_socket_kind_error("recvfrom", "UDP")); - } - None => return Err(python_socket_missing_error(python_socket_id)), - }; - process.validate_capability_alias( - &NativeCapabilityKey::UdpSocket(native_socket_id.clone()), - CapabilityKind::UdpSocket, - )?; - let socket = process - .udp_sockets - .get(&native_socket_id) - .ok_or_else(|| python_socket_backend_missing_error(python_socket_id))?; - let event = socket - .poll(&mut vm.kernel, process.kernel_pid, Duration::ZERO) - .await?; - let wait_timeout = python_socket_wait_timeout(request, socket.reactor_limits); - if event.is_none() && !wait_timeout.is_zero() { - return Ok(PythonSocketOp::Wait(PythonSocketWait { - source: PythonSocketWaitSource::Notify(Arc::clone( - &socket.read_event_notify, - )), - timeout: wait_timeout, - task_class: agentos_runtime::TaskClass::Udp, - })); - } - python_udp_event_response(event, max, &resources).map(|response| match response { - PythonSocketResponse::Uncharged(response) => { - PythonSocketOp::Immediate(response) - } - PythonSocketResponse::Charged(response) => PythonSocketOp::Charged(response), - }) - } - _ => Err(SidecarError::InvalidState(String::from( - "non-socket python RPC reached the socket dispatcher unexpectedly", - ))), - } - } - - fn defer_python_native_tcp_connect( - &mut self, - vm_id: &str, - process_id: &str, - request_id: u64, - resolved: ResolvedTcpConnectAddr, - ) -> Result { - debug_assert!(!resolved.use_kernel_loopback); - let ( - connection_id, - session_id, - runtime, - resources, - limits, - pending_capability, - native_socket_id, - python_socket_id, - ) = { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let pending_capability = - reserve_capability(&vm.capabilities, CapabilityKind::TcpSocket)?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "python socket connect for reaped vm/process", - )) - })?; - let native_socket_id = process.allocate_tcp_socket_id(); - let python_socket_id = process.next_python_socket_id; - process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1); - ( - vm.connection_id.clone(), - vm.session_id.clone(), - process.runtime_context.clone(), - vm.capabilities.resources(), - reactor_io_limits(&process.limits), - pending_capability, - native_socket_id, - python_socket_id, - ) - }; - let task_runtime = runtime.clone(); - let sender = self.process_event_sender.clone(); - let event_notify = Arc::clone(&self.process_event_notify); - let vm_id = vm_id.to_owned(); - let process_id = process_id.to_owned(); - runtime - .spawn(agentos_runtime::TaskClass::Socket, async move { - let result = match tokio::time::timeout( - limits.operation_deadline, - tokio::net::TcpStream::connect(resolved.actual_addr), - ) - .await - { - Ok(Ok(stream)) => { - let built = stream - .local_addr() - .map_err(sidecar_net_error) - .and_then(|local_addr| { - stream - .into_std() - .map_err(sidecar_net_error) - .and_then(|stream| { - ActiveTcpSocket::from_stream( - stream, - None, - local_addr, - resolved.guest_remote_addr, - resources, - task_runtime, - limits, - ) - }) - }); - match built { - Ok(socket) => Ok(PendingPythonTcpConnect { - native_socket_id, - python_socket_id, - socket, - pending_capability, - }), - Err(error) => Err(deferred_connect_error(error)), - } - } - Ok(Err(error)) => Err(deferred_connect_error(sidecar_net_error(error))), - Err(_) => Err(crate::state::DeferredRpcError { - code: String::from("ETIMEDOUT"), - message: format!( - "TCP connect exceeded {}ms; raise limits.reactor.operationDeadlineMs", - limits.operation_deadline.as_millis() - ), - }), - }; - if sender - .send(ProcessEventEnvelope { - connection_id, - session_id, - vm_id, - process_id, - event: ActiveExecutionEvent::PythonSocketConnectCompletion( - Box::new(PythonSocketConnectCompletion { request_id, result }), - ), - }) - .await - .is_err() - { - eprintln!( - "ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: Python TCP connect completion could not be delivered" - ); - } else { - event_notify.notify_one(); - } - }) - .map_err(SidecarError::from)?; - Ok(PythonSocketOp::Deferred) - } - - fn python_socket_async_context( - &self, - vm_id: &str, - process_id: &str, - ) -> Result<(agentos_runtime::RuntimeContext, PythonVfsRpcResponder), SidecarError> { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm.active_processes.get(process_id).ok_or_else(|| { - SidecarError::InvalidState(String::from("python socket op for reaped vm/process")) - })?; - Ok(( - vm.runtime_context.clone(), - process.execution.python_vfs_rpc_responder()?, - )) - } - - fn schedule_python_socket_wait( - &self, - vm_id: &str, - process_id: &str, - mut request: PythonVfsRpcRequest, - wait: PythonSocketWait, - ) -> Result<(), SidecarError> { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let runtime = vm.runtime_context.clone(); - let connection_id = vm.connection_id.clone(); - let session_id = vm.session_id.clone(); - let vm_id = vm_id.to_owned(); - let process_id = process_id.to_owned(); - let sender = self.process_event_sender.clone(); - let event_notify = Arc::clone(&self.process_event_notify); - request.timeout_ms = Some(0); - let cancellation = runtime.clone(); - runtime - .spawn(wait.task_class, async move { - let readiness = async move { - match wait.source { - PythonSocketWaitSource::Notify(notify) => { - let _ = tokio::time::timeout(wait.timeout, notify.notified()).await; - } - } - }; - tokio::select! { - () = readiness => {} - () = cancellation.admission_closed() => return, - } - if !cancellation.admission_is_open() { - return; - } - if sender - .send(ProcessEventEnvelope { - connection_id, - session_id, - vm_id, - process_id, - event: ActiveExecutionEvent::PythonVfsRpcRequest(Box::new(request)), - }) - .await - .is_err() - { - eprintln!( - "ERR_AGENTOS_PROCESS_EVENT_CHANNEL_CLOSED: Python socket readiness completion could not be delivered" - ); - } else { - event_notify.notify_one(); - } - }) - .map_err(SidecarError::from)?; - Ok(()) - } - - fn remove_python_socket( - &mut self, - vm_id: &str, - process_id: &str, - request: &PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(socket_id) = request.socket_id else { - return Ok(()); - }; - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let Some(process) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let Some(socket) = process.python_sockets.get(&socket_id) else { - return Ok(()); - }; - match socket { - PythonHostSocket::Tcp { socket_id, .. } => process.validate_capability_alias( - &NativeCapabilityKey::TcpSocket(socket_id.clone()), - CapabilityKind::TcpSocket, - )?, - PythonHostSocket::Udp { socket_id } => process.validate_capability_alias( - &NativeCapabilityKey::UdpSocket(socket_id.clone()), - CapabilityKind::UdpSocket, - )?, - } - let socket = process - .python_sockets - .remove(&socket_id) - .expect("validated Python socket alias must remain present"); - match socket { - PythonHostSocket::Tcp { - socket_id: native_socket_id, - .. - } => { - if let Some(socket) = process.tcp_sockets.remove(&native_socket_id) { - release_tcp_socket_handle( - process, - &native_socket_id, - socket, - &mut vm.kernel, - &kernel_readiness, - ); - } - } - PythonHostSocket::Udp { - socket_id: native_socket_id, - } => { - if let Some(socket) = process.udp_sockets.remove(&native_socket_id) { - release_udp_socket_handle( - process, - &native_socket_id, - socket, - &mut vm.kernel, - &kernel_readiness, - )?; - } - } - } - Ok(()) - } - - pub(in crate::execution) fn respond_python_rpc( - &mut self, - vm_id: &str, - process_id: &str, - request_id: u64, - response: Result, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let Some(process) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let result = match response { - Ok(payload) => process - .execution - .respond_python_vfs_rpc_success(request_id, payload), - Err(error) => process.execution.respond_python_vfs_rpc_error( - request_id, - "ERR_AGENTOS_PYTHON_VFS_RPC", - error.to_string(), - ), - }; - match result { - Ok(()) => Ok(()), - Err(error) if is_broken_pipe_error(&error) => Ok(()), - Err(error) => Err(error), - } - } -} - -#[cfg(test)] -mod python_socket_accounting_tests { - use super::{ - decode_python_socket_payload, encode_python_socket_bytes, - reserve_plain_socket_write_payload, - }; - use agentos_runtime::accounting::{ResourceClass, ResourceLedger, ResourceLimit}; - use std::sync::Arc; - - #[test] - fn adapter_copies_are_charged_before_decode_encode_and_plain_write() { - let resources = Arc::new(ResourceLedger::root( - "python-socket-accounting", - [ - ( - ResourceClass::BufferedBytes, - ResourceLimit::new(16, "limits.resources.maxSocketBufferedBytes"), - ), - ( - ResourceClass::HandleCommands, - ResourceLimit::new(1, "limits.reactor.maxHandleCommands"), - ), - ( - ResourceClass::HandleCommandBytes, - ResourceLimit::new(4, "limits.reactor.maxHandleCommandBytes"), - ), - ], - )); - - let decoded = decode_python_socket_payload(Some("dGVzdA=="), &resources) - .expect("decode four charged bytes"); - assert_eq!(decoded.bytes, b"test"); - assert_eq!(resources.usage(ResourceClass::BufferedBytes).used, 4); - - let (encoded, encoded_reservation) = encode_python_socket_bytes(&decoded.bytes, &resources) - .expect("reserve base64 response before encoding"); - assert_eq!(encoded, "dGVzdA=="); - assert_eq!(resources.usage(ResourceClass::BufferedBytes).used, 12); - drop(encoded_reservation); - - let write = reserve_plain_socket_write_payload(&resources, &decoded.bytes) - .expect("reserve aggregate and command bytes before plain write copy"); - assert_eq!(resources.usage(ResourceClass::BufferedBytes).used, 8); - assert_eq!(resources.usage(ResourceClass::HandleCommands).used, 1); - assert_eq!(resources.usage(ResourceClass::HandleCommandBytes).used, 4); - drop(write); - drop(decoded); - assert!(resources.is_zero()); - } - - #[test] - fn adapter_decode_limit_rejects_before_payload_allocation() { - let resources = Arc::new(ResourceLedger::root( - "python-socket-small-buffer", - [( - ResourceClass::BufferedBytes, - ResourceLimit::new(3, "limits.resources.maxSocketBufferedBytes"), - )], - )); - let error = decode_python_socket_payload(Some("dGVzdA=="), &resources) - .expect_err("four decoded bytes exceed the configured three-byte budget"); - assert!(error.to_string().contains("ERR_AGENTOS_RESOURCE_LIMIT")); - assert!(resources.is_zero()); - } -} diff --git a/crates/native-sidecar/src/execution/python/subprocess.rs b/crates/native-sidecar/src/execution/python/subprocess.rs deleted file mode 100644 index f5f6ec1010..0000000000 --- a/crates/native-sidecar/src/execution/python/subprocess.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::super::*; - -impl NativeSidecar -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - pub(in crate::execution) async fn handle_python_subprocess_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(command) = request.command.clone() else { - return self.respond_python_rpc( - vm_id, - process_id, - request.id, - Err(SidecarError::InvalidState(String::from( - "python subprocessRun requires a command", - ))), - ); - }; - let (internal_bootstrap_env, cwd) = { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); - }; - let Some(process) = vm.active_processes.get(process_id) else { - return Ok(()); - }; - let virtual_home = guest_virtual_home(vm); - let cwd = request.cwd.clone().or_else(|| { - guest_runtime_path_for_host_path( - &vm.guest_env, - &virtual_home, - &vm.host_cwd, - &process.host_cwd.to_string_lossy(), - ) - }); - ( - sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env), - cwd, - ) - }; - let result = self - .begin_javascript_child_process_sync( - vm_id, - process_id, - JavascriptChildProcessSpawnRequest { - command, - args: request.args.clone(), - options: JavascriptChildProcessSpawnOptions { - cwd, - env: request.env.clone(), - input: None, - internal_bootstrap_env, - shell: request.shell, - detached: false, - stdio: vec![ - String::from("pipe"), - String::from("pipe"), - String::from("pipe"), - ], - timeout: None, - kill_signal: None, - ..JavascriptChildProcessSpawnOptions::default() - }, - }, - request.max_buffer, - PendingChildProcessSyncCompletion::Python { - request_id: request.id, - }, - ) - .await; - match result { - Ok(()) => Ok(()), - Err(error) => self.respond_python_rpc(vm_id, process_id, request.id, Err(error)), - } - } -} diff --git a/crates/native-sidecar/src/execution/signals.rs b/crates/native-sidecar/src/execution/signals.rs index 8f45603e4d..08ccb5890e 100644 --- a/crates/native-sidecar/src/execution/signals.rs +++ b/crates/native-sidecar/src/execution/signals.rs @@ -1,5 +1,69 @@ use super::*; +fn kernel_signal_action_from_registration( + registration: &SignalHandlerRegistration, +) -> Result { + use agentos_kernel::process_table::{SignalAction, SignalDisposition}; + + let mask_signals = registration + .mask + .iter() + .copied() + .map(|signal| { + i32::try_from(signal).map_err(|_| { + SidecarError::host("EINVAL", format!("invalid signal number {signal}")) + }) + }) + .collect::, _>>()?; + let mask = SignalSet::from_signals(mask_signals) + .map_err(|error| SidecarError::host(error.code(), error.to_string()))?; + Ok(SignalAction { + disposition: match registration.action { + SignalDispositionAction::Default => SignalDisposition::Default, + SignalDispositionAction::Ignore => SignalDisposition::Ignore, + SignalDispositionAction::User => SignalDisposition::User, + }, + mask, + flags: registration.flags, + }) +} + +pub(crate) fn apply_kernel_signal_registration( + process: &ActiveProcess, + signal: u32, + registration: &SignalHandlerRegistration, +) -> Result<(), SidecarError> { + let signal = i32::try_from(signal) + .map_err(|_| SidecarError::host("EINVAL", format!("invalid signal number {signal}")))?; + let action = kernel_signal_action_from_registration(registration)?; + process + .kernel_handle + .signal_action(signal, Some(action)) + .map_err(kernel_error)?; + Ok(()) +} + +pub(crate) fn protocol_signal_registration( + action: agentos_kernel::process_table::SignalAction, +) -> SignalHandlerRegistration { + use agentos_kernel::process_table::SignalDisposition; + + SignalHandlerRegistration { + action: match action.disposition { + SignalDisposition::Default => SignalDispositionAction::Default, + SignalDisposition::Ignore => SignalDispositionAction::Ignore, + SignalDisposition::User => SignalDispositionAction::User, + }, + mask: action + .mask + .signals() + .into_iter() + .map(|signal| signal as u32) + .collect(), + flags: action.flags, + } +} + /// Applies a kill signal to a tracked child execution. Shared-runtime /// executions for lethal signals are terminated directly with a synthetic /// signal exit so child polls observe a prompt close; everything else routes @@ -8,184 +72,43 @@ pub(super) fn terminate_tracked_child_process_for_signal( kernel: &mut SidecarKernel, child: &mut ActiveProcess, signal: i32, - registration: Option<&SignalHandlerRegistration>, + _registration: Option<&SignalHandlerRegistration>, ) -> Result<(), SidecarError> { - if signal == 0 { - return kernel - .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal) - .map_err(kernel_error); - } - // The runtime may have published its terminal event before the parent has // polled and reaped it. Keep that queued exit authoritative and make a // cleanup kill idempotent instead of sending a late terminate command to a - // completed V8 session. - if child.execution.has_exited() { - return Ok(()); - } - - if signal == libc::SIGCONT { - apply_active_process_default_signal(kernel, child, signal)?; - match registration.map(|registration| ®istration.action) { - Some(SignalDispositionAction::User) => { - if matches!(&child.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) - { - child.queue_pending_wasm_signal(signal)?; - } else if let Some(session) = child.execution.javascript_v8_session_handle() { - dispatch_v8_session_signal(session, signal); - } else if !dispatch_v8_process_signal(child, signal)? { - return Err(SidecarError::InvalidState(format!( - "unsupported guest SIGCONT handler delivery for pid {}", - child.kernel_pid - ))); - } - } - Some(SignalDispositionAction::Default | SignalDispositionAction::Ignore) | None => {} - } + // completed execution session. + if signal != 0 && child.execution.has_exited() { return Ok(()); } - - // SIGKILL and SIGSTOP are uncatchable. Every other signal first honors the - // guest disposition. Shared WASM consumes user signals cooperatively at - // syscall boundaries instead of receiving an OS signal on a Tokio worker. - if !matches!(signal, libc::SIGKILL | libc::SIGSTOP) { - match registration.map(|registration| ®istration.action) { - Some(SignalDispositionAction::Ignore) => return Ok(()), - Some(SignalDispositionAction::User) => { - if matches!(&child.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) - { - return child.queue_pending_wasm_signal(signal); - } - if let Some(session) = child.execution.javascript_v8_session_handle().filter(|_| { - matches!(&child.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime()) - }) { - dispatch_v8_session_signal(session, signal); - return Ok(()); - } - if dispatch_v8_process_signal(child, signal)? { - return Ok(()); - } - return Err(SidecarError::InvalidState(format!( - "unsupported guest signal handler delivery for pid {}", - child.kernel_pid - ))); - } - Some(SignalDispositionAction::Default) | None => {} - } - } - - if matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGURG") - ) { - return Ok(()); - } - apply_active_process_default_signal(kernel, child, signal) + kernel + .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal) + .map_err(kernel_error)?; + child.apply_runtime_controls() } fn sidecar_error_is_esrch(error: &SidecarError) -> bool { - error.to_string().contains("ESRCH") + guest_error_code(error) == Some("ESRCH") } -pub(crate) fn apply_active_process_default_signal( - kernel: &mut SidecarKernel, - process: &mut ActiveProcess, - signal: i32, -) -> Result<(), SidecarError> { - if matches!( - signal, - libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU - ) { - if process.execution.uses_shared_v8_runtime() { - process.execution.pause()?; - } else { - signal_runtime_process(process.execution.child_pid(), signal)?; - } - return kernel - .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) - .map_err(kernel_error); - } - if signal == libc::SIGCONT { - // Linux resumes a stopped process even when SIGCONT is ignored or has - // a handler. Handler delivery is layered on by the caller afterwards. - if process.execution.uses_shared_v8_runtime() { - process.execution.resume()?; - } else { - signal_runtime_process(process.execution.child_pid(), signal)?; - } - return kernel - .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) - .map_err(kernel_error); - } - - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(kernel, process)?; - } - - if process.execution.uses_shared_v8_runtime() { - process.exit_signal = (signal != 0).then_some(signal); - process.exit_core_dumped = false; - process.execution.terminate()?; - if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) { - process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?; - } - return Ok(()); - } - - signal_runtime_process(process.execution.child_pid(), signal) -} - -pub(super) fn map_wasm_signal_registration( - registration: agentos_execution::wasm::WasmSignalHandlerRegistration, -) -> SignalHandlerRegistration { - SignalHandlerRegistration { - action: match registration.action { - agentos_execution::wasm::WasmSignalDispositionAction::Default => { - crate::protocol::SignalDispositionAction::Default - } - agentos_execution::wasm::WasmSignalDispositionAction::Ignore => { - crate::protocol::SignalDispositionAction::Ignore - } - agentos_execution::wasm::WasmSignalDispositionAction::User => { - crate::protocol::SignalDispositionAction::User - } - }, - mask: registration.mask, - flags: registration.flags, - } +pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> { + agentos_native_sidecar_core::canonical_signal_name(signal) } -pub(super) fn map_node_signal_registration( - registration: NodeSignalHandlerRegistration, +pub(super) fn map_execution_signal_registration( + registration: ExecutionSignalHandlerRegistration, ) -> SignalHandlerRegistration { SignalHandlerRegistration { action: match registration.action { - NodeSignalDispositionAction::Default => SignalDispositionAction::Default, - NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore, - NodeSignalDispositionAction::User => SignalDispositionAction::User, + ExecutionSignalDispositionAction::Default => SignalDispositionAction::Default, + ExecutionSignalDispositionAction::Ignore => SignalDispositionAction::Ignore, + ExecutionSignalDispositionAction::User => SignalDispositionAction::User, }, mask: registration.mask, flags: registration.flags, } } -fn process_signal_state_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str { - child_path.last().copied().unwrap_or(process_id) -} - -pub(super) fn reset_caught_signal_dispositions_after_exec( - signal_states: &mut BTreeMap>, - process_id: &str, - child_path: &[&str], -) -> String { - let signal_key = process_signal_state_key(process_id, child_path).to_owned(); - if let Some(registrations) = signal_states.get_mut(&signal_key) { - registrations - .retain(|_, registration| registration.action == SignalDispositionAction::Ignore); - } - signal_key -} - pub(super) fn javascript_child_process_sync_input_bytes( value: Option<&Value>, ) -> Result>, SidecarError> { @@ -209,49 +132,6 @@ pub(super) fn javascript_child_process_sync_input_bytes( // reconcile_mounts, resolve_cwd moved to crate::vm -fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> { - match signal { - libc::SIGHUP => Some("SIGHUP"), - libc::SIGINT => Some("SIGINT"), - libc::SIGUSR1 => Some("SIGUSR1"), - libc::SIGALRM => Some("SIGALRM"), - libc::SIGCONT => Some("SIGCONT"), - libc::SIGTERM => Some("SIGTERM"), - libc::SIGCHLD => Some("SIGCHLD"), - libc::SIGWINCH => Some("SIGWINCH"), - _ => None, - } -} - -pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> { - agentos_native_sidecar_core::canonical_signal_name(signal) -} - -pub(super) fn dispatch_v8_process_signal( - process: &ActiveProcess, - signal: i32, -) -> Result { - if signal_name_for_stream_event(signal).is_none() { - return Ok(false); - } - let Some(session) = process.execution.javascript_v8_session_handle() else { - return Ok(false); - }; - session - .publish_signal(signal) - .map_err(|error| SidecarError::Execution(error.to_string()))?; - Ok(true) -} - -pub(super) fn dispatch_v8_session_signal(session: V8SessionHandle, signal: i32) { - if signal_name_for_stream_event(signal).is_none() { - return; - } - if let Err(error) = session.publish_signal(signal) { - eprintln!("ERR_AGENTOS_SIGNAL_DELIVERY: could not enqueue signal {signal}: {error}"); - } -} - pub(crate) fn parse_signal(signal: &str) -> Result { let trimmed = signal.trim(); if trimmed.is_empty() { @@ -448,207 +328,21 @@ where .vms .get_mut(vm_id) .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?; - let signal_action = vm - .signal_states - .get(process_id) - .and_then(|handlers| handlers.get(&(signal as u32))) - .map(|registration| registration.action.clone()) - .unwrap_or(SignalDispositionAction::Default); let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}")) })?; - let kernel_pid = process.kernel_pid; + if !matches!(signal, 0 | libc::SIGCONT) { - // A guest parked in a deferred kernel-wait sync RPC is blocked in a - // native bridge wait the kill cannot interrupt; answer the parked - // RPC first so the termination can take effect. + // An executor blocked in a deferred kernel wait must be released so + // it can observe the durable control checkpoint/termination. flush_parked_kernel_wait_rpc(process); } - enum KillBehavior { - Binding, - SharedV8StateOnly, - SharedV8Pause, - SharedV8Continue, - SharedV8Terminate, - SharedV8DispatchOrTerminate, - Noop, - HostPid(u32), - } - - let behavior = match &process.execution { - ActiveExecution::Binding(_) => KillBehavior::Binding, - _ if process.execution.uses_shared_v8_runtime() && signal == 0 => { - KillBehavior::SharedV8StateOnly - } - _ if process.execution.uses_shared_v8_runtime() - && matches!( - signal, - libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU - ) - && (signal == libc::SIGSTOP - || signal_action == SignalDispositionAction::Default) => - { - KillBehavior::SharedV8Pause - } - _ if process.execution.uses_shared_v8_runtime() - && matches!(signal, libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU) - && signal_action == SignalDispositionAction::Ignore => - { - KillBehavior::Noop - } - _ if process.execution.uses_shared_v8_runtime() && signal == libc::SIGCONT => { - KillBehavior::SharedV8Continue - } - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() && signal == SIGKILL => - { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Wasm(execution) - if execution.uses_shared_v8_runtime() && signal == SIGKILL => - { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8DispatchOrTerminate - } - ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8DispatchOrTerminate - } - ActiveExecution::Python(execution) - if execution.uses_shared_v8_runtime() - && signal_action != SignalDispositionAction::Default => - { - KillBehavior::SharedV8DispatchOrTerminate - } - ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => { - KillBehavior::Noop - } - _ => KillBehavior::HostPid(process.execution.child_pid()), - }; + vm.kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) + .map_err(kernel_error)?; + process.apply_runtime_controls()?; - match behavior { - KillBehavior::Binding => { - let ActiveExecution::Binding(execution) = &process.execution else { - unreachable!("kill behavior must match tool execution"); - }; - if signal != 0 { - execution.cancelled.store(true, Ordering::Relaxed); - process.exit_signal = Some(signal); - process.exit_core_dumped = false; - process.queue_pending_execution_event(ActiveExecutionEvent::Exited( - 128 + signal, - ))?; - } - } - KillBehavior::SharedV8StateOnly => { - vm.kernel - .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) - .map_err(kernel_error)?; - } - KillBehavior::SharedV8Pause => { - process.execution.pause()?; - vm.kernel - .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) - .map_err(kernel_error)?; - } - KillBehavior::SharedV8Continue => { - process.execution.resume()?; - vm.kernel - .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) - .map_err(kernel_error)?; - if matches!(&process.execution, ActiveExecution::Javascript(_)) { - if !dispatch_v8_process_signal(process, signal)? { - return Err(SidecarError::InvalidState(format!( - "unsupported guest SIGCONT handler delivery for pid {kernel_pid}" - ))); - } - } else if signal_action == SignalDispositionAction::User { - if matches!(&process.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) - { - process.queue_pending_wasm_signal(signal)?; - } else { - return Err(SidecarError::InvalidState(format!( - "unsupported guest SIGCONT handler delivery for pid {kernel_pid}" - ))); - } - } - } - KillBehavior::SharedV8Terminate => { - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(&mut vm.kernel, process)?; - } - process.exit_signal = (signal != 0).then_some(signal); - process.exit_core_dumped = false; - process.execution.terminate()?; - let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_)) - || (signal == SIGKILL - && matches!(process.execution, ActiveExecution::Javascript(_))); - if signal != 0 && needs_synthetic_exit { - process.queue_pending_execution_event(ActiveExecutionEvent::Exited( - 128 + signal, - ))?; - } - } - KillBehavior::SharedV8DispatchOrTerminate => { - if signal != 0 { - let is_shared_wasm = matches!( - &process.execution, - ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() - ); - if is_shared_wasm { - match signal_action { - SignalDispositionAction::Ignore => {} - SignalDispositionAction::User => { - process.queue_pending_wasm_signal(signal)?; - } - SignalDispositionAction::Default => { - if !matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGURG") - ) { - process.exit_signal = Some(signal); - process.exit_core_dumped = false; - process.execution.terminate()?; - process.queue_pending_execution_event( - ActiveExecutionEvent::Exited(128 + signal), - )?; - } - } - } - } else if matches!(process.execution, ActiveExecution::Python(_)) { - match signal_action { - SignalDispositionAction::Ignore => {} - SignalDispositionAction::User => { - return Err(SidecarError::InvalidState(format!( - "unsupported guest signal handler delivery for pid {kernel_pid}" - ))); - } - SignalDispositionAction::Default => { - process.exit_signal = Some(signal); - process.exit_core_dumped = false; - process.execution.terminate()?; - } - } - } else if !dispatch_v8_process_signal(process, signal)? { - process.exit_signal = Some(signal); - process.exit_core_dumped = false; - process.execution.terminate()?; - } - } - } - KillBehavior::Noop => {} - KillBehavior::HostPid(pid) => { - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(&mut vm.kernel, process)?; - } - signal_runtime_process(pid, signal)?; - } - } emit_security_audit_event( &self.bridge, vm_id, @@ -661,7 +355,11 @@ where (String::from("signal"), signal_name), ( String::from("host_pid"), - process.execution.child_pid().to_string(), + process + .execution + .native_process_id() + .map(|process_id| process_id.to_string()) + .unwrap_or_else(|| String::from("embedded")), ), ]), ); @@ -682,9 +380,10 @@ where let signal = parse_signal(signal_name)?; let located = { let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + String::from("unknown VM during process.kill"), + )); }; let alive = vm .kernel @@ -692,9 +391,10 @@ where .get(&target_kernel_pid) .is_some_and(|info| info.status != ProcessStatus::Exited); if !alive { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process {target_kernel_pid}" - ))); + return Err(SidecarError::host( + "ESRCH", + format!("no such process {target_kernel_pid}"), + )); } vm.active_processes.iter().find_map(|(process_id, root)| { Self::active_process_path_by_kernel_pid(root, target_kernel_pid) @@ -710,26 +410,16 @@ where let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(()); }; - let signal_key = path.last().map(String::as_str).unwrap_or(&process_id); - let registration = vm - .signal_states - .get(signal_key) - .and_then(|handlers| handlers.get(&(signal as u32))) - .cloned(); let Some(root) = vm.active_processes.get_mut(&process_id) else { return Ok(()); }; let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process {target_kernel_pid}" - ))); + return Err(SidecarError::host( + "ESRCH", + format!("no such process {target_kernel_pid}"), + )); }; - terminate_tracked_child_process_for_signal( - &mut vm.kernel, - target, - signal, - registration.as_ref(), - )?; + terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal, None)?; emit_security_audit_event( &self.bridge, vm_id, @@ -748,9 +438,7 @@ where return Ok(()); }; let target_pid = i32::try_from(target_kernel_pid).map_err(|_| { - SidecarError::InvalidState(format!( - "EINVAL: invalid process pid {target_kernel_pid}" - )) + SidecarError::host("EINVAL", format!("invalid process pid {target_kernel_pid}")) })?; vm.kernel .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal) @@ -784,9 +472,10 @@ where parse_signal(signal_name)?; let members = { let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + return Err(SidecarError::host( + "ESRCH", + String::from("unknown VM during process.kill"), + )); }; vm.kernel .list_processes() @@ -796,9 +485,10 @@ where .collect::>() }; if members.is_empty() { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process group {pgid}" - ))); + return Err(SidecarError::host( + "ESRCH", + format!("no such process group {pgid}"), + )); } let mut caller_is_member = false; @@ -852,7 +542,11 @@ where }; for kernel_pid in tracked_members { - match self.signal_vm_kernel_pid(vm_id, kernel_pid, signal_name) { + match self.apply_kernel_generated_signal_to_tracked_runtime( + vm_id, + kernel_pid, + signal_name, + ) { Ok(()) => {} // A process can exit after the group snapshot but before the // tracked runtime is notified. Linux still considers the @@ -863,6 +557,43 @@ where } Ok(()) } + + /// Applies control state that the kernel has already published to a + /// tracked runtime endpoint. This must not call a kernel signal API: doing + /// so would enqueue the same kernel-generated signal twice (for example, + /// the `SIGWINCH` emitted by `KernelVm::pty_resize`). + fn apply_kernel_generated_signal_to_tracked_runtime( + &mut self, + vm_id: &str, + target_kernel_pid: u32, + signal_name: &str, + ) -> Result<(), SidecarError> { + let signal = parse_signal(signal_name)?; + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| SidecarError::host("ESRCH", format!("unknown VM {vm_id}")))?; + let (process_id, path) = vm + .active_processes + .iter() + .find_map(|(process_id, root)| { + Self::active_process_path_by_kernel_pid(root, target_kernel_pid) + .map(|path| (process_id.clone(), path)) + }) + .ok_or_else(|| { + SidecarError::host("ESRCH", format!("no tracked process {target_kernel_pid}")) + })?; + let root = vm + .active_processes + .get_mut(&process_id) + .ok_or_else(|| SidecarError::host("ESRCH", "tracked process disappeared"))?; + let target = Self::active_process_by_owned_path_mut(root, &path) + .ok_or_else(|| SidecarError::host("ESRCH", "tracked process disappeared"))?; + if !matches!(signal, 0 | libc::SIGCONT) { + flush_parked_kernel_wait_rpc(target); + } + target.apply_runtime_controls() + } } #[cfg(test)] diff --git a/crates/native-sidecar/src/execution/stdio.rs b/crates/native-sidecar/src/execution/stdio.rs index 339a4a460c..5aa6308959 100644 --- a/crates/native-sidecar/src/execution/stdio.rs +++ b/crates/native-sidecar/src/execution/stdio.rs @@ -30,6 +30,16 @@ fn wait_fd_until(fd: BorrowedFd<'_>, deadline: Instant, interest: PollFlags) -> } } +fn socket_write_deadline_error(limit: Duration) -> SidecarError { + sidecar_net_error(std::io::Error::new( + std::io::ErrorKind::TimedOut, + format!( + "ERR_AGENTOS_OPERATION_DEADLINE: socket write exceeded {}ms; raise limits.reactor.operationDeadlineMs", + limit.as_millis() + ), + )) +} + pub(super) fn write_all_nonblocking( stream: &mut S, contents: &[u8], @@ -38,10 +48,14 @@ pub(super) fn write_all_nonblocking( where S: Write + AsFd, { - let deadline = Instant::now() + limits.operation_deadline; + let mut deadline = OperationDeadlineTracker::new(limits.operation_deadline); let mut remaining = contents; let mut operations = 0; while !remaining.is_empty() { + deadline.observe("synchronous socket write"); + if deadline.expired() { + return Err(socket_write_deadline_error(limits.operation_deadline)); + } if operations >= limits.operation_quantum.max(1) { std::thread::yield_now(); operations = 0; @@ -60,14 +74,13 @@ where } Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - if !wait_fd_writable_until(stream.as_fd(), deadline) { - return Err(sidecar_net_error(std::io::Error::new( - std::io::ErrorKind::TimedOut, - format!( - "ERR_AGENTOS_OPERATION_DEADLINE: socket write exceeded {}ms; raise limits.reactor.operationDeadlineMs", - limits.operation_deadline.as_millis() - ), - ))); + deadline.observe("synchronous socket write"); + if !wait_fd_writable_until(stream.as_fd(), deadline.next_edge()) { + deadline.observe("synchronous socket write"); + if !deadline.expired() { + continue; + } + return Err(socket_write_deadline_error(limits.operation_deadline)); } } Err(error) => return Err(sidecar_net_error(error)), @@ -79,7 +92,7 @@ where pub(super) fn service_javascript_kernel_stdin_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let (max_bytes, timeout_ms) = parse_kernel_stdin_read_args(request)?; let timeout_ms = timeout_ms.ok_or_else(|| { @@ -87,6 +100,20 @@ pub(super) fn service_javascript_kernel_stdin_sync_rpc( "an indefinite __kernel_stdin_read must use the deferred readiness path", )) })?; + typed_kernel_stdin_read(kernel, process, max_bytes, timeout_ms) +} + +pub(super) fn typed_kernel_stdin_read( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + max_bytes: usize, + timeout_ms: u64, +) -> Result { + // The sidecar writer is nonblocking, so input larger than the kernel pipe + // remains in a bounded owner-side backlog. Every compatibility read must + // refill that pipe before probing it; otherwise the guest stalls after the + // first pipe-capacity chunk even though writeStdin already accepted more. + flush_pending_kernel_stdin(kernel, process)?; kernel_stdin_read_response( kernel, process.kernel_pid, @@ -98,7 +125,7 @@ pub(super) fn service_javascript_kernel_stdin_sync_rpc( /// Parse `__kernel_stdin_read` args: (max bytes, requested timeout ms). pub(crate) fn parse_kernel_stdin_read_args( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result<(usize, Option), SidecarError> { let max_bytes = javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")? @@ -147,7 +174,7 @@ pub(crate) fn kernel_stdin_read_response( Ok(None) => Ok(json!({ "done": true, })), - Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null), + Err(error) if guest_error_code(&error) == Some("EAGAIN") => Ok(Value::Null), Err(error) => Err(error), } } @@ -155,7 +182,7 @@ pub(crate) fn kernel_stdin_read_response( pub(super) fn service_javascript_pty_set_raw_mode_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?; process.tty_raw_mode_generation = kernel @@ -193,7 +220,7 @@ pub(super) fn release_inherited_child_raw_mode( pub(super) fn service_javascript_kernel_isatty_sync_rpc( kernel: &mut SidecarKernel, process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_isatty fd")?; let is_tty = kernel @@ -205,7 +232,7 @@ pub(super) fn service_javascript_kernel_isatty_sync_rpc( pub(super) fn service_javascript_kernel_tty_size_sync_rpc( kernel: &mut SidecarKernel, process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_size fd")?; let size = kernel @@ -217,6 +244,177 @@ pub(super) fn service_javascript_kernel_tty_size_sync_rpc( })) } +pub(super) fn service_javascript_kernel_tty_set_size_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_set_size fd")?; + let cols = javascript_sync_rpc_arg_u32(&request.args, 1, "__kernel_tty_set_size cols")?; + let rows = javascript_sync_rpc_arg_u32(&request.args, 2, "__kernel_tty_set_size rows")?; + let cols = u16::try_from(cols).map_err(|_| { + SidecarError::Host(HostServiceError::new("EINVAL", "TTY columns exceed u16")) + })?; + let rows = u16::try_from(rows) + .map_err(|_| SidecarError::Host(HostServiceError::new("EINVAL", "TTY rows exceed u16")))?; + kernel + .pty_resize(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, cols, rows) + .map_err(kernel_error)?; + Ok(Value::Null) +} + +const TTY_IFLAG_ICRNL: u32 = 1 << 0; +const TTY_OFLAG_OPOST: u32 = 1 << 1; +const TTY_OFLAG_ONLCR: u32 = 1 << 2; +const TTY_LFLAG_ICANON: u32 = 1 << 3; +const TTY_LFLAG_ECHO: u32 = 1 << 4; +const TTY_LFLAG_ISIG: u32 = 1 << 5; + +pub(super) fn service_javascript_kernel_tcgetattr_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetattr fd")?; + let termios = kernel + .tcgetattr(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_error)?; + let mut flags = 0_u32; + if termios.icrnl { + flags |= TTY_IFLAG_ICRNL; + } + if termios.opost { + flags |= TTY_OFLAG_OPOST; + } + if termios.onlcr { + flags |= TTY_OFLAG_ONLCR; + } + if termios.icanon { + flags |= TTY_LFLAG_ICANON; + } + if termios.echo { + flags |= TTY_LFLAG_ECHO; + } + if termios.isig { + flags |= TTY_LFLAG_ISIG; + } + Ok(json!({ + "flags": flags, + "cc": [ + termios.cc.vintr, + termios.cc.vquit, + termios.cc.vsusp, + termios.cc.veof, + termios.cc.verase, + termios.cc.vkill, + termios.cc.vwerase, + ], + })) +} + +pub(super) fn service_javascript_kernel_tcsetattr_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcsetattr fd")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "__kernel_tcsetattr flags")?; + let cc = request + .args + .get(2) + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::Host(HostServiceError::new( + "EINVAL", + "TTY control characters must be an array", + )) + })?; + if cc.len() != 7 { + return Err(SidecarError::Host(HostServiceError::new( + "EINVAL", + format!( + "TTY control character array must contain 7 bytes, observed {}", + cc.len() + ), + ))); + } + let mut parsed = [0_u8; 7]; + for (index, value) in cc.iter().enumerate() { + let value = value + .as_u64() + .and_then(|value| u8::try_from(value).ok()) + .ok_or_else(|| { + SidecarError::Host(HostServiceError::new( + "EINVAL", + "TTY control character must be a byte", + )) + })?; + parsed[index] = value; + } + kernel + .tcsetattr( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + agentos_kernel::pty::PartialTermios { + icrnl: Some(flags & TTY_IFLAG_ICRNL != 0), + opost: Some(flags & TTY_OFLAG_OPOST != 0), + onlcr: Some(flags & TTY_OFLAG_ONLCR != 0), + icanon: Some(flags & TTY_LFLAG_ICANON != 0), + echo: Some(flags & TTY_LFLAG_ECHO != 0), + isig: Some(flags & TTY_LFLAG_ISIG != 0), + cc: Some(agentos_kernel::pty::PartialTermiosControlChars { + vintr: Some(parsed[0]), + vquit: Some(parsed[1]), + vsusp: Some(parsed[2]), + veof: Some(parsed[3]), + verase: Some(parsed[4]), + vkill: Some(parsed[5]), + vwerase: Some(parsed[6]), + }), + }, + ) + .map_err(kernel_error)?; + Ok(Value::Null) +} + +pub(super) fn service_javascript_kernel_tcgetpgrp_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetpgrp fd")?; + kernel + .tcgetpgrp(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(Value::from) + .map_err(kernel_error) +} + +pub(super) fn service_javascript_kernel_tcsetpgrp_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcsetpgrp fd")?; + let pgid = javascript_sync_rpc_arg_u32(&request.args, 1, "__kernel_tcsetpgrp pgid")?; + kernel + .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, pgid) + .map(|()| Value::Null) + .map_err(kernel_error) +} + +pub(super) fn service_javascript_kernel_tcgetsid_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + request: &HostRpcRequest, +) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tcgetsid fd")?; + kernel + .tcgetsid(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(Value::from) + .map_err(kernel_error) +} + /// A TTY in raw mode (no echo, no canonical) — like cfmakeraw. Full-screen apps /// (vim) run raw and drive their own cursor/CRLF, so their output must be passed /// through untouched, NOT round-tripped through the slave->process_output->master @@ -263,15 +461,20 @@ pub(crate) fn drain_tty_master_output( pub(super) fn service_javascript_kernel_stdio_write_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?; let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?; - if fd != 1 && fd != 2 { - return Err(SidecarError::InvalidState(format!( - "__kernel_stdio_write only supports fd 1/2, got {fd}" - ))); - } + typed_kernel_stdio_write(kernel, process, fd, chunk) +} + +pub(super) fn typed_kernel_stdio_write( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + fd: u32, + chunk: Vec, +) -> Result { + let is_stdout = kernel_stdio_output_is_stdout(kernel, process.kernel_pid, fd)?; // COOKED TTY (line shell): route the write through the PTY slave so it flows // through process_output (ONLCR) into the master output buffer interleaved @@ -279,15 +482,9 @@ pub(super) fn service_javascript_kernel_stdio_write_sync_rpc( // ONLCR + echo reach the host. stderr shares the master, merging onto Stdout. let raw_mode = tty_is_raw_mode(kernel, process); if process.tty_master_fd.is_some() && !raw_mode { - let written = if fd == 1 { - kernel - .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk) - .map_err(kernel_error)? - } else { - kernel - .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk) - .map_err(kernel_error)? - }; + let written = kernel + .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &chunk) + .map_err(kernel_error)?; if let Some(master_bytes) = drain_tty_master_output(kernel, process)? { process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(master_bytes))?; } @@ -305,23 +502,7 @@ pub(super) fn service_javascript_kernel_stdio_write_sync_rpc( .fd_write_nonblocking(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &chunk) .map_err(kernel_error)? }; - if written > 0 - && process.tty_master_fd.is_none() - && kernel - .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) - .map_err(kernel_error)? - .filetype - == agentos_kernel::fd_table::FILETYPE_REGULAR_FILE - { - crate::filesystem::mirror_kernel_fd_contents_to_process_shadow( - kernel, - process, - process.kernel_pid, - fd, - )?; - } - - let event = if fd == 1 { + let event = if is_stdout { ActiveExecutionEvent::Stdout(chunk[..written].to_vec()) } else { ActiveExecutionEvent::Stderr(chunk[..written].to_vec()) @@ -331,27 +512,73 @@ pub(super) fn service_javascript_kernel_stdio_write_sync_rpc( Ok(json!(written)) } +/// Classify an fd against the kernel's authoritative stdio descriptions. +/// +/// The path identifies ordinary stdout/stderr aliases and preserves cross-dup +/// routing (`1>&2`/`2>&1`). A PTY slave instead has a `/dev/pts/...` path, so +/// terminal aliases must be matched by open-file-description identity. Only an +/// alias of canonical fd 1 or 2 qualifies; an unrelated PTY is not host stdio. +pub(super) fn kernel_stdio_output_is_stdout( + kernel: &SidecarKernel, + kernel_pid: u32, + fd: u32, +) -> Result { + let descriptor_path = kernel + .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)?; + match descriptor_path.as_str() { + "/dev/stdout" => return Ok(true), + "/dev/stderr" => return Ok(false), + _ => {} + } + + if kernel + .isatty(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)? + { + let description = kernel + .fd_description_identity(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)? + .0; + for (stdio_fd, is_stdout) in [(1, true), (2, false)] { + match kernel.fd_description_identity(EXECUTION_DRIVER_NAME, kernel_pid, stdio_fd) { + Ok((stdio_description, _)) if description == stdio_description => { + return Ok(is_stdout); + } + Ok(_) => {} + Err(error) if error.code() == "EBADF" => {} + Err(error) => return Err(kernel_error(error)), + } + } + } + + Err(SidecarError::host( + "EINVAL", + format!("__kernel_stdio_write fd {fd} does not reference stdout or stderr"), + )) +} + pub(crate) fn service_javascript_kernel_fd_write_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_write fd")?; let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "fd_write data")?; let written = kernel .fd_write_nonblocking(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &chunk) .map_err(kernel_error)?; - // `process.fd_write` is the WASM runner's kernel-fd path. The kernel VFS is - // authoritative here, including host-backed mounts; mirroring the whole - // regular file after each chunk makes streamed writes quadratic and fails - // once the growing file exceeds the configured single-read bound. + // Executor host calls use the kernel VFS as their source of truth, + // including host-backed mounts. Mirroring the whole regular file after + // each chunk makes streamed writes quadratic and fails once the growing + // file exceeds the configured single-read bound. Ok(Value::from(written)) } pub(super) fn service_javascript_kernel_poll_sync_rpc( kernel: &mut SidecarKernel, process: &ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?; kernel_poll_response(kernel, process.kernel_pid, &fd_requests, timeout_ms) @@ -359,7 +586,7 @@ pub(super) fn service_javascript_kernel_poll_sync_rpc( /// Parse `__kernel_poll` args: (fd list, requested timeout ms). pub(crate) fn parse_kernel_poll_args( - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result<(Vec, i32), SidecarError> { let fd_requests: Vec = serde_json::from_value( request @@ -460,6 +687,33 @@ pub(crate) fn install_kernel_stdin_pipe( Ok(write_fd) } +/// Match Node's `stdio: "ignore"` contract by keeping fd 0 open on +/// `/dev/null`. Closing fd 0 is observably different: guest code that probes or +/// reads stdin receives `EBADF`, while native Node presents an immediate EOF. +pub(crate) fn install_kernel_ignored_stdin( + kernel: &mut SidecarKernel, + pid: u32, +) -> Result<(), SidecarError> { + let null_fd = kernel + .fd_open( + EXECUTION_DRIVER_NAME, + pid, + "/dev/null", + agentos_kernel::fd_table::O_RDONLY, + None, + ) + .map_err(kernel_error)?; + if null_fd == 0 { + return Ok(()); + } + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, null_fd, 0) + .map_err(kernel_error)?; + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, null_fd) + .map_err(kernel_error) +} + pub(super) fn requested_pty_window_size(env: &BTreeMap) -> Option<(u16, u16)> { let cols = env .get("COLUMNS") @@ -472,9 +726,7 @@ pub(super) fn requested_pty_window_size(env: &BTreeMap) -> Optio Some((cols, rows)) } -pub(super) fn javascript_child_process_stdin_mode( - request: &JavascriptChildProcessSpawnRequest, -) -> &str { +pub(super) fn javascript_child_process_stdin_mode(request: &ProcessLaunchRequest) -> &str { request .options .stdio @@ -488,13 +740,6 @@ pub(crate) fn write_kernel_process_stdin( process: &mut ActiveProcess, chunk: &[u8], ) -> Result<(), SidecarError> { - // Non-TTY JavaScript uses the in-process local stdin bridge, not a kernel - // fd; a TTY JavaScript process (tty_master_fd set) DOES route through the - // kernel PTY master, exactly like wasm/python, so line discipline + echo - // apply. - if process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_none() { - return Ok(()); - } let Some(writer_fd) = process.kernel_stdin_writer_fd else { return Ok(()); }; @@ -505,28 +750,46 @@ pub(crate) fn write_kernel_process_stdin( if let Some(echo) = drain_tty_master_output(kernel, process)? { process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?; } - forward_tty_slave_input_to_javascript(kernel, process)?; return Ok(()); } - if process + let observed_process_bytes = process .pending_kernel_stdin .total - .saturating_add(chunk.len()) - > process.limits.process.pending_stdin_bytes - { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_PENDING_STDIN_BYTES_LIMIT: child stdin queue exceeds limits.process.pendingStdinBytes ({})", - process.limits.process.pending_stdin_bytes - ))); + .saturating_add(chunk.len()); + if observed_process_bytes > process.limits.process.pending_stdin_bytes { + let limit = process.limits.process.pending_stdin_bytes; + return Err(SidecarError::Host( + HostServiceError::new( + "ERR_AGENTOS_PENDING_STDIN_BYTES_LIMIT", + format!("child stdin queue exceeds limits.process.pendingStdinBytes ({limit})"), + ) + .with_details(json!({ + "limitName": "limits.process.pendingStdinBytes", + "limit": limit, + "observed": observed_process_bytes, + })), + )); } if !process .vm_pending_stdin_bytes_budget .try_reserve(chunk.len()) { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_VM_PENDING_STDIN_BYTES_LIMIT: VM child stdin queues exceed limits.process.pendingStdinBytes ({})", - process.vm_pending_stdin_bytes_budget.limit() - ))); + let limit = process.vm_pending_stdin_bytes_budget.limit(); + let observed = process + .vm_pending_stdin_bytes_budget + .used() + .saturating_add(chunk.len()); + return Err(SidecarError::Host( + HostServiceError::new( + "ERR_AGENTOS_VM_PENDING_STDIN_BYTES_LIMIT", + format!("VM child stdin queues exceed limits.process.pendingStdinBytes ({limit})"), + ) + .with_details(json!({ + "limitName": "limits.process.pendingStdinBytes", + "limit": limit, + "observed": observed, + })), + )); } process.pending_kernel_stdin.push(chunk); process @@ -641,9 +904,10 @@ fn recheck_ready_deferred_fd_reads( match descriptor { Ok((fd, length)) => { process.clear_deferred_kernel_wait_rpc(); - if process - .execution - .claim_javascript_sync_rpc_response(request.id)? + if request + .reply + .claim() + .map_err(|error| SidecarError::Execution(error.to_string()))? { match kernel.fd_read_with_timeout_result( EXECUTION_DRIVER_NAME, @@ -652,44 +916,35 @@ fn recheck_ready_deferred_fd_reads( length, Some(Duration::ZERO), ) { - Ok(Some(bytes)) => process - .execution - .respond_claimed_javascript_sync_rpc_success( - request.id, - javascript_sync_rpc_bytes_value(&bytes), - )?, - Ok(None) => process - .execution - .respond_claimed_javascript_sync_rpc_success( - request.id, - javascript_sync_rpc_bytes_value(&[]), - )?, + Ok(Some(bytes)) => request + .reply + .succeed(HostCallReply::Json(host_bytes_value(&bytes))) + .map_err(|error| SidecarError::Execution(error.to_string()))?, + Ok(None) => request + .reply + .succeed(HostCallReply::Json(host_bytes_value(&[]))) + .map_err(|error| SidecarError::Execution(error.to_string()))?, Err(error) => { let error = kernel_error(error); - process - .execution - .respond_claimed_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - )?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } } } Err(error) => { process.clear_deferred_kernel_wait_rpc(); - if process - .execution - .claim_javascript_sync_rpc_response(request.id)? + if request + .reply + .claim() + .map_err(|error| SidecarError::Execution(error.to_string()))? { - process - .execution - .respond_claimed_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - )?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } } @@ -720,22 +975,15 @@ fn recheck_ready_deferred_fd_writes( match response { Ok(response) => { process.clear_deferred_kernel_wait_rpc(); - process - .execution - .respond_javascript_sync_rpc_response(request.id, response.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + settle_execution_host_call(&request.reply, Ok(response.into()))?; } - Err(error) if javascript_sync_rpc_error_code(&error) == "EAGAIN" => {} + Err(error) if host_service_error_code(&error) == "EAGAIN" => {} Err(error) => { process.clear_deferred_kernel_wait_rpc(); - process - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - javascript_sync_rpc_error_message(&error), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; + request + .reply + .fail(host_service_error(&error)) + .map_err(|error| SidecarError::Execution(error.to_string()))?; } } } @@ -767,45 +1015,6 @@ where } } -/// For a TTY JavaScript guest, cooked input becomes readable on the PTY slave -/// only after line discipline runs (on newline/VEOF in canonical mode; every -/// byte in raw mode). The V8 isolate has no kernel-fd read loop of its own — -/// its stdin is the stream-stdin dispatch fed by `execution.write_stdin` — so -/// right after each master write, drain whatever the discipline released on -/// the slave (fd 0) and forward it to the isolate. A `None` read is the -/// discipline's VEOF: propagate it as end-of-stdin so `process.stdin` emits -/// `end`. Wasm/python guests read the slave themselves and are skipped. -pub(super) fn forward_tty_slave_input_to_javascript( - kernel: &mut SidecarKernel, - process: &mut ActiveProcess, -) -> Result<(), SidecarError> { - if process.tty_master_fd.is_none() - || !matches!(process.execution, ActiveExecution::Javascript(_)) - { - return Ok(()); - } - loop { - match kernel.fd_read_with_timeout_result( - EXECUTION_DRIVER_NAME, - process.kernel_pid, - 0, - MAX_PTY_BUFFER_BYTES, - Some(Duration::ZERO), - ) { - Ok(Some(bytes)) if !bytes.is_empty() => { - process.execution.write_stdin(&bytes)?; - } - Ok(Some(_)) => return Ok(()), - Ok(None) => { - process.execution.close_stdin()?; - return Ok(()); - } - Err(error) if error.code() == "EAGAIN" => return Ok(()), - Err(error) => return Err(kernel_error(error)), - } - } -} - pub(crate) fn close_kernel_process_stdin( kernel: &mut SidecarKernel, process: &mut ActiveProcess, @@ -864,4 +1073,85 @@ mod tests { assert_ne!(flags & agentos_kernel::fd_table::O_NONBLOCK, 0); } + + #[test] + fn ignored_stdin_is_open_dev_null_and_reads_eof() { + let mut config = KernelVmConfig::new("vm-ignored-stdin"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn kernel process"); + + install_kernel_ignored_stdin(&mut kernel, process.pid()).expect("install ignored stdin"); + assert_eq!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, process.pid(), 0, 16) + .expect("read ignored stdin"), + Vec::::new() + ); + assert_eq!( + kernel + .fd_path(EXECUTION_DRIVER_NAME, process.pid(), 0) + .expect("inspect ignored stdin"), + "/dev/null" + ); + } + + #[test] + fn kernel_stdio_classification_preserves_cross_dup_and_pty_identity() { + let mut config = KernelVmConfig::new("vm-kernel-stdio-classification"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let process = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn kernel process"); + let pid = process.pid(); + + let stderr_alias = kernel + .fd_dup(EXECUTION_DRIVER_NAME, pid, 2) + .expect("duplicate stderr"); + assert!(!kernel_stdio_output_is_stdout(&kernel, pid, stderr_alias).unwrap()); + + let (_unrelated_master, unrelated_slave, _) = kernel + .open_pty(EXECUTION_DRIVER_NAME, pid) + .expect("open unrelated PTY"); + let error = kernel_stdio_output_is_stdout(&kernel, pid, unrelated_slave) + .expect_err("unrelated PTY must not become host stdout"); + assert_eq!(error.code(), Some("EINVAL")); + + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, unrelated_slave, 1) + .expect("install PTY stdout"); + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, unrelated_slave, 2) + .expect("install PTY stderr"); + let pty_alias = kernel + .fd_dup(EXECUTION_DRIVER_NAME, pid, 2) + .expect("duplicate PTY stderr"); + assert!( + kernel_stdio_output_is_stdout(&kernel, pid, pty_alias).unwrap(), + "the shared PTY stream is surfaced as ordered stdout" + ); + } } diff --git a/crates/native-sidecar/src/filesystem.rs b/crates/native-sidecar/src/filesystem.rs index b7aee16a1d..ad8b7b5707 100644 --- a/crates/native-sidecar/src/filesystem.rs +++ b/crates/native-sidecar/src/filesystem.rs @@ -1,91 +1,48 @@ //! Guest filesystem and VFS dispatch extracted from service.rs. -use crate::execution::{ - host_path_from_runtime_guest_mappings, is_protected_agentos_shadow_sync_path, - sync_active_process_host_writes_to_kernel, -}; -use crate::protocol::{ - GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, RequestFrame, - ResponsePayload, -}; +use crate::protocol::{GuestFilesystemCallRequest, RequestFrame, ResponsePayload}; use crate::service::{ - javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, javascript_sync_rpc_arg_u32_optional, - javascript_sync_rpc_arg_u64, javascript_sync_rpc_arg_u64_optional, - javascript_sync_rpc_bytes_arg, javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, - javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, kernel_error, - log_stale_process_event, normalize_host_path, normalize_path, path_is_within_root, + host_bytes_value, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, + javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, + javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg, + javascript_sync_rpc_encoding, javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, + kernel_error, normalize_path, }; use crate::state::{ - ActiveExecutionEvent, ActiveProcess, BridgeError, ShadowNodeType, ShadowSyncInventoryEntry, - SidecarKernel, VmState, EXECUTION_DRIVER_NAME, PYTHON_VFS_RPC_GUEST_ROOT, + ActiveExecutionEvent, ActiveProcess, BridgeError, SidecarKernel, EXECUTION_DRIVER_NAME, }; use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError}; -use base64::Engine; -use nix::errno::Errno; -use nix::fcntl::OFlag; -use nix::libc; - -// The universal resolver (`crate::plugins::host_dir::confine`) never returns a metadata-only `O_PATH` -// handle (macOS has no `O_PATH`); a read-only open stands in as the anchor and -// every operation is performed fd-relative, so `O_RDONLY` is the portable -// anchor open mode. `O_TMPFILE` is never passed by any caller (it appears only -// in a defensive `intersects` check), so an empty flag matches exactly. -const O_PATH_ANCHOR: OFlag = OFlag::O_RDONLY; -#[cfg(target_os = "linux")] -const CHMOD_PATH_ANCHOR: OFlag = OFlag::O_PATH; -#[cfg(not(target_os = "linux"))] -const CHMOD_PATH_ANCHOR: OFlag = OFlag::O_RDONLY; -const O_TMPFILE_FLAG: OFlag = OFlag::empty(); use agentos_execution::{ - JavascriptSyncRpcRequest, LocalResolvedModuleFormat, ModuleFsReader, ModuleResolveMode, - ModuleResolver, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload, - PythonVfsRpcStat, + backend::HostServiceError, HostRpcRequest, LocalResolvedModuleFormat, ModuleFsReader, + ModuleResolveMode, ModuleResolver, }; use agentos_kernel::kernel::is_internal_unnamed_file_name; -use agentos_kernel::vfs::{ - VirtualFileSystem, VirtualStat, VirtualTimeSpec, VirtualUtimeSpec, RENAME_EXCHANGE, - RENAME_NOREPLACE, -}; -use agentos_native_sidecar_core::{ - decode_guest_filesystem_content, handle_guest_filesystem_call as core_guest_filesystem_call, -}; -use nix::sys::stat::{utimensat, Mode, UtimensatFlags}; -use nix::sys::time::TimeSpec; -use serde::Deserialize; +use agentos_kernel::vfs::{VirtualStat, VirtualTimeSpec, VirtualUtimeSpec}; +use agentos_native_sidecar_core::handle_guest_filesystem_call as core_guest_filesystem_call; +use nix::libc; use serde_json::{json, Map, Value}; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::env; -use std::ffi::OsString; use std::fmt; -use std::fs::{self, OpenOptions}; -use std::io::{Read, Write}; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; -use std::os::unix::fs::{symlink, FileExt, MetadataExt, PermissionsExt}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::fs; use std::sync::{Mutex, OnceLock}; use std::time::Instant; -const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide"; -static NEXT_SHADOW_RENAME_EXCHANGE_ID: AtomicU64 = AtomicU64::new(1); - fn kernel_path_error( operation: &str, path: &str, error: impl Into, ) -> SidecarError { let error = error.into(); - let base = kernel_error(error); + let base = SidecarError::host( + error.code(), + format!("{operation} {path}: {}", error.message()), + ); if std::env::var_os("AGENTOS_TRACE_FS_ERRORS").is_some() { eprintln!("[agent-os-fs-error] operation={operation} path={path} error={base}"); } - match base { - SidecarError::Kernel(message) => { - SidecarError::Kernel(format!("{operation} {path}: {message}")) - } - other => other, - } + base } fn classify_fiemap_ranges( @@ -119,153 +76,9 @@ fn classify_fiemap_ranges( classified } -const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache"; const UTIME_NOW_NSEC: i64 = libc::UTIME_NOW; const UTIME_OMIT_NSEC: i64 = libc::UTIME_OMIT; -/// Backstop bound on a guest-controlled `ftruncate` length for a mapped host fd. -/// The kernel's configured truncate-size limit is the primary enforcement for -/// paths visible in the VFS; this caps the raw host `set_len` (and covers fds -/// with no kernel-visible guest path) so a hostile length cannot create an -/// enormous sparse host file or drive an unbounded sidecar-side mirror read. -const MAX_MAPPED_TRUNCATE_BYTES: u64 = 4 * 1024 * 1024 * 1024; - -#[derive(Debug, Clone)] -struct MappedRuntimeHostPath { - guest_path: String, - host_root: PathBuf, - host_path: PathBuf, -} - -#[derive(Debug, Clone)] -enum MappedRuntimeHostAccess { - Writable(MappedRuntimeHostPath), - ReadOnly(MappedRuntimeHostPath), -} - -/// An owned file descriptor resolved strictly beneath a mount root by -/// [`crate::plugins::host_dir::confine::resolve_beneath`]. All operations go through the fd (fd-relative -/// `*at` calls, `fstat`, fd `read`/`write`) — never a recovered path string — so -/// they stay confined to the resolved object and TOCTOU-safe. The `OwnedFd` -/// closes the descriptor on drop. -#[derive(Debug)] -struct AnchoredFd { - fd: OwnedFd, -} - -impl AnchoredFd { - /// `fstat` the resolved object. - fn metadata(&self) -> std::io::Result { - nix::sys::stat::fstat(self.as_raw_fd()) - .map(|stat| HostStat::from_filestat(&stat)) - .map_err(errno_to_io) - } - - /// Read the entire resolved file via the fd. - fn read_bytes(&self) -> std::io::Result> { - read_all_from_fd(self.fd.as_fd()) - } - - /// Read the entire resolved file via the fd as UTF-8. - fn read_to_string(&self) -> std::io::Result { - let bytes = self.read_bytes()?; - String::from_utf8(bytes) - .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error)) - } - - /// Write `data` to the resolved file via the fd (the fd must have been opened - /// writable). - fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> { - write_all_to_fd(self.fd.as_fd(), data) - } - - /// `fchmod` the resolved object. - fn set_mode(&self, mode: u32) -> std::io::Result<()> { - let result = nix::sys::stat::fchmod( - self.as_raw_fd(), - Mode::from_bits_truncate(mode as nix::libc::mode_t), - ); - #[cfg(target_os = "linux")] - if result == Err(Errno::EBADF) { - // Linux rejects fchmod(2) on O_PATH handles. Resolve the stable - // descriptor through procfs so chmod follows the already-confined - // inode rather than reopening the guest-controlled pathname. - return fs::set_permissions( - format!("/proc/self/fd/{}", self.as_raw_fd()), - fs::Permissions::from_mode(mode & 0o7777), - ); - } - result.map_err(errno_to_io) - } - - /// `futimens` the resolved object. - fn set_times(&self, atime: &TimeSpec, mtime: &TimeSpec) -> std::io::Result<()> { - nix::sys::stat::futimens(self.as_raw_fd(), atime, mtime).map_err(errno_to_io) - } - - /// Consume the handle, yielding the owned fd (e.g. to build a persistent - /// [`std::fs::File`]). - fn into_owned_fd(self) -> OwnedFd { - self.fd - } -} - -impl AsRawFd for AnchoredFd { - fn as_raw_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} - -/// Read an entire file from `fd` into a `Vec`, using fd `read` (no path re-open). -fn read_all_from_fd(fd: BorrowedFd<'_>) -> std::io::Result> { - let mut out = Vec::new(); - let mut buf = [0_u8; 65536]; - loop { - let read = nix::unistd::read(fd.as_raw_fd(), &mut buf).map_err(errno_to_io)?; - if read == 0 { - break; - } - out.extend_from_slice(&buf[..read]); - } - Ok(out) -} - -/// Write all of `data` to `fd`, using fd `write` (no path re-open). -fn write_all_to_fd(fd: BorrowedFd<'_>, mut data: &[u8]) -> std::io::Result<()> { - while !data.is_empty() { - let written = nix::unistd::write(fd, data).map_err(errno_to_io)?; - if written == 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::WriteZero, - "failed to write whole buffer to mapped host fd", - )); - } - data = &data[written..]; - } - Ok(()) -} - -#[derive(Debug)] -struct MappedRuntimeOpenedPath { - handle: AnchoredFd, - host_path: PathBuf, -} - -#[derive(Debug)] -struct MappedRuntimeParentPath { - directory: AnchoredFd, - host_path: PathBuf, - child_name: OsString, -} - -#[derive(Debug, Deserialize)] -struct RuntimeGuestPathMappingWire { - #[serde(rename = "guestPath")] - guest_path: String, - #[serde(rename = "hostPath")] - host_path: String, -} - fn parse_timespec_seconds(value: f64, label: &str) -> Result { if !value.is_finite() { return Err(SidecarError::InvalidState(format!( @@ -363,113 +176,6 @@ fn parse_utime_arg( parse_utime_spec_value(value, label) } -fn metadata_timespec( - metadata: &fs::Metadata, - access_time: bool, -) -> Result { - let (sec, nsec) = if access_time { - (metadata.atime(), metadata.atime_nsec()) - } else { - (metadata.mtime(), metadata.mtime_nsec()) - }; - VirtualTimeSpec::new(sec, nsec.clamp(0, 999_999_999) as u32) - .map_err(|error| SidecarError::InvalidState(format!("invalid host metadata time: {error}"))) -} - -fn resolve_host_utime(spec: VirtualUtimeSpec, existing: VirtualTimeSpec) -> TimeSpec { - match spec { - VirtualUtimeSpec::Set(spec) => TimeSpec::new(spec.sec, spec.nsec as libc::c_long), - VirtualUtimeSpec::Now => TimeSpec::new(0, libc::UTIME_NOW), - VirtualUtimeSpec::Omit => TimeSpec::new(existing.sec, libc::UTIME_OMIT), - } -} - -fn apply_host_path_utimens( - host_path: &Path, - atime: VirtualUtimeSpec, - mtime: VirtualUtimeSpec, - follow_symlinks: bool, - context: &str, -) -> Result<(), SidecarError> { - let existing = match (atime, mtime) { - (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { - let metadata = if follow_symlinks { - fs::metadata(host_path) - } else { - fs::symlink_metadata(host_path) - } - .map_err(|error| { - SidecarError::Io(format!( - "{context}: failed to stat {}: {error}", - host_path.display() - )) - })?; - Some(( - metadata_timespec(&metadata, true)?, - metadata_timespec(&metadata, false)?, - )) - } - _ => None, - }; - let existing_atime = existing - .as_ref() - .map(|(atime, _)| *atime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let existing_mtime = existing - .as_ref() - .map(|(_, mtime)| *mtime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let times = [ - resolve_host_utime(atime, existing_atime), - resolve_host_utime(mtime, existing_mtime), - ]; - let flags = if follow_symlinks { - UtimensatFlags::FollowSymlink - } else { - UtimensatFlags::NoFollowSymlink - }; - utimensat(None, host_path, ×[0], ×[1], flags).map_err(|error| { - SidecarError::Io(format!( - "{context}: failed to update {}: {error}", - host_path.display() - )) - }) -} - -fn apply_host_file_utimens( - file: &fs::File, - atime: VirtualUtimeSpec, - mtime: VirtualUtimeSpec, - context: &str, -) -> Result<(), SidecarError> { - let existing = match (atime, mtime) { - (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { - let metadata = file - .metadata() - .map_err(|error| SidecarError::Io(format!("{context}: failed to stat: {error}")))?; - Some(( - metadata_timespec(&metadata, true)?, - metadata_timespec(&metadata, false)?, - )) - } - _ => None, - }; - let existing_atime = existing - .as_ref() - .map(|(atime, _)| *atime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let existing_mtime = existing - .as_ref() - .map(|(_, mtime)| *mtime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - nix::sys::stat::futimens( - file.as_raw_fd(), - &resolve_host_utime(atime, existing_atime), - &resolve_host_utime(mtime, existing_mtime), - ) - .map_err(|error| SidecarError::Io(format!("{context}: failed to set times: {error}"))) -} - pub(crate) async fn guest_filesystem_call( sidecar: &mut NativeSidecar, request: &RequestFrame, @@ -494,10 +200,8 @@ where )); } }; - sync_guest_filesystem_shadow_before_call(vm, &payload)?; - let response = core_guest_filesystem_call(&mut vm.kernel, payload.clone()) + let response = core_guest_filesystem_call(&mut vm.kernel, payload) .map_err(native_guest_filesystem_core_error)?; - mirror_guest_filesystem_shadow_after_call(vm, &payload)?; response }; @@ -510,548 +214,12 @@ where fn native_guest_filesystem_core_error( error: agentos_native_sidecar_core::SidecarCoreError, ) -> SidecarError { - let message = error.to_string(); - if message - .split_once(':') - .is_some_and(|(code, _)| is_posix_errno_code(code)) - { - SidecarError::Kernel(message) - } else { - SidecarError::InvalidState(message) - } -} - -fn is_posix_errno_code(code: &str) -> bool { - code.len() >= 2 - && code.starts_with('E') - && code[1..] - .bytes() - .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') -} - -fn sync_guest_filesystem_shadow_before_call( - vm: &mut VmState, - payload: &GuestFilesystemCallRequest, -) -> Result<(), SidecarError> { - match payload.operation { - GuestFilesystemOperation::ReadFile - | GuestFilesystemOperation::Pread - | GuestFilesystemOperation::Pwrite - | GuestFilesystemOperation::Exists - | GuestFilesystemOperation::Stat - | GuestFilesystemOperation::Lstat - | GuestFilesystemOperation::ReadDirRecursive - | GuestFilesystemOperation::Remove - | GuestFilesystemOperation::Copy - | GuestFilesystemOperation::Move => { - // Pwrite is a partial write that preserves the unmodified bytes, so - // the existing shadow content must be present in the kernel before - // the call, exactly like a read. - sync_active_shadow_path_to_kernel(vm, &payload.path)?; - } - GuestFilesystemOperation::WriteFile - | GuestFilesystemOperation::CreateDir - | GuestFilesystemOperation::Mkdir - | GuestFilesystemOperation::ReadDir - | GuestFilesystemOperation::RemoveFile - | GuestFilesystemOperation::RemoveDir - | GuestFilesystemOperation::Rename - | GuestFilesystemOperation::Realpath - | GuestFilesystemOperation::Symlink - | GuestFilesystemOperation::ReadLink - | GuestFilesystemOperation::Link - | GuestFilesystemOperation::Chmod - | GuestFilesystemOperation::Chown - | GuestFilesystemOperation::Utimes - | GuestFilesystemOperation::Truncate => {} - } - Ok(()) -} - -fn mirror_guest_filesystem_shadow_after_call( - vm: &mut VmState, - payload: &GuestFilesystemCallRequest, -) -> Result<(), SidecarError> { - match payload.operation { - GuestFilesystemOperation::WriteFile => { - let bytes = decode_guest_filesystem_content( - &payload.path, - payload.content.as_deref(), - payload.encoding.clone(), - ) - .map_err(|error| SidecarError::InvalidState(error.to_string()))?; - mirror_guest_file_write_to_shadow(vm, &payload.path, &bytes)?; - refresh_shadow_inventory_path(vm, &payload.path)?; - } - GuestFilesystemOperation::Pwrite => { - // A positional write only carries the changed region; mirror the - // full post-write file from the kernel so the shadow stays faithful. - let bytes = vm.kernel.read_file(&payload.path).map_err(kernel_error)?; - mirror_guest_file_write_to_shadow(vm, &payload.path, &bytes)?; - refresh_shadow_inventory_path(vm, &payload.path)?; - } - GuestFilesystemOperation::CreateDir | GuestFilesystemOperation::Mkdir => { - mirror_guest_directory_write_to_shadow(vm, &payload.path)?; - // A mkdir result can only add the requested empty directory and, - // for recursive mkdir, missing ancestors. Inventory those nodes - // directly instead of performing a guest-visible recursive readdir - // that would require an unrelated `fs.readdir` permission. - refresh_shadow_inventory_node(vm, &payload.path)?; - } - GuestFilesystemOperation::RemoveFile | GuestFilesystemOperation::RemoveDir => { - remove_guest_shadow_path(vm, &payload.path)?; - forget_shadow_inventory_path(vm, &payload.path); - } - GuestFilesystemOperation::Remove => { - remove_guest_shadow_path(vm, &payload.path)?; - forget_shadow_inventory_path(vm, &payload.path); - } - GuestFilesystemOperation::Copy => { - let destination = payload.destination_path.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem copy requires a destination_path", - )) - })?; - remove_guest_shadow_path(vm, destination)?; - mirror_guest_subtree_to_shadow(vm, destination)?; - refresh_shadow_inventory_path(vm, destination)?; - } - GuestFilesystemOperation::Move => { - let destination = payload.destination_path.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem move requires a destination_path", - )) - })?; - remove_guest_shadow_path(vm, &payload.path)?; - remove_guest_shadow_path(vm, destination)?; - mirror_guest_subtree_to_shadow(vm, destination)?; - forget_shadow_inventory_path(vm, &payload.path); - refresh_shadow_inventory_path(vm, destination)?; - } - GuestFilesystemOperation::Rename => { - let destination = payload.destination_path.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem rename requires a destination_path", - )) - })?; - rename_guest_shadow_path(vm, &payload.path, destination)?; - forget_shadow_inventory_path(vm, &payload.path); - refresh_shadow_inventory_path(vm, destination)?; - } - GuestFilesystemOperation::Symlink => { - let target = payload.target.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem symlink requires a target", - )) - })?; - mirror_guest_symlink_to_shadow(vm, &payload.path, target)?; - refresh_shadow_inventory_path(vm, &payload.path)?; - } - GuestFilesystemOperation::Link => { - let destination = payload.destination_path.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem link requires a destination_path", - )) - })?; - mirror_guest_link_to_shadow(vm, &payload.path, destination)?; - refresh_shadow_inventory_path(vm, &payload.path)?; - refresh_shadow_inventory_path(vm, destination)?; - } - GuestFilesystemOperation::Chmod => { - let mode = payload.mode.ok_or_else(|| { - SidecarError::InvalidState(String::from("guest filesystem chmod requires a mode")) - })?; - mirror_guest_chmod_to_shadow(vm, &payload.path, mode)?; - refresh_shadow_inventory_node(vm, &payload.path)?; - } - GuestFilesystemOperation::Utimes => { - let atime_ms = payload.atime_ms.ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem utimes requires atime_ms", - )) - })?; - let mtime_ms = payload.mtime_ms.ok_or_else(|| { - SidecarError::InvalidState(String::from( - "guest filesystem utimes requires mtime_ms", - )) - })?; - mirror_guest_utimes_to_shadow( - vm, - &payload.path, - VirtualUtimeSpec::Set(VirtualTimeSpec::from_millis(atime_ms)), - VirtualUtimeSpec::Set(VirtualTimeSpec::from_millis(mtime_ms)), - true, - )?; - refresh_shadow_inventory_node(vm, &payload.path)?; - } - GuestFilesystemOperation::Truncate => { - let len = payload.len.ok_or_else(|| { - SidecarError::InvalidState(String::from("guest filesystem truncate requires len")) - })?; - mirror_guest_truncate_to_shadow(vm, &payload.path, len)?; - refresh_shadow_inventory_node(vm, &payload.path)?; - } - GuestFilesystemOperation::ReadFile - | GuestFilesystemOperation::Pread - | GuestFilesystemOperation::Exists - | GuestFilesystemOperation::Stat - | GuestFilesystemOperation::Lstat - | GuestFilesystemOperation::ReadDir - | GuestFilesystemOperation::ReadDirRecursive - | GuestFilesystemOperation::Realpath - | GuestFilesystemOperation::ReadLink - | GuestFilesystemOperation::Chown => {} - } - Ok(()) -} - -/// Keep the deletion/type inventory current when a wire filesystem mutation -/// mirrors kernel state into the host shadow. Without this write-side update, -/// a host runtime can delete a freshly-created shadow path before the next -/// read-side reconciliation and the kernel copy will be resurrected because -/// that pathname was never part of the previous inventory. -fn refresh_shadow_inventory_path(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?; - let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? else { - forget_shadow_inventory_path(vm, &guest_path); - return Ok(()); - }; - updates.insert( - guest_path.clone(), - ShadowSyncInventoryEntry::present(node_type), - ); - if node_type == ShadowNodeType::Directory { - let entries = vm - .kernel - .read_dir_recursive(&guest_path, None) - .map_err(kernel_error)?; - for entry in entries { - let path = normalize_path(&entry.path); - if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &path)? { - updates.insert(path, ShadowSyncInventoryEntry::present(node_type)); - } - } - } - - // Commit only after the complete replacement inventory has been built. - // A failed stat/read must leave the previous deletion baseline intact. - forget_shadow_inventory_path(vm, &guest_path); - vm.shadow_sync_inventory.extend(updates); - Ok(()) -} - -/// Refresh only a pathname's structural type and any newly mirrored ancestors. -/// Freshly created directories are empty, and metadata operations such as -/// chmod/utimes must not recursively read a directory after making it mode 000: -/// Linux reports the operation as successful, and existing descendant inventory -/// remains valid even though readdir is now denied. -fn refresh_shadow_inventory_node(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?; - if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? { - updates.insert( - guest_path.clone(), - ShadowSyncInventoryEntry::present(node_type), - ); - } - vm.shadow_sync_inventory.extend(updates); - Ok(()) -} - -fn collect_shadow_inventory_ancestors( - vm: &mut VmState, - guest_path: &str, -) -> Result, SidecarError> { - // `create_dir_all` may have created ancestors as part of mirroring a leaf. - // Record those too so removing a newly-created empty parent directly from - // the shadow has the same unlink/rmdir effect in the kernel VFS. - let mut ancestors = Vec::new(); - let mut cursor = guest_path.to_owned(); - while let Some(parent) = Path::new(&cursor).parent() { - let parent = normalize_path(&parent.to_string_lossy()); - if parent == "/" { - break; - } - ancestors.push(parent.clone()); - cursor = parent; - } - let mut updates = BTreeMap::new(); - for ancestor in ancestors.into_iter().rev() { - if shadow_inventory_kernel_node_type(vm, &ancestor)? == Some(ShadowNodeType::Directory) { - updates.insert( - ancestor, - ShadowSyncInventoryEntry::present(ShadowNodeType::Directory), - ); - } - } - Ok(updates) -} - -fn shadow_inventory_kernel_node_type( - vm: &mut VmState, - guest_path: &str, -) -> Result, SidecarError> { - // This inventory is trusted sidecar bookkeeping after the guest mutation has - // already passed its operation-specific permission check. Re-entering through - // `KernelVm::lstat` would incorrectly require a separate guest `fs.stat` - // permission for each parent that the shadow mirror records. - let stat = match vm.kernel.filesystem_mut().inner_mut().lstat(guest_path) { - Ok(stat) => stat, - Err(error) if error.code() == "ENOENT" => return Ok(None), - Err(error) => return Err(kernel_error(error.into())), - }; - let node_type = if stat.is_symbolic_link { - ShadowNodeType::Symlink - } else if stat.is_directory { - ShadowNodeType::Directory - } else { - ShadowNodeType::File - }; - Ok(Some(node_type)) -} - -fn forget_shadow_inventory_path(vm: &mut VmState, guest_path: &str) { - let guest_path = normalize_path(guest_path); - vm.shadow_sync_inventory - .retain(|path, _| !shadow_inventory_path_is_at_or_below(path, &guest_path)); -} - -fn shadow_inventory_path_is_at_or_below(path: &str, prefix: &str) -> bool { - path == prefix || (prefix != "/" && path.starts_with(&format!("{prefix}/"))) -} - -pub(crate) fn handle_python_vfs_rpc_request( - sidecar: &mut NativeSidecar, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, -) -> Result<(), SidecarError> -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - let Some(vm) = sidecar.vms.get(vm_id) else { - log_stale_process_event(&sidecar.bridge, vm_id, process_id, "python VFS RPC"); - return Ok(()); - }; - if !vm.active_processes.contains_key(process_id) { - log_stale_process_event(&sidecar.bridge, vm_id, process_id, "python VFS RPC"); - return Ok(()); - } - - let response = match normalize_python_vfs_rpc_path(&request.path) { - Ok(path) => { - let Some(vm) = sidecar.vms.get_mut(vm_id) else { - log_stale_process_event(&sidecar.bridge, vm_id, process_id, "python VFS RPC"); - return Ok(()); - }; - match request.method { - PythonVfsRpcMethod::Read => vm - .kernel - .read_file(&path) - .map(|content| PythonVfsRpcResponsePayload::Read { - content_base64: base64::engine::general_purpose::STANDARD.encode(content), - }) - .map_err(kernel_error), - PythonVfsRpcMethod::Write => { - let content_base64 = request.content_base64.as_deref().ok_or_else(|| { - SidecarError::InvalidState(format!( - "python VFS fsWrite for {} requires contentBase64", - path - )) - })?; - let bytes = base64::engine::general_purpose::STANDARD - .decode(content_base64) - .map_err(|error| { - SidecarError::InvalidState(format!( - "invalid base64 python VFS content for {}: {error}", - path - )) - })?; - vm.kernel - .write_file(&path, bytes) - .map(|()| PythonVfsRpcResponsePayload::Empty) - .map_err(kernel_error) - } - PythonVfsRpcMethod::Stat => vm - .kernel - .stat(&path) - .map(|stat| PythonVfsRpcResponsePayload::Stat { - stat: PythonVfsRpcStat { - mode: stat.mode, - size: stat.size, - is_directory: stat.is_directory, - is_symbolic_link: stat.is_symbolic_link, - }, - }) - .map_err(kernel_error), - // Like Stat but does NOT follow symlinks, so the runner can - // represent a host-preexisting symlink as a link node. - PythonVfsRpcMethod::Lstat => vm - .kernel - .lstat(&path) - .map(|stat| PythonVfsRpcResponsePayload::Stat { - stat: PythonVfsRpcStat { - mode: stat.mode, - size: stat.size, - is_directory: stat.is_directory, - is_symbolic_link: stat.is_symbolic_link, - }, - }) - .map_err(kernel_error), - PythonVfsRpcMethod::ReadDir => vm - .kernel - .read_dir(&path) - .map(|entries| PythonVfsRpcResponsePayload::ReadDir { entries }) - .map_err(kernel_error), - PythonVfsRpcMethod::Mkdir => vm - .kernel - .mkdir(&path, request.recursive) - .map(|()| PythonVfsRpcResponsePayload::Empty) - .map_err(kernel_error), - // Mirror the delete/rename into the host-side shadow too, the - // same way the wire `GuestFilesystemOperation` handlers do — - // otherwise a later shadow→kernel sync would resurrect the - // entry the guest just removed. - PythonVfsRpcMethod::Unlink => { - match vm.kernel.remove_file(&path).map_err(kernel_error) { - Ok(()) => remove_guest_shadow_path(vm, &path).map(|()| { - forget_shadow_inventory_path(vm, &path); - PythonVfsRpcResponsePayload::Empty - }), - Err(error) => Err(error), - } - } - PythonVfsRpcMethod::Rmdir => { - match vm.kernel.remove_dir(&path).map_err(kernel_error) { - Ok(()) => remove_guest_shadow_path(vm, &path).map(|()| { - forget_shadow_inventory_path(vm, &path); - PythonVfsRpcResponsePayload::Empty - }), - Err(error) => Err(error), - } - } - PythonVfsRpcMethod::Rename => { - let destination = request.destination.as_deref().ok_or_else(|| { - SidecarError::InvalidState(format!( - "python VFS fsRename for {} requires destination", - path - )) - })?; - let destination = normalize_python_vfs_rpc_path(destination)?; - match vm.kernel.rename(&path, &destination).map_err(kernel_error) { - Ok(()) => { - rename_guest_shadow_path(vm, &path, &destination).and_then(|()| { - forget_shadow_inventory_path(vm, &path); - refresh_shadow_inventory_path(vm, &destination)?; - Ok(PythonVfsRpcResponsePayload::Empty) - }) - } - Err(error) => Err(error), - } - } - // Kernel-direct (no shadow mirror): guest Python writes/creates - // land only in the kernel VFS, so mirroring create/modify ops into - // the host-side shadow would leave empty stubs that a later - // shadow->kernel sync resurrects over real content. (Delete/rename - // still mirror — to *remove* stale wire-written shadow entries.) - PythonVfsRpcMethod::Symlink => { - let target = request.target.clone().ok_or_else(|| { - SidecarError::InvalidState(format!( - "python VFS fsSymlink for {} requires a target", - path - )) - })?; - vm.kernel - .symlink(&target, &path) - .map(|()| PythonVfsRpcResponsePayload::Empty) - .map_err(kernel_error) - } - PythonVfsRpcMethod::ReadLink => vm - .kernel - .read_link(&path) - .map(|target| PythonVfsRpcResponsePayload::SymlinkTarget { target }) - .map_err(kernel_error), - // `setattr` carries any of mode/uid/gid/atime+mtime; apply each - // present field to the host VFS. - PythonVfsRpcMethod::Setattr => { - (|| -> Result { - // Mirror metadata into the host shadow only when the entry - // already exists there (a host-mounted / wire-written file), - // so the next shadow->kernel reconcile keeps the guest's - // change. Never *create* a shadow stub for a kernel-only - // guest file (that resurrected empty content). - let mirror = shadow_host_path_for_guest(&vm.cwd, &path).exists(); - if let Some(mode) = request.mode { - vm.kernel.chmod(&path, mode).map_err(kernel_error)?; - if mirror { - mirror_guest_chmod_to_shadow(vm, &path, mode)?; - } - } - // uid/gid apply independently (`os.chown(p, uid, -1)` keeps - // the other side); fill the missing side from the current - // owner rather than dropping the whole chown. - if request.uid.is_some() || request.gid.is_some() { - let current = vm.kernel.stat(&path).map_err(kernel_error)?; - let uid = request.uid.unwrap_or(current.uid); - let gid = request.gid.unwrap_or(current.gid); - vm.kernel.chown(&path, uid, gid).map_err(kernel_error)?; - } - if let (Some(atime_ms), Some(mtime_ms)) = - (request.atime_ms, request.mtime_ms) - { - vm.kernel - .utimes(&path, atime_ms, mtime_ms) - .map_err(kernel_error)?; - if mirror { - mirror_guest_utimes_to_shadow( - vm, - &path, - VirtualUtimeSpec::Set(VirtualTimeSpec::from_millis(atime_ms)), - VirtualUtimeSpec::Set(VirtualTimeSpec::from_millis(mtime_ms)), - true, - )?; - } - } - Ok(PythonVfsRpcResponsePayload::Empty) - })() - } - PythonVfsRpcMethod::HttpRequest - | PythonVfsRpcMethod::DnsLookup - | PythonVfsRpcMethod::SubprocessRun - | PythonVfsRpcMethod::SocketConnect - | PythonVfsRpcMethod::SocketSend - | PythonVfsRpcMethod::SocketRecv - | PythonVfsRpcMethod::SocketClose - | PythonVfsRpcMethod::UdpCreate - | PythonVfsRpcMethod::UdpSendto - | PythonVfsRpcMethod::UdpRecvfrom => Err(SidecarError::InvalidState(String::from( - "python non-filesystem RPC reached filesystem dispatcher unexpectedly", - ))), - } - } - Err(error) => Err(error), - }; - - let Some(vm) = sidecar.vms.get_mut(vm_id) else { - log_stale_process_event(&sidecar.bridge, vm_id, process_id, "python VFS RPC"); - return Ok(()); - }; - let Some(process) = vm.active_processes.get_mut(process_id) else { - log_stale_process_event(&sidecar.bridge, vm_id, process_id, "python VFS RPC"); - return Ok(()); - }; - - match response { - Ok(payload) => process - .execution - .respond_python_vfs_rpc_success(request.id, payload), - Err(error) => process.execution.respond_python_vfs_rpc_error( - request.id, - "ERR_AGENTOS_PYTHON_VFS_RPC", - error.to_string(), - ), + match error.code() { + Some(code) => SidecarError::Host(agentos_execution::backend::HostServiceError::new( + code, + error.message(), + )), + None => SidecarError::InvalidState(error.to_string()), } } @@ -1073,24 +241,12 @@ where "Ignoring stale filesystem request during {context}: VM {vm_id} was already reaped" ), }; - let _ = sidecar.bridge.emit_log(vm_id, message.clone()); - SidecarError::InvalidState(message) -} - -pub(crate) fn normalize_python_vfs_rpc_path(path: &str) -> Result { - if !path.starts_with('/') { - return Err(SidecarError::InvalidState(format!( - "python VFS RPC path {path} must be absolute within {PYTHON_VFS_RPC_GUEST_ROOT}" - ))); + if let Err(error) = sidecar.bridge.emit_log(vm_id, message.clone()) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_EMIT: failed to emit stale filesystem request diagnostic for VM {vm_id}: {error:?}" + ); } - - // Root is `/`: Python may address the whole guest VFS. Textual `..` segments - // are resolved by `normalize_path`, and the kernel enforces fs permissions - // plus mount-confinement (the resolve-beneath walk refuses escaping symlinks) - // on every op — so confinement is the kernel's job, not a prefix check here. - let normalized = normalize_path(path); - debug_assert_eq!(PYTHON_VFS_RPC_GUEST_ROOT, "/"); - Ok(normalized) + SidecarError::InvalidState(message) } /// Kernel-VFS-backed reader for resolver unit tests and kernel-only callers. @@ -1101,32 +257,32 @@ struct KernelModuleFsReader<'a> { #[cfg(test)] impl ModuleFsReader for KernelModuleFsReader<'_> { - fn canonical_guest_path(&mut self, guest_path: &str) -> Option { - self.kernel.realpath(guest_path).ok() + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { + module_kernel_optional(self.kernel.realpath(guest_path)) } - fn read_to_string(&mut self, guest_path: &str) -> Option { - let bytes = self.kernel.read_file(guest_path).ok()?; - String::from_utf8(bytes).ok() + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError> { + let Some(bytes) = module_kernel_optional(self.kernel.read_file(guest_path))? else { + return Ok(None); + }; + module_utf8(guest_path, bytes).map(Some) } - fn path_is_dir(&mut self, guest_path: &str) -> Option { - self.kernel - .stat(guest_path) - .ok() - .map(|stat| stat.is_directory) + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError> { + Ok(module_kernel_optional(self.kernel.stat(guest_path))?.map(|stat| stat.is_directory)) } - fn path_exists(&mut self, guest_path: &str) -> bool { - self.kernel.exists(guest_path).unwrap_or(false) + fn path_exists(&mut self, guest_path: &str) -> Result { + Ok(self.path_is_dir(guest_path)?.is_some()) } } -/// Module reader for live JavaScript processes. In the NodeRuntime embedding, -/// guest filesystem calls operate on the process' mapped host shadow first and -/// reconcile back to the kernel on exit. Module resolution must therefore check -/// that same process shadow before falling back to the sidecar kernel, otherwise -/// `fs.writeFileSync(...); await import(...)` observes an older filesystem. +/// Module reader for live JavaScript processes. Kernel VFS state is the sole +/// mutable guest filesystem authority, so module resolution observes exactly +/// the same files and metadata as embedded `fs` and standalone WASM. struct ProcessModuleFsReader<'a> { kernel: &'a mut SidecarKernel, process: &'a ActiveProcess, @@ -1136,97 +292,68 @@ impl ProcessModuleFsReader<'_> { fn normalize_guest_path(&self, guest_path: &str) -> String { normalize_process_filesystem_rpc_path(self.process, guest_path) } +} - fn mapped_host_path(&self, guest_path: &str) -> Option { - mapped_runtime_host_path_for_read(self.kernel, self.process, guest_path) - } - - fn materialize_mapped_path( +impl ModuleFsReader for ProcessModuleFsReader<'_> { + fn canonical_guest_path( &mut self, guest_path: &str, - mapped: &MappedRuntimeHostPath, - ) -> Result<(), SidecarError> { - materialize_mapped_host_path_from_kernel( - self.kernel, + ) -> Result, HostServiceError> { + let normalized = self.normalize_guest_path(guest_path); + module_kernel_optional(self.kernel.realpath_for_process( + EXECUTION_DRIVER_NAME, self.process.kernel_pid, - guest_path, - mapped, - ) - } - - fn open_mapped_path( - &mut self, - guest_path: &str, - operation: &'static str, - flags: OFlag, - ) -> Option { - let mapped = self.mapped_host_path(guest_path)?; - self.materialize_mapped_path(guest_path, &mapped).ok()?; - open_mapped_runtime_beneath(&mapped, operation, flags, Mode::empty()).ok() + &normalized, + )) } -} -impl ModuleFsReader for ProcessModuleFsReader<'_> { - fn canonical_guest_path(&mut self, guest_path: &str) -> Option { + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError> { let normalized = self.normalize_guest_path(guest_path); - if let Some(mapped) = self.mapped_host_path(&normalized) { - if self.materialize_mapped_path(&normalized, &mapped).is_ok() { - if let Ok(opened) = open_mapped_runtime_beneath( - &mapped, - "module.realpath", - O_PATH_ANCHOR, - Mode::empty(), - ) { - if let Some(resolved) = - mapped_runtime_resolved_guest_path(&mapped, &opened.host_path) - { - return Some(resolved); - } - } - } - } - self.kernel.realpath(&normalized).ok() + let Some(bytes) = module_kernel_optional(self.kernel.read_file_for_process( + EXECUTION_DRIVER_NAME, + self.process.kernel_pid, + &normalized, + ))? + else { + return Ok(None); + }; + module_utf8(&normalized, bytes).map(Some) } - fn read_to_string(&mut self, guest_path: &str) -> Option { + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError> { let normalized = self.normalize_guest_path(guest_path); - if let Some(opened) = self.open_mapped_path(&normalized, "module.readFile", OFlag::O_RDONLY) - { - if let Ok(source) = opened.handle.read_to_string() { - return Some(source); - } - } - - let bytes = self.kernel.read_file(&normalized).ok()?; - String::from_utf8(bytes).ok() + Ok(module_kernel_optional(self.kernel.stat_for_process( + EXECUTION_DRIVER_NAME, + self.process.kernel_pid, + &normalized, + ))? + .map(|stat| stat.is_directory)) } - fn path_is_dir(&mut self, guest_path: &str) -> Option { - let normalized = self.normalize_guest_path(guest_path); - if let Some(opened) = self.open_mapped_path(&normalized, "module.stat", O_PATH_ANCHOR) { - if let Ok(metadata) = opened.handle.metadata() { - return Some(metadata.is_directory); - } - } - - self.kernel - .stat(&normalized) - .ok() - .map(|stat| stat.is_directory) + fn path_exists(&mut self, guest_path: &str) -> Result { + Ok(self.path_is_dir(guest_path)?.is_some()) } +} - fn path_exists(&mut self, guest_path: &str) -> bool { - let normalized = self.normalize_guest_path(guest_path); - if self - .open_mapped_path(&normalized, "module.exists", O_PATH_ANCHOR) - .is_some() - { - return true; - } - self.kernel.exists(&normalized).unwrap_or(false) +fn module_kernel_optional( + result: Result, +) -> Result, HostServiceError> { + match result { + Ok(value) => Ok(Some(value)), + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => Ok(None), + Err(error) => Err(HostServiceError::new(error.code(), error.to_string())), } } +fn module_utf8(path: &str, bytes: Vec) -> Result { + String::from_utf8(bytes).map_err(|error| { + HostServiceError::new( + "EILSEQ", + format!("module filesystem file {path} is not valid UTF-8: {error}"), + ) + }) +} + /// Resolve / load / format / batch-resolve module requests against the kernel /// VFS. Routed here from `service_javascript_sync_rpc` for the /// `__resolve_module` / `__load_file` / `__module_format` / @@ -1256,7 +383,7 @@ fn is_bare_module_specifier(specifier: &str) -> bool { pub(crate) fn service_javascript_module_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { // Self-contained package processes (agent adapters, packed JS commands) // carry their whole dependency closure inside the package mount. A bare @@ -1267,7 +394,11 @@ pub(crate) fn service_javascript_module_sync_rpc( .env .get("AGENTOS_GUEST_ENTRYPOINT") .and_then(|entrypoint| agentos_package_version_root(entrypoint)); - let mut cache = std::mem::take(&mut process.module_resolution_cache); + // Resolution must observe live kernel mount/VFS state. Until the kernel + // exposes a generation token suitable for a keyed immutable cache, keep + // this cache request-local so a rename, symlink change, or mount update can + // never leave the V8 adapter with stale authority. + let mut cache = agentos_execution::LocalModuleResolutionCache::default(); let value = { let reader = ProcessModuleFsReader { kernel, @@ -1287,13 +418,17 @@ pub(crate) fn service_javascript_module_sync_rpc( _ if request.method == "_resolveModuleSync" => ModuleResolveMode::Require, _ => ModuleResolveMode::Import, }; - let mut resolved = resolver.resolve_module(specifier, parent, mode); + let mut resolved = resolver + .resolve_module(specifier, parent, mode) + .map_err(SidecarError::Host)?; if resolved.is_none() && is_bare_module_specifier(specifier) { if let Some(fallback_from) = package_fallback_from .as_deref() .filter(|fallback| *fallback != parent) { - resolved = resolver.resolve_module(specifier, fallback_from, mode); + resolved = resolver + .resolve_module(specifier, fallback_from, mode) + .map_err(SidecarError::Host)?; } } if resolved.is_none() && std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { @@ -1305,6 +440,7 @@ pub(crate) fn service_javascript_module_sync_rpc( let path = javascript_sync_rpc_arg_str(&request.args, 0, "module load path")?; resolver .load_file(path) + .map_err(SidecarError::Host)? .map(Value::String) .unwrap_or(Value::Null) } @@ -1312,24 +448,22 @@ pub(crate) fn service_javascript_module_sync_rpc( let path = javascript_sync_rpc_arg_str(&request.args, 0, "module format path")?; resolver .module_format(path) + .map_err(SidecarError::Host)? .map(|format: LocalResolvedModuleFormat| { Value::String(String::from(format.as_str())) }) .unwrap_or(Value::Null) } - "__batch_resolve_modules" | "_batchResolveModules" => { - resolver.batch_resolve_modules(&request.args) - } + "__batch_resolve_modules" | "_batchResolveModules" => resolver + .batch_resolve_modules(&request.args) + .map_err(SidecarError::Host)?, other => { - process.module_resolution_cache = cache; return Err(SidecarError::InvalidState(format!( "unsupported JavaScript module sync RPC method {other}" ))); } } }; - process.module_resolution_cache = cache; - Ok(value) } @@ -1375,6 +509,7 @@ fn fs_sync_phases_enabled() -> bool { fn record_fs_sync_phase(method: &str, elapsed_ns: u128) { let phases = FS_SYNC_PHASES.get_or_init(|| Mutex::new(BTreeMap::new())); let Ok(mut phases) = phases.lock() else { + eprintln!("ERR_AGENTOS_DIAGNOSTIC_STATE: filesystem sync statistics lock is poisoned"); return; }; let stats = phases.entry(method.to_string()).or_default(); @@ -1399,67 +534,19 @@ fn record_fs_sync_phase(method: &str, elapsed_ns: u128) { stats.calls )); } - let _ = fs::write(path, output); -} - -fn fs_sync_request_marks_host_write_dirty( - request: &JavascriptSyncRpcRequest, -) -> Result { - Ok(match request.method.as_str() { - "fs.open" | "fs.openSync" => { - let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem open flags")?; - mapped_host_open_is_writable(flags) - } - "fs.write" - | "fs.writeSync" - | "fs.writevSync" - | "fs.writeFileSync" - | "fs.promises.writeFile" - | "fs.mkdirSync" - | "fs.mknodSync" - | "fs.promises.mkdir" - | "fs.copyFileSync" - | "fs.promises.copyFile" - | "fs.symlinkSync" - | "fs.promises.symlink" - | "fs.linkSync" - | "fs.openTmpfileSync" - | "fs.linkFdSync" - | "fs.promises.link" - | "fs.renameSync" - | "fs.renameAt2Sync" - | "fs.promises.rename" - | "fs.rmdirSync" - | "fs.promises.rmdir" - | "fs.unlinkSync" - | "fs.promises.unlink" - | "fs.chmodSync" - | "fs.chmodForProcessSync" - | "fs.promises.chmod" - | "fs.chownSync" - | "fs.promises.chown" - | "fs.utimesSync" - | "fs.promises.utimes" - | "fs.lutimesSync" - | "fs.promises.lutimes" - | "fs.futimesSync" => true, - "fs.ftruncateSync" - | "fs.truncateForProcessSync" - | "fs.fallocateSync" - | "fs.insertRangeSync" - | "fs.collapseRangeSync" - | "fs.punchHoleSync" - | "fs.zeroRangeSync" => true, - "fs.setxattrSync" | "fs.removexattrSync" => true, - _ => false, - }) + if let Err(error) = fs::write(&path, output) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_WRITE: failed to write filesystem sync statistics to {}: {error}", + path.to_string_lossy() + ); + } } pub(crate) fn service_javascript_fs_read_sync_rpc( kernel: &mut SidecarKernel, - process: &mut ActiveProcess, + _process: &mut ActiveProcess, kernel_pid: u32, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result, SidecarError> { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem read fd")?; let length = usize::try_from(javascript_sync_rpc_arg_u64( @@ -1472,14 +559,6 @@ pub(crate) fn service_javascript_fs_read_sync_rpc( })?; let position = javascript_sync_rpc_arg_u64_optional(&request.args, 2, "filesystem read position")?; - if let Some(mapped) = process.mapped_host_fd_mut(fd) { - let value = read_mapped_host_fd(mapped, fd, length, position)?; - return javascript_sync_rpc_bytes_arg( - std::slice::from_ref(&value), - 0, - "filesystem mapped read response", - ); - } match position { Some(offset) => kernel.fd_pread(EXECUTION_DRIVER_NAME, kernel_pid, fd, length, offset), None => kernel.fd_read(EXECUTION_DRIVER_NAME, kernel_pid, fd, length), @@ -1491,14 +570,9 @@ pub(crate) fn service_javascript_fs_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, kernel_pid: u32, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, ) -> Result { let _phase_timer = FsSyncPhaseTimer::start(request.method.as_str()); - if process.runtime != GuestRuntimeKind::WebAssembly - && fs_sync_request_marks_host_write_dirty(request)? - { - process.mark_host_write_dirty(); - } match request.method.as_str() { "fs.open" | "fs.openSync" => { let phase_start = Instant::now(); @@ -1510,59 +584,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_arg_u32_optional(&request.args, 2, "filesystem open mode")?; record_fs_sync_subphase(request.method.as_str(), "parse", phase_start); let phase_start = Instant::now(); - match mapped_runtime_host_path( - kernel, - process, - path, - mapped_host_open_is_writable(flags), - ) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - record_fs_sync_subphase( - request.method.as_str(), - "mapped_host_match", - phase_start, - ); - let phase_start = Instant::now(); - materialize_mapped_host_path_from_kernel( - kernel, - kernel_pid, - path, - &mapped_host, - )?; - record_fs_sync_subphase( - request.method.as_str(), - "materialize_mapped_host", - phase_start, - ); - let phase_start = Instant::now(); - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.open", - OFlag::from_bits_truncate(flags as i32), - Mode::from_bits_truncate(mode.unwrap_or(0o666) as _), - )?; - record_fs_sync_subphase( - request.method.as_str(), - "open_mapped_beneath", - phase_start, - ); - let phase_start = Instant::now(); - return open_mapped_host_fd(kernel, process, opened, Some(path.to_string())) - .inspect(|_| { - record_fs_sync_subphase( - request.method.as_str(), - "open_mapped_fd", - phase_start, - ); - }); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } - record_fs_sync_subphase(request.method.as_str(), "mapped_host_none", phase_start); - let phase_start = Instant::now(); kernel .fd_open(EXECUTION_DRIVER_NAME, kernel_pid, path, flags, mode) .map(|fd| json!(fd)) @@ -1581,7 +602,7 @@ pub(crate) fn service_javascript_fs_sync_rpc( "fs.blockingIoTimeoutMsSync" => Ok(json!(kernel.resource_limits().max_blocking_read_ms)), "fs.read" | "fs.readSync" => { service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request) - .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map(|bytes| host_bytes_value(&bytes)) } "fs.write" | "fs.writeSync" => { let phase_start = Instant::now(); @@ -1598,12 +619,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( )?; record_fs_sync_subphase(request.method.as_str(), "parse", phase_start); let phase_start = Instant::now(); - if let Some(mapped) = process.mapped_host_fd_mut(fd) { - record_fs_sync_subphase(request.method.as_str(), "mapped_fd_match", phase_start); - return write_mapped_host_fd(mapped, fd, &contents, position); - } - record_fs_sync_subphase(request.method.as_str(), "mapped_fd_none", phase_start); - let phase_start = Instant::now(); let written = match position { Some(offset) => kernel .fd_pwrite(EXECUTION_DRIVER_NAME, kernel_pid, fd, &contents, offset) @@ -1626,10 +641,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( }; process.queue_pending_execution_event(event)?; record_fs_sync_subphase(request.method.as_str(), "queue_stdio_event", phase_start); - } else { - let phase_start = Instant::now(); - mirror_kernel_fd_contents_to_process_shadow(kernel, process, kernel_pid, fd)?; - record_fs_sync_subphase(request.method.as_str(), "mirror_shadow", phase_start); } Ok(json!(written)) } @@ -1650,20 +661,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( record_fs_sync_subphase(request.method.as_str(), "parse", phase_start); let mut total_written = 0usize; - if let Some(mapped) = process.mapped_host_fd_mut(fd) { - record_fs_sync_subphase(request.method.as_str(), "mapped_fd_match", phase_start); - let mut next_position = position; - for buffer in buffers { - let written = write_all_mapped_host_fd(mapped, fd, buffer, next_position)?; - total_written = total_written.saturating_add(written); - if let Some(position) = &mut next_position { - *position = position.saturating_add(written as u64); - } - } - return Ok(json!(total_written)); - } - record_fs_sync_subphase(request.method.as_str(), "mapped_fd_none", phase_start); - let surfaces_stdio = position.is_none() && kernel_fd_surfaces_stdio_event(kernel, kernel_pid, fd)?; let mut next_position = position; @@ -1681,9 +678,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( .map_err(kernel_error)?, }; if written == 0 { - return Err(SidecarError::Execution(format!( - "EIO: filesystem writev made no progress on fd {fd}" - ))); + return Err(SidecarError::host( + "EIO", + format!("filesystem writev made no progress on fd {fd}"), + )); } offset += written; total_written = total_written.saturating_add(written); @@ -1703,35 +701,11 @@ pub(crate) fn service_javascript_fs_sync_rpc( ActiveExecutionEvent::Stderr(combined_stdio) }; process.queue_pending_execution_event(event)?; - } else { - mirror_kernel_fd_contents_to_process_shadow(kernel, process, kernel_pid, fd)?; } Ok(json!(total_written)) } - "fs.dupSync" => { - let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem dup fd")?; - if let Some(mapped) = process.mapped_host_fd(fd) { - let duplicate = crate::state::ActiveMappedHostFd { - file: mapped.file.try_clone().map_err(|error| { - SidecarError::Io(format!( - "failed to duplicate mapped guest fd {fd}: {error}" - )) - })?, - path: mapped.path.clone(), - guest_path: mapped.guest_path.clone(), - }; - return Ok(json!(process.allocate_mapped_host_fd(duplicate))); - } - kernel - .fd_dup(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map(Value::from) - .map_err(kernel_error) - } "fs.close" | "fs.closeSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem close fd")?; - if process.close_mapped_host_fd(fd) { - return Ok(Value::Null); - } kernel .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, fd) .map(|()| Value::Null) @@ -1769,32 +743,8 @@ pub(crate) fn service_javascript_fs_sync_rpc( .map(|()| Value::Null) .map_err(kernel_error) } - "fs._getPathSync" => { - let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem path fd")?; - if let Some(mapped) = process.mapped_host_fd(fd) { - return Ok(Value::String( - mapped - .guest_path - .clone() - .unwrap_or_else(|| mapped.path.to_string_lossy().into_owned()), - )); - } - kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map(Value::String) - .map_err(kernel_error) - } "fs.fstat" | "fs.fstatSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fstat fd")?; - if let Some(mapped) = process.mapped_host_fd(fd) { - let metadata = mapped.file.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to stat mapped guest fd {fd} -> {}: {error}", - mapped.path.display() - )) - })?; - return Ok(javascript_sync_rpc_host_stat_value(&metadata)); - } kernel .fd_stat(EXECUTION_DRIVER_NAME, kernel_pid, fd) .map_err(kernel_error)?; @@ -1805,18 +755,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( } "fs.fsyncSync" | "fs.fdatasyncSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem sync fd")?; - if let Some(mapped) = process.mapped_host_fd(fd) { - return mapped - .file - .sync_all() - .map(|()| Value::Null) - .map_err(|error| { - SidecarError::Io(format!( - "failed to sync mapped guest fd {fd} -> {}: {error}", - mapped.path.display() - )) - }); - } kernel .fd_sync(EXECUTION_DRIVER_NAME, kernel_pid, fd) .map(|()| Value::Null) @@ -1837,9 +775,8 @@ pub(crate) fn service_javascript_fs_sync_rpc( .unwrap_or(0); kernel .truncate_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &path, length) - .map_err(|error| kernel_path_error("fs.truncate", &path, error))?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(|error| kernel_path_error("fs.truncate", &path, error)) } "fs.fallocateSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fallocate fd")?; @@ -1847,14 +784,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_arg_u64(&request.args, 1, "filesystem fallocate offset")?; let length = javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem fallocate length")?; - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; kernel .fd_allocate(EXECUTION_DRIVER_NAME, kernel_pid, fd, offset, length) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.insertRangeSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem insert-range fd")?; @@ -1862,14 +795,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_arg_u64(&request.args, 1, "filesystem insert-range offset")?; let length = javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem insert-range length")?; - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; kernel .fd_insert_range(EXECUTION_DRIVER_NAME, kernel_pid, fd, offset, length) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.collapseRangeSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem collapse-range fd")?; @@ -1877,14 +806,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_arg_u64(&request.args, 1, "filesystem collapse-range offset")?; let length = javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem collapse-range length")?; - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; kernel .fd_collapse_range(EXECUTION_DRIVER_NAME, kernel_pid, fd, offset, length) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.punchHoleSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem punch-hole fd")?; @@ -1892,14 +817,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_arg_u64(&request.args, 1, "filesystem punch-hole offset")?; let length = javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem punch-hole length")?; - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; kernel .fd_punch_hole(EXECUTION_DRIVER_NAME, kernel_pid, fd, offset, length) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.zeroRangeSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem zero-range fd")?; @@ -1910,9 +831,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let keep_size = javascript_sync_rpc_arg_u32(&request.args, 3, "filesystem zero-range keep-size")? != 0; - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; kernel .fd_zero_range( EXECUTION_DRIVER_NAME, @@ -1922,9 +840,8 @@ pub(crate) fn service_javascript_fs_sync_rpc( length, keep_size, ) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.fiemapSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fiemap fd")?; @@ -1948,35 +865,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem chmod path")?; let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")?; - let mut result = - kernel.chmod_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &path, mode); - if result.as_ref().is_err_and(|error| error.code() == "ENOENT") { - let shadow_path = process_shadow_host_path(process, &path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "filesystem chmod cannot resolve process shadow path for {path}" - )) - })?; - let contents = fs::read(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to materialize chmod target {}: {error}", - shadow_path.display() - )) - })?; - kernel - .write_file_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - &path, - contents, - Some(mode), - ) - .map_err(|error| kernel_path_error("fs.chmod", &path, error))?; - result = kernel.chmod_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &path, mode); - } - result.map_err(|error| kernel_path_error("fs.chmod", &path, error))?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - mirror_process_mode_to_shadow(process, &path, mode)?; - Ok(Value::Null) + kernel + .chmod_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &path, mode) + .map(|()| Value::Null) + .map_err(|error| kernel_path_error("fs.chmod", &path, error)) } "fs.ftruncateSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem ftruncate fd")?; @@ -1986,59 +878,19 @@ pub(crate) fn service_javascript_fs_sync_rpc( "filesystem ftruncate length", )? .unwrap_or(0); - if let Some(mapped_guest_path) = process - .mapped_host_fd_mut(fd) - .map(|mapped| mapped.guest_path.clone()) - { - // `length` is guest-controlled. Bound it before resizing the host - // file so a hostile value cannot create an enormous sparse host - // file. For a VFS-visible guest path the kernel truncate below is - // the primary (configured) size enforcement and mirrors the new - // length without reading the whole host file into sidecar memory. - if length > MAX_MAPPED_TRUNCATE_BYTES { - return Err(SidecarError::Io(format!( - "ftruncate length {length} exceeds maximum \ - {MAX_MAPPED_TRUNCATE_BYTES} for mapped guest fd {fd}" - ))); - } - if let Some(guest_path) = mapped_guest_path.as_deref() { - kernel - .truncate_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path, length) - .map_err(|error| kernel_path_error("fs.ftruncate", guest_path, error))?; - } - let mapped = process.mapped_host_fd_mut(fd).ok_or_else(|| { - SidecarError::Io(format!("mapped guest fd {fd} disappeared during ftruncate")) - })?; - mapped.file.set_len(length).map_err(|error| { - SidecarError::Io(format!("failed to truncate mapped guest fd {fd}: {error}")) - })?; - if let Some(guest_path) = mapped_guest_path.as_deref() { - mirror_kernel_path_to_process_shadow(kernel, process, guest_path)?; - } - return Ok(Value::Null); - } let fd_stat = kernel .fd_stat(EXECUTION_DRIVER_NAME, kernel_pid, fd) .map_err(kernel_error)?; if (fd_stat.flags & libc::O_ACCMODE as u32) == libc::O_RDONLY as u32 { - return Err(SidecarError::Execution(format!( - "EBADF: file descriptor {fd} is not open for writing" - ))); + return Err(SidecarError::host( + "EBADF", + format!("file descriptor {fd} is not open for writing"), + )); } kernel .fd_truncate(EXECUTION_DRIVER_NAME, kernel_pid, fd, length) - .map_err(kernel_error)?; - if kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .ok() - .is_some_and(|path| kernel.exists(&path).unwrap_or(false)) - { - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, &path)?; - } - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.readFileSync" | "fs.promises.readFile" => { let path = javascript_sync_rpc_path_arg( @@ -2049,35 +901,13 @@ pub(crate) fn service_javascript_fs_sync_rpc( )?; let path = path.as_str(); let encoding = javascript_sync_rpc_encoding(&request.args); - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.readFile", - OFlag::O_RDONLY, - Mode::empty(), - )?; - let content = opened.handle.read_bytes().map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest file {} -> {}: {error}", - path, - opened.host_path.display() - )) - })?; - return Ok(match encoding.as_deref() { - Some("utf8") | Some("utf-8") => { - Value::String(String::from_utf8_lossy(&content).into_owned()) - } - _ => javascript_sync_rpc_bytes_value(&content), - }); - } kernel .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) .map(|content| match encoding.as_deref() { Some("utf8") | Some("utf-8") => { Value::String(String::from_utf8_lossy(&content).into_owned()) } - _ => javascript_sync_rpc_bytes_value(&content), + _ => host_bytes_value(&content), }) .map_err(kernel_error) } @@ -2094,31 +924,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( } else { javascript_sync_rpc_bytes_arg(&request.args, 1, "filesystem writeFile contents")? }; - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.writeFile", - OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC, - Mode::from_bits_truncate( - javascript_sync_rpc_option_u32(&request.args, 2, "mode")? - .unwrap_or(0o666) as _, - ), - )?; - opened.handle.write_bytes(&contents).map_err(|error| { - SidecarError::Io(format!( - "failed to write mapped guest file {} -> {}: {error}", - path, - opened.host_path.display() - )) - })?; - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .write_file_for_process( EXECUTION_DRIVER_NAME, @@ -2127,9 +932,8 @@ pub(crate) fn service_javascript_fs_sync_rpc( contents, javascript_sync_rpc_option_u32(&request.args, 2, "mode")?, ) - .map_err(|error| kernel_path_error("fs.writeFile", path, error))?; - mirror_kernel_path_to_process_shadow(kernel, process, path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(|error| kernel_path_error("fs.writeFile", path, error)) } "fs.statfsSync" => { let path = @@ -2149,23 +953,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem stat path")?; let path = path.as_str(); - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.stat", - O_PATH_ANCHOR, - Mode::empty(), - )?; - let metadata = opened.handle.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to stat mapped guest path {} -> {}: {error}", - path, - opened.host_path.display() - )) - })?; - return Ok(metadata.to_value()); - } kernel .stat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) .map(javascript_sync_rpc_stat_value) @@ -2175,11 +962,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem lstat path")?; let path = path.as_str(); - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let metadata = mapped_runtime_symlink_metadata(&mapped_host, "fs.lstat")?; - return Ok(metadata.to_value()); - } kernel .lstat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) .map(javascript_sync_rpc_stat_value) @@ -2198,29 +980,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let path = path.as_str(); let recursive = javascript_sync_rpc_option_bool(&request.args, 1, "recursive").unwrap_or(false); - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - if mapped_runtime_relative_path(&mapped_host)? == Path::new(".") { - create_mapped_runtime_root_directory(&mapped_host, recursive)?; - } else { - if recursive { - ensure_mapped_runtime_parent_dirs(&mapped_host, "fs.mkdir")?; - let parent = - open_mapped_runtime_parent_beneath(&mapped_host, "fs.mkdir")?; - create_mapped_runtime_directory(&parent, path, true)?; - } else { - let parent = - open_mapped_runtime_parent_beneath(&mapped_host, "fs.mkdir")?; - create_mapped_runtime_directory(&parent, path, false)?; - } - } - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .mkdir_for_process( EXECUTION_DRIVER_NAME, @@ -2271,26 +1030,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( javascript_sync_rpc_option_bool(&request.args, 2, "effective IDs").unwrap_or(false); let valid_mask = libc::R_OK as u32 | libc::W_OK as u32 | libc::X_OK as u32; if mode & !valid_mask != 0 { - return Err(SidecarError::Execution(format!( - "EINVAL: invalid filesystem access mode {mode:o}" - ))); - } - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.access", - O_PATH_ANCHOR, - Mode::empty(), - )?; - opened.handle.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to access mapped guest path {} -> {}: {error}", - path, - opened.host_path.display() - )) - })?; - return Ok(Value::Null); + return Err(SidecarError::host( + "EINVAL", + format!("invalid filesystem access mode {mode:o}"), + )); } kernel .access_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, mode, effective_ids) @@ -2312,82 +1055,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( "filesystem copyFile destination", )?; let destination = destination.as_str(); - let source_host = mapped_runtime_host_path(kernel, process, source, false); - let destination_host = mapped_runtime_host_path(kernel, process, destination, true); - if matches!(destination_host, Some(MappedRuntimeHostAccess::ReadOnly(_))) { - return Err(read_only_mapped_runtime_host_path_error(destination)); - } - if source_host.is_some() || destination_host.is_some() { - let contents = match source_host { - Some(MappedRuntimeHostAccess::Writable(ref mapped_host)) => { - let opened = open_mapped_runtime_beneath( - mapped_host, - "fs.copyFile source", - OFlag::O_RDONLY, - Mode::empty(), - )?; - opened.handle.read_bytes().map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest file {} -> {}: {error}", - source, - opened.host_path.display() - )) - })? - } - Some(MappedRuntimeHostAccess::ReadOnly(ref mapped_host)) => { - let opened = open_mapped_runtime_beneath( - mapped_host, - "fs.copyFile source", - OFlag::O_RDONLY, - Mode::empty(), - )?; - opened.handle.read_bytes().map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest file {} -> {}: {error}", - source, - opened.host_path.display() - )) - })? - } - None => kernel - .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, source) - .map_err(kernel_error)?, - }; - return match destination_host { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.copyFile destination", - OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC, - Mode::from_bits_truncate(0o666), - )?; - opened - .handle - .write_bytes(&contents) - .map(|()| Value::Null) - .map_err(|error| { - SidecarError::Io(format!( - "failed to write mapped guest file {} -> {}: {error}", - destination, - opened.host_path.display() - )) - }) - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - Err(read_only_mapped_runtime_host_path_error(destination)) - } - None => kernel - .write_file_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - destination, - contents, - None, - ) - .map(|()| Value::Null) - .map_err(kernel_error), - }; - } let contents = kernel .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, source) .map_err(kernel_error)?; @@ -2406,19 +1073,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem exists path")?; let path = path.as_str(); - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let exists = match open_mapped_runtime_beneath( - &mapped_host, - "fs.exists", - O_PATH_ANCHOR, - Mode::empty(), - ) { - Ok(opened) => opened.handle.metadata().is_ok(), - Err(_) => false, - }; - return Ok(Value::Bool(exists)); - } kernel .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) .map(Value::Bool) @@ -2432,11 +1086,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( "filesystem readlink path", )?; let path = path.as_str(); - if let Some(mapped_host) = mapped_runtime_host_path_for_read(kernel, process, path) { - materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; - let target = read_mapped_runtime_link(&mapped_host, path, "fs.readlink")?; - return Ok(Value::String(target.to_string_lossy().into_owned())); - } kernel .read_link_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) .map(Value::String) @@ -2448,26 +1097,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let link_path = javascript_sync_rpc_path_arg(process, &request.args, 1, "filesystem symlink path")?; let link_path = link_path.as_str(); - match mapped_runtime_host_path(kernel, process, link_path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - ensure_mapped_runtime_parent_dirs(&mapped_host, "fs.symlink")?; - let parent = open_mapped_runtime_parent_beneath(&mapped_host, "fs.symlink")?; - let host_path = parent.host_path.join(&parent.child_name); - remove_shadow_path_if_exists(&host_path, link_path)?; - mapped_child_symlink(&parent, target).map_err(|error| { - SidecarError::Io(format!( - "failed to create mapped guest symlink {} -> {} ({target}): {error}", - link_path, - host_path.display() - )) - })?; - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(link_path)); - } - None => {} - } kernel .symlink_for_process(EXECUTION_DRIVER_NAME, kernel_pid, target, link_path) .map(|()| Value::Null) @@ -2500,26 +1129,10 @@ pub(crate) fn service_javascript_fs_sync_rpc( "filesystem rename destination", )?; let destination = destination.as_str(); - let source_host = mapped_runtime_host_path(kernel, process, source, true); - let destination_host = mapped_runtime_host_path(kernel, process, destination, true); - if matches!(source_host, Some(MappedRuntimeHostAccess::ReadOnly(_))) { - return Err(read_only_mapped_runtime_host_path_error(source)); - } - if matches!(destination_host, Some(MappedRuntimeHostAccess::ReadOnly(_))) { - return Err(read_only_mapped_runtime_host_path_error(destination)); - } - if source_host.is_some() || destination_host.is_some() { - return rename_mapped_host_path(source, source_host, destination, destination_host); - } kernel .rename_for_process(EXECUTION_DRIVER_NAME, kernel_pid, source, destination) - .map_err(kernel_error)?; - // Mirror the rename into the process shadow tree, otherwise the - // exit-time shadow->kernel sync resurrects the stale source path - // (the shadow walk only copies entries in, it cannot express - // deletions). - rename_process_shadow_path(process, source, destination)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.renameAt2Sync" => { let source = javascript_sync_rpc_path_arg( @@ -2538,23 +1151,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( let destination = destination.as_str(); let flags = javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem renameat2 flags")?; - let source_host = mapped_runtime_host_path(kernel, process, source, true); - let destination_host = mapped_runtime_host_path(kernel, process, destination, true); - if matches!(source_host, Some(MappedRuntimeHostAccess::ReadOnly(_))) { - return Err(read_only_mapped_runtime_host_path_error(source)); - } - if matches!(destination_host, Some(MappedRuntimeHostAccess::ReadOnly(_))) { - return Err(read_only_mapped_runtime_host_path_error(destination)); - } - if source_host.is_some() || destination_host.is_some() { - return rename_mapped_host_path_at2( - source, - source_host, - destination, - destination_host, - flags, - ); - } kernel .rename_at2_for_process( EXECUTION_DRIVER_NAME, @@ -2563,149 +1159,32 @@ pub(crate) fn service_javascript_fs_sync_rpc( destination, flags, ) - .map_err(kernel_error)?; - rename_process_shadow_path_at2(process, source, destination, flags)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.rmdirSync" | "fs.promises.rmdir" => { let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem rmdir path")?; let path = path.as_str(); - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - let parent = open_mapped_runtime_parent_beneath(&mapped_host, "fs.rmdir")?; - let host_path = parent.host_path.join(&parent.child_name); - mapped_child_remove_dir(&parent).map_err(|error| { - SidecarError::Io(format!( - "failed to remove mapped guest directory {} -> {}: {error}", - path, - host_path.display() - )) - })?; - // Mirror the deletion into the kernel for the same reason as - // fs.unlink below: readdir/stat merge kernel state, so a - // kernel-backed directory would otherwise resurrect. - if let Err(error) = - kernel.remove_dir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - { - if error.code() != "ENOENT" { - return Err(kernel_error(error)); - } - } - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .remove_dir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - .map_err(kernel_error)?; - // Mirror the removal into the process shadow tree, otherwise the - // exit-time shadow->kernel sync resurrects the deleted directory. - remove_process_shadow_path(process, path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.unlinkSync" | "fs.promises.unlink" => { let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem unlink path")?; let path = path.as_str(); - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - // Mapped paths are a merged view of the process shadow and - // kernel VFS. A file created by WASM exists only in the - // kernel until a JavaScript operation materializes it. If - // the shadow leaf is absent, unlink the kernel entry - // directly: copying file contents merely to delete them - // would be wasteful and would incorrectly require target - // read permission. If the shadow leaf exists, remove both - // representations below. - if !mapped_runtime_host_path_exists(&mapped_host)? { - return kernel - .remove_file(path) - .map(|()| Value::Null) - .map_err(kernel_error); - } - let parent = open_mapped_runtime_parent_beneath(&mapped_host, "fs.unlink")?; - let host_path = parent.host_path.join(&parent.child_name); - mapped_child_remove_file(&parent).map_err(|error| { - SidecarError::Io(format!( - "failed to remove mapped guest file {} -> {}: {error}", - path, - host_path.display() - )) - })?; - // The shadow cannot express deletions, and readdir/stat now - // merge kernel state into the mapped view — without a kernel - // removal a kernel-backed file (e.g. created by a wasm - // command) would resurrect in the very listing that follows - // the unlink. Best-effort: absent kernel entries are fine. - if let Err(error) = - kernel.remove_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - { - if error.code() != "ENOENT" { - return Err(kernel_error(error)); - } - } - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .remove_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - .map_err(kernel_error)?; - // Mirror the deletion into the process shadow tree: wasm guest - // deletions route kernel-direct, and without removing the shadow - // copy the exit-time shadow->kernel sync resurrects the file for - // later builtins in the same shell and for subsequent execs. - remove_process_shadow_path(process, path)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.chmodSync" | "fs.promises.chmod" => { let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem chmod path")?; let path = path.as_str(); let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")?; - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - materialize_mapped_host_path_from_kernel( - kernel, - kernel_pid, - path, - &mapped_host, - )?; - let opened = open_mapped_runtime_beneath( - &mapped_host, - "fs.chmod", - CHMOD_PATH_ANCHOR, - Mode::empty(), - )?; - if kernel - .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - .map_err(kernel_error)? - { - kernel - .chmod_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, mode) - .map_err(kernel_error)?; - } - opened.handle.set_mode(mode & 0o7777).map_err(|error| { - SidecarError::Io(format!( - "failed to chmod mapped guest path {} -> {}: {error}", - path, - opened.host_path.display() - )) - })?; - return Ok(Value::Null); - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .chmod_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, mode) .map(|()| Value::Null) @@ -2721,18 +1200,11 @@ pub(crate) fn service_javascript_fs_sync_rpc( request.method.as_str(), "fs.lchownSync" | "fs.promises.lchown" ); - let mut result = if is_lchown { + let result = if is_lchown { kernel.lchown_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, uid, gid) } else { kernel.chown_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, uid, gid, true) }; - if is_lchown - && result.as_ref().is_err_and(|error| error.code() == "ENOENT") - && materialize_process_shadow_symlink(kernel, process, kernel_pid, path)? - { - result = - kernel.lchown_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, uid, gid); - } result.map(|()| Value::Null).map_err(kernel_error) } "fs.getxattrSync" => { @@ -2754,7 +1226,15 @@ pub(crate) fn service_javascript_fs_sync_rpc( name, follow_symlinks, ) - .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map(|bytes| host_bytes_value(&bytes)) + .map_err(kernel_error) + } + "fs.fgetxattrSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fgetxattr fd")?; + let name = javascript_sync_rpc_arg_str(&request.args, 1, "filesystem xattr name")?; + kernel + .fd_get_xattr_for_process(EXECUTION_DRIVER_NAME, kernel_pid, fd, name) + .map(|bytes| host_bytes_value(&bytes)) .map_err(kernel_error) } "fs.listxattrSync" => { @@ -2777,6 +1257,13 @@ pub(crate) fn service_javascript_fs_sync_rpc( .map(|names| json!(names)) .map_err(kernel_error) } + "fs.flistxattrSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem flistxattr fd")?; + kernel + .fd_list_xattrs_for_process(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map(|names| json!(names)) + .map_err(kernel_error) + } "fs.setxattrSync" => { let path = javascript_sync_rpc_path_arg( process, @@ -2800,15 +1287,18 @@ pub(crate) fn service_javascript_fs_sync_rpc( flags, follow_symlinks, ) - .map_err(kernel_error)?; - if name == "system.posix_acl_access" { - let mode = kernel - .stat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path.as_str()) - .map_err(kernel_error)? - .mode; - mirror_process_mode_to_shadow(process, path.as_str(), mode)?; - } - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "fs.fsetxattrSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fsetxattr fd")?; + let name = javascript_sync_rpc_arg_str(&request.args, 1, "filesystem xattr name")?; + let value = javascript_sync_rpc_bytes_arg(&request.args, 2, "filesystem xattr value")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 3, "filesystem xattr flags")?; + kernel + .fd_set_xattr_for_process(EXECUTION_DRIVER_NAME, kernel_pid, fd, name, value, flags) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.removexattrSync" => { let path = javascript_sync_rpc_path_arg( @@ -2832,6 +1322,14 @@ pub(crate) fn service_javascript_fs_sync_rpc( .map(|()| Value::Null) .map_err(kernel_error) } + "fs.fremovexattrSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fremovexattr fd")?; + let name = javascript_sync_rpc_arg_str(&request.args, 1, "filesystem xattr name")?; + kernel + .fd_remove_xattr_for_process(EXECUTION_DRIVER_NAME, kernel_pid, fd, name) + .map(|()| Value::Null) + .map_err(kernel_error) + } "fs.utimesSync" | "fs.promises.utimes" | "fs.lutimesSync" | "fs.promises.lutimes" => { let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem utimes path")?; @@ -2842,100 +1340,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( request.method.as_str(), "fs.lutimesSync" | "fs.promises.lutimes" ); - if let Some(shadow_path) = process_shadow_host_path(process, path) { - if fs::symlink_metadata(&shadow_path).is_ok() { - let result = kernel.utimes_spec_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - path, - atime, - mtime, - follow_symlinks, - ); - if let Err(error) = result { - if error.code() != "ENOENT" { - return Err(kernel_error(error)); - } - } - apply_host_path_utimens( - &shadow_path, - atime, - mtime, - follow_symlinks, - &format!("failed to update process shadow path times {path}"), - )?; - return Ok(Value::Null); - } - } - match mapped_runtime_host_path(kernel, process, path, true) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) => { - let mapped_host_exists = if mapped_runtime_host_path_exists(&mapped_host)? { - true - } else { - materialize_mapped_host_path_from_kernel( - kernel, - kernel_pid, - path, - &mapped_host, - )?; - mapped_runtime_host_path_exists(&mapped_host)? - }; - if mapped_host_exists { - let context = format!("failed to update mapped guest path times {path}"); - // Resolve the host target up front and hold the handle across - // the kernel update so the apply below operates on the verified - // fd. (The handle must stay alive: a `/proc/self/fd` path is - // only valid while its fd is open, and the macOS fd-relative - // path needs the live parent fd.) - let follow_handle = if follow_symlinks { - Some(open_mapped_runtime_beneath( - &mapped_host, - "fs.utimes", - O_PATH_ANCHOR, - Mode::empty(), - )?) - } else { - None - }; - let parent_handle = if follow_symlinks { - None - } else { - Some(open_mapped_runtime_parent_beneath( - &mapped_host, - "fs.lutimes", - )?) - }; - if kernel - .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - .map_err(kernel_error)? - { - let result = kernel.utimes_spec_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - path, - atime, - mtime, - follow_symlinks, - ); - if let Err(error) = result { - if error.code() != "ENOENT" { - return Err(kernel_error(error)); - } - } - } - if let Some(opened) = &follow_handle { - apply_anchored_fd_utimens(&opened.handle, atime, mtime, &context)?; - } else if let Some(parent) = &parent_handle { - apply_mapped_child_utimens(parent, atime, mtime, &context)?; - } - return Ok(Value::Null); - } - } - Some(MappedRuntimeHostAccess::ReadOnly(_)) => { - return Err(read_only_mapped_runtime_host_path_error(path)); - } - None => {} - } kernel .utimes_spec_for_process( EXECUTION_DRIVER_NAME, @@ -2945,37 +1349,13 @@ pub(crate) fn service_javascript_fs_sync_rpc( mtime, follow_symlinks, ) - .map_err(kernel_error)?; - Ok(Value::Null) + .map(|()| Value::Null) + .map_err(kernel_error) } "fs.futimesSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem futimes fd")?; let atime = parse_utime_arg(&request.args, 1, "filesystem futimes atime")?; let mtime = parse_utime_arg(&request.args, 2, "filesystem futimes mtime")?; - if let Some(mapped) = process.mapped_host_fd(fd) { - if let Some(guest_path) = mapped.guest_path.as_deref() { - let result = kernel.utimes_spec_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - guest_path, - atime, - mtime, - true, - ); - if let Err(error) = result { - if error.code() != "ENOENT" { - return Err(kernel_error(error)); - } - } - } - return apply_host_file_utimens( - &mapped.file, - atime, - mtime, - &format!("failed to update mapped guest fd {fd} times"), - ) - .map(|()| Value::Null); - } kernel .futimes(EXECUTION_DRIVER_NAME, kernel_pid, fd, atime, mtime) .map(|()| Value::Null) @@ -3014,195 +1394,24 @@ pub(crate) fn javascript_sync_rpc_path_arg( let path = javascript_sync_rpc_arg_str(args, index, label)?; let path = normalize_process_filesystem_rpc_path(process, path); if path.split('/').any(is_internal_unnamed_file_name) { - return Err(SidecarError::Kernel(format!( - "ENOENT: no such file or directory: {path}" - ))); + return Err(SidecarError::host( + "ENOENT", + format!("no such file or directory: {path}"), + )); } Ok(path) } fn normalize_process_filesystem_rpc_path(process: &ActiveProcess, path: &str) -> String { - let host_path = Path::new(path); - if host_path.is_absolute() { - let normalized_host_path = normalize_host_path(host_path); - if let Some(guest_path) = - guest_path_from_runtime_host_mappings(process, &normalized_host_path) - { - return guest_path; - } - if let Some(sandbox_root) = process.shadow_root.as_ref() { - if let Ok(suffix) = normalized_host_path.strip_prefix(sandbox_root) { - let suffix = suffix.to_string_lossy(); - return normalize_path(&format!("/{}", suffix.trim_start_matches('/'))); - } - } - } - path.to_owned() -} - -fn guest_path_from_runtime_host_mappings( - process: &ActiveProcess, - host_path: &Path, -) -> Option { - runtime_guest_host_mappings(process) - .into_iter() - .filter_map(|(guest_path, host_root)| { - host_path.strip_prefix(&host_root).ok().map(|suffix| { - let suffix = suffix.to_string_lossy(); - normalize_path(&format!( - "{}/{}", - guest_path.trim_end_matches('/'), - suffix.trim_start_matches('/') - )) - }) - }) - .max_by_key(String::len) -} - -fn runtime_guest_host_mappings(process: &ActiveProcess) -> Vec<(String, PathBuf)> { - let Some(mappings) = process - .env - .get("AGENTOS_GUEST_PATH_MAPPINGS") - .and_then(|value| serde_json::from_str::>(value).ok()) - else { - return Vec::new(); - }; - mappings - .into_iter() - .filter_map(|mapping| { - if mapping.guest_path.is_empty() || mapping.host_path.is_empty() { - return None; - } - let host_root = PathBuf::from(mapping.host_path); - let normalized_host_root = if host_root.is_absolute() { - normalize_host_path(&host_root) - } else { - normalize_host_path(&std::env::current_dir().ok()?.join(host_root)) - }; - Some((normalize_path(&mapping.guest_path), normalized_host_root)) - }) - .collect() -} - -pub(crate) fn mirror_kernel_fd_contents_to_process_shadow( - kernel: &mut SidecarKernel, - process: &ActiveProcess, - kernel_pid: u32, - fd: u32, -) -> Result<(), SidecarError> { - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) - .map_err(kernel_error)?; - let path = normalize_process_filesystem_rpc_path(process, &path); - mirror_kernel_path_to_process_shadow(kernel, process, &path) -} - -fn mirror_kernel_path_to_process_shadow( - kernel: &mut SidecarKernel, - process: &ActiveProcess, - guest_path: &str, -) -> Result<(), SidecarError> { - // WASM processes deliberately route filesystem calls through the kernel - // sync RPC path. The kernel VFS (including non-root mounts) is therefore - // their source of truth; they never read the JavaScript process shadow. - // Mirroring here is both redundant and harmful for streamed writes because - // it rereads the entire growing file after every bounded chunk. - if process_prefers_kernel_fs_sync_rpc(process) { - return Ok(()); - } - let normalized_guest_path = normalize_path(guest_path); - // Mounted host paths are already updated by the mapped-runtime write path. - // Mirroring them would read the complete kernel file after each chunk, - // making sequential writes quadratic. - if host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path).is_some() { - return Ok(()); - } - let Some(shadow_path) = process_shadow_host_path(process, &normalized_guest_path) else { - return Ok(()); - }; - // This is internal reconciliation after the guest has already completed a - // permitted write. Reading the resulting bytes as the guest would wrongly - // reject write-only files even though no contents are returned to guest - // code; the trusted sidecar only mirrors them into this VM's own shadow. - let bytes = kernel - .read_file(&normalized_guest_path) - .map_err(kernel_error)?; - write_process_shadow_file(&shadow_path, &normalized_guest_path, &bytes) -} - -fn mirror_process_mode_to_shadow( - process: &ActiveProcess, - guest_path: &str, - mode: u32, -) -> Result<(), SidecarError> { - let Some(shadow_path) = process_shadow_host_path(process, guest_path) else { - return Ok(()); - }; - match fs::symlink_metadata(&shadow_path) { - Ok(metadata) if !metadata.file_type().is_symlink() => { - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(mode & 0o7777)).map_err( - |error| { - SidecarError::Io(format!( - "failed to mirror ACL mode for {} into process shadow: {error}", - normalize_path(guest_path) - )) - }, - ) - } - Ok(_) => Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(SidecarError::Io(format!( - "failed to inspect process shadow for ACL mode {}: {error}", - normalize_path(guest_path) - ))), - } -} - -fn write_process_shadow_file( - shadow_path: &Path, - guest_path: &str, - bytes: &[u8], -) -> Result<(), SidecarError> { - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for {}: {error}", - normalize_path(guest_path) - )) - })?; - } - match fs::symlink_metadata(shadow_path) { - Ok(metadata) if metadata.file_type().is_symlink() => { - fs::remove_file(shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to replace shadow symlink for {}: {error}", - normalize_path(guest_path) - )) - })?; - } - Ok(metadata) if metadata.is_dir() => { - fs::remove_dir_all(shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to replace shadow directory for {}: {error}", - normalize_path(guest_path) - )) - })?; - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow path for {}: {error}", - normalize_path(guest_path) - ))); - } - } - fs::write(shadow_path, bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror kernel file {} into process shadow: {error}", - normalize_path(guest_path) + if path.starts_with('/') { + normalize_path(path) + } else { + normalize_path(&format!( + "{}/{}", + process.guest_cwd.trim_end_matches('/'), + path )) - }) + } } fn javascript_sync_rpc_stat_value(stat: VirtualStat) -> Value { @@ -3231,2436 +1440,128 @@ fn javascript_sync_rpc_stat_value(stat: VirtualStat) -> Value { Value::Object(value) } -fn javascript_sync_rpc_host_stat_value(metadata: &fs::Metadata) -> Value { - let mut value = Map::with_capacity(15); - value.insert("mode".to_string(), Value::from(metadata.mode())); - value.insert("size".to_string(), Value::from(metadata.size())); - value.insert("blocks".to_string(), Value::from(metadata.blocks())); - value.insert("dev".to_string(), Value::from(metadata.dev())); - value.insert("rdev".to_string(), Value::from(metadata.rdev())); - value.insert("isDirectory".to_string(), Value::from(metadata.is_dir())); - value.insert( - "isSymbolicLink".to_string(), - Value::from(metadata.file_type().is_symlink()), - ); - value.insert( - "atimeMs".to_string(), - Value::from(metadata.atime() * 1000 + (metadata.atime_nsec() / 1_000_000)), - ); - value.insert( - "mtimeMs".to_string(), - Value::from(metadata.mtime() * 1000 + (metadata.mtime_nsec() / 1_000_000)), - ); - value.insert( - "ctimeMs".to_string(), - Value::from(metadata.ctime() * 1000 + (metadata.ctime_nsec() / 1_000_000)), - ); - value.insert( - "birthtimeMs".to_string(), - Value::from(metadata.ctime() * 1000 + (metadata.ctime_nsec() / 1_000_000)), - ); - value.insert("ino".to_string(), Value::from(metadata.ino())); - value.insert("nlink".to_string(), Value::from(metadata.nlink())); - value.insert("uid".to_string(), Value::from(metadata.uid())); - value.insert("gid".to_string(), Value::from(metadata.gid())); - Value::Object(value) -} - -fn mapped_runtime_host_path( - kernel: &SidecarKernel, - process: &ActiveProcess, - guest_path: &str, - writable: bool, -) -> Option { - if process_prefers_kernel_fs_sync_rpc(process) { - return None; - } - - let normalized = if guest_path.starts_with('/') { - normalize_path(guest_path) - } else { - normalize_path(&format!( - "{}/{}", - process.guest_cwd.trim_end_matches('/'), - guest_path - )) - }; - let mappings = process - .env - .get("AGENTOS_GUEST_PATH_MAPPINGS") - .and_then(|value| serde_json::from_str::>(value).ok())?; - let mut sorted_mappings = mappings - .into_iter() - .filter_map(|mapping| { - (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some(( - normalize_path(&mapping.guest_path), - PathBuf::from(mapping.host_path), - )) - }) - .collect::>(); - sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len())); - let readable_roots = runtime_host_access_roots(process, "AGENTOS_EXTRA_FS_READ_PATHS")?; - let writable_roots = writable - .then(|| runtime_host_access_roots(process, "AGENTOS_EXTRA_FS_WRITE_PATHS")) - .flatten() - .unwrap_or_default(); - - for (guest_root, host_root) in sorted_mappings { - let normalized_host_root = if host_root.is_absolute() { - normalize_host_path(&host_root) - } else { - normalize_host_path(&std::env::current_dir().ok()?.join(host_root)) - }; - if guest_root != "/" - && normalized != guest_root - && !normalized.starts_with(&format!("{guest_root}/")) - { - continue; - } - if guest_root == "/" && !normalized.starts_with('/') { - continue; - } - if process.runtime == GuestRuntimeKind::JavaScript - && process.shadow_root.as_ref().is_some_and(|shadow_root| { - guest_root == "/" - || normalized_host_root.starts_with(normalize_host_path(shadow_root)) - }) - { - // Embedded JavaScript is kernel-backed. The root host mapping is a - // staging shadow for runtimes that execute against host paths, not - // an independent filesystem namespace. Child cwd mappings inside - // that shadow are staging paths too. Let JavaScript read and write - // the shared kernel VFS so a file created after fork is immediately - // visible to every process in the VM. More-specific mappings to - // explicit host_dir/module_access roots outside the shadow remain - // host-backed. - continue; - } - if guest_root == "/" - && kernel.mounted_filesystems().iter().any(|mount| { - mount.path != "/" - && (normalized == mount.path - || normalized.starts_with(&format!("{}/", mount.path))) - }) - { - // The root mapping is only a process-shadow fallback. A non-root - // kernel mount is authoritative unless a more-specific host mapping - // matched earlier in this loop. - continue; - } - - let suffix = if guest_root == "/" { - normalized.trim_start_matches('/') - } else { - normalized - .strip_prefix(&guest_root) - .unwrap_or_default() - .trim_start_matches('/') - }; - let host_path = if suffix.is_empty() { - normalized_host_root.clone() - } else { - normalized_host_root.join(suffix) - }; - - let is_asset_path = guest_root == PYTHON_PYODIDE_GUEST_ROOT - || normalized == PYTHON_PYODIDE_GUEST_ROOT - || normalized.starts_with(&format!("{PYTHON_PYODIDE_GUEST_ROOT}/")); - let is_cache_path = guest_root == PYTHON_PYODIDE_CACHE_GUEST_ROOT - || normalized == PYTHON_PYODIDE_CACHE_GUEST_ROOT - || normalized.starts_with(&format!("{PYTHON_PYODIDE_CACHE_GUEST_ROOT}/")); - if is_asset_path && !writable { - return Some(MappedRuntimeHostAccess::Writable(MappedRuntimeHostPath { - guest_path: normalized.clone(), - host_root: normalized_host_root.clone(), - host_path, - })); - } - if is_cache_path { - return Some(MappedRuntimeHostAccess::Writable(MappedRuntimeHostPath { - guest_path: normalized.clone(), - host_root: normalized_host_root.clone(), - host_path, - })); - } - - let Some(read_root) = readable_roots - .iter() - .find(|root| path_is_within_root(&host_path, root)) - .cloned() - else { - continue; - }; - if !writable { - return Some(MappedRuntimeHostAccess::Writable(MappedRuntimeHostPath { - guest_path: normalized.clone(), - host_root: read_root.clone(), - host_path, - })); - } - if let Some(write_root) = writable_roots - .iter() - .find(|root| path_is_within_root(&host_path, root)) - .cloned() - { - return Some(MappedRuntimeHostAccess::Writable(MappedRuntimeHostPath { - guest_path: normalized.clone(), - host_root: write_root.clone(), - host_path, - })); - } - if guest_root != "/" { - return Some(MappedRuntimeHostAccess::ReadOnly(MappedRuntimeHostPath { - guest_path: normalized.clone(), - host_root: read_root.clone(), - host_path, - })); - } - } - - None -} - -fn mapped_runtime_host_path_for_read( - kernel: &SidecarKernel, - process: &ActiveProcess, - guest_path: &str, -) -> Option { - match mapped_runtime_host_path(kernel, process, guest_path, false) { - Some(MappedRuntimeHostAccess::Writable(mapped_host)) - | Some(MappedRuntimeHostAccess::ReadOnly(mapped_host)) => Some(mapped_host), - None => None, - } -} - -fn process_shadow_host_path(process: &ActiveProcess, guest_path: &str) -> Option { - let normalized_guest_path = normalized_process_guest_path(process, guest_path); - let shadow_root = process.shadow_root.as_ref()?; - Some(shadow_host_path_for_guest( - shadow_root, - &normalized_guest_path, - )) -} - -fn materialize_process_shadow_symlink( - kernel: &mut SidecarKernel, - process: &ActiveProcess, - kernel_pid: u32, - guest_path: &str, -) -> Result { - let Some(shadow_path) = process_shadow_host_path(process, guest_path) else { - return Ok(false); - }; - let metadata = match fs::symlink_metadata(&shadow_path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect process shadow symlink {}: {error}", - shadow_path.display() - ))) - } - }; - if !metadata.file_type().is_symlink() { - return Ok(false); - } - let target = fs::read_link(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to read process shadow symlink {}: {error}", - shadow_path.display() - )) - })?; - kernel - .symlink_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - &target.to_string_lossy(), - guest_path, - ) - .map_err(kernel_error)?; - Ok(true) -} - -fn normalized_process_guest_path(process: &ActiveProcess, guest_path: &str) -> String { - if guest_path.starts_with('/') { - normalize_path(guest_path) - } else { - normalize_path(&format!( - "{}/{}", - process.guest_cwd.trim_end_matches('/'), - guest_path - )) - } -} - -fn process_prefers_kernel_fs_sync_rpc(process: &ActiveProcess) -> bool { - (process.runtime == GuestRuntimeKind::WebAssembly - // A WASM command executes inside the JavaScript WASI runner, so the - // process record is JavaScript even though its filesystem is still the - // kernel-authoritative WASM path. - || process.env.contains_key("AGENTOS_WASM_MODULE_PATH")) - && process.shadow_root.is_some() -} - -fn runtime_host_access_roots(process: &ActiveProcess, key: &str) -> Option> { - process - .env - .get(key) - .and_then(|value| serde_json::from_str::>(value).ok()) - .map(|roots| { - roots - .into_iter() - .map(PathBuf::from) - .map(|root| normalize_host_path(&root)) - .collect() - }) -} - -fn mapped_runtime_child_mount_basenames(process: &ActiveProcess, guest_path: &str) -> Vec { - let normalized = normalize_path(guest_path); - let mappings = process - .env - .get("AGENTOS_GUEST_PATH_MAPPINGS") - .and_then(|value| serde_json::from_str::>(value).ok()) - .unwrap_or_default(); - let mut basenames = BTreeSet::new(); - for mapping in mappings { - let guest_root = normalize_path(&mapping.guest_path); - if guest_root == "/" || guest_root == normalized { - continue; - } - if mapped_runtime_parent_path(&guest_root) == normalized { - basenames.insert(mapped_runtime_basename(&guest_root)); - } - } - basenames.into_iter().collect() -} - -fn mapped_runtime_parent_path(path: &str) -> String { - let normalized = normalize_path(path); - let parent = Path::new(&normalized) - .parent() - .unwrap_or_else(|| Path::new("/")); - let value = parent.to_string_lossy(); - if value.is_empty() { - String::from("/") - } else { - value.into_owned() - } -} - -fn mapped_runtime_basename(path: &str) -> String { - let normalized = normalize_path(path); - Path::new(&normalized) - .file_name() - .map(|value| value.to_string_lossy().into_owned()) - .unwrap_or_else(|| String::from("/")) -} - -fn read_only_mapped_runtime_host_path_error(guest_path: &str) -> SidecarError { - SidecarError::Kernel(format!("EROFS: read-only filesystem: {guest_path}")) -} - -/// Open `relative` strictly beneath the mapped mount root, returning the owned -/// fd and the resolved (diagnostic-only) host path via the universal -/// resolve-beneath walk in [`crate::plugins::host_dir::confine`]. See that -/// module for why `openat2` is not used. -fn mapped_runtime_open_fd( - host_root: &Path, - relative: &Path, - flags: OFlag, - mode: Mode, -) -> Result { - crate::plugins::host_dir::confine::resolve_beneath(host_root, relative, flags, mode) -} - -fn mapped_runtime_relative_path(mapped: &MappedRuntimeHostPath) -> Result { - let normalized_root = normalize_host_path(&mapped.host_root); - let normalized_path = normalize_host_path(&mapped.host_path); - if !path_is_within_root(&normalized_path, &normalized_root) { - return Err(mapped_runtime_host_path_escape_error( - mapped, - &normalized_path, - )); - } - let relative = normalized_path - .strip_prefix(&normalized_root) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to relativize mapped guest path {} ({} against {}): {error}", - mapped.guest_path, - normalized_path.display(), - normalized_root.display() - )) - })?; - Ok(if relative.as_os_str().is_empty() { - PathBuf::from(".") - } else { - relative.to_path_buf() - }) -} - -/// Re-express the resolver's confined, symlink-resolved host path in the guest -/// namespace. Node resolves a module's real path before walking ancestor -/// `node_modules` directories; preserving the original symlink spelling here -/// breaks pnpm's `.pnpm//node_modules` dependency layout. -fn mapped_runtime_resolved_guest_path( - mapped: &MappedRuntimeHostPath, - resolved_host_path: &Path, -) -> Option { - let requested_relative = mapped_runtime_relative_path(mapped).ok()?; - let canonical_root = fs::canonicalize(&mapped.host_root).ok()?; - let resolved_relative = resolved_host_path.strip_prefix(&canonical_root).ok()?; - - let normalized_guest = normalize_path(&mapped.guest_path); - let requested_suffix = requested_relative.to_string_lossy().replace('\\', "/"); - let guest_root = if requested_suffix == "." || requested_suffix.is_empty() { - normalized_guest - } else { - let suffix = format!("/{requested_suffix}"); - let prefix = normalized_guest.strip_suffix(&suffix)?; - if prefix.is_empty() { - String::from("/") - } else { - prefix.to_owned() - } - }; - let resolved_suffix = resolved_relative.to_string_lossy().replace('\\', "/"); - Some(normalize_path(&format!( - "{}/{}", - guest_root.trim_end_matches('/'), - resolved_suffix - ))) -} - -fn open_mapped_runtime_beneath( - mapped: &MappedRuntimeHostPath, - operation: &str, - flags: OFlag, - mode: Mode, -) -> Result { - let relative = mapped_runtime_relative_path(mapped)?; - let open_mode = if flags.intersects(OFlag::O_CREAT | O_TMPFILE_FLAG) { - mode - } else { - Mode::empty() - }; - let resolved = mapped_runtime_open_fd(&mapped.host_root, &relative, flags, open_mode) - .map_err(|error| mapped_runtime_open_error(operation, mapped, error))?; - Ok(MappedRuntimeOpenedPath { - handle: AnchoredFd { fd: resolved.fd }, - host_path: resolved.real_path, - }) -} - -fn open_mapped_runtime_directory_beneath( - mapped: &MappedRuntimeHostPath, - operation: &str, - relative: &Path, -) -> Result { - let resolved = mapped_runtime_open_fd( - &mapped.host_root, - relative, - OFlag::O_DIRECTORY | OFlag::O_RDONLY, - Mode::empty(), - ) - .map_err(|error| mapped_runtime_open_error(operation, mapped, error))?; - Ok(MappedRuntimeOpenedPath { - handle: AnchoredFd { fd: resolved.fd }, - host_path: resolved.real_path, - }) -} - -fn open_mapped_runtime_parent_beneath( - mapped: &MappedRuntimeHostPath, - operation: &str, -) -> Result { - let relative = mapped_runtime_relative_path(mapped)?; - let child_name = relative.file_name().ok_or_else(|| { - SidecarError::InvalidState(format!( - "{operation}: mapped guest path {} has no parent-relative basename", - mapped.guest_path - )) - })?; - let parent_relative = relative - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - let directory = open_mapped_runtime_directory_beneath(mapped, operation, parent_relative)?; - Ok(MappedRuntimeParentPath { - directory: directory.handle, - host_path: directory.host_path, - child_name: child_name.to_os_string(), - }) -} - -/// Platform-neutral lstat result. Lets the mapped-runtime lstat path produce the -/// same guest-facing stat value from either a `std::fs::Metadata` (Linux, and -/// the macOS root case) or a raw `fstatat` result (macOS fd-relative child -/// lstat), so the operation stays fd-relative on macOS without a `std::fs` -/// metadata handle. -struct HostStat { - mode: u32, - size: u64, - blocks: u64, - dev: u64, - rdev: u64, - is_directory: bool, - is_symbolic_link: bool, - atime_ms: i64, - mtime_ms: i64, - ctime_ms: i64, - ino: u64, - nlink: u64, - uid: u32, - gid: u32, -} - -impl HostStat { - #[cfg_attr(not(test), allow(dead_code))] - fn is_dir(&self) -> bool { - self.is_directory - } - - fn to_value(&self) -> Value { - json!({ - "mode": self.mode, - "size": self.size, - "blocks": self.blocks, - "dev": self.dev, - "rdev": self.rdev, - "isDirectory": self.is_directory, - "isSymbolicLink": self.is_symbolic_link, - "atimeMs": self.atime_ms, - "mtimeMs": self.mtime_ms, - "ctimeMs": self.ctime_ms, - "birthtimeMs": self.ctime_ms, - "ino": self.ino, - "nlink": self.nlink, - "uid": self.uid, - "gid": self.gid, - }) - } -} - -impl From<&fs::Metadata> for HostStat { - fn from(metadata: &fs::Metadata) -> Self { - Self { - mode: metadata.mode(), - size: metadata.size(), - blocks: metadata.blocks(), - dev: metadata.dev(), - rdev: metadata.rdev(), - is_directory: metadata.is_dir(), - is_symbolic_link: metadata.file_type().is_symlink(), - atime_ms: metadata.atime() * 1000 + (metadata.atime_nsec() / 1_000_000), - mtime_ms: metadata.mtime() * 1000 + (metadata.mtime_nsec() / 1_000_000), - ctime_ms: metadata.ctime() * 1000 + (metadata.ctime_nsec() / 1_000_000), - ino: metadata.ino(), - nlink: metadata.nlink(), - uid: metadata.uid(), - gid: metadata.gid(), - } - } -} - -impl HostStat { - // `FileStat` field widths differ by platform (e.g. `st_dev`/`st_nlink` are - // narrower on macOS than on Linux), so these casts are load-bearing on macOS - // even though they are same-type on Linux. - #[allow(clippy::unnecessary_cast)] - fn from_filestat(stat: &nix::sys::stat::FileStat) -> Self { - use nix::sys::stat::SFlag; - let fmt = stat.st_mode & SFlag::S_IFMT.bits(); - Self { - mode: stat.st_mode as u32, - size: stat.st_size as u64, - blocks: stat.st_blocks as u64, - dev: stat.st_dev as u64, - rdev: stat.st_rdev as u64, - is_directory: fmt == SFlag::S_IFDIR.bits(), - is_symbolic_link: fmt == SFlag::S_IFLNK.bits(), - atime_ms: stat.st_atime * 1000 + (stat.st_atime_nsec / 1_000_000), - mtime_ms: stat.st_mtime * 1000 + (stat.st_mtime_nsec / 1_000_000), - ctime_ms: stat.st_ctime * 1000 + (stat.st_ctime_nsec / 1_000_000), - ino: stat.st_ino, - nlink: stat.st_nlink as u64, - uid: stat.st_uid, - gid: stat.st_gid, - } - } -} - -fn mapped_child_lstat(parent: &MappedRuntimeParentPath) -> std::io::Result { - let stat = nix::sys::stat::fstatat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) - .map_err(errno_to_io)?; - Ok(HostStat::from_filestat(&stat)) -} - -fn mapped_runtime_symlink_metadata( - mapped: &MappedRuntimeHostPath, - operation: &str, -) -> Result { - let relative = mapped_runtime_relative_path(mapped)?; - if relative == Path::new(".") { - return fs::symlink_metadata(&mapped.host_path) - .map(|metadata| HostStat::from(&metadata)) - .map_err(|error| { - SidecarError::Io(format!( - "failed to lstat mapped guest path {} -> {}: {error}", - mapped.guest_path, - mapped.host_path.display() - )) - }); - } - - let parent = open_mapped_runtime_parent_beneath(mapped, operation)?; - let host_path = parent.host_path.join(&parent.child_name); - mapped_child_lstat(&parent).map_err(|error| { - SidecarError::Io(format!( - "failed to lstat mapped guest path {} -> {}: {error}", - mapped.guest_path, - host_path.display() - )) - }) -} - -fn read_mapped_runtime_link( - mapped: &MappedRuntimeHostPath, - guest_path: &str, - operation: &str, -) -> Result { - if mapped_runtime_relative_path(mapped)? == Path::new(".") { - return fs::read_link(&mapped.host_path).map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest symlink {} -> {}: {error}", - guest_path, - mapped.host_path.display() - )) - }); - } - - let parent = open_mapped_runtime_parent_beneath(mapped, operation)?; - let host_path = parent.host_path.join(&parent.child_name); - mapped_child_read_link(&parent).map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest symlink {} -> {}: {error}", - guest_path, - host_path.display() - )) - }) -} - -// --------------------------------------------------------------------------- -// Mapped-runtime child operations. -// -// Each operation is performed with an fd-relative `*at` call anchored on the -// resolved parent fd — TOCTOU-safe and portable across Linux, macOS, and -// gVisor. This is the single universal implementation (there is no longer a -// Linux `/proc/self/fd`-append variant). -// --------------------------------------------------------------------------- - -fn errno_to_io(error: Errno) -> std::io::Error { - std::io::Error::from_raw_os_error(error as i32) -} - -fn create_dir_at(dir: &AnchoredFd, name: &std::ffi::OsStr) -> std::io::Result<()> { - nix::sys::stat::mkdirat(Some(dir.as_raw_fd()), name, Mode::from_bits_truncate(0o777)) - .map_err(errno_to_io) -} - -fn mapped_child_create_dir(parent: &MappedRuntimeParentPath) -> std::io::Result<()> { - create_dir_at(&parent.directory, parent.child_name.as_os_str()) -} - -fn mapped_child_is_dir(parent: &MappedRuntimeParentPath) -> std::io::Result { - use nix::sys::stat::SFlag; - let stat = nix::sys::stat::fstatat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) - .map_err(errno_to_io)?; - Ok(stat.st_mode & SFlag::S_IFMT.bits() == SFlag::S_IFDIR.bits()) -} - -fn mapped_child_remove_dir(parent: &MappedRuntimeParentPath) -> std::io::Result<()> { - nix::unistd::unlinkat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - nix::unistd::UnlinkatFlags::RemoveDir, - ) - .map_err(errno_to_io) -} - -fn mapped_child_remove_file(parent: &MappedRuntimeParentPath) -> std::io::Result<()> { - nix::unistd::unlinkat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - nix::unistd::UnlinkatFlags::NoRemoveDir, - ) - .map_err(errno_to_io) -} - -fn mapped_child_symlink(parent: &MappedRuntimeParentPath, target: &str) -> std::io::Result<()> { - nix::unistd::symlinkat( - target, - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - ) - .map_err(errno_to_io) -} - -fn mapped_child_read_link(parent: &MappedRuntimeParentPath) -> std::io::Result { - nix::fcntl::readlinkat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - ) - .map(PathBuf::from) - .map_err(errno_to_io) -} - -/// Set access/modification times on a mapped child without following symlinks -/// (lutimes), using an fd-relative `utimensat` anchored on the resolved parent -/// fd. -fn apply_mapped_child_utimens( - parent: &MappedRuntimeParentPath, - atime: VirtualUtimeSpec, - mtime: VirtualUtimeSpec, - context: &str, -) -> Result<(), SidecarError> { - let existing = match (atime, mtime) { - (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { - let stat = nix::sys::stat::fstatat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) - .map_err(|error| SidecarError::Io(format!("{context}: failed to stat: {error}")))?; - Some(( - VirtualTimeSpec { - sec: stat.st_atime, - nsec: stat.st_atime_nsec.max(0) as u32, - }, - VirtualTimeSpec { - sec: stat.st_mtime, - nsec: stat.st_mtime_nsec.max(0) as u32, - }, - )) - } - _ => None, - }; - let existing_atime = existing - .as_ref() - .map(|(atime, _)| *atime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let existing_mtime = existing - .as_ref() - .map(|(_, mtime)| *mtime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let times = [ - resolve_host_utime(atime, existing_atime), - resolve_host_utime(mtime, existing_mtime), - ]; - utimensat( - Some(parent.directory.as_raw_fd()), - parent.child_name.as_os_str(), - ×[0], - ×[1], - UtimensatFlags::NoFollowSymlink, - ) - .map_err(|error| SidecarError::Io(format!("{context}: failed to set times: {error}"))) -} - -/// Set access/modification times on an already-resolved (symlink-followed) -/// handle via fd-relative `futimens`. Used for the follow-symlink `utimes` path; -/// `Omit` reads the existing time from the same fd (`fstat`), preserving -/// nanosecond precision. -fn apply_anchored_fd_utimens( - handle: &AnchoredFd, - atime: VirtualUtimeSpec, - mtime: VirtualUtimeSpec, - context: &str, -) -> Result<(), SidecarError> { - let existing = match (atime, mtime) { - (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { - let stat = nix::sys::stat::fstat(handle.as_raw_fd()) - .map_err(|error| SidecarError::Io(format!("{context}: failed to stat: {error}")))?; - Some(( - VirtualTimeSpec { - sec: stat.st_atime, - nsec: stat.st_atime_nsec.max(0) as u32, - }, - VirtualTimeSpec { - sec: stat.st_mtime, - nsec: stat.st_mtime_nsec.max(0) as u32, - }, - )) - } - _ => None, - }; - let existing_atime = existing - .as_ref() - .map(|(atime, _)| *atime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let existing_mtime = existing - .as_ref() - .map(|(_, mtime)| *mtime) - .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); - let times = [ - resolve_host_utime(atime, existing_atime), - resolve_host_utime(mtime, existing_mtime), - ]; - handle - .set_times(×[0], ×[1]) - .map_err(|error| SidecarError::Io(format!("{context}: failed to set times: {error}"))) -} - -fn mapped_child_rename( - source: &MappedRuntimeParentPath, - destination: &MappedRuntimeParentPath, -) -> std::io::Result<()> { - // Same-filesystem rename is fd-relative (TOCTOU-safe). A cross-device rename - // (EXDEV) cannot be done with `renameat`, so fall back to a copy+unlink — but - // still fd-relative, anchored on the CONFINED parent dir fds, never on - // `host_path` (a `confine::Resolved::real_path`, which is diagnostic-only and - // whose ancestors a concurrent guest could swap for an escaping symlink). - match nix::fcntl::renameat( - Some(source.directory.as_raw_fd()), - source.child_name.as_os_str(), - Some(destination.directory.as_raw_fd()), - destination.child_name.as_os_str(), - ) { - Ok(()) => Ok(()), - Err(Errno::EXDEV) => move_across_devices_at( - source.directory.fd.as_fd(), - source.child_name.as_os_str(), - destination.directory.fd.as_fd(), - destination.child_name.as_os_str(), - ), - Err(error) => Err(errno_to_io(error)), - } -} - -fn mapped_child_rename_at2( - source: &MappedRuntimeParentPath, - destination: &MappedRuntimeParentPath, - flags: u32, -) -> std::io::Result<()> { - if flags == 0 { - return mapped_child_rename(source, destination); - } - - #[cfg(all(target_os = "linux", target_env = "gnu"))] - { - let flags = nix::fcntl::RenameFlags::from_bits(flags) - .ok_or_else(|| std::io::Error::from_raw_os_error(libc::EINVAL))?; - nix::fcntl::renameat2( - Some(source.directory.as_raw_fd()), - source.child_name.as_os_str(), - Some(destination.directory.as_raw_fd()), - destination.child_name.as_os_str(), - flags, - ) - .map_err(errno_to_io) - } - - #[cfg(not(all(target_os = "linux", target_env = "gnu")))] - { - let _ = (source, destination, flags); - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "renameat2 flags require a Linux host for mapped host paths", - )) - } -} - -fn create_mapped_runtime_directory( - parent: &MappedRuntimeParentPath, - guest_path: &str, - recursive: bool, -) -> Result<(), SidecarError> { - match mapped_child_create_dir(parent) { - Ok(()) => Ok(()), - Err(error) if recursive && error.kind() == std::io::ErrorKind::AlreadyExists => { - match mapped_child_is_dir(parent) { - Ok(true) => Ok(()), - Ok(false) => Err(SidecarError::Io(format!( - "failed to create mapped guest directory {} -> {}: file exists and is not a directory", - guest_path, - parent.host_path.join(&parent.child_name).display() - ))), - Err(metadata_error) => Err(SidecarError::Io(format!( - "failed to inspect existing mapped guest directory {} -> {}: {metadata_error}", - guest_path, - parent.host_path.join(&parent.child_name).display() - ))), - } - } - Err(error) => Err(SidecarError::Io(format!( - "failed to create mapped guest directory {} -> {}: {error}", - guest_path, - parent.host_path.join(&parent.child_name).display() - ))), - } -} - -fn create_mapped_runtime_root_directory( - mapped: &MappedRuntimeHostPath, - recursive: bool, -) -> Result<(), SidecarError> { - let relative = mapped_runtime_relative_path(mapped)?; - if relative != Path::new(".") { - return Err(SidecarError::InvalidState(format!( - "fs.mkdir: mapped guest path {} is not the mapped root", - mapped.guest_path - ))); - } - - if recursive { - match fs::create_dir_all(&mapped.host_path) { - Ok(()) => Ok(()), - Err(error) => Err(SidecarError::Io(format!( - "failed to create mapped guest directory {} -> {}: {error}", - mapped.guest_path, - mapped.host_path.display() - ))), - } - } else { - match fs::create_dir(&mapped.host_path) { - Ok(()) => Ok(()), - Err(error) => Err(SidecarError::Io(format!( - "failed to create mapped guest directory {} -> {}: {error}", - mapped.guest_path, - mapped.host_path.display() - ))), - } - } -} - -fn ensure_mapped_runtime_parent_dirs( - mapped: &MappedRuntimeHostPath, - operation: &str, -) -> Result<(), SidecarError> { - let relative = mapped_runtime_relative_path(mapped)?; - let Some(parent_relative) = relative - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - else { - return Ok(()); - }; - if parent_relative == Path::new(".") { - return Ok(()); - } - - for index in 0..parent_relative.components().count() { - let prefix = parent_relative - .components() - .take(index + 1) - .collect::(); - if open_mapped_runtime_directory_beneath(mapped, operation, &prefix).is_ok() { - continue; - } - - let prefix_parent = prefix - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - let prefix_name = prefix.file_name().ok_or_else(|| { - SidecarError::InvalidState(format!( - "{operation}: invalid mapped guest directory prefix for {}", - mapped.guest_path - )) - })?; - let parent_dir = open_mapped_runtime_directory_beneath(mapped, operation, prefix_parent)?; - create_dir_at(&parent_dir.handle, prefix_name).map_err(|error| { - SidecarError::Io(format!( - "{operation}: failed to create mapped guest parent {} under {}: {error}", - mapped.guest_path, - parent_dir.host_path.display() - )) - })?; - } - - Ok(()) -} - -fn mapped_runtime_open_error( - operation: &str, - mapped: &MappedRuntimeHostPath, - error: Errno, -) -> SidecarError { - match error { - Errno::EXDEV => mapped_runtime_host_path_escape_error(mapped, &mapped.host_path), - other => SidecarError::Io(format!( - "{operation}: failed to open mapped guest path {} beneath {}: {}", - mapped.guest_path, - mapped.host_root.display(), - std::io::Error::from_raw_os_error(other as i32) - )), - } -} - -fn mapped_runtime_host_path_escape_error( - mapped: &MappedRuntimeHostPath, - resolved: &Path, -) -> SidecarError { - SidecarError::Io(format!( - "mapped guest path {} escapes mapped host root {} via {}", - mapped.guest_path, - mapped.host_root.display(), - resolved.display() - )) -} - -fn mapped_host_open_is_writable(flags: u32) -> bool { - let access_mode = flags & libc::O_ACCMODE as u32; - access_mode == libc::O_WRONLY as u32 - || access_mode == libc::O_RDWR as u32 - || flags & libc::O_APPEND as u32 != 0 - || flags & libc::O_CREAT as u32 != 0 - || flags & libc::O_TRUNC as u32 != 0 -} - -fn mapped_runtime_exists_error(mapped: &MappedRuntimeHostPath, error: Errno) -> SidecarError { - if error == Errno::EXDEV { - return mapped_runtime_host_path_escape_error(mapped, &mapped.host_path); - } - SidecarError::Io(format!( - "failed to inspect mapped guest path {} -> {}: {}", - mapped.guest_path, - mapped.host_path.display(), - std::io::Error::from_raw_os_error(error as i32) - )) -} - -/// Confined existence check (lstat semantics) for a mapped guest path. Resolves -/// the PARENT strictly beneath the mapped root via the universal `confine` walk -/// (which refuses ancestor `..`/symlink escapes) and `lstat`s the leaf through -/// the anchored parent fd — never a path-based `fs::symlink_metadata`, whose -/// ancestor resolution a guest could redirect out of the mapped root by swapping -/// an ancestor for a symlink, leaking an out-of-root existence bit. A missing -/// leaf OR a missing/non-directory ancestor yields `Ok(false)`; an escape yields -/// a typed error. -fn mapped_runtime_host_path_exists(mapped: &MappedRuntimeHostPath) -> Result { - use crate::plugins::host_dir::confine; - - let relative = mapped_runtime_relative_path(mapped)?; - let leaf = match relative.file_name() { - Some(name) => name.to_os_string(), - // `.` is the mapped root itself: open it directly to test existence. - None => { - return match confine::resolve_dir_anchor_beneath(&mapped.host_root, Path::new(".")) { - Ok(_) => Ok(true), - Err(Errno::ENOENT) | Err(Errno::ENOTDIR) => Ok(false), - Err(error) => Err(mapped_runtime_exists_error(mapped, error)), - }; - } - }; - let parent_relative = relative - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")) - .to_path_buf(); - - let parent = match confine::resolve_dir_anchor_beneath(&mapped.host_root, &parent_relative) { - Ok(resolved) => resolved, - // A missing (or non-directory) ancestor means the leaf cannot exist yet. - Err(Errno::ENOENT) | Err(Errno::ENOTDIR) => return Ok(false), - Err(error) => return Err(mapped_runtime_exists_error(mapped, error)), - }; - match nix::sys::stat::fstatat( - Some(parent.fd.as_raw_fd()), - leaf.as_os_str(), - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) { - Ok(_) => Ok(true), - Err(Errno::ENOENT) => Ok(false), - Err(error) => Err(mapped_runtime_exists_error(mapped, error)), - } -} - -fn materialize_mapped_host_path_from_kernel( - kernel: &mut SidecarKernel, - kernel_pid: u32, - guest_path: &str, - mapped: &MappedRuntimeHostPath, -) -> Result<(), SidecarError> { - if mapped_runtime_host_path_exists(mapped)? { - return Ok(()); - } - - if !kernel - .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path) - .map_err(kernel_error)? - { - return Ok(()); - } - - let stat = kernel - .lstat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path) - .map_err(kernel_error)?; - - if stat.is_symbolic_link { - let target = kernel - .read_link_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path) - .map_err(kernel_error)?; - ensure_mapped_runtime_parent_dirs(mapped, "fs.materialize")?; - let parent = open_mapped_runtime_parent_beneath(mapped, "fs.materialize")?; - mapped_child_symlink(&parent, &target).map_err(|error| { - SidecarError::Io(format!( - "failed to materialize mapped guest symlink {} -> {} ({target}): {error}", - guest_path, - parent.host_path.join(&parent.child_name).display() - )) - })?; - return Ok(()); - } else if stat.is_directory { - if mapped_runtime_relative_path(mapped)? == Path::new(".") { - create_mapped_runtime_root_directory(mapped, true)?; - } else { - ensure_mapped_runtime_parent_dirs(mapped, "fs.materialize")?; - let parent = open_mapped_runtime_parent_beneath(mapped, "fs.materialize")?; - create_mapped_runtime_directory(&parent, guest_path, true)?; - } - } else { - let bytes = kernel - .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path) - .map_err(kernel_error)?; - ensure_mapped_runtime_parent_dirs(mapped, "fs.materialize")?; - let opened = open_mapped_runtime_beneath( - mapped, - "fs.materialize", - OFlag::O_CREAT | OFlag::O_TRUNC | OFlag::O_WRONLY, - Mode::from_bits_truncate((stat.mode & 0o7777) as _), - )?; - opened.handle.write_bytes(&bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to materialize mapped guest file {} -> {}: {error}", - guest_path, - opened.host_path.display() - )) - })?; - } - - let opened = - open_mapped_runtime_beneath(mapped, "fs.materialize", O_PATH_ANCHOR, Mode::empty())?; - opened - .handle - .set_mode(stat.mode & 0o7777) - .map_err(|error| { - SidecarError::Io(format!( - "failed to set permissions for materialized mapped guest path {} -> {}: {error}", - guest_path, - opened.host_path.display() - )) - })?; - - Ok(()) -} - -/// Register a persistent guest file handle backed by an already-resolved -/// mapped-host fd. The resolve-beneath open already applied the guest's access -/// mode and creation flags, so the owned fd is turned directly into a -/// [`std::fs::File`] — no path re-open, so there is no TOCTOU window and no -/// `/proc/self/fd` dependency. -fn open_mapped_host_fd( - kernel: &SidecarKernel, - process: &mut ActiveProcess, - opened: MappedRuntimeOpenedPath, - guest_path: Option, -) -> Result { - if let Some(limit) = kernel.resource_limits().max_open_fds { - let observed = kernel - .resource_snapshot() - .open_fds - .saturating_add(process.mapped_host_fds.len()); - if observed >= limit { - return Err(SidecarError::InvalidState(format!( - "EMFILE: VM open file descriptor limit {limit} reached (limits.resources.maxOpenFds); raise the limit to open more mapped host files" - ))); - } - } - let host_path = opened.host_path; - let file = std::fs::File::from(opened.handle.into_owned_fd()); - let fd = process.allocate_mapped_host_fd(crate::state::ActiveMappedHostFd { - file, - path: host_path, - guest_path, - }); - Ok(json!(fd)) -} - -fn read_mapped_host_fd( - mapped: &mut crate::state::ActiveMappedHostFd, - fd: u32, - length: usize, - position: Option, -) -> Result { - let mut bytes = vec![0_u8; length]; - let read = match position { - Some(offset) => mapped.file.read_at(&mut bytes, offset), - None => mapped.file.read(&mut bytes), - } - .map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest fd {fd} -> {}: {error}", - mapped.path.display() - )) - })?; - bytes.truncate(read); - Ok(javascript_sync_rpc_bytes_value(&bytes)) -} - -fn write_mapped_host_fd( - mapped: &mut crate::state::ActiveMappedHostFd, - fd: u32, - contents: &[u8], - position: Option, -) -> Result { - let written = match position { - Some(offset) => mapped.file.write_at(contents, offset), - None => mapped.file.write(contents), - } - .map_err(|error| { - SidecarError::Io(format!( - "failed to write mapped guest fd {fd} -> {}: {error}", - mapped.path.display() - )) - })?; - Ok(json!(written)) -} - -fn write_all_mapped_host_fd( - mapped: &mut crate::state::ActiveMappedHostFd, - fd: u32, - contents: &[u8], - position: Option, -) -> Result { - let mut total = 0usize; - while total < contents.len() { - let write_position = position.map(|offset| offset.saturating_add(total as u64)); - let written = match write_position { - Some(offset) => mapped.file.write_at(&contents[total..], offset), - None => mapped.file.write(&contents[total..]), - } - .map_err(|error| { - SidecarError::Io(format!( - "failed to write mapped guest fd {fd} -> {}: {error}", - mapped.path.display() - )) - })?; - if written == 0 { - return Err(SidecarError::Execution(format!( - "EIO: filesystem write made no progress on mapped fd {fd}" - ))); - } - total = total.saturating_add(written); - } - Ok(total) -} - -fn read_le_u32(payload: &[u8], offset: &mut usize, label: &str) -> Result { - let end = offset - .checked_add(4) - .ok_or_else(|| SidecarError::InvalidState(format!("filesystem {label} offset overflow")))?; - let bytes = payload.get(*offset..end).ok_or_else(|| { - SidecarError::InvalidState(format!("truncated filesystem {label} payload")) - })?; - *offset = end; - Ok(u32::from_le_bytes( - bytes.try_into().expect("slice length checked"), - )) -} - -fn decode_javascript_writev_raw_payload(payload: &[u8]) -> Result, SidecarError> { - let mut offset = 0usize; - let count = read_le_u32(payload, &mut offset, "writev count")? as usize; - let mut buffers = Vec::with_capacity(count); - for _ in 0..count { - let len = read_le_u32(payload, &mut offset, "writev buffer length")? as usize; - let end = offset.checked_add(len).ok_or_else(|| { - SidecarError::InvalidState(String::from("filesystem writev payload length overflow")) - })?; - let buffer = payload.get(offset..end).ok_or_else(|| { - SidecarError::InvalidState(String::from("truncated filesystem writev payload")) - })?; - buffers.push(buffer); - offset = end; - } - if offset != payload.len() { - return Err(SidecarError::InvalidState(String::from( - "filesystem writev payload has trailing bytes", - ))); - } - Ok(buffers) -} - -fn rename_mapped_host_path( - source: &str, - source_host: Option, - destination: &str, - destination_host: Option, -) -> Result { - match (source_host, destination_host) { - ( - Some(MappedRuntimeHostAccess::Writable(source_host)), - Some(MappedRuntimeHostAccess::Writable(destination_host)), - ) => { - if normalize_host_path(&source_host.host_root) - != normalize_host_path(&destination_host.host_root) - { - return Err(SidecarError::Kernel(format!( - "EXDEV: invalid cross-device link: {source} -> {destination}" - ))); - } - let source_parent = open_mapped_runtime_parent_beneath(&source_host, "fs.rename")?; - let destination_parent = - open_mapped_runtime_parent_beneath(&destination_host, "fs.rename")?; - let source_host_path = source_parent.host_path.join(&source_parent.child_name); - let destination_host_path = destination_parent - .host_path - .join(&destination_parent.child_name); - mapped_child_rename(&source_parent, &destination_parent) - .map(|()| Value::Null) - .map_err(|error| { - SidecarError::Io(format!( - "failed to rename mapped guest path {} -> {} ({} -> {}): {error}", - source, - destination, - source_host_path.display(), - destination_host_path.display() - )) - }) - } - (Some(MappedRuntimeHostAccess::ReadOnly(_)), _) => { - Err(read_only_mapped_runtime_host_path_error(source)) - } - (_, Some(MappedRuntimeHostAccess::ReadOnly(_))) => { - Err(read_only_mapped_runtime_host_path_error(destination)) - } - _ => Err(SidecarError::Kernel(format!( - "EXDEV: invalid cross-device link: {source} -> {destination}" - ))), - } -} - -fn rename_mapped_host_path_at2( - source: &str, - source_host: Option, - destination: &str, - destination_host: Option, - flags: u32, -) -> Result { - match (source_host, destination_host) { - ( - Some(MappedRuntimeHostAccess::Writable(source_host)), - Some(MappedRuntimeHostAccess::Writable(destination_host)), - ) => { - if normalize_host_path(&source_host.host_root) - != normalize_host_path(&destination_host.host_root) - { - return Err(SidecarError::Kernel(format!( - "EXDEV: invalid cross-device link: {source} -> {destination}" - ))); - } - let source_parent = open_mapped_runtime_parent_beneath(&source_host, "fs.renameAt2")?; - let destination_parent = - open_mapped_runtime_parent_beneath(&destination_host, "fs.renameAt2")?; - mapped_child_rename_at2(&source_parent, &destination_parent, flags) - .map(|()| Value::Null) - .map_err(|error| { - SidecarError::Io(format!( - "failed to renameat2 mapped guest path {source} -> {destination} with flags {flags:#x}: {error}" - )) - }) - } - (Some(MappedRuntimeHostAccess::ReadOnly(_)), _) => { - Err(read_only_mapped_runtime_host_path_error(source)) - } - (_, Some(MappedRuntimeHostAccess::ReadOnly(_))) => { - Err(read_only_mapped_runtime_host_path_error(destination)) - } - _ => Err(SidecarError::Kernel(format!( - "EXDEV: invalid cross-device link: {source} -> {destination}" - ))), - } -} - -/// Cross-device move of `(src_dir, src_name)` to `(dst_dir, dst_name)` performed -/// entirely fd-relative against the CONFINED parent directory fds — copy then -/// unlink, recursing into directories with `openat(O_NOFOLLOW)` subdir fds. -/// -/// This replaces a path-based `fs::copy`/`fs::rename` fallback that operated on -/// `confine::Resolved::real_path` strings: those re-traverse from `/` and follow -/// any ancestor symlink, so a guest racing `rmdir a; ln -s /etc a` could redirect -/// the copy outside the mapped root. Anchoring every syscall on the pinned parent -/// fds (and `O_NOFOLLOW` on every `openat`) keeps the move strictly confined: -/// a leaf swapped to a symlink fails closed (`ELOOP`) rather than being followed, -/// except a genuine symlink leaf, which is recreated verbatim (never dereferenced). -fn move_across_devices_at( - src_dir: BorrowedFd<'_>, - src_name: &std::ffi::OsStr, - dst_dir: BorrowedFd<'_>, - dst_name: &std::ffi::OsStr, -) -> std::io::Result<()> { - move_across_devices_at_depth(src_dir, src_name, dst_dir, dst_name, 0) -} - -/// Maximum directory nesting a single cross-device move will descend. A hostile -/// guest can nest directories arbitrarily deep (fd-relative `mkdirat` is not -/// `PATH_MAX`-bounded), and unbounded recursion here would overflow the sidecar -/// thread stack — a SIGSEGV that aborts every co-tenant VM — or exhaust file -/// descriptors (two held per level). Bounded by default per the runtime's -/// resource-safety invariant; deeper trees fail with the typed error below. -const MAX_CROSS_DEVICE_MOVE_DEPTH: u32 = 256; - -fn move_across_devices_at_depth( - src_dir: BorrowedFd<'_>, - src_name: &std::ffi::OsStr, - dst_dir: BorrowedFd<'_>, - dst_name: &std::ffi::OsStr, - depth: u32, -) -> std::io::Result<()> { - use nix::sys::stat::SFlag; - - if depth > MAX_CROSS_DEVICE_MOVE_DEPTH { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!( - "cross-device move exceeded max directory depth {MAX_CROSS_DEVICE_MOVE_DEPTH} \ - (raise MAX_CROSS_DEVICE_MOVE_DEPTH to allow deeper trees)" - ), - )); - } - - let stat = nix::sys::stat::fstatat( - Some(src_dir.as_raw_fd()), - src_name, - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) - .map_err(errno_to_io)?; - remove_dest_at(dst_dir, dst_name)?; - - let fmt = stat.st_mode & SFlag::S_IFMT.bits(); - let perm = Mode::from_bits_truncate((stat.st_mode & 0o7777) as _); - - if fmt == SFlag::S_IFLNK.bits() { - let target = - nix::fcntl::readlinkat(Some(src_dir.as_raw_fd()), src_name).map_err(errno_to_io)?; - nix::unistd::symlinkat(target.as_os_str(), Some(dst_dir.as_raw_fd()), dst_name) - .map_err(errno_to_io)?; - nix::unistd::unlinkat( - Some(src_dir.as_raw_fd()), - src_name, - nix::unistd::UnlinkatFlags::NoRemoveDir, - ) - .map_err(errno_to_io)?; - return Ok(()); - } - - if fmt == SFlag::S_IFDIR.bits() { - // Create the destination owner-writable/searchable so the non-root - // sidecar can populate it even when the source mode lacks owner - // write/exec (e.g. `0o555`); the exact source mode is restored by the - // trailing `fchmod` after all children are copied. - nix::sys::stat::mkdirat(Some(dst_dir.as_raw_fd()), dst_name, perm | Mode::S_IRWXU) - .map_err(errno_to_io)?; - let src_sub = open_child_beneath(src_dir, src_name, true)?; - let dst_sub = open_child_beneath(dst_dir, dst_name, true)?; - for (name, _kind) in - crate::plugins::host_dir::confine::read_dir(src_sub.as_fd()).map_err(errno_to_io)? - { - move_across_devices_at_depth( - src_sub.as_fd(), - &name, - dst_sub.as_fd(), - &name, - depth + 1, - )?; - } - // Restore the source directory's exact mode (mkdirat used a temporary - // owner-writable mode above, and applied the umask). - nix::sys::stat::fchmod(dst_sub.as_raw_fd(), perm).map_err(errno_to_io)?; - nix::unistd::unlinkat( - Some(src_dir.as_raw_fd()), - src_name, - nix::unistd::UnlinkatFlags::RemoveDir, - ) - .map_err(errno_to_io)?; - return Ok(()); - } - - if fmt != SFlag::S_IFREG.bits() { - // Only regular files, directories, and symlinks are movable. Special - // files (FIFO/socket/device) cannot be created through this VFS (no - // `mknod`), so a node here was placed by the host operator; refuse it - // rather than block indefinitely on an `O_RDONLY` open of a FIFO. - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "cross-device move: unsupported non-regular file in mapped root", - )); - } - - // Regular file: stream the bytes fd→fd. - let src_fd = open_child_beneath(src_dir, src_name, false)?; - let dst_fd = rustix::fs::openat( - dst_dir, - dst_name, - rustix::fs::OFlags::WRONLY - | rustix::fs::OFlags::CREATE - | rustix::fs::OFlags::EXCL - | rustix::fs::OFlags::NOFOLLOW - | rustix::fs::OFlags::CLOEXEC, - rustix::fs::Mode::from_bits_truncate((stat.st_mode & 0o7777) as _), - ) - .map_err(|error| std::io::Error::from_raw_os_error(error.raw_os_error()))?; - if let Err(error) = copy_fd_to_fd(src_fd.as_fd(), dst_fd.as_fd()) { - // Never leave a truncated destination behind on a failed move. This - // cleanup is best-effort; the ORIGINAL copy error is what propagates. - drop(dst_fd); - let _ = nix::unistd::unlinkat( - Some(dst_dir.as_raw_fd()), - dst_name, - nix::unistd::UnlinkatFlags::NoRemoveDir, - ); - return Err(error); - } - nix::unistd::unlinkat( - Some(src_dir.as_raw_fd()), - src_name, - nix::unistd::UnlinkatFlags::NoRemoveDir, - ) - .map_err(errno_to_io) -} - -/// `openat` a single child of `dir` with `O_NOFOLLOW` (fails closed with `ELOOP` -/// if the child is a symlink), returning an owned fd. `directory` opens it -/// `O_DIRECTORY | O_RDONLY`; otherwise `O_RDONLY`. -fn open_child_beneath( - dir: BorrowedFd<'_>, - name: &std::ffi::OsStr, - directory: bool, -) -> std::io::Result { - let mut flags = - rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::CLOEXEC; - if directory { - flags |= rustix::fs::OFlags::DIRECTORY; - } - rustix::fs::openat(dir, name, flags, rustix::fs::Mode::empty()) - .map_err(|error| std::io::Error::from_raw_os_error(error.raw_os_error())) -} - -/// Copy all bytes from `src` to `dst`, streaming through a fixed buffer (no whole -/// -file allocation), using fd `read`/`write`. -fn copy_fd_to_fd(src: BorrowedFd<'_>, dst: BorrowedFd<'_>) -> std::io::Result<()> { - let mut buf = [0_u8; 65536]; - loop { - let read = nix::unistd::read(src.as_raw_fd(), &mut buf).map_err(errno_to_io)?; - if read == 0 { - break; - } - write_all_to_fd(dst, &buf[..read])?; - } - Ok(()) -} - -/// Remove an existing destination entry (fd-relative, nofollow): a file or -/// symlink is unlinked, a directory is `rmdir`ed (fails if non-empty, matching -/// rename-replace semantics), a missing entry is a no-op. -fn remove_dest_at(dst_dir: BorrowedFd<'_>, name: &std::ffi::OsStr) -> std::io::Result<()> { - use nix::sys::stat::SFlag; - match nix::sys::stat::fstatat( - Some(dst_dir.as_raw_fd()), - name, - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW, - ) { - Ok(stat) => { - let flags = if stat.st_mode & SFlag::S_IFMT.bits() == SFlag::S_IFDIR.bits() { - nix::unistd::UnlinkatFlags::RemoveDir - } else { - nix::unistd::UnlinkatFlags::NoRemoveDir - }; - nix::unistd::unlinkat(Some(dst_dir.as_raw_fd()), name, flags).map_err(errno_to_io) - } - Err(Errno::ENOENT) => Ok(()), - Err(error) => Err(errno_to_io(error)), - } -} - -fn mapped_readdir_entry_is_directory( - mapped_host: &MappedRuntimeHostPath, - directory: &MappedRuntimeOpenedPath, - guest_dir_path: &str, - name: &std::ffi::OsStr, - kind: crate::plugins::host_dir::confine::EntryKind, -) -> Option { - match kind { - crate::plugins::host_dir::confine::EntryKind::Directory => Some(true), - crate::plugins::host_dir::confine::EntryKind::Other => Some(false), - // A symlink entry is followed by re-resolving it beneath the same root - // (fd-anchored), then classifying the target via `fstat`. - crate::plugins::host_dir::confine::EntryKind::Symlink => { - let name_str = name.to_str()?; - let child = MappedRuntimeHostPath { - guest_path: normalize_path(&format!( - "{}/{}", - guest_dir_path.trim_end_matches('/'), - name_str - )), - host_root: mapped_host.host_root.clone(), - host_path: directory.host_path.join(name), - }; - let opened = open_mapped_runtime_beneath( - &child, - "fs.readdir entry", - O_PATH_ANCHOR, - Mode::empty(), - ) - .ok()?; - opened.handle.metadata().map(|stat| stat.is_directory).ok() - } - } -} - -pub(crate) fn service_javascript_fs_readdir_entries( - kernel: &mut SidecarKernel, - process: &ActiveProcess, - kernel_pid: u32, - path: &str, -) -> Result, SidecarError> { - if let Some(MappedRuntimeHostAccess::Writable(mapped_host)) = - mapped_runtime_host_path(kernel, process, path, false) - { - let mut typed: BTreeMap = BTreeMap::new(); - match open_mapped_runtime_beneath( - &mapped_host, - "fs.readdir", - OFlag::O_DIRECTORY | OFlag::O_RDONLY, - Mode::empty(), - ) { - Ok(directory) => { - let entries = - crate::plugins::host_dir::confine::read_dir(directory.handle.fd.as_fd()) - .map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest directory {} -> {}: {}", - path, - directory.host_path.display(), - std::io::Error::from_raw_os_error(error as i32) - )) - })?; - for (name, kind) in entries { - if let Some(is_dir) = mapped_readdir_entry_is_directory( - &mapped_host, - &directory, - path, - &name, - kind, - ) { - let Ok(name) = name.into_string() else { - continue; - }; - typed.insert(name, is_dir); - } - } - } - // The host dir simply not existing yet is fine — fall through to the - // kernel VFS. Test existence through the confined walk (not a - // path-based `symlink_metadata`, whose ancestors a guest could - // redirect out of the mapped root); on a resolve error, keep the - // original readdir error rather than swallowing it. - Err(_) - if mapped_runtime_host_path_exists(&mapped_host) - .map(|exists| !exists) - .unwrap_or(false) => {} - Err(error) => return Err(error), - } - match kernel.read_dir_with_types_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) { - Ok(entries) => { - for entry in entries { - typed.entry(entry.name).or_insert(entry.is_directory); - } - } - Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => {} - Err(error) => return Err(kernel_error(error)), - } - for name in mapped_runtime_child_mount_basenames(process, path) { - typed.entry(name).or_insert(true); - } - return Ok(typed); - } - - kernel - .read_dir_with_types_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) - .map(|entries| { - entries - .into_iter() - .map(|entry| (entry.name, entry.is_directory)) - .collect() - }) - .map_err(kernel_error) -} - -pub(crate) fn service_javascript_fs_readdir_raw_sync_rpc( - kernel: &mut SidecarKernel, - process: &ActiveProcess, - kernel_pid: u32, - request: &JavascriptSyncRpcRequest, -) -> Result, SidecarError> { - let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem readdir path")?; - let entries = - service_javascript_fs_readdir_entries(kernel, process, kernel_pid, path.as_str())?; - encode_javascript_readdir_raw_payload(entries) -} - -fn encode_javascript_readdir_raw_payload( - entries: BTreeMap, -) -> Result, SidecarError> { - let mut payload = Vec::new(); - for (name, is_dir) in entries - .into_iter() - .filter(|(name, _)| name != "." && name != "..") - { - let name = name.into_bytes(); - let name_len = u32::try_from(name.len()).map_err(|_| { - SidecarError::InvalidState(String::from("filesystem readdir entry name too long")) - })?; - payload.push(u8::from(is_dir)); - payload.extend_from_slice(&name_len.to_le_bytes()); - payload.extend_from_slice(&name); - } - Ok(payload) -} - -/// Like `javascript_sync_rpc_readdir_value` but carries each entry's -/// directory-ness as `{name, isDirectory}`. The guest's `normalizeReaddirEntries` -/// consumes these objects directly for `withFileTypes`, avoiding a per-entry stat -/// RPC, and extracts `.name` for the plain string form. -fn javascript_sync_rpc_readdir_typed_value(entries: BTreeMap) -> Value { - json!(entries - .into_iter() - .filter(|(name, _)| name != "." && name != "..") - .map(|(name, is_dir)| json!({ "name": name, "isDirectory": is_dir })) - .collect::>()) -} - -fn mirror_guest_file_write_to_shadow( - vm: &mut VmState, - guest_path: &str, - bytes: &[u8], -) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let shadow_path = if guest_path == "/" { - vm.cwd.clone() - } else { - vm.cwd.join(guest_path.trim_start_matches('/')) - }; - - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for {}: {error}", - guest_path - )) - })?; - } - - match fs::symlink_metadata(&shadow_path) { - Ok(metadata) if metadata.file_type().is_symlink() => { - fs::remove_file(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to replace shadow symlink for {}: {error}", - guest_path - )) - })?; - } - Ok(metadata) if metadata.is_dir() => { - fs::remove_dir_all(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to replace shadow directory for {}: {error}", - guest_path - )) - })?; - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow path for {}: {error}", - guest_path - ))); - } - } - fs::write(&shadow_path, bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest file {} into shadow root: {error}", - guest_path - )) - })?; - - let stat = vm.kernel.lstat(&guest_path).map_err(kernel_error)?; - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( - |error| { - SidecarError::Io(format!( - "failed to set shadow mode for {}: {error}", - guest_path - )) - }, - )?; - - Ok(()) -} - -fn mirror_guest_directory_write_to_shadow( - vm: &mut VmState, - guest_path: &str, -) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let shadow_path = shadow_host_path_for_guest(&vm.cwd, &guest_path); - - fs::create_dir_all(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest directory {} into shadow root: {error}", - guest_path - )) - })?; - - let stat = vm.kernel.lstat(&guest_path).map_err(kernel_error)?; - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err( - |error| { - SidecarError::Io(format!( - "failed to set shadow mode for directory {}: {error}", - guest_path - )) - }, - )?; - - Ok(()) -} - -fn ensure_guest_path_materialized_in_shadow( - vm: &mut VmState, - guest_path: &str, -) -> Result { - let guest_path = normalize_path(guest_path); - let shadow_path = shadow_host_path_for_guest(&vm.cwd, &guest_path); - if fs::symlink_metadata(&shadow_path).is_ok() { - return Ok(shadow_path); - } - - let stat = vm.kernel.lstat(&guest_path).map_err(kernel_error)?; - if stat.is_symbolic_link { - let target = vm.kernel.read_link(&guest_path).map_err(kernel_error)?; - mirror_guest_symlink_to_shadow(vm, &guest_path, &target)?; - } else if stat.is_directory { - mirror_guest_directory_write_to_shadow(vm, &guest_path)?; - } else { - let bytes = vm.kernel.read_file(&guest_path).map_err(kernel_error)?; - mirror_guest_file_write_to_shadow(vm, &guest_path, &bytes)?; - } - - Ok(shadow_path) -} - -fn mirror_guest_subtree_to_shadow(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - ensure_guest_path_materialized_in_shadow(vm, &guest_path)?; - let stat = vm.kernel.lstat(&guest_path).map_err(kernel_error)?; - if !stat.is_directory || stat.is_symbolic_link { - return Ok(()); - } - - let entries = vm - .kernel - .read_dir_recursive(&guest_path, None) - .map_err(kernel_error)?; - for entry in entries { - ensure_guest_path_materialized_in_shadow(vm, &entry.path)?; - } - Ok(()) -} - -fn mirror_guest_symlink_to_shadow( - vm: &mut VmState, - guest_path: &str, - target: &str, -) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let shadow_path = shadow_host_path_for_guest(&vm.cwd, &guest_path); - let shadow_target = shadow_symlink_target_for_guest(&vm.cwd, &guest_path, target); - - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for symlink {}: {error}", - guest_path - )) - })?; - } - - remove_shadow_path_if_exists(&shadow_path, &guest_path)?; - symlink(&shadow_target, &shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest symlink {} into shadow root: {error}", - guest_path - )) - }) -} - -fn mirror_guest_link_to_shadow( - vm: &mut VmState, - source_path: &str, - destination_path: &str, -) -> Result<(), SidecarError> { - let source_path = normalize_path(source_path); - let destination_path = normalize_path(destination_path); - let source_shadow_path = ensure_guest_path_materialized_in_shadow(vm, &source_path)?; - let destination_shadow_path = shadow_host_path_for_guest(&vm.cwd, &destination_path); - - if let Some(parent) = destination_shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for link {}: {error}", - destination_path - )) - })?; - } - - remove_shadow_path_if_exists(&destination_shadow_path, &destination_path)?; - fs::hard_link(&source_shadow_path, &destination_shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest link {} -> {} into shadow root: {error}", - source_path, destination_path - )) - }) -} - -fn mirror_guest_chmod_to_shadow( - vm: &mut VmState, - guest_path: &str, - mode: u32, -) -> Result<(), SidecarError> { - let shadow_path = ensure_guest_path_materialized_in_shadow(vm, guest_path)?; - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(mode & 0o7777)).map_err(|error| { - SidecarError::Io(format!( - "failed to set shadow mode for {}: {error}", - normalize_path(guest_path) - )) - }) -} - -fn mirror_guest_utimes_to_shadow( - vm: &mut VmState, - guest_path: &str, - atime: VirtualUtimeSpec, - mtime: VirtualUtimeSpec, - follow_symlinks: bool, -) -> Result<(), SidecarError> { - let shadow_path = ensure_guest_path_materialized_in_shadow(vm, guest_path)?; - apply_host_path_utimens( - &shadow_path, - atime, - mtime, - follow_symlinks, - &format!( - "failed to mirror guest utimes for {} into shadow root", - normalize_path(guest_path) - ), - ) -} - -fn mirror_guest_truncate_to_shadow( - vm: &mut VmState, - guest_path: &str, - len: u64, -) -> Result<(), SidecarError> { - let shadow_path = ensure_guest_path_materialized_in_shadow(vm, guest_path)?; - OpenOptions::new() - .write(true) - .open(&shadow_path) - .and_then(|file| file.set_len(len)) - .map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest truncate for {} into shadow root: {error}", - normalize_path(guest_path) - )) - }) -} - -fn remove_guest_shadow_path(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { - let guest_path = normalize_path(guest_path); - let shadow_path = shadow_host_path_for_guest(&vm.cwd, &guest_path); - remove_shadow_path_if_exists(&shadow_path, &guest_path) -} - -fn rename_guest_shadow_path( - vm: &mut VmState, - from_path: &str, - to_path: &str, -) -> Result<(), SidecarError> { - let from_path = normalize_path(from_path); - let to_path = normalize_path(to_path); - let from_shadow_path = shadow_host_path_for_guest(&vm.cwd, &from_path); - let to_shadow_path = shadow_host_path_for_guest(&vm.cwd, &to_path); - - match fs::symlink_metadata(&from_shadow_path) { - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - remove_shadow_path_if_exists(&to_shadow_path, &to_path)?; - return Ok(()); - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow rename source {}: {error}", - from_shadow_path.display() - ))); - } - } - - if let Some(parent) = to_shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for rename {} -> {}: {error}", - from_path, to_path - )) - })?; - } - - remove_shadow_path_if_exists(&to_shadow_path, &to_path)?; - fs::rename(&from_shadow_path, &to_shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest rename {} -> {} into shadow root: {error}", - from_path, to_path - )) - })?; - - Ok(()) -} - -fn remove_shadow_path_if_exists(shadow_path: &Path, guest_path: &str) -> Result<(), SidecarError> { - match fs::symlink_metadata(shadow_path) { - Ok(metadata) => { - if metadata.is_dir() && !metadata.file_type().is_symlink() { - fs::remove_dir_all(shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to remove shadow directory for {}: {error}", - guest_path - )) - })?; - } else { - fs::remove_file(shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to remove shadow path for {}: {error}", - guest_path - )) - })?; - } - Ok(()) - } - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(SidecarError::Io(format!( - "failed to inspect shadow path for {}: {error}", - guest_path - ))), - } -} - -fn sync_active_shadow_path_to_kernel( - vm: &mut VmState, - guest_path: &str, -) -> Result<(), SidecarError> { - sync_active_process_host_writes_to_kernel(vm)?; - let guest_path = normalize_path(guest_path); - if is_protected_agentos_shadow_sync_path(&guest_path) { - return Ok(()); - } - let mut host_paths = active_process_shadow_host_paths_for_guest(vm, &guest_path); - if host_paths.is_empty() && !vm.kernel.exists(&guest_path).unwrap_or(false) { - host_paths.push(shadow_host_path_for_guest(&vm.cwd, &guest_path)); - } - - for host_path in host_paths { - let metadata = match fs::symlink_metadata(&host_path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to stat host shadow path {}: {error}", - host_path.display() - ))); - } - }; - - if metadata.file_type().is_symlink() { - sync_host_symlink_to_kernel(vm, &guest_path, &host_path)?; - return Ok(()); - } - - if metadata.is_dir() { - sync_host_directory_to_kernel(vm, &guest_path, &metadata)?; - return Ok(()); - } - - if metadata.is_file() { - sync_host_file_to_kernel(vm, &guest_path, &host_path, &metadata)?; - return Ok(()); - } - } - - Ok(()) -} - -fn active_process_shadow_host_paths_for_guest(vm: &VmState, guest_path: &str) -> Vec { - let mut candidates = Vec::new(); - let mut seen = BTreeSet::new(); - - for process in vm.active_processes.values() { - if let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) { - push_unique_host_path(&mut candidates, &mut seen, host_path); - } - } - - candidates -} - -fn push_unique_host_path( - candidates: &mut Vec, - seen: &mut BTreeSet, - host_path: PathBuf, -) { - if seen.insert(host_path.clone()) { - candidates.push(host_path); - } -} - -fn shadow_host_path_for_guest(shadow_root: &Path, guest_path: &str) -> PathBuf { - if guest_path == "/" { - shadow_root.to_path_buf() - } else { - shadow_root.join(guest_path.trim_start_matches('/')) - } -} - -fn shadow_symlink_target_for_guest(shadow_root: &Path, guest_path: &str, target: &str) -> PathBuf { - if !target.starts_with('/') { - return PathBuf::from(target); - } - - let link_shadow_path = shadow_host_path_for_guest(shadow_root, guest_path); - let link_parent = link_shadow_path.parent().unwrap_or(shadow_root); - let target_shadow_path = shadow_host_path_for_guest(shadow_root, target); - relative_path_from(link_parent, &target_shadow_path) -} - -fn relative_path_from(base_dir: &Path, target: &Path) -> PathBuf { - let base_components: Vec<_> = base_dir.components().collect(); - let target_components: Vec<_> = target.components().collect(); - - let mut shared_prefix = 0; - while shared_prefix < base_components.len() - && shared_prefix < target_components.len() - && base_components[shared_prefix] == target_components[shared_prefix] - { - shared_prefix += 1; - } - - let mut relative = PathBuf::new(); - for _ in shared_prefix..base_components.len() { - relative.push(".."); - } - for component in target_components.iter().skip(shared_prefix) { - relative.push(component.as_os_str()); - } - - if relative.as_os_str().is_empty() { - PathBuf::from(".") - } else { - relative - } -} - -fn resolve_process_guest_path_to_host( - process: &ActiveProcess, - guest_path: &str, -) -> Option { - let normalized_guest_path = if guest_path.starts_with('/') { - normalize_path(guest_path) - } else { - normalize_path(&format!( - "{}/{}", - process.guest_cwd.trim_end_matches('/'), - guest_path - )) - }; - if let Some(host_path) = - host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path) - { - return Some(host_path); - } - let normalized_guest_cwd = normalize_path(&process.guest_cwd); - let mut host_root = process.host_cwd.clone(); - for _ in normalized_guest_cwd - .trim_start_matches('/') - .split('/') - .filter(|segment| !segment.is_empty()) - { - host_root = host_root.parent()?.to_path_buf(); - } - Some(shadow_host_path_for_guest( - &host_root, - &normalized_guest_path, - )) -} - -/// Removes the host shadow copy of `guest_path` after a kernel-direct guest -/// deletion so the exit-time shadow->kernel sync cannot resurrect it. -pub(crate) fn remove_process_shadow_path( - process: &ActiveProcess, - guest_path: &str, -) -> Result<(), SidecarError> { - let Some(shadow_path) = process_shadow_host_path(process, guest_path) else { - return Ok(()); - }; - remove_shadow_path_if_exists(&shadow_path, guest_path) -} - -/// Mirrors a kernel-direct guest rename into the host shadow tree. If the -/// source shadow entry is missing the stale destination copy is still removed -/// so the shadow walk cannot resurrect pre-rename content. -pub(crate) fn rename_process_shadow_path( - process: &ActiveProcess, - source: &str, - destination: &str, -) -> Result<(), SidecarError> { - let Some(source_shadow) = process_shadow_host_path(process, source) else { - return Ok(()); - }; - let Some(destination_shadow) = process_shadow_host_path(process, destination) else { - return Ok(()); - }; - - if fs::symlink_metadata(&source_shadow).is_err() { - return remove_shadow_path_if_exists(&destination_shadow, destination); - } - - if let Some(parent) = destination_shadow.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for rename {source} -> {destination}: {error}" - )) - })?; - } - remove_shadow_path_if_exists(&destination_shadow, destination)?; - fs::rename(&source_shadow, &destination_shadow).map_err(|error| { - SidecarError::Io(format!( - "failed to mirror guest rename {source} -> {destination} into shadow root: {error}" - )) - }) -} - -fn rename_process_shadow_path_at2( - process: &ActiveProcess, - source: &str, - destination: &str, - flags: u32, -) -> Result<(), SidecarError> { - match flags { - 0 | RENAME_NOREPLACE => rename_process_shadow_path(process, source, destination), - RENAME_EXCHANGE => { - let Some(source_shadow) = process_shadow_host_path(process, source) else { - return Ok(()); - }; - let Some(destination_shadow) = process_shadow_host_path(process, destination) else { - return Ok(()); - }; - if fs::symlink_metadata(&source_shadow).is_err() - || fs::symlink_metadata(&destination_shadow).is_err() - { - return Ok(()); - } - - let parent = source_shadow.parent().ok_or_else(|| { - SidecarError::Io(format!("shadow rename source has no parent: {source}")) - })?; - let temporary = (0..128) - .find_map(|_| { - let id = NEXT_SHADOW_RENAME_EXCHANGE_ID.fetch_add(1, Ordering::Relaxed); - let candidate = parent.join(format!(".agentos-rename-exchange-{id}")); - if fs::symlink_metadata(&candidate).is_err() { - Some(candidate) - } else { - None - } - }) - .ok_or_else(|| { - SidecarError::Io(String::from( - "could not allocate a bounded shadow rename-exchange path", - )) - })?; - fs::rename(&source_shadow, &temporary).map_err(|error| { - SidecarError::Io(format!( - "failed to stage shadow rename exchange {source} -> {destination}: {error}" - )) - })?; - if let Err(error) = fs::rename(&destination_shadow, &source_shadow) { - let rollback = fs::rename(&temporary, &source_shadow); - return Err(SidecarError::Io(format!( - "failed to exchange shadow rename {source} -> {destination}: {error}; rollback: {rollback:?}" - ))); - } - if let Err(error) = fs::rename(&temporary, &destination_shadow) { - let rollback_destination = fs::rename(&source_shadow, &destination_shadow); - let rollback_source = fs::rename(&temporary, &source_shadow); - return Err(SidecarError::Io(format!( - "failed to complete shadow rename exchange {source} -> {destination}: {error}; rollback destination: {rollback_destination:?}; rollback source: {rollback_source:?}" - ))); - } - Ok(()) - } - _ => Err(SidecarError::Kernel(format!( - "EINVAL: invalid renameat2 flags: {flags:#x}" - ))), - } -} - -fn sync_host_directory_to_kernel( - vm: &mut VmState, - guest_path: &str, - metadata: &fs::Metadata, -) -> Result<(), SidecarError> { - vm.kernel.mkdir(guest_path, true).map_err(kernel_error)?; - vm.kernel - .chmod(guest_path, metadata.permissions().mode() & 0o7777) - .map_err(kernel_error)?; - Ok(()) -} - -fn sync_host_file_to_kernel( - vm: &mut VmState, - guest_path: &str, - host_path: &Path, - metadata: &fs::Metadata, -) -> Result<(), SidecarError> { - ensure_guest_parent_dir(vm, guest_path)?; - let bytes = fs::read(host_path).map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow file {}: {error}", - host_path.display() - )) - })?; - vm.kernel - .write_file(guest_path, bytes) - .map_err(kernel_error)?; - vm.kernel - .chmod(guest_path, metadata.permissions().mode() & 0o7777) - .map_err(kernel_error)?; - Ok(()) -} - -fn sync_host_symlink_to_kernel( - vm: &mut VmState, - guest_path: &str, - host_path: &Path, -) -> Result<(), SidecarError> { - ensure_guest_parent_dir(vm, guest_path)?; - let target = fs::read_link(host_path).map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow symlink {}: {error}", - host_path.display() - )) +fn read_le_u32(payload: &[u8], offset: &mut usize, label: &str) -> Result { + let end = offset + .checked_add(4) + .ok_or_else(|| SidecarError::InvalidState(format!("filesystem {label} offset overflow")))?; + let bytes = payload.get(*offset..end).ok_or_else(|| { + SidecarError::InvalidState(format!("truncated filesystem {label} payload")) })?; - - let target = restore_guest_symlink_target_from_shadow(vm, guest_path, host_path, &target) - .unwrap_or_else(|| target.to_string_lossy().into_owned()); - - replace_guest_symlink(vm, guest_path, &target) + *offset = end; + Ok(u32::from_le_bytes( + bytes.try_into().expect("slice length checked"), + )) } -fn restore_guest_symlink_target_from_shadow( - vm: &VmState, - guest_path: &str, - host_path: &Path, - shadow_target: &Path, -) -> Option { - if shadow_target.is_absolute() { - return None; +fn decode_javascript_writev_raw_payload(payload: &[u8]) -> Result, SidecarError> { + let mut offset = 0usize; + let count = read_le_u32(payload, &mut offset, "writev count")? as usize; + let mut buffers = Vec::with_capacity(count); + for _ in 0..count { + let len = read_le_u32(payload, &mut offset, "writev buffer length")? as usize; + let end = offset.checked_add(len).ok_or_else(|| { + SidecarError::InvalidState(String::from("filesystem writev payload length overflow")) + })?; + let buffer = payload.get(offset..end).ok_or_else(|| { + SidecarError::InvalidState(String::from("truncated filesystem writev payload")) + })?; + buffers.push(buffer); + offset = end; } - - let existing_target = vm.kernel.read_link(guest_path).ok()?; - if !existing_target.starts_with('/') { - return None; + if offset != payload.len() { + return Err(SidecarError::InvalidState(String::from( + "filesystem writev payload has trailing bytes", + ))); } + Ok(buffers) +} - let host_parent = host_path.parent().unwrap_or(&vm.cwd); - let resolved_host_target = normalize_host_path(&host_parent.join(shadow_target)); - let normalized_shadow_root = normalize_host_path(&vm.cwd); - if resolved_host_target == normalized_shadow_root { - return Some(String::from("/")); +pub(crate) fn service_javascript_fs_readdir_entries( + kernel: &mut SidecarKernel, + _process: &ActiveProcess, + kernel_pid: u32, + path: &str, +) -> Result, SidecarError> { + let entries = kernel + .read_dir_with_types_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) + .map_err(kernel_error)?; + let mut typed = BTreeMap::new(); + for entry in entries { + // The existing Node compatibility surface reports a symlink to a + // directory as a directory. Preserve that behavior while keeping the + // kernel filesystem authoritative; only symlinks require a followed + // stat, so ordinary entries retain the one-pass typed readdir path. + let is_directory = if entry.is_symbolic_link { + let child_path = if path == "/" { + format!("/{name}", name = entry.name) + } else { + format!( + "{parent}/{name}", + parent = path.trim_end_matches('/'), + name = entry.name + ) + }; + match kernel.stat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &child_path) { + Ok(stat) => stat.is_directory, + Err(error) if error.code() == "ENOENT" => false, + Err(error) => return Err(kernel_error(error)), + } + } else { + entry.is_directory + }; + typed.insert(entry.name, is_directory); } - - resolved_host_target - .strip_prefix(&normalized_shadow_root) - .ok() - .map(|suffix| format!("/{}", suffix.to_string_lossy().trim_start_matches('/'))) + Ok(typed) } -fn replace_guest_symlink( - vm: &mut VmState, - guest_path: &str, - target: &str, -) -> Result<(), SidecarError> { - if vm.kernel.symlink(target, guest_path).is_ok() { - return Ok(()); - } +pub(crate) fn service_javascript_fs_readdir_raw_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + kernel_pid: u32, + request: &HostRpcRequest, +) -> Result, SidecarError> { + let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem readdir path")?; + let entries = + service_javascript_fs_readdir_entries(kernel, process, kernel_pid, path.as_str())?; + encode_javascript_readdir_raw_payload(entries) +} - if let Ok(existing_target) = vm.kernel.read_link(guest_path) { - if existing_target == target { - return Ok(()); - } +fn encode_javascript_readdir_raw_payload( + entries: BTreeMap, +) -> Result, SidecarError> { + let mut payload = Vec::new(); + for (name, is_dir) in entries + .into_iter() + .filter(|(name, _)| name != "." && name != "..") + { + let name = name.into_bytes(); + let name_len = u32::try_from(name.len()).map_err(|_| { + SidecarError::InvalidState(String::from("filesystem readdir entry name too long")) + })?; + payload.push(u8::from(is_dir)); + payload.extend_from_slice(&name_len.to_le_bytes()); + payload.extend_from_slice(&name); } - - let _ = vm.kernel.remove_file(guest_path); - let _ = vm.kernel.remove_dir(guest_path); - vm.kernel - .symlink(target, guest_path) - .map_err(kernel_error)?; - Ok(()) + Ok(payload) } -fn ensure_guest_parent_dir(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { - let Some(parent) = Path::new(guest_path).parent() else { - return Ok(()); - }; - let parent = parent.to_string_lossy(); - if parent.is_empty() || parent == "/" { - return Ok(()); - } - vm.kernel - .mkdir(&normalize_path(&parent), true) - .map_err(kernel_error)?; - Ok(()) +/// Like `javascript_sync_rpc_readdir_value` but carries each entry's +/// directory-ness as `{name, isDirectory}`. The guest's `normalizeReaddirEntries` +/// consumes these objects directly for `withFileTypes`, avoiding a per-entry stat +/// RPC, and extracts `.name` for the plain string form. +fn javascript_sync_rpc_readdir_typed_value(entries: BTreeMap) -> Value { + json!(entries + .into_iter() + .filter(|(name, _)| name != "." && name != "..") + .map(|(name, is_dir)| json!({ "name": name, "isDirectory": is_dir })) + .collect::>()) } #[cfg(test)] mod tests { - use super::{ - classify_fiemap_ranges, create_mapped_runtime_directory, - create_mapped_runtime_root_directory, mapped_runtime_host_path_exists, - mapped_runtime_relative_path, mapped_runtime_resolved_guest_path, - mapped_runtime_symlink_metadata, materialize_mapped_host_path_from_kernel, - move_across_devices_at, open_mapped_runtime_beneath, open_mapped_runtime_parent_beneath, - read_mapped_runtime_link, rename_mapped_host_path, MappedRuntimeHostAccess, - MappedRuntimeHostPath, SidecarError, O_PATH_ANCHOR, - }; - use crate::execution::javascript_sync_rpc_error_code; - use crate::state::{SidecarKernel, EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND}; - use agentos_kernel::command_registry::CommandDriver; - use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use super::classify_fiemap_ranges; + use crate::state::SidecarKernel; + use agentos_kernel::kernel::KernelVmConfig; use agentos_kernel::mount_table::MountTable; use agentos_kernel::permissions::Permissions; use agentos_kernel::vfs::MemoryFileSystem; @@ -5678,20 +1579,9 @@ mod tests { ] ); } - use std::os::fd::AsFd; - use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; - fn writable_mapping(guest_path: &str, host_root: &str) -> MappedRuntimeHostAccess { - let host_root = PathBuf::from(host_root); - MappedRuntimeHostAccess::Writable(MappedRuntimeHostPath { - guest_path: guest_path.to_owned(), - host_path: host_root.join("file.txt"), - host_root: host_root.clone(), - }) - } - fn temp_dir(prefix: &str) -> PathBuf { let path = std::env::temp_dir().join(format!( "{prefix}-{}", @@ -5704,475 +1594,6 @@ mod tests { path } - // Exercises the fd-relative cross-device move (the EXDEV rename fallback): - // a nested tree (file with a preserved non-default mode, a relative symlink, - // and a subdirectory) is moved anchored on the parent dir fds, and the source - // is removed. Directly drives `move_across_devices_at` (renameat would not - // return EXDEV within one filesystem). - #[test] - fn move_across_devices_copies_tree_fd_relative_and_removes_source() { - let root = temp_dir("mapped-xdev-move"); - let src_parent = root.join("src"); - let dst_parent = root.join("dst"); - fs::create_dir_all(&src_parent).expect("src parent"); - fs::create_dir_all(&dst_parent).expect("dst parent"); - - let item = src_parent.join("item"); - fs::create_dir(&item).expect("item dir"); - fs::write(item.join("a.txt"), b"hello").expect("a.txt"); - fs::set_permissions(item.join("a.txt"), fs::Permissions::from_mode(0o640)) - .expect("chmod a.txt"); - std::os::unix::fs::symlink("a.txt", item.join("link")).expect("relative symlink"); - fs::create_dir(item.join("sub")).expect("sub dir"); - fs::write(item.join("sub/b.txt"), b"world").expect("b.txt"); - // A subdirectory whose non-default mode must be restored exactly on the - // destination after it is populated (the dest is created owner-writable - // during population, then fchmod'd back). - fs::create_dir(item.join("mode")).expect("mode dir"); - fs::write(item.join("mode/c.txt"), b"c").expect("c.txt"); - fs::set_permissions(item.join("mode"), fs::Permissions::from_mode(0o700)) - .expect("chmod mode dir"); - - let src_dir = fs::File::open(&src_parent).expect("open src parent dir"); - let dst_dir = fs::File::open(&dst_parent).expect("open dst parent dir"); - move_across_devices_at( - src_dir.as_fd(), - std::ffi::OsStr::new("item"), - dst_dir.as_fd(), - std::ffi::OsStr::new("moved"), - ) - .expect("cross-device move"); - - let moved = dst_parent.join("moved"); - assert_eq!( - fs::read(moved.join("a.txt")).expect("moved a.txt"), - b"hello" - ); - assert_eq!( - fs::symlink_metadata(moved.join("a.txt")) - .expect("moved a.txt meta") - .permissions() - .mode() - & 0o777, - 0o640, - "file mode must be preserved" - ); - assert_eq!( - fs::read_link(moved.join("link")).expect("moved link"), - PathBuf::from("a.txt"), - "relative symlink recreated verbatim" - ); - assert_eq!( - fs::read(moved.join("sub/b.txt")).expect("moved b.txt"), - b"world" - ); - assert_eq!( - fs::read(moved.join("mode/c.txt")).expect("moved mode/c.txt"), - b"c" - ); - assert_eq!( - fs::symlink_metadata(moved.join("mode")) - .expect("moved mode dir") - .permissions() - .mode() - & 0o777, - 0o700, - "the exact dir mode must be restored after population" - ); - assert!(!item.exists(), "source tree must be removed after the move"); - - fs::remove_dir_all(&root).expect("cleanup"); - } - - // The cross-device move must be depth-bounded: a hostile guest can nest - // directories arbitrarily deep, and unbounded recursion would overflow the - // sidecar stack (SIGSEGV). A tree past the limit fails closed with a typed, - // limit-naming error instead of crashing. - #[test] - fn move_across_devices_rejects_excessive_directory_depth() { - let root = temp_dir("mapped-xdev-depth"); - let src_parent = root.join("src"); - let dst_parent = root.join("dst"); - fs::create_dir_all(&src_parent).expect("src parent"); - fs::create_dir_all(&dst_parent).expect("dst parent"); - - let mut deep = src_parent.join("item"); - fs::create_dir(&deep).expect("item"); - for level in 0..300 { - deep.push(format!("d{level}")); - fs::create_dir(&deep).expect("nested dir"); - } - - let src_dir = fs::File::open(&src_parent).expect("open src parent"); - let dst_dir = fs::File::open(&dst_parent).expect("open dst parent"); - let error = move_across_devices_at( - src_dir.as_fd(), - std::ffi::OsStr::new("item"), - dst_dir.as_fd(), - std::ffi::OsStr::new("moved"), - ) - .expect_err("a tree deeper than the limit must be rejected"); - assert!( - error.to_string().contains("max directory depth"), - "expected a depth-limit error, got: {error}" - ); - - fs::remove_dir_all(&root).expect("cleanup"); - } - - // S2: the mapped existence check must NOT follow an ancestor symlink out of - // the mapped root. A path-based `symlink_metadata` would follow `a -> outside` - // and report the out-of-root file as existing (an existence-bit leak); the - // confined walk refuses the escape instead. - #[test] - fn mapped_runtime_exists_refuses_ancestor_symlink_escape() { - let root = temp_dir("mapped-exists-escape"); - let mapped_root = root.join("mapped"); - let outside = root.join("outside"); - fs::create_dir_all(&mapped_root).expect("mapped root"); - fs::create_dir_all(&outside).expect("outside dir"); - fs::write(outside.join("secret"), b"x").expect("outside secret"); - std::os::unix::fs::symlink(&outside, mapped_root.join("a")).expect("ancestor symlink"); - - let mapped = MappedRuntimeHostPath { - guest_path: "/a/secret".to_string(), - host_path: mapped_root.join("a").join("secret"), - host_root: mapped_root.clone(), - }; - let result = mapped_runtime_host_path_exists(&mapped); - assert!( - result.is_err(), - "ancestor-symlink escape must be refused (not followed to report the \ - out-of-root file as existing), got {result:?}" - ); - - // A legitimate in-root path resolves without following anything outside. - fs::write(mapped_root.join("real.txt"), b"y").expect("in-root file"); - let in_root = MappedRuntimeHostPath { - guest_path: "/real.txt".to_string(), - host_path: mapped_root.join("real.txt"), - host_root: mapped_root.clone(), - }; - assert!( - mapped_runtime_host_path_exists(&in_root).expect("in-root exists check"), - "an in-root path must be reported as existing" - ); - - fs::remove_dir_all(&root).expect("cleanup"); - } - - fn test_kernel_with_process() -> (SidecarKernel, u32) { - let mut config = KernelVmConfig::new("vm-mapped-materialize"); - config.permissions = Permissions::allow_all(); - let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); - kernel - .register_driver(CommandDriver::new( - EXECUTION_DRIVER_NAME, - [JAVASCRIPT_COMMAND], - )) - .expect("register execution driver"); - let handle = kernel - .spawn_process( - JAVASCRIPT_COMMAND, - Vec::new(), - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel process"); - (kernel, handle.pid()) - } - - #[test] - fn rename_mapped_host_path_reports_exdev_for_cross_mount_guest_errno() { - for (source_host, destination_host) in [ - ( - Some(writable_mapping( - "/mapped/file.txt", - "/tmp/secure-exec-mapped-source", - )), - None, - ), - ( - None, - Some(writable_mapping( - "/mapped-dst/file.txt", - "/tmp/secure-exec-mapped-destination", - )), - ), - ] { - let error = rename_mapped_host_path( - "/mapped/file.txt", - source_host, - "/kernel/file.txt", - destination_host, - ) - .expect_err("cross-mount rename should fail with EXDEV"); - assert!( - matches!(error, SidecarError::Kernel(ref message) if message.starts_with("EXDEV:")), - "expected EXDEV kernel error, got {error:?}" - ); - assert_eq!(javascript_sync_rpc_error_code(&error), "EXDEV"); - } - } - - #[test] - fn mapped_runtime_parent_treats_single_segment_relative_paths_as_root_children() { - let host_root = std::env::temp_dir().join(format!( - "agentos-native-sidecar-fs-parent-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos() - )); - fs::create_dir_all(&host_root).expect("create mapped host root"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/workspace"), - host_root: host_root.clone(), - host_path: host_root.join("workspace"), - }; - - assert_eq!( - mapped_runtime_relative_path(&mapped).expect("relative path"), - PathBuf::from("workspace") - ); - - let parent = open_mapped_runtime_parent_beneath(&mapped, "test") - .expect("open mapped parent for root child"); - // `host_path` is the resolved fd's real path, which is canonical (on - // macOS the temp dir resolves through the `/private` firmlink), so - // compare against the canonicalized root rather than the raw value. - assert_eq!( - parent.host_path, - fs::canonicalize(&host_root).expect("canonicalize host root") - ); - assert_eq!(parent.child_name.to_string_lossy(), "workspace"); - } - - #[test] - fn mapped_module_realpath_preserves_pnpm_dependency_ancestor() { - let host_root = temp_dir("mapped-module-pnpm-realpath"); - let package_dir = host_root.join(".pnpm/consumer@1.0.0/node_modules/consumer"); - fs::create_dir_all(&package_dir).expect("create pnpm package directory"); - fs::write( - package_dir.join("index.js"), - "module.exports = require('dep');", - ) - .expect("write package entry"); - std::os::unix::fs::symlink( - ".pnpm/consumer@1.0.0/node_modules/consumer", - host_root.join("consumer"), - ) - .expect("create top-level package symlink"); - - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/root/node_modules/consumer/index.js"), - host_root: host_root.clone(), - host_path: host_root.join("consumer/index.js"), - }; - let opened = open_mapped_runtime_beneath( - &mapped, - "test.module.realpath", - O_PATH_ANCHOR, - nix::sys::stat::Mode::empty(), - ) - .expect("resolve mapped module path"); - - assert_eq!( - mapped_runtime_resolved_guest_path(&mapped, &opened.host_path).as_deref(), - Some("/root/node_modules/.pnpm/consumer@1.0.0/node_modules/consumer/index.js"), - ); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - } - - #[test] - fn mapped_runtime_root_lstat_uses_root_metadata_without_parent_basename() { - let host_root = std::env::temp_dir().join(format!( - "agentos-native-sidecar-fs-root-lstat-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos() - )); - fs::create_dir_all(&host_root).expect("create mapped host root"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/node_modules"), - host_root: host_root.clone(), - host_path: host_root.clone(), - }; - - let metadata = mapped_runtime_symlink_metadata(&mapped, "test").expect("lstat mapped root"); - assert!(metadata.is_dir(), "expected mapped root directory metadata"); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - } - - #[test] - fn mapped_runtime_root_readlink_uses_root_path_without_parent_basename() { - let host_parent = std::env::temp_dir().join(format!( - "agentos-native-sidecar-fs-root-readlink-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos() - )); - let host_target = host_parent.join("target"); - let host_link = host_parent.join("link"); - fs::create_dir_all(&host_target).expect("create mapped host target"); - std::os::unix::fs::symlink(&host_target, &host_link).expect("create mapped host link"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/"), - host_root: host_link.clone(), - host_path: host_link, - }; - - let target = read_mapped_runtime_link(&mapped, "/", "test").expect("read mapped root link"); - assert_eq!(target, host_target); - - fs::remove_dir_all(&host_parent).expect("remove mapped host parent"); - } - - #[test] - fn recursive_mapped_directory_create_accepts_existing_directory() { - let host_root = std::env::temp_dir().join(format!( - "agentos-native-sidecar-fs-existing-dir-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos() - )); - let existing_dir = host_root.join("workspace"); - fs::create_dir_all(&existing_dir).expect("create existing mapped directory"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/workspace"), - host_root: host_root.clone(), - host_path: existing_dir, - }; - - let parent = open_mapped_runtime_parent_beneath(&mapped, "test") - .expect("open mapped parent for root child"); - create_mapped_runtime_directory(&parent, "/workspace", true) - .expect("recursive mkdir should accept an existing directory"); - let non_recursive_error = create_mapped_runtime_directory(&parent, "/workspace", false) - .expect_err("non-recursive mkdir should keep EEXIST behavior"); - assert!( - matches!(non_recursive_error, SidecarError::Io(ref message) if message.contains("File exists")), - "expected File exists error, got {non_recursive_error:?}" - ); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - } - - #[test] - fn recursive_mapped_root_directory_create_accepts_existing_directory() { - let host_root = std::env::temp_dir().join(format!( - "agentos-native-sidecar-fs-existing-root-dir-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos() - )); - fs::create_dir_all(&host_root).expect("create mapped host root"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/"), - host_root: host_root.clone(), - host_path: host_root.clone(), - }; - - create_mapped_runtime_root_directory(&mapped, true) - .expect("recursive root mkdir should accept an existing directory"); - let non_recursive_error = create_mapped_runtime_root_directory(&mapped, false) - .expect_err("non-recursive root mkdir should keep EEXIST behavior"); - assert!( - matches!(non_recursive_error, SidecarError::Io(ref message) if message.contains("File exists")), - "expected File exists error, got {non_recursive_error:?}" - ); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - } - - #[test] - fn materialize_mapped_host_path_does_not_follow_symlinked_parents() { - let host_root = temp_dir("agentos-native-sidecar-fs-materialize-root"); - let outside = temp_dir("agentos-native-sidecar-fs-materialize-outside"); - std::os::unix::fs::symlink(&outside, host_root.join("link")) - .expect("create escape symlink"); - - let (mut kernel, pid) = test_kernel_with_process(); - kernel - .write_file_for_process( - EXECUTION_DRIVER_NAME, - pid, - "/workspace/link/out.txt", - b"secret".to_vec(), - Some(0o644), - ) - .expect("seed guest file"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/workspace/link/out.txt"), - host_root: host_root.clone(), - host_path: host_root.join("link/out.txt"), - }; - - materialize_mapped_host_path_from_kernel( - &mut kernel, - pid, - "/workspace/link/out.txt", - &mapped, - ) - .expect_err("symlinked parent must not be followed during materialization"); - - assert!( - !outside.join("out.txt").exists(), - "materialization wrote through a symlinked mapped parent" - ); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - fs::remove_dir_all(&outside).expect("remove outside dir"); - } - - #[test] - fn materialize_mapped_host_path_writes_regular_files_beneath_root() { - let host_root = temp_dir("agentos-native-sidecar-fs-materialize-file"); - let (mut kernel, pid) = test_kernel_with_process(); - kernel - .write_file_for_process( - EXECUTION_DRIVER_NAME, - pid, - "/workspace/out.txt", - b"secret".to_vec(), - Some(0o640), - ) - .expect("seed guest file"); - let mapped = MappedRuntimeHostPath { - guest_path: String::from("/workspace/out.txt"), - host_root: host_root.clone(), - host_path: host_root.join("out.txt"), - }; - - materialize_mapped_host_path_from_kernel(&mut kernel, pid, "/workspace/out.txt", &mapped) - .expect("materialize regular mapped file"); - - let host_path = host_root.join("out.txt"); - assert_eq!( - fs::read(&host_path).expect("read materialized file"), - b"secret" - ); - assert_eq!( - fs::metadata(&host_path) - .expect("materialized metadata") - .permissions() - .mode() - & 0o777, - 0o640 - ); - - fs::remove_dir_all(&host_root).expect("remove mapped host root"); - } - // Companion to the execution-crate `faithful_pnpm_symlink_layout_*` host // test, but resolving through the *kernel VFS* via a read-only `host_dir` // mount at `/root/node_modules` — the real VM path. A faithful pnpm tree @@ -6265,11 +1686,13 @@ mod tests { // Importer is the top-level symlink path. The ancestor walk finds `dep` // via pnpm's sibling symlink in consumer's store dir (pointing at // dep@2.0.0) — no `.pnpm` scan. Resolution reads entirely through the VFS. - let resolved = resolver.resolve_module( - "dep", - "/root/node_modules/consumer/index.mjs", - ModuleResolveMode::Import, - ); + let resolved = resolver + .resolve_module( + "dep", + "/root/node_modules/consumer/index.mjs", + ModuleResolveMode::Import, + ) + .expect("resolve dep through kernel VFS"); assert_eq!( resolved.as_deref(), Some("/root/node_modules/.pnpm/consumer@1.0.0/node_modules/dep/index.mjs"), @@ -6279,6 +1702,7 @@ mod tests { // And the resolved source loads through the VFS too. let source = resolver .load_file("/root/node_modules/.pnpm/consumer@1.0.0/node_modules/dep/index.mjs") + .expect("read resolved dep source via kernel VFS") .expect("load resolved dep source via kernel VFS"); assert_eq!(source, "export const wanted = 2;"); @@ -6348,11 +1772,13 @@ mod tests { let mut cache = LocalModuleResolutionCache::default(); let mut resolver = ModuleResolver::new(reader, &mut cache); - let resolved = resolver.resolve_module( - "dep", - "/root/node_modules/consumer/index.mjs", - ModuleResolveMode::Import, - ); + let resolved = resolver + .resolve_module( + "dep", + "/root/node_modules/consumer/index.mjs", + ModuleResolveMode::Import, + ) + .expect("resolve dep through host-dir module reader"); assert_eq!( resolved.as_deref(), Some("/root/node_modules/.pnpm/consumer@1.0.0/node_modules/dep/index.mjs"), @@ -6361,6 +1787,7 @@ mod tests { let source = resolver .load_file("/root/node_modules/.pnpm/consumer@1.0.0/node_modules/dep/index.mjs") + .expect("read resolved dep source via host_dir reader") .expect("load resolved dep source via host_dir reader"); assert_eq!(source, "export const wanted = 2;"); @@ -6376,11 +1803,10 @@ mod tests { .expect("build host_dir module reader"); let mut escape_cache = LocalModuleResolutionCache::default(); let mut escape_resolver = ModuleResolver::new(escape_reader, &mut escape_cache); - let escaped = escape_resolver.load_file("/root/node_modules/escape-link/escaped.js"); - assert!( - escaped.is_none(), - "escaping symlink must not read through the mount", - ); + let escaped = escape_resolver + .load_file("/root/node_modules/escape-link/escaped.js") + .expect_err("escaping symlink must produce a typed confinement error"); + assert_eq!(escaped.code, "EACCES"); fs::remove_dir_all(node_modules.parent().expect("temp parent")).expect("remove temp tree"); fs::remove_dir_all(&outside).ok(); @@ -6445,7 +1871,9 @@ mod tests { for _ in 0..iterations { let mut harness = ModuleResolutionTestHarness::new(&root); for pkg in &imports { - let _ = harness.resolve_require(pkg, from); + harness + .resolve_require(pkg, from) + .expect("host resolver perf fixture must resolve package"); } } let host_elapsed = host_start.elapsed(); @@ -6479,7 +1907,10 @@ mod tests { &mut cache, ); for pkg in &imports { - let _ = resolver.resolve_module(pkg, from, ModuleResolveMode::Require); + resolver + .resolve_module(pkg, from, ModuleResolveMode::Require) + .expect("kernel resolver perf fixture must not fail") + .expect("kernel resolver perf fixture must resolve package"); } } let vfs_elapsed = vfs_start.elapsed(); diff --git a/crates/native-sidecar/src/limits.rs b/crates/native-sidecar/src/limits.rs index e424228012..32370dd081 100644 --- a/crates/native-sidecar/src/limits.rs +++ b/crates/native-sidecar/src/limits.rs @@ -14,6 +14,7 @@ pub use agentos_native_sidecar_core::limits::{ DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT, DEFAULT_BINDING_TIMEOUT_MS, DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES, DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES, DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES, DEFAULT_MAX_FETCH_RESPONSE_BYTES, + DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_BYTES, DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_COUNT, DEFAULT_PROCESS_PENDING_EVENT_BYTES, DEFAULT_PROCESS_PENDING_EVENT_COUNT, DEFAULT_PROCESS_PENDING_STDIN_BYTES, DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS, DEFAULT_PYTHON_MAX_OLD_SPACE_MB, DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES, diff --git a/crates/native-sidecar/src/plugins/host_dir.rs b/crates/native-sidecar/src/plugins/host_dir.rs index 6f1e417e5a..5e0be17338 100644 --- a/crates/native-sidecar/src/plugins/host_dir.rs +++ b/crates/native-sidecar/src/plugins/host_dir.rs @@ -9,10 +9,8 @@ use nix::libc; // performed fd-relative (`fstat`/`fchmod`/`fchown`/`futimens`), so `O_RDONLY` is // the portable anchor open mode. const O_PATH_ANCHOR: OFlag = OFlag::O_RDONLY; -use agentos_execution::{ - GuestModuleReader, LocalModuleResolutionCache, ModuleFsReader, ModuleResolveMode, - ModuleResolver, -}; +#[cfg(test)] +use agentos_execution::ModuleFsReader; use agentos_kernel::mount_plugin::{ FileSystemPluginFactory, OpenFileSystemPluginRequest, PluginError, }; @@ -34,6 +32,7 @@ use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd}; use std::os::unix::fs::{FileExt, MetadataExt}; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; +#[cfg(test)] use vfs::posix::TarFileSystem; const MAX_HOST_DIR_READ_BYTES: usize = DEFAULT_MAX_PREAD_BYTES; @@ -957,6 +956,12 @@ impl HostDirFilesystem { }; let stat = fstatat(Some(parent_dir.as_raw_fd()), name, flags) .map_err(|error| io_error_to_vfs("utimes", virtual_path, nix_to_io(error)))?; + Self::utime_specs_from_file_stat(stat) + } + + fn utime_specs_from_file_stat( + stat: nix::sys::stat::FileStat, + ) -> VfsResult<(VirtualTimeSpec, VirtualTimeSpec)> { let atime = VirtualTimeSpec::new( stat.st_atime, stat.st_atime_nsec.clamp(0, 999_999_999) as u32, @@ -983,6 +988,39 @@ impl HostDirFilesystem { mtime: VirtualUtimeSpec, follow_symlinks: bool, ) -> VfsResult<()> { + let (normalized, relative) = self.relative_virtual_path(path); + if relative.file_name().is_none() { + let existing = match (atime, mtime) { + (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { + let stat = fstat(self.host_root_dir.as_raw_fd()).map_err(|error| { + io_error_to_vfs("utimes", &normalized, nix_to_io(error)) + })?; + Some(Self::utime_specs_from_file_stat(stat)?) + } + _ => None, + }; + let existing_atime = existing + .as_ref() + .map(|(atime, _)| *atime) + .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); + let existing_mtime = existing + .as_ref() + .map(|(_, mtime)| *mtime) + .unwrap_or(VirtualTimeSpec { sec: 0, nsec: 0 }); + let times = [ + Self::resolve_utime_timespec(atime, existing_atime), + Self::resolve_utime_timespec(mtime, existing_mtime), + ]; + return utimensat( + Some(self.host_root_dir.as_raw_fd()), + Path::new("."), + ×[0], + ×[1], + UtimensatFlags::NoFollowSymlink, + ) + .map_err(|error| io_error_to_vfs("utimes", &normalized, nix_to_io(error))); + } + let (parent_dir, _, name, normalized) = self.split_parent(path, false)?; if follow_symlinks { // `utimes` (follow) rejects a symlink leaf, matching `chmod`/`chown`; @@ -1531,6 +1569,7 @@ impl VirtualFileSystem for HostDirFilesystem { // plugin and not the module-reader path (which the real lib build does use). #[allow(dead_code)] #[derive(Clone)] +#[cfg(test)] struct HostDirModuleMount { /// Normalized guest mount point, e.g. `/root/node_modules`. guest_prefix: String, @@ -1544,12 +1583,14 @@ struct HostDirModuleMount { /// from the shared identity-keyed archive cache and never touches the kernel. #[allow(dead_code)] #[derive(Clone)] +#[cfg(test)] enum ModuleMountBackend { Host(HostDirFilesystem), Tar(TarFileSystem), } #[allow(dead_code)] +#[cfg(test)] impl ModuleMountBackend { fn realpath(&self, path: &str) -> VfsResult { match self { @@ -1581,6 +1622,7 @@ impl ModuleMountBackend { } #[allow(dead_code)] +#[cfg(test)] impl HostDirModuleMount { /// If `guest_path` falls under this mount, return the mount-relative virtual /// path (always absolute, e.g. `/foo/index.js`). @@ -1627,6 +1669,7 @@ impl HostDirModuleMount { /// `session/new` bootstrap awaiting the adapter's response on that same loop). #[allow(dead_code)] #[derive(Clone)] +#[cfg(test)] pub(crate) struct HostDirModuleReader { /// Mounts sorted longest-`guest_prefix`-first so the most specific mount /// wins (mirrors the kernel mount table's longest-prefix dispatch). @@ -1634,6 +1677,7 @@ pub(crate) struct HostDirModuleReader { } #[allow(dead_code)] +#[cfg(test)] impl HostDirModuleReader { /// Build a reader from `(guest_path, host_path)` pairs for the VM's read-only /// `host_dir`/`module_access` mounts. Mounts whose host root cannot be opened @@ -1725,71 +1769,77 @@ impl HostDirModuleReader { } } +#[cfg(test)] impl ModuleFsReader for HostDirModuleReader { - fn canonical_guest_path(&mut self, guest_path: &str) -> Option { - let (index, relative) = self.mount_index_for(guest_path)?; + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, agentos_execution::backend::HostServiceError> { + let Some((index, relative)) = self.mount_index_for(guest_path) else { + return Ok(None); + }; let mount = &self.mounts[index]; // `realpath` returns a mount-relative virtual path; re-express it as a // guest path so the resolver keeps operating in the guest namespace. - let resolved = mount.filesystem.realpath(&relative).ok()?; - Some(mount.guest_path_for_relative(&resolved)) + let Some(resolved) = module_vfs_optional(mount.filesystem.realpath(&relative))? else { + return Ok(None); + }; + Ok(Some(mount.guest_path_for_relative(&resolved))) } - fn read_to_string(&mut self, guest_path: &str) -> Option { - let (index, relative) = self.mount_index_for(guest_path)?; - let bytes = self.mounts[index].filesystem.read_file(&relative).ok()?; - String::from_utf8(bytes).ok() + fn read_to_string( + &mut self, + guest_path: &str, + ) -> Result, agentos_execution::backend::HostServiceError> { + let Some((index, relative)) = self.mount_index_for(guest_path) else { + return Ok(None); + }; + let Some(bytes) = module_vfs_optional(self.mounts[index].filesystem.read_file(&relative))? + else { + return Ok(None); + }; + String::from_utf8(bytes).map(Some).map_err(|error| { + agentos_execution::backend::HostServiceError::new( + "EILSEQ", + format!("module filesystem file {guest_path} is not valid UTF-8: {error}"), + ) + }) } - fn path_is_dir(&mut self, guest_path: &str) -> Option { - let (index, relative) = self.mount_index_for(guest_path)?; + fn path_is_dir( + &mut self, + guest_path: &str, + ) -> Result, agentos_execution::backend::HostServiceError> { + let Some((index, relative)) = self.mount_index_for(guest_path) else { + return Ok(None); + }; // `stat` follows symlinks (O_PATH, no O_NOFOLLOW), so a symlinked package // directory reports as a directory just like `fs.statSync` would. - self.mounts[index] - .filesystem - .stat(&relative) - .ok() - .map(|stat| stat.is_directory) - } - - fn path_exists(&mut self, guest_path: &str) -> bool { - match self.mount_index_for(guest_path) { - Some((index, relative)) => self.mounts[index].filesystem.exists(&relative), - None => false, - } + Ok( + module_vfs_optional(self.mounts[index].filesystem.stat(&relative))? + .map(|stat| stat.is_directory), + ) } -} - -/// Session-thread module reader: the mounted `HostDirModuleReader` plus a -/// persistent resolution cache, so the V8 isolate thread can both resolve -/// specifiers and read source DIRECTLY (same mount + resolve-beneath -/// confinement, same `ModuleResolver` semantics as the bridge), skipping the -/// per-module `_resolveModule`/`_loadFile` bridge round-trips. -pub(crate) struct SessionModuleReader { - reader: HostDirModuleReader, - cache: LocalModuleResolutionCache, -} -impl SessionModuleReader { - pub(crate) fn new(reader: HostDirModuleReader) -> Self { - Self { - reader, - cache: LocalModuleResolutionCache::default(), - } + fn path_exists( + &mut self, + guest_path: &str, + ) -> Result { + Ok(self.path_is_dir(guest_path)?.is_some()) } } -impl GuestModuleReader for SessionModuleReader { - fn read_module_source(&mut self, resolved_guest_path: &str) -> Option { - self.reader.read_to_string(resolved_guest_path) - } - - fn resolve_module(&mut self, specifier: &str, referrer: &str) -> Option { - // Mirror the bridge's `_resolveModule` exactly: import mode, same reader, - // same persisted cache. - let reader: &mut dyn ModuleFsReader = &mut self.reader; - let mut resolver = ModuleResolver::new(reader, &mut self.cache); - resolver.resolve_module(specifier, referrer, ModuleResolveMode::Import) +#[cfg(test)] +fn module_vfs_optional( + result: VfsResult, +) -> Result, agentos_execution::backend::HostServiceError> { + match result { + Ok(value) => Ok(Some(value)), + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => Ok(None), + Err(error) => Err(agentos_execution::backend::HostServiceError::new( + error.code(), + error.to_string(), + )), } } @@ -1915,28 +1965,38 @@ mod tar_module_reader_tests { ) .expect("reader"); let probe = "/opt/agentos/pkgs/pi/0.2.1/node_modules/@anthropic-ai/sdk/package.json"; - assert!(reader.path_exists(probe), "packed package.json must exist"); assert!( - reader.read_to_string(probe).is_some(), + reader.path_exists(probe).expect("stat packed package.json"), + "packed package.json must exist" + ); + assert!( + reader + .read_to_string(probe) + .expect("read packed package.json") + .is_some(), "packed package.json must read" ); let mut cache = Default::default(); let dyn_reader: &mut dyn ModuleFsReader = &mut reader; let mut resolver = ModuleResolver::new(dyn_reader, &mut cache); - let resolved = resolver.resolve_module( - "@anthropic-ai/sdk", - "/opt/agentos/pkgs/pi/0.2.1/node_modules/@agentos-software/pi/dist/adapter.js", - ModuleResolveMode::Require, - ); + let resolved = resolver + .resolve_module( + "@anthropic-ai/sdk", + "/opt/agentos/pkgs/pi/0.2.1/node_modules/@agentos-software/pi/dist/adapter.js", + ModuleResolveMode::Require, + ) + .expect("resolve require from packed tar"); assert!( resolved.is_some(), "require-mode resolution from the packed tar" ); - let resolved_import = resolver.resolve_module( - "@anthropic-ai/sdk", - "/opt/agentos/pkgs/pi/0.2.1/node_modules/@agentos-software/pi/dist/adapter.js", - ModuleResolveMode::Import, - ); + let resolved_import = resolver + .resolve_module( + "@anthropic-ai/sdk", + "/opt/agentos/pkgs/pi/0.2.1/node_modules/@agentos-software/pi/dist/adapter.js", + ModuleResolveMode::Import, + ) + .expect("resolve import from packed tar"); assert!( resolved_import.is_some(), "import-mode resolution from the packed tar" diff --git a/crates/native-sidecar/src/service.rs b/crates/native-sidecar/src/service.rs index f13b78a394..31f5712b10 100644 --- a/crates/native-sidecar/src/service.rs +++ b/crates/native-sidecar/src/service.rs @@ -1,21 +1,21 @@ use crate::bindings::register_host_callbacks; use crate::bridge::{build_mount_plugin_registry, MountPluginContext}; pub(crate) use crate::execution::{ - apply_active_process_default_signal, build_javascript_socket_path_context, - canonical_signal_name, deferred_kernel_wait_request_for_process, - dispatch_loopback_http_request_deferred, error_code, flush_pending_kernel_stdin, - format_tcp_resource, ignore_stale_javascript_sync_rpc_response, javascript_sync_rpc_arg_i32, - javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, javascript_sync_rpc_arg_u32_optional, - javascript_sync_rpc_arg_u64, javascript_sync_rpc_arg_u64_optional, - javascript_sync_rpc_bytes_arg, javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, - javascript_sync_rpc_error_code, javascript_sync_rpc_may_make_fd_readable, + apply_kernel_signal_registration, build_socket_path_context, + deferred_kernel_wait_request_for_process, dispatch_loopback_http_request_deferred, error_code, + flush_pending_kernel_stdin, format_tcp_resource, host_bytes_value, host_service_error_code, + javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, + javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, + javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg, + javascript_sync_rpc_encoding, javascript_sync_rpc_may_make_fd_readable, javascript_sync_rpc_may_make_fd_writable, javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, kernel_poll_response, kernel_stdin_read_response, mark_execute_exit_event_queued, parse_kernel_poll_args, parse_kernel_stdin_read_args, parse_signal, record_execute_exit_event_queue_wait, record_execute_phase, sanitize_javascript_child_process_internal_bootstrap_env, service_javascript_kernel_fd_write_sync_rpc, service_javascript_sync_rpc, - JavascriptSyncRpcServiceRequest, LoopbackHttpDispatchRequest, + settle_execution_host_call, HickoryDnsResolver, JavascriptSyncRpcServiceRequest, + LoopbackHttpDispatchRequest, }; use crate::extension::{ Extension, ExtensionBufferedProcessOutput, ExtensionContext, ExtensionFuture, ExtensionHost, @@ -25,18 +25,17 @@ use crate::filesystem::guest_filesystem_call as filesystem_guest_filesystem_call use crate::limits::DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT; use crate::protocol::{ CloseStdinRequest, DisposeReason, EventFrame, EventPayload, ExecuteRequest, ExtEnvelope, - GuestFilesystemCallRequest, GuestFilesystemResultResponse, JavascriptChildProcessSpawnOptions, - JavascriptChildProcessSpawnRequest, KillProcessRequest, OpenSessionRequest, OwnershipScope, - ProcessKilledResponse, ProcessStartedResponse, RejectedResponse, RequestFrame, RequestId, - RequestPayload, ResponseFrame, ResponsePayload, SidecarRequestFrame, SidecarRequestPayload, - SidecarResponseFrame, SidecarResponsePayload, SidecarResponseTracker, - SidecarResponseTrackerError, SignalDispositionAction, StdinClosedResponse, - StdinWrittenResponse, VmLifecycleState, WriteStdinRequest, + GuestFilesystemCallRequest, GuestFilesystemResultResponse, KillProcessRequest, + OpenSessionRequest, OwnershipScope, ProcessKilledResponse, ProcessStartedResponse, + RejectedResponse, RequestFrame, RequestId, RequestPayload, ResponseFrame, ResponsePayload, + SidecarRequestFrame, SidecarRequestPayload, SidecarResponseFrame, SidecarResponsePayload, + SidecarResponseTracker, SidecarResponseTrackerError, StdinClosedResponse, StdinWrittenResponse, + VmLifecycleState, WriteStdinRequest, }; use crate::state::{ - ActiveExecutionEvent, BridgeError, ConnectionState, EventSinkTransport, JavascriptSocketFamily, - JavascriptSocketPathContext, ProcessEventEnvelope, QuarantinedVmGeneration, SessionState, - SharedBridge, SharedEventSink, SharedSidecarRequestClient, SidecarRequestTransport, VmState, + ActiveExecutionEvent, BridgeError, ConnectionState, EventSinkTransport, ExecutionHostCall, + ProcessEventEnvelope, QuarantinedVmGeneration, SessionState, SharedBridge, SharedEventSink, + SharedSidecarRequestClient, SidecarRequestTransport, SocketFamily, SocketPathContext, VmState, EXECUTION_DRIVER_NAME, }; use crate::NativeSidecarBridge; @@ -46,10 +45,11 @@ use agentos_bridge::{ FilesystemPermissionRequest, LifecycleEventRecord, LifecycleState, LogLevel, LogRecord, NetworkAccess, NetworkPermissionRequest, StructuredEventRecord, }; +use agentos_execution::backend::ExecutionBackendKind; +use agentos_execution::host::{ProcessLaunchOptions, ProcessLaunchRequest}; use agentos_execution::{ record_sync_bridge_request_observed, JavascriptExecutionEngine, JavascriptExecutionError, - JavascriptSyncRpcRequest, PythonExecutionEngine, PythonExecutionError, WasmExecutionEngine, - WasmExecutionError, + PythonExecutionEngine, PythonExecutionError, WasmExecutionEngine, WasmExecutionError, }; use agentos_kernel::kernel::KernelError; use agentos_kernel::mount_plugin::{FileSystemPluginRegistry, PluginError}; @@ -64,9 +64,9 @@ use agentos_native_sidecar_core::permissions::{ permission_mode_to_kernel_decision, }; use agentos_native_sidecar_core::{ - apply_process_signal_state_update, authenticated_response as shared_authenticated_response, - parse_process_signal_state_request, reject as shared_reject, respond as shared_respond, - route_request_payload, session_opened_response, unsupported_host_callback_direction_dispatch, + authenticated_response as shared_authenticated_response, parse_process_signal_state_request, + reject as shared_reject, respond as shared_respond, route_request_payload, + session_opened_response, unsupported_host_callback_direction_dispatch, validate_authenticate_versions, vm_lifecycle_event as shared_vm_lifecycle_event, AuthenticateVersionError, RequestRoute, }; @@ -75,11 +75,12 @@ use agentos_vm_config::{FsPermissionScope, PermissionMode, PermissionsPolicy}; // root_fs types moved to crate::vm use agentos_kernel::vfs::VfsError; use serde::Deserialize; -use serde_json::{json, Value}; +#[cfg(test)] +use serde_json::json; +use serde_json::Value; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::fmt; use std::fs; -use std::os::unix::fs::PermissionsExt; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; @@ -113,20 +114,17 @@ pub(crate) const MAX_OUTBOUND_SIDECAR_REQUESTS: usize = pub(crate) const MAX_COMPLETED_SIDECAR_RESPONSES: usize = agentos_runtime::DEFAULT_PROTOCOL_MAX_COMPLETED_RESPONSES; pub(crate) fn process_event_queue_overflow_error(limit: usize) -> SidecarError { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_PROCESS_EVENT_LIMIT: process event queue exceeded {limit} pending events; raise runtime.protocol.maxProcessEvents" + SidecarError::host("ERR_AGENTOS_PROCESS_EVENT_LIMIT", format!("process event queue exceeded {limit} pending events; raise runtime.protocol.maxProcessEvents" )) } fn sidecar_response_pending_overflow_error(limit: usize) -> SidecarError { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_PENDING_RESPONSE_LIMIT: sidecar response tracker exceeded {limit} pending responses; raise runtime.protocol.maxPendingResponses" + SidecarError::host("ERR_AGENTOS_PENDING_RESPONSE_LIMIT", format!("sidecar response tracker exceeded {limit} pending responses; raise runtime.protocol.maxPendingResponses" )) } fn outbound_sidecar_request_queue_overflow_error(limit: usize) -> SidecarError { - SidecarError::InvalidState(format!( - "ERR_AGENTOS_OUTBOUND_REQUEST_LIMIT: outbound sidecar request queue exceeded {limit} pending requests; raise runtime.protocol.maxOutboundRequests" + SidecarError::host("ERR_AGENTOS_OUTBOUND_REQUEST_LIMIT", format!("outbound sidecar request queue exceeded {limit} pending requests; raise runtime.protocol.maxOutboundRequests" )) } @@ -158,7 +156,7 @@ struct LegacyJavascriptChildProcessSpawnOptions { // hand-copied field list previously dropped POSIX spawn attributes and fd // mappings without an error. #[serde(flatten)] - options: JavascriptChildProcessSpawnOptions, + options: ProcessLaunchOptions, #[serde(default, rename = "maxBuffer")] max_buffer: Option, } @@ -180,9 +178,9 @@ fn is_javascript_loopback_host(host: &str) -> bool { pub(crate) fn parse_javascript_child_process_spawn_request( vm: &VmState, args: &[Value], -) -> Result<(JavascriptChildProcessSpawnRequest, Option), SidecarError> { +) -> Result<(ProcessLaunchRequest, Option), SidecarError> { if let Some(value) = args.first().cloned() { - if let Ok(request) = serde_json::from_value::(value) { + if let Ok(request) = serde_json::from_value::(value) { return Ok((request, None)); } } @@ -200,7 +198,7 @@ pub(crate) fn parse_javascript_child_process_spawn_request( let options = parsed_options.options; Ok(( - JavascriptChildProcessSpawnRequest { + ProcessLaunchRequest { command, args: parsed_args, options, @@ -235,6 +233,8 @@ impl SharedBridge { permissions: Arc::new(Mutex::new(BTreeMap::new())), #[cfg(test)] set_vm_permissions_outcomes: Arc::new(Mutex::new(VecDeque::new())), + #[cfg(test)] + emit_lifecycle_outcomes: Arc::new(Mutex::new(VecDeque::new())), } } } @@ -281,6 +281,21 @@ where vm_id: &str, state: LifecycleState, ) -> Result<(), SidecarError> { + #[cfg(test)] + { + let outcome = self + .emit_lifecycle_outcomes + .lock() + .map_err(|_| { + SidecarError::Bridge(String::from( + "native sidecar test lifecycle outcome lock poisoned", + )) + })? + .pop_front(); + if let Some(Some(error)) = outcome { + return Err(error); + } + } self.with_mut(|bridge| { bridge.emit_lifecycle(LifecycleEventRecord { vm_id: vm_id.to_owned(), @@ -290,6 +305,23 @@ where }) } + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn queue_emit_lifecycle_result( + &self, + result: Result<(), SidecarError>, + ) -> Result<(), SidecarError> { + self.emit_lifecycle_outcomes + .lock() + .map_err(|_| { + SidecarError::Bridge(String::from( + "native sidecar test lifecycle outcome lock poisoned", + )) + })? + .push_back(result.err()); + Ok(()) + } + pub(crate) fn emit_log( &self, vm_id: &str, @@ -439,10 +471,10 @@ where } let message = match decision.reason.as_deref() { - Some(reason) => format!("EACCES: permission denied, {resource}: {reason}"), - None => format!("EACCES: permission denied, {resource}"), + Some(reason) => format!("permission denied, {resource}: {reason}"), + None => format!("permission denied, {resource}"), }; - Err(SidecarError::Execution(message)) + Err(SidecarError::host("EACCES", message)) } /// Revalidate an authority-expanding network operation against both the @@ -476,10 +508,10 @@ where return Ok(()); } let message = match decision.reason.as_deref() { - Some(reason) => format!("EACCES: permission denied, {resource}: {reason}"), - None => format!("EACCES: permission denied, {resource}"), + Some(reason) => format!("permission denied, {resource}: {reason}"), + None => format!("permission denied, {resource}"), }; - Err(SidecarError::Execution(message)) + Err(SidecarError::host("EACCES", message)) }; if let Some(permissions) = permissions { @@ -601,7 +633,17 @@ where domain: &str, resource: Option<&str>, ) -> Option { - let stored = self.permissions.lock().ok()?; + let stored = match self.permissions.lock() { + Ok(stored) => stored, + Err(_) => { + eprintln!( + "ERR_AGENTOS_PERMISSION_STATE: permission policy lock is poisoned for VM {vm_id}; denying {capability} fail-closed" + ); + return Some(PermissionDecision::deny( + "permission policy state is unavailable", + )); + } + }; let permissions = stored.get(vm_id)?; let mode = evaluate_permissions_policy(permissions, domain, capability, resource); Some(permission_mode_to_kernel_decision(mode, capability)) @@ -684,9 +726,9 @@ fn poll_future_once(future: std::pin::Pin<&mut F>) -> Op // ConnectionState, SessionState, VmConfiguration, VmState moved to crate::state -// JavascriptSocketPathContext, JavascriptSocketFamily, VmListenPolicy moved to crate::state +// SocketPathContext, SocketFamily, VmListenPolicy moved to crate::state -impl JavascriptSocketPathContext { +impl SocketPathContext { pub(crate) fn loopback_port_allowed(&self, port: u16) -> bool { self.loopback_exempt_ports.contains(&port) || self @@ -701,7 +743,7 @@ impl JavascriptSocketPathContext { pub(crate) fn translate_tcp_loopback_port( &self, - family: JavascriptSocketFamily, + family: SocketFamily, port: u16, ) -> Option { self.tcp_loopback_guest_to_host_ports @@ -711,15 +753,15 @@ impl JavascriptSocketPathContext { pub(crate) fn http_loopback_target( &self, - family: JavascriptSocketFamily, + family: SocketFamily, port: u16, - ) -> Option<&crate::state::JavascriptHttpLoopbackTarget> { + ) -> Option<&crate::state::HttpLoopbackTarget> { self.http_loopback_targets.get(&(family, port)) } pub(crate) fn translate_udp_loopback_port( &self, - family: JavascriptSocketFamily, + family: SocketFamily, port: u16, ) -> Option { self.udp_loopback_guest_to_host_ports @@ -729,7 +771,7 @@ impl JavascriptSocketPathContext { pub(crate) fn guest_udp_port_for_host_port( &self, - family: JavascriptSocketFamily, + family: SocketFamily, port: u16, ) -> Option { self.udp_loopback_host_to_guest_ports @@ -870,9 +912,8 @@ where "native sidecar expected_auth_token must not be empty", ))); } - let dns_resolver: agentos_kernel::dns::SharedDnsResolver = Arc::new( - agentos_kernel::dns::HickoryDnsResolver::with_runtime(runtime_context.clone()), - ); + let dns_resolver: agentos_kernel::dns::SharedDnsResolver = + Arc::new(HickoryDnsResolver::new(runtime_context.clone())); let cache_root = config.compile_cache_root.clone().unwrap_or_else(|| { std::env::temp_dir().join(format!( @@ -1057,9 +1098,10 @@ where let limit = self.config.runtime.resources.max_capabilities; let used = self.vms.len().saturating_add(self.quarantined_vms.len()); if used >= limit { - return Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_VM_GENERATION_LIMIT: tracked={used} limit={limit}; raise runtime.resources.maxCapabilities" - ))); + return Err(SidecarError::host( + "ERR_AGENTOS_VM_GENERATION_LIMIT", + format!("tracked={used} limit={limit}; raise runtime.resources.maxCapabilities"), + )); } Ok(()) } @@ -1076,10 +1118,13 @@ where } let limit = self.config.runtime.resources.max_capabilities; if self.quarantined_vms.len() >= limit { - return Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_VM_QUARANTINE_LIMIT: quarantined={} limit={limit}; raise runtime.resources.maxCapabilities", - self.quarantined_vms.len() - ))); + return Err(SidecarError::host( + "ERR_AGENTOS_VM_QUARANTINE_LIMIT", + format!( + "quarantined={} limit={limit}; raise runtime.resources.maxCapabilities", + self.quarantined_vms.len() + ), + )); } self.quarantined_vms.insert(generation, quarantined); self.observe_active_vm_generations(); @@ -1099,6 +1144,21 @@ where return false; }; match event { + ActiveExecutionEvent::Common(agentos_execution::backend::ExecutionEvent::Output { + stream, + bytes, + }) => { + match stream { + agentos_execution::backend::OutputStream::Stdout => { + buffer.append_stdout(bytes.as_slice(), DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT) + } + agentos_execution::backend::OutputStream::Stderr => { + buffer.append_stderr(bytes.as_slice(), DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT) + } + } + true + } + ActiveExecutionEvent::Common(_) => false, ActiveExecutionEvent::Stdout(chunk) => { buffer.append_stdout(chunk, DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT); true @@ -1107,10 +1167,10 @@ where buffer.append_stderr(chunk, DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT); true } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) + ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } | ActiveExecutionEvent::Exited(_) => false, } @@ -1358,17 +1418,27 @@ where vm_bytes = vm_bytes.saturating_add(pending.retained_bytes()); } if vm_count >= limits.pending_event_count { - return Err(SidecarError::InvalidState(format!( - "VM {} process event queue exceeded {} events (limits.process.pendingEventCount)", - envelope.vm_id, limits.pending_event_count - ))); + return Err(SidecarError::host_resource_limit( + "limits.process.pendingEventCount", + limits.pending_event_count, + vm_count.saturating_add(1), + format!( + "VM {} process event queue exceeded {} events (limits.process.pendingEventCount); raise limits.process.pendingEventCount", + envelope.vm_id, limits.pending_event_count + ), + )); } let next_bytes = vm_bytes.saturating_add(envelope.retained_bytes()); if next_bytes > limits.pending_event_bytes { - return Err(SidecarError::InvalidState(format!( - "VM {} process event queue exceeded {} retained bytes (limits.process.pendingEventBytes)", - envelope.vm_id, limits.pending_event_bytes - ))); + return Err(SidecarError::host_resource_limit( + "limits.process.pendingEventBytes", + limits.pending_event_bytes, + next_bytes, + format!( + "VM {} process event queue exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + envelope.vm_id, limits.pending_event_bytes + ), + )); } Ok(()) } @@ -1498,8 +1568,7 @@ where .get(&ownership.vm_id) .and_then(|vm| vm.runtime_context.terminal_failure()) { - let error = SidecarError::Execution(format!( - "ERR_AGENTOS_VM_TASK_FAILED: vm_id={} class={:?} owner={} reason={:?}; dispose and recreate this VM generation", + let error = SidecarError::host("ERR_AGENTOS_VM_TASK_FAILED", format!("vm_id={} class={:?} owner={} reason={:?}; dispose and recreate this VM generation", ownership.vm_id, report.class, report.owner, report.reason )); return Ok(DispatchResult { @@ -1878,7 +1947,7 @@ where request: &RequestFrame, payload: crate::protocol::AuthenticateRequest, ) -> Result { - let _ = self.connection_id_for(&request.ownership)?; + self.connection_id_for(&request.ownership)?; if let Err(error) = self.validate_auth_token(&payload.auth_token) { let mut fields = audit_fields([ (String::from("source"), payload.client_name.clone()), @@ -2081,37 +2150,10 @@ where // dispose_vm_internal, terminate_vm_processes, wait_for_vm_processes_to_exit moved to crate::vm - // kill_process_internal, handle_execution_event, handle_python_vfs_rpc_request, - // resolve_javascript_child_process_execution, spawn_javascript_child_process, - // poll_javascript_child_process, write_javascript_child_process_stdin, - // close_javascript_child_process_stdin, kill_javascript_child_process moved to crate::execution - - /// Whether a `__kernel_stdin_read` / `__kernel_poll` RPC may be serviced - /// via the non-blocking deferral path. Non-TTY JavaScript keeps its - /// in-process local stdin bridge (serviced inline by the fallback arm). - fn kernel_wait_rpc_is_deferrable( - &self, - vm_id: &str, - process_id: &str, - request: &JavascriptSyncRpcRequest, - ) -> bool { - let Some(vm) = self.vms.get(vm_id) else { - return false; - }; - let Some(process) = vm.active_processes.get(process_id) else { - return false; - }; - if request.method == "__kernel_stdin_read" - && matches!( - process.execution, - crate::state::ActiveExecution::Javascript(_) - ) - && process.tty_master_fd.is_none() - { - return false; - } - true - } + // kill_process_internal, handle_execution_event, + // resolve_javascript_child_process_execution, spawn_child_process, + // poll_child_process, write_child_process_stdin, + // Child-process lifecycle operations moved to crate::execution. /// Service `__kernel_stdin_read` / `__kernel_poll` without blocking the /// dispatch loop. Probes readiness with a zero timeout; when not ready and @@ -2126,8 +2168,9 @@ where &mut self, vm_id: &str, process_id: &str, - request: &JavascriptSyncRpcRequest, - ) -> Result, SidecarError> { + call: &ExecutionHostCall, + ) -> Result, SidecarError> { + let request = &call.request; let requested_timeout_ms = match request.method.as_str() { "process.fd_write" => None, "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1, @@ -2156,15 +2199,30 @@ where } else { requested_timeout_ms }; + let requested_deadline = requested_timeout_ms + .map(crate::execution::checked_deferred_guest_wait_deadline) + .transpose() + .map_err(SidecarError::from)?; // Reading from the pipe frees capacity. Top it off before every root // process read/poll probe, matching the descendant-process path, and // deliver a deferred close only after all accepted bytes are written. flush_pending_kernel_stdin(&mut vm.kernel, process)?; let kernel_pid = process.kernel_pid; let kernel_stdin_reader_fd = process.kernel_stdin_reader_fd; - let deadline = match &process.deferred_kernel_wait_rpc { - Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline, - _ => requested_timeout_ms.map(|timeout_ms| now + Duration::from_millis(timeout_ms)), + let same_parked_call = process + .deferred_kernel_wait_rpc + .as_ref() + .is_some_and(|(parked, _)| parked.id == request.id); + if !same_parked_call { + process.deferred_kernel_wait_deadline_warned = false; + } + let deadline = if same_parked_call { + process + .deferred_kernel_wait_rpc + .as_ref() + .and_then(|(_, deadline)| *deadline) + } else { + requested_deadline }; let probe = match request.method.as_str() { "process.fd_write" => { @@ -2199,22 +2257,38 @@ where } Err(error) if request.method == "process.fd_write" - && javascript_sync_rpc_error_code(&error) == "EAGAIN" => + && host_service_error_code(&error) == "EAGAIN" => { (Value::Null, false) } Err(error) => { - process.deferred_kernel_wait_rpc = None; + process.clear_deferred_kernel_wait_rpc(); return Err(error); } }; + let mut operation_deadline = if request.method == "process.fd_write" { + deadline.map(|deadline| { + crate::execution::OperationDeadlineTracker::from_deadline( + deadline, + Duration::from_millis(vm.limits.reactor.operation_deadline_ms), + process.deferred_kernel_wait_deadline_warned, + ) + }) + } else { + None + }; + if !ready { + if let Some(deadline) = operation_deadline.as_mut() { + deadline.observe("deferred root-process fd write"); + process.deferred_kernel_wait_deadline_warned = deadline.warning_emitted(); + } + } if request.method == "process.fd_write" && !ready && deadline.is_some_and(|deadline| now >= deadline) { - process.deferred_kernel_wait_rpc = None; - return Err(SidecarError::Execution(format!( - "ETIMEDOUT: pipe write exceeded limits.reactor.operationDeadlineMs ({} ms); raise that limit for slower readers", + process.clear_deferred_kernel_wait_rpc(); + return Err(SidecarError::host("ETIMEDOUT", format!("pipe write exceeded limits.reactor.operationDeadlineMs ({} ms); raise that limit for slower readers", vm.limits.reactor.operation_deadline_ms ))); } @@ -2222,17 +2296,20 @@ where || requested_timeout_ms == Some(0) || deadline.is_some_and(|deadline| now >= deadline) { - process.deferred_kernel_wait_rpc = None; + process.clear_deferred_kernel_wait_rpc(); return Ok(Some(probe.into())); } let connection_id = vm.connection_id.clone(); let session_id = vm.session_id.clone(); let runtime = vm.runtime_context.clone(); - let remaining = deadline.map(|deadline| deadline.saturating_duration_since(now)); + let remaining = operation_deadline + .as_ref() + .map(crate::execution::OperationDeadlineTracker::remaining_until_next_edge) + .or_else(|| deadline.map(|deadline| deadline.saturating_duration_since(now))); let sender = self.process_event_sender.clone(); let event_notify = Arc::clone(&self.process_event_notify); - let waiter_request = request.clone(); + let waiter_request = call.clone(); let envelope_vm_id = vm_id.to_owned(); let envelope_process_id = process_id.to_owned(); runtime @@ -2254,7 +2331,7 @@ where session_id, vm_id: envelope_vm_id, process_id: envelope_process_id, - event: ActiveExecutionEvent::JavascriptSyncRpcRequest(waiter_request), + event: ActiveExecutionEvent::HostRpcRequest(waiter_request), }) .await .is_err() @@ -2273,7 +2350,7 @@ where let Some(process) = vm.active_processes.get_mut(process_id) else { return Ok(None); }; - process.deferred_kernel_wait_rpc = Some((request.clone(), deadline)); + process.deferred_kernel_wait_rpc = Some((call.clone(), deadline)); Ok(None) } @@ -2281,8 +2358,9 @@ where &mut self, vm_id: &str, process_id: &str, - request: JavascriptSyncRpcRequest, + call: ExecutionHostCall, ) -> Result<(), SidecarError> { + let request = &call.request; record_sync_bridge_request_observed(request.id, &request.method); let Some(vm) = self.vms.get(vm_id) else { log_stale_process_event(&self.bridge, vm_id, process_id, "javascript sync RPC"); @@ -2303,147 +2381,164 @@ where .filter(|request| request.method == "process.fd_write") }; - let response: Result = - match request.method.as_str() { - _ if deferrable_fd_write.is_some() => { - let normalized = deferrable_fd_write - .as_ref() - .expect("guarded deferred fd_write request"); - match self.service_deferrable_kernel_wait_rpc(vm_id, process_id, normalized) { - Ok(Some(response)) => Ok(response), - Ok(None) => return Ok(()), - Err(error) => Err(error), - } - } - "child_process.spawn" => { - let Some(vm) = self.vms.get(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC child_process.spawn", - ); - return Ok(()); - }; - let (payload, _) = - parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.spawn_javascript_child_process(vm_id, process_id, payload) - .await - .map(Into::into) - } - "child_process.spawn_sync" => { - let Some(vm) = self.vms.get(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC child_process.spawn_sync", - ); - return Ok(()); - }; - let (payload, max_buffer) = - parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.defer_javascript_child_process_sync(vm_id, process_id, payload, max_buffer) - .await - } - "child_process.poll" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.poll child id", - )?; - let wait_ms = javascript_sync_rpc_arg_u64_optional( - &request.args, - 1, - "child_process.poll wait ms", - )? - .unwrap_or_default(); - self.poll_javascript_child_process(vm_id, process_id, child_process_id, wait_ms) - .await - .map(Into::into) + let response: Result = match request + .method + .as_str() + { + _ if deferrable_fd_write.is_some() => { + let normalized = deferrable_fd_write + .as_ref() + .expect("guarded deferred fd_write request"); + let normalized_call = ExecutionHostCall { + request: normalized.clone(), + reply: call.reply.clone(), + }; + match self.service_deferrable_kernel_wait_rpc(vm_id, process_id, &normalized_call) { + Ok(Some(response)) => Ok(response), + Ok(None) => return Ok(()), + Err(error) => Err(error), } - "child_process.write_stdin" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.write_stdin child id", - )?; - let chunk = javascript_sync_rpc_bytes_arg( - &request.args, - 1, - "child_process.write_stdin chunk", - )?; - self.write_javascript_child_process_stdin( + } + "child_process.spawn" => { + let Some(vm) = self.vms.get(vm_id) else { + log_stale_process_event( + &self.bridge, vm_id, process_id, - child_process_id, - &chunk, - ) - .map(|()| Value::Null.into()) - } - "child_process.close_stdin" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.close_stdin child id", - )?; - self.close_javascript_child_process_stdin(vm_id, process_id, child_process_id) - .map(|()| Value::Null.into()) - } - "child_process.kill" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.kill child id", - )?; - let signal = - javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?; - self.kill_javascript_child_process(vm_id, process_id, child_process_id, signal) - .map(|()| Value::Null.into()) - } - "process.exec_fd_image_commit" => { + "javascript sync RPC child_process.spawn", + ); + return Ok(()); + }; + let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?; + self.spawn_child_process(vm_id, process_id, payload) + .await + .map(Into::into) + } + "child_process.spawn_sync" => { + let Some(vm) = self.vms.get(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC child_process.spawn_sync", + ); + return Ok(()); + }; + let (payload, max_buffer) = + parse_javascript_child_process_spawn_request(vm, &request.args)?; + self.defer_javascript_child_process_sync(vm_id, process_id, payload, max_buffer) + .await + } + "child_process.poll" => { + let child_process_id = + javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?; + let wait_ms = javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "child_process.poll wait ms", + )? + .unwrap_or_default(); + self.poll_child_process(vm_id, process_id, child_process_id, wait_ms) + .await + .map(Into::into) + } + "child_process.write_stdin" => { + let child_process_id = javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.write_stdin child id", + )?; + let chunk = javascript_sync_rpc_bytes_arg( + &request.args, + 1, + "child_process.write_stdin chunk", + )?; + self.write_child_process_stdin(vm_id, process_id, child_process_id, &chunk)?; + Ok(Value::Null.into()) + } + "child_process.close_stdin" => { + let child_process_id = javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.close_stdin child id", + )?; + self.close_child_process_stdin(vm_id, process_id, child_process_id)?; + Ok(Value::Null.into()) + } + "child_process.kill" => { + let child_process_id = + javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?; + let signal = + javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?; + self.kill_javascript_child_process(vm_id, process_id, child_process_id, signal)?; + Ok(Value::Null.into()) + } + "process.kill" => { + let target_pid = + javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; + let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; + let parsed_signal = parse_signal(signal)?; + if parsed_signal == 0 { let Some(vm) = self.vms.get(vm_id) else { log_stale_process_event( &self.bridge, vm_id, process_id, - "javascript sync RPC process.exec_fd_image_commit", + "javascript sync RPC process.kill", ); return Ok(()); }; - let (payload, _) = - parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.commit_wasm_fd_process_image(vm_id, process_id, &[], payload)?; - Ok(json!({ "committed": true }).into()) - } - "process.exec" => { - let Some(vm) = self.vms.get(vm_id) else { + if !vm.active_processes.contains_key(process_id) { log_stale_process_event( &self.bridge, vm_id, process_id, - "javascript sync RPC process.exec", + "javascript sync RPC process.kill", ); return Ok(()); + } + vm.kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) + .map(|()| Value::Null.into()) + .map_err(kernel_error) + } else if target_pid < 0 { + let caller_kernel_pid = { + let Some(vm) = self.vms.get(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC process.kill", + ); + return Ok(()); + }; + let Some(caller) = vm.active_processes.get(process_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC process.kill", + ); + return Ok(()); + }; + caller.kernel_pid }; - let (payload, _) = - parse_javascript_child_process_spawn_request(vm, &request.args)?; - let local_replacement = payload.options.local_replacement; - match self.exec_javascript_process_image(vm_id, process_id, &[], payload) { - Ok(()) if local_replacement => Ok(json!({ "committed": true }).into()), - // Success destroys the blocked old image. Never reply: - // returning would resume instructions after execve. - Ok(()) => return Ok(()), + let pgid = target_pid.unsigned_abs(); + match self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal) { + Ok(true) => self + .apply_self_process_kill(vm_id, process_id, parsed_signal) + .map(Into::into), + Ok(false) => Ok(Value::Null.into()), Err(error) => Err(error), } - } - "process.kill" => { - let target_pid = - javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; - let signal = - javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; - let parsed_signal = parse_signal(signal)?; - if parsed_signal == 0 { + } else { + enum ProcessKillTarget { + SelfProcess, + Child(String), + TopLevel(String), + KernelPid(u32), + } + let target = { let Some(vm) = self.vms.get(vm_id) else { log_stale_process_event( &self.bridge, @@ -2453,7 +2548,7 @@ where ); return Ok(()); }; - if !vm.active_processes.contains_key(process_id) { + let Some(caller) = vm.active_processes.get(process_id) else { log_stale_process_event( &self.bridge, vm_id, @@ -2461,291 +2556,224 @@ where "javascript sync RPC process.kill", ); return Ok(()); - } - vm.kernel - .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) - .map(|()| Value::Null.into()) - .map_err(kernel_error) - } else if target_pid < 0 { - let caller_kernel_pid = { - let Some(vm) = self.vms.get(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC process.kill", - ); - return Ok(()); - }; - let Some(caller) = vm.active_processes.get(process_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC process.kill", - ); - return Ok(()); - }; - caller.kernel_pid }; - let pgid = target_pid.unsigned_abs(); - match self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal) { - Ok(true) => self - .apply_self_process_kill(vm_id, process_id, parsed_signal) - .map(Into::into), - Ok(false) => Ok(Value::Null.into()), - Err(error) => Err(error), + let caller_pid = i32::try_from(caller.kernel_pid).map_err(|_| { + SidecarError::InvalidState("caller pid exceeds i32".into()) + })?; + if caller_pid == target_pid { + ProcessKillTarget::SelfProcess + } else if let Some((child_process_id, _)) = caller + .child_processes + .iter() + .find(|(_, child)| i32::try_from(child.kernel_pid) == Ok(target_pid)) + { + ProcessKillTarget::Child(child_process_id.clone()) + } else if let Some((target_process_id, _)) = + vm.active_processes.iter().find(|(_, process)| { + i32::try_from(process.kernel_pid) == Ok(target_pid) + }) + { + ProcessKillTarget::TopLevel(target_process_id.clone()) + } else { + let target_kernel_pid = u32::try_from(target_pid).map_err(|_| { + SidecarError::host( + "EINVAL", + format!("invalid process pid {target_pid}"), + ) + })?; + ProcessKillTarget::KernelPid(target_kernel_pid) } - } else { - enum ProcessKillTarget { - SelfProcess, - Child(String), - TopLevel(String), - KernelPid(u32), + }; + match target { + ProcessKillTarget::SelfProcess => self + .apply_self_process_kill(vm_id, process_id, parsed_signal) + .map(Into::into), + ProcessKillTarget::Child(child_process_id) => { + self.kill_javascript_child_process( + vm_id, + process_id, + &child_process_id, + signal, + )?; + Ok(Value::Null.into()) } - let target = { - let Some(vm) = self.vms.get(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC process.kill", - ); - return Ok(()); - }; - let Some(caller) = vm.active_processes.get(process_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC process.kill", - ); - return Ok(()); - }; - let caller_pid = i32::try_from(caller.kernel_pid).map_err(|_| { - SidecarError::InvalidState("caller pid exceeds i32".into()) - })?; - if caller_pid == target_pid { - ProcessKillTarget::SelfProcess - } else if let Some((child_process_id, _)) = - caller.child_processes.iter().find(|(_, child)| { - i32::try_from(child.kernel_pid) == Ok(target_pid) - }) - { - ProcessKillTarget::Child(child_process_id.clone()) - } else if let Some((target_process_id, _)) = - vm.active_processes.iter().find(|(_, process)| { - i32::try_from(process.kernel_pid) == Ok(target_pid) - }) - { - ProcessKillTarget::TopLevel(target_process_id.clone()) - } else { - let target_kernel_pid = - u32::try_from(target_pid).map_err(|_| { - SidecarError::InvalidState(format!( - "EINVAL: invalid process pid {target_pid}" - )) - })?; - ProcessKillTarget::KernelPid(target_kernel_pid) - } - }; - match target { - ProcessKillTarget::SelfProcess => self - .apply_self_process_kill(vm_id, process_id, parsed_signal) - .map(Into::into), - ProcessKillTarget::Child(child_process_id) => { - self.kill_javascript_child_process( - vm_id, - process_id, - &child_process_id, - signal, - )?; - Ok(Value::Null.into()) - } - ProcessKillTarget::TopLevel(target_process_id) => { - self.kill_process_internal(vm_id, &target_process_id, signal)?; - Ok(Value::Null.into()) - } - ProcessKillTarget::KernelPid(target_kernel_pid) => { - // Grandchildren and untracked kernel processes are - // resolved VM-wide instead of failing with an - // unknown-pid error. - self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal) - .map(|()| Value::Null.into()) - } + ProcessKillTarget::TopLevel(target_process_id) => { + self.kill_process_internal(vm_id, &target_process_id, signal)?; + Ok(Value::Null.into()) + } + ProcessKillTarget::KernelPid(target_kernel_pid) => { + // Grandchildren and untracked kernel processes are + // resolved VM-wide instead of failing with an + // unknown-pid error. + self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal) + .map(|()| Value::Null.into()) } } } - "process.signal_state" => { - let (signal, registration) = parse_process_signal_state_request(&request.args) - .map_err(|error| SidecarError::InvalidState(error.to_string()))?; - let Some(vm) = self.vms.get_mut(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC process.signal_state", - ); - return Ok(()); - }; - apply_process_signal_state_update( - &mut vm.signal_states, + } + "process.signal_state" => { + let (signal, registration) = parse_process_signal_state_request(&request.args) + .map_err(SidecarError::from)?; + let Some(vm) = self.vms.get_mut(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, process_id, - signal, - registration, + "javascript sync RPC process.signal_state", ); - Ok(Value::Null.into()) - } - "net.http_request" => { - let payload = request - .args - .first() - .cloned() - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "net.http_request requires a request payload", - )) - }) - .and_then(|value| { - serde_json::from_value::(value).map_err( - |error| { - SidecarError::InvalidState(format!( - "invalid net.http_request payload: {error}" - )) - }, - ) - })?; - if !is_javascript_loopback_host(&payload.host) { - return Err(SidecarError::Execution(format!( - "EACCES: HTTP loopback request requires a loopback host, got {}", + return Ok(()); + }; + let process = vm.active_processes.get(process_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} has no active process {process_id}" + )) + })?; + apply_kernel_signal_registration(process, signal, ®istration)?; + Ok(Value::Null.into()) + } + "net.http_request" => { + let payload = request + .args + .first() + .cloned() + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "net.http_request requires a request payload", + )) + }) + .and_then(|value| { + serde_json::from_value::(value).map_err( + |error| { + SidecarError::InvalidState(format!( + "invalid net.http_request payload: {error}" + )) + }, + ) + })?; + if !is_javascript_loopback_host(&payload.host) { + return Err(SidecarError::host( + "EACCES", + format!( + "HTTP loopback request requires a loopback host, got {}", payload.host - ))); - } - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(&payload.host, payload.port), - )?; - let Some(vm) = self.vms.get_mut(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC net.http_request", - ); - return Ok(()); - }; - let socket_paths = build_javascript_socket_path_context(vm)?; - let target_is_current = - [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6] - .iter() - .any(|family| { - socket_paths - .http_loopback_target(*family, payload.port) - .is_some_and(|target| { - target.process_id == payload.process_id - && target.server_id == payload.server_id - }) - }); - if !target_is_current { - return Err(SidecarError::InvalidState(format!( - "unknown HTTP loopback target {}:{} for server {} in process {}", - payload.host, payload.port, payload.server_id, payload.process_id - ))); - } - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let capabilities = vm.capabilities.clone(); - let Some(target_process) = vm.active_processes.get_mut(&payload.process_id) - else { - return Err(SidecarError::InvalidState(format!( - "unknown HTTP loopback process {}", - payload.process_id - ))); - }; - dispatch_loopback_http_request_deferred(LoopbackHttpDispatchRequest { - bridge: &self.bridge, + ), + )); + } + self.bridge.require_network_access( + vm_id, + NetworkOperation::Http, + format_tcp_resource(&payload.host, payload.port), + )?; + let Some(vm) = self.vms.get_mut(vm_id) else { + log_stale_process_event( + &self.bridge, vm_id, - dns: &vm.dns, - socket_paths: &socket_paths, - kernel: &mut vm.kernel, - kernel_readiness, - process: target_process, - server_id: payload.server_id, - request_json: &payload.request, - capabilities, - }) + process_id, + "javascript sync RPC net.http_request", + ); + return Ok(()); + }; + let socket_paths = build_socket_path_context(vm)?; + let target_is_current = + [SocketFamily::Ipv4, SocketFamily::Ipv6] + .iter() + .any(|family| { + socket_paths + .http_loopback_target(*family, payload.port) + .is_some_and(|target| { + target.process_id == payload.process_id + && target.server_id == payload.server_id + }) + }); + if !target_is_current { + return Err(SidecarError::InvalidState(format!( + "unknown HTTP loopback target {}:{} for server {} in process {}", + payload.host, payload.port, payload.server_id, payload.process_id + ))); } - "__kernel_stdio_write" - if self + let Some(target_process) = vm.active_processes.get_mut(&payload.process_id) else { + return Err(SidecarError::InvalidState(format!( + "unknown HTTP loopback process {}", + payload.process_id + ))); + }; + dispatch_loopback_http_request_deferred(LoopbackHttpDispatchRequest { + process: target_process, + server_id: payload.server_id, + request_json: &payload.request, + }) + } + "__kernel_stdio_write" + if self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .is_some_and(|process| process.tty_master_owner.is_some()) => + { + let (writer_kernel_pid, owner) = { + let process = self .vms .get(vm_id) .and_then(|vm| vm.active_processes.get(process_id)) - .is_some_and(|process| process.tty_master_owner.is_some()) => - { - let (writer_kernel_pid, owner) = { - let process = self - .vms - .get(vm_id) - .and_then(|vm| vm.active_processes.get(process_id)) - .expect("guarded by match arm"); - ( - process.kernel_pid, - process.tty_master_owner.expect("guarded by match arm"), - ) - }; - self.service_shared_tty_stdio_write(vm_id, writer_kernel_pid, owner, &request) - .map(Into::into) - } - "__kernel_stdin_read" | "__kernel_poll" - if self.kernel_wait_rpc_is_deferrable(vm_id, process_id, &request) => - { - match self.service_deferrable_kernel_wait_rpc(vm_id, process_id, &request) { - Ok(Some(response)) => Ok(response), - // Parked: an off-loop waiter re-enqueues this request as a - // process event when kernel poll state changes. - Ok(None) => return Ok(()), - Err(error) => Err(error), - } + .expect("guarded by match arm"); + ( + process.kernel_pid, + process.tty_master_owner.expect("guarded by match arm"), + ) + }; + self.service_shared_tty_stdio_write(vm_id, writer_kernel_pid, owner, &request) + .map(Into::into) + } + "__kernel_stdin_read" | "__kernel_poll" => { + match self.service_deferrable_kernel_wait_rpc(vm_id, process_id, &call) { + Ok(Some(response)) => Ok(response), + // Parked: an off-loop waiter re-enqueues this request as a + // process event when kernel poll state changes. + Ok(None) => return Ok(()), + Err(error) => Err(error), } - _ => { - let Some(vm) = self.vms.get_mut(vm_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC bridge dispatch", - ); - return Ok(()); - }; - let socket_paths = build_javascript_socket_path_context(vm)?; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let capabilities = vm.capabilities.clone(); - let Some(process) = vm.active_processes.get_mut(process_id) else { - log_stale_process_event( - &self.bridge, - vm_id, - process_id, - "javascript sync RPC bridge dispatch", - ); - return Ok(()); - }; - service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { - bridge: &self.bridge, + } + _ => { + let Some(vm) = self.vms.get_mut(vm_id) else { + log_stale_process_event( + &self.bridge, vm_id, - dns: &vm.dns, - socket_paths: &socket_paths, - kernel: &mut vm.kernel, - kernel_readiness, - process, - sync_request: &request, - capabilities, - }) - .await - } - }; + process_id, + "javascript sync RPC bridge dispatch", + ); + return Ok(()); + }; + let socket_paths = build_socket_path_context(vm)?; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let capabilities = vm.capabilities.clone(); + let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); + let Some(process) = vm.active_processes.get_mut(process_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC bridge dispatch", + ); + return Ok(()); + }; + service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { + bridge: &self.bridge, + vm_id, + dns: &vm.dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + sync_request: &request, + capabilities, + managed_descriptions: Some(managed_descriptions), + }) + .await + } + }; let response = match response { - Ok(crate::execution::JavascriptSyncRpcServiceResponse::Deferred { + Ok(crate::execution::HostServiceResponse::Deferred { receiver, timeout, task_class, @@ -2766,7 +2794,7 @@ where let event_notify = Arc::clone(&self.process_event_notify); let envelope_vm_id = vm_id.to_owned(); let envelope_process_id = process_id.to_owned(); - let request_id = request.id; + let reply = call.reply.clone(); let method = request.method.clone(); runtime .spawn(task_class, async move { @@ -2779,11 +2807,18 @@ where message: format!( "deferred sync RPC response channel closed for {method}" ), + details: None, }) }) }; let result = match timeout { - Some(timeout) => match tokio::time::timeout(timeout, receive).await { + Some(timeout) => match crate::execution::operation_deadline_timeout( + &method, + timeout, + receive, + ) + .await + { Ok(result) => result, Err(_) => Err(crate::state::DeferredRpcError { code: String::from("ERR_AGENTOS_DEFERRED_RPC_TIMEOUT"), @@ -2791,6 +2826,7 @@ where "{method} exceeded limits.reactor.operationDeadlineMs ({} ms); raise that limit for slower peers", timeout.as_millis() ), + details: None, }), }, None => receive.await, @@ -2801,8 +2837,8 @@ where session_id, vm_id: envelope_vm_id, process_id: envelope_process_id, - event: ActiveExecutionEvent::JavascriptSyncRpcCompletion( - crate::state::JavascriptSyncRpcCompletion { request_id, result }, + event: ActiveExecutionEvent::HostCallCompletion( + crate::state::HostCallCompletion { reply, result }, ), }) .await @@ -2841,8 +2877,7 @@ where ); return Ok(()); }; - let shadow_root = vm.cwd.clone(); - let Some(process) = vm.active_processes.get_mut(process_id) else { + if !vm.active_processes.contains_key(process_id) { log_stale_process_event( &self.bridge, vm_id, @@ -2850,91 +2885,32 @@ where "javascript sync RPC response delivery", ); return Ok(()); - }; - - if response.is_ok() - && matches!( - request.method.as_str(), - "fs.chmodSync" | "fs.promises.chmod" - ) - { - let guest_path = - javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?; - let mode = - javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777; - let host_path = - shadow_host_path_for_process(&shadow_root, &process.guest_cwd, guest_path); - if host_path.exists() { - fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err( - |error| { - SidecarError::Io(format!( - "failed to mirror chmod to shadow path {}: {error}", - host_path.display() - )) - }, - )?; - } } - match response { - Ok(result) => process - .execution - .respond_javascript_sync_rpc_response(request.id, result) - .or_else(ignore_stale_javascript_sync_rpc_response), - Err(error) => { - tracing::warn!( - method = %request.method, - error = %error, - "JavaScript sync RPC failed" - ); - process - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - ) - .or_else(ignore_stale_javascript_sync_rpc_response) - } + if let Err(error) = &response { + tracing::warn!( + method = %call.request.method, + error = %error, + "executor host RPC failed" + ); } + settle_execution_host_call(&call.reply, response) } - /// Applies a `process.kill` aimed at the calling process itself and - /// returns the self-delivery action payload for the bridge. + /// Applies a `process.kill` aimed at the calling process itself. + /// + /// Signal delivery is exclusively kernel-owned. In particular, do not + /// return the legacy `self` action payload: the JavaScript shim would + /// synchronously deliver that payload in addition to the runtime-neutral + /// checkpoint already published by `kill_process_internal`. fn apply_self_process_kill( &mut self, vm_id: &str, process_id: &str, parsed_signal: i32, ) -> Result { - let action = self - .vms - .get(vm_id) - .and_then(|vm| vm.signal_states.get(process_id)) - .and_then(|handlers| handlers.get(&(parsed_signal as u32))) - .map(|registration| registration.action.clone()) - .unwrap_or(SignalDispositionAction::Default); - if action == SignalDispositionAction::Default - && parsed_signal != 0 - && !matches!( - canonical_signal_name(parsed_signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) - { - if let Some(vm) = self.vms.get_mut(vm_id) { - if let Some(process) = vm.active_processes.get_mut(process_id) { - apply_active_process_default_signal(&mut vm.kernel, process, parsed_signal)?; - } - } - } - Ok(json!({ - "self": true, - "action": match action { - SignalDispositionAction::Default => "default", - SignalDispositionAction::Ignore => "ignore", - SignalDispositionAction::User => "user", - }, - })) + self.kill_process_internal(vm_id, process_id, &parsed_signal.to_string())?; + Ok(Value::Null) } pub(crate) fn vm_ids_for_scope( @@ -3018,8 +2994,7 @@ where && quarantined.session_id == session_id }) { let snapshot = quarantined.reconciliation_snapshot(); - return Err(SidecarError::InvalidState(format!( - "ERR_AGENTOS_VM_QUARANTINED: vm_id={vm_id} generation={} reason={:?} active_tasks={} outstanding_capabilities={} ledger_zero={} integrity_ok={}", + return Err(SidecarError::host("ERR_AGENTOS_VM_QUARANTINED", format!("vm_id={vm_id} generation={} reason={:?} active_tasks={} outstanding_capabilities={} ledger_zero={} integrity_ok={}", quarantined.generation, quarantined.reason, snapshot.active_tasks, @@ -3174,6 +3149,59 @@ where } fn reject_error(&self, request: &RequestFrame, error: &SidecarError) -> ResponseFrame { + if let SidecarError::Host(host_error) = error { + if host_error.code == "ERR_AGENTOS_RESOURCE_LIMIT" { + let details = host_error.details.as_ref(); + let limit_name = details + .and_then(|value| value.get("limitName")) + .and_then(Value::as_str) + .map(str::to_owned); + let configured_limit = details + .and_then(|value| value.get("limit")) + .and_then(Value::as_u64); + let requested = details + .and_then(|value| value.get("observed")) + .and_then(Value::as_u64); + let configuration_path = details + .and_then(|value| value.get("configPath")) + .and_then(Value::as_str) + .map(str::to_owned) + .or_else(|| limit_name.clone()); + let vm_id = match &request.ownership { + OwnershipScope::VmOwnership(owner) => Some(owner.vm_id.clone()), + OwnershipScope::ConnectionOwnership(_) + | OwnershipScope::SessionOwnership(_) => None, + }; + let session_generation = vm_id + .as_ref() + .and_then(|vm_id| self.vms.get(vm_id)) + .map(|vm| vm.generation); + return self.respond( + request, + ResponsePayload::Rejected(RejectedResponse { + code: host_error.code.clone(), + message: host_error.message.clone(), + limit_name, + configured_limit, + current_usage: None, + requested, + unit: Some(String::from("items")), + scope: Some(if vm_id.is_some() { + String::from("vm") + } else { + String::from("process") + }), + vm_id, + session_generation, + capability_id: None, + operation: None, + configuration_path, + retryable: Some(false), + errno: Some(String::from("ENOBUFS")), + }), + ); + } + } let SidecarError::ResourceLimit(limit) = error else { return self.reject(request, error_code(error), &error.to_string()); }; @@ -3309,6 +3337,17 @@ where &mut self, response: SidecarResponseFrame, ) -> Result<(), SidecarError> { + let completed_limit = self.config.runtime.protocol.max_completed_responses; + if self.completed_sidecar_responses.len() >= completed_limit { + return Err(SidecarError::host_resource_limit( + "runtime.protocol.maxCompletedResponses", + completed_limit, + self.completed_sidecar_responses.len().saturating_add(1), + format!( + "completed sidecar response queue reached {completed_limit} retained responses; drain responses or raise runtime.protocol.maxCompletedResponses" + ), + )); + } match self.pending_sidecar_responses.accept_response(&response) { Ok(()) => {} // A response for a request that is no longer pending (its owning VM @@ -3337,29 +3376,6 @@ where .insert(response.request_id, response); self.completed_sidecar_responses_gauge .observe_depth(self.completed_sidecar_responses.len()); - let completed_limit = self.config.runtime.protocol.max_completed_responses; - while self.completed_sidecar_responses.len() > completed_limit { - match self.completed_sidecar_response_order.pop_front() { - // Only a response that was never retrieved is a real loss; an id - // already taken via take_sidecar_response leaves a stale order - // entry that removes to None and is not a dropped response. - Some(evicted) => { - if self.completed_sidecar_responses.remove(&evicted).is_some() { - tracing::warn!( - code = "WARN_AGENTOS_COMPLETED_RESPONSE_LIMIT", - queue = "completed_sidecar_responses", - evicted_request_id = evicted, - capacity = completed_limit, - configuration_path = "runtime.protocol.maxCompletedResponses", - "dropping an unretrieved completed sidecar response to stay within configured cap; raise runtime.protocol.maxCompletedResponses to retain more completions (response lost)" - ); - self.completed_sidecar_responses_gauge - .observe_depth(self.completed_sidecar_responses.len()); - } - } - None => break, - } - } Ok(()) } @@ -3430,6 +3446,29 @@ where impl Drop for NativeSidecar { fn drop(&mut self) { + fn request_shutdown(process: &mut crate::state::ActiveProcess) { + for child in process.child_processes.values_mut() { + request_shutdown(child); + } + if let Err(error) = process.execution.terminate() { + eprintln!( + "ERR_AGENTOS_PROCESS_DROP_SHUTDOWN: failed to request shutdown for kernel pid {}: {error}", + process.kernel_pid + ); + } + } + + // Execution engines are declared before `vms`, so Rust's default field + // drop order would tear the engines down while VM-owned execution + // handles are still live. Request backend shutdown and release every VM + // generation first so sessions can drain against a live engine/runtime. + for vm in self.vms.values_mut() { + for process in vm.active_processes.values_mut() { + request_shutdown(process); + } + } + self.vms.clear(); + self.quarantined_vms.clear(); if let Some(task) = self.kernel_reaper_task.take() { task.abort(); } @@ -3752,27 +3791,6 @@ fn unexpected_extension_host_response(operation: &str, payload: ResponsePayload) } } -fn shadow_host_path_for_process( - shadow_root: &Path, - process_guest_cwd: &str, - guest_path: &str, -) -> PathBuf { - let normalized_guest_path = if guest_path.starts_with('/') { - normalize_path(guest_path) - } else { - normalize_path(&format!( - "{}/{}", - process_guest_cwd.trim_end_matches('/'), - guest_path - )) - }; - if normalized_guest_path == "/" { - shadow_root.to_path_buf() - } else { - shadow_root.join(normalized_guest_path.trim_start_matches('/')) - } -} - fn sidecar_response_tracker_error(error: SidecarResponseTrackerError) -> SidecarError { SidecarError::InvalidState(format!( "invalid sidecar response correlation state: {error}" @@ -3897,12 +3915,16 @@ pub(crate) fn log_stale_process_event( B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - let _ = bridge.emit_log( + if let Err(error) = bridge.emit_log( vm_id, format!( "Ignoring stale process event during {context}: VM {vm_id} process {process_id} was already reaped" ), - ); + ) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_EMIT: failed to emit stale process diagnostic for VM {vm_id} process {process_id}: {error:?}" + ); + } } // filesystem_operation_label moved to crate::vm @@ -3981,7 +4003,10 @@ pub(crate) fn dirname(path: &str) -> String { } pub(crate) fn kernel_error(error: KernelError) -> SidecarError { - SidecarError::Kernel(error.to_string()) + SidecarError::Host(agentos_execution::backend::HostServiceError::new( + error.code(), + error.to_string(), + )) } pub(crate) fn plugin_error(error: PluginError) -> SidecarError { @@ -3989,19 +4014,88 @@ pub(crate) fn plugin_error(error: PluginError) -> SidecarError { } pub(crate) fn javascript_error(error: JavascriptExecutionError) -> SidecarError { - SidecarError::Execution(error.to_string()) + match error { + JavascriptExecutionError::EventChannelClosed => SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::Javascript, + }, + other => SidecarError::Execution(other.to_string()), + } } pub(crate) fn wasm_error(error: WasmExecutionError) -> SidecarError { - SidecarError::Execution(error.to_string()) + let message = error.to_string(); + match error { + WasmExecutionError::EventChannelClosed => SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::WebAssembly, + }, + WasmExecutionError::NativeBinaryNotSupported { .. } => { + SidecarError::host("ERR_NATIVE_BINARY_NOT_SUPPORTED", message) + } + WasmExecutionError::DeterministicFuelUnsupported { .. } => { + SidecarError::host("ENOTSUP", message) + } + _ => SidecarError::Execution(message), + } +} + +#[cfg(test)] +mod execution_error_tests { + use super::*; + use agentos_execution::wasm::NativeBinaryFormat; + + #[test] + fn native_binary_rejection_preserves_its_guest_error_code() { + let error = wasm_error(WasmExecutionError::NativeBinaryNotSupported { + path: PathBuf::from("/tmp/fake-rg"), + header: vec![0x7f, b'E', b'L', b'F'], + format: NativeBinaryFormat::Elf, + }); + + assert_eq!(error.code(), Some("ERR_NATIVE_BINARY_NOT_SUPPORTED")); + } + + #[test] + fn deterministic_fuel_rejection_preserves_enotsup() { + let error = wasm_error(WasmExecutionError::DeterministicFuelUnsupported { fuel: 42 }); + + assert_eq!(error.code(), Some("ENOTSUP")); + assert!(error.to_string().contains("deterministic WebAssembly fuel")); + } + + #[test] + fn closed_execution_channels_preserve_the_backend_kind() { + assert_eq!( + javascript_error(JavascriptExecutionError::EventChannelClosed), + SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::Javascript, + } + ); + assert_eq!( + python_error(PythonExecutionError::EventChannelClosed), + SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::Python, + } + ); + assert_eq!( + wasm_error(WasmExecutionError::EventChannelClosed), + SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::WebAssembly, + } + ); + } } pub(crate) fn python_error(error: PythonExecutionError) -> SidecarError { - SidecarError::Execution(error.to_string()) + match error { + PythonExecutionError::EventChannelClosed => SidecarError::ExecutionEventChannelClosed { + backend: ExecutionBackendKind::Python, + }, + other => SidecarError::Execution(other.to_string()), + } } pub(crate) fn vfs_error(error: VfsError) -> SidecarError { - SidecarError::Kernel(error.to_string()) + SidecarError::host(error.code(), error.message()) } /// Actionable guidance shown when guest package resolution fails because the packages live in a @@ -4133,13 +4227,9 @@ mod legacy_child_spawn_options_tests { .map(String::as_str), Some("node:path") ); - assert_eq!( - options - .internal_bootstrap_env - .get("AGENTOS_WASM_INITIAL_SIGNAL_MASK") - .map(String::as_str), - Some("[10]") - ); + assert!(!options + .internal_bootstrap_env + .contains_key("AGENTOS_WASM_INITIAL_SIGNAL_MASK")); assert!(!options .internal_bootstrap_env .contains_key("AGENTOS_NOT_ALLOWED")); diff --git a/crates/native-sidecar/src/state.rs b/crates/native-sidecar/src/state.rs index 4de9874181..30230ecd48 100644 --- a/crates/native-sidecar/src/state.rs +++ b/crates/native-sidecar/src/state.rs @@ -14,8 +14,15 @@ use agentos_bridge::{ BridgeTypes, FilesystemSnapshot, }; use agentos_execution::{ - v8_host::V8SessionHandle, JavascriptExecution, JavascriptSyncRpcRequest, PythonExecution, - PythonVfsRpcRequest, WasmExecution, + backend::{ + DescendantOutputOwnership, DescendantWaitOwnership, DirectHostReplyHandle, + ExecutionBackendKind, ExecutionEvent, ExecutionWakeHandle, HostServiceError, + }, + host::{ + BoundedUsize, BoundedVec, KernelPollInterest, SocketAddress, + SocketDomain as HostSocketDomain, SocketKind as HostSocketKind, WaitTarget, + }, + HostRpcRequest, JavascriptExecution, PythonExecution, WasmExecution, }; use agentos_kernel::fd_table::TransferredFd; use agentos_kernel::kernel::{KernelProcessHandle, KernelVm}; @@ -23,18 +30,20 @@ use agentos_kernel::mount_table::MountTable; use agentos_kernel::root_fs::RootFilesystemMode; use agentos_kernel::socket_table::SocketId; use agentos_native_sidecar_core::VmLayerStore; -use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger, SharedReservation}; +use agentos_runtime::accounting::{ + LimitError, Reservation, ResourceClass, ResourceLedger, SharedReservation, +}; +use agentos_runtime::fairness::FairWorkTurn; use agentos_runtime::RuntimeContext; use agentos_vm_config as vm_config; use agentos_vm_config::PermissionsPolicy; use rusqlite::Connection; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Value}; use socket2::Socket; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; -use std::fs::File; use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; @@ -56,6 +65,119 @@ pub(crate) type SidecarKernel = KernelVm; pub(crate) type KernelSocketReadinessRegistry = Arc; pub(crate) type HostNetTransferDescriptionRegistry = Arc>>; +pub(crate) type ManagedHostNetDescriptionRegistry = + Arc>>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DeferredGuestWaitKind { + Process { target: WaitTarget, options: u32 }, + Sleep, +} + +#[derive(Debug)] +pub(crate) struct DeferredGuestWait { + pub(crate) kind: DeferredGuestWaitKind, + pub(crate) reply: DirectHostReplyHandle, + pub(crate) deadline: Option, + pub(crate) wake_task: Option>, +} + +/// One bounded kernel-poll request parked on the executor's direct reply +/// lane. The kernel remains the readiness source of truth; the retained task +/// owns only cloneable notifier/deadline state and authorizes a later +/// zero-timeout probe on the sidecar owner thread. +#[derive(Debug)] +pub(crate) struct DeferredKernelPoll { + pub(crate) interests: BoundedVec, + pub(crate) reply: DirectHostReplyHandle, + pub(crate) deadline: Option, + pub(crate) wake_task: Option>, + /// Kernel-owned temporary mask scope for a combined ppoll. The sidecar + /// restores this before publishing a caught signal or settling the reply. + pub(crate) temporary_signal_mask_token: Option, + /// Distinguishes the combined kernel/managed-fd path from the legacy + /// kernel-only compatibility operation. + pub(crate) combined: bool, +} + +/// One bounded descriptor read parked on the executor's direct reply lane. +/// The sidecar owner thread performs every destructive read; the retained task +/// owns only cloneable readiness/deadline state and schedules a later probe. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DeferredKernelReadResponse { + DescriptorBytes, + KernelStdin, +} + +#[derive(Debug)] +pub(crate) struct DeferredKernelRead { + pub(crate) fd: u32, + pub(crate) max_bytes: BoundedUsize, + pub(crate) response: DeferredKernelReadResponse, + pub(crate) reply: DirectHostReplyHandle, + pub(crate) deadline: Instant, + pub(crate) wake_task: Option>, +} + +/// Sidecar-owned semantic state for one compatibility WASM host-network open +/// description. The kernel description id is the map key, so dup/fork and +/// SCM_RIGHTS aliases observe one transport and one option/address lifecycle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ManagedHostNetRoute { + Unbound, + TcpBound { reservation_id: String }, + UnixBound { listener_id: String }, + TcpSocket(String), + UnixSocket(String), + TcpListener(String), + UnixListener(String), + UdpSocket(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ManagedHostNetDescription { + pub(crate) domain: HostSocketDomain, + pub(crate) kind: HostSocketKind, + pub(crate) lease: Arc, + /// Per-process reactor projection for this one canonical kernel + /// description. Mutable guest semantics below are shared once per + /// description; only the opaque process-local resource id varies. + pub(crate) routes: BTreeMap, + pub(crate) bound_address: Option, + pub(crate) local_address: Option, + pub(crate) peer_address: Option, + pub(crate) receive_timeout_ms: Option, + pub(crate) no_delay: bool, + pub(crate) keep_alive: bool, +} + +impl ManagedHostNetDescription { + pub(crate) fn new( + domain: HostSocketDomain, + kind: HostSocketKind, + lease: TransferredFd, + kernel_pid: u32, + ) -> Self { + let mut routes = BTreeMap::new(); + routes.insert(kernel_pid, ManagedHostNetRoute::Unbound); + Self { + domain, + kind, + lease: Arc::new(lease), + routes, + bound_address: None, + local_address: None, + peer_address: None, + receive_timeout_ms: None, + no_delay: false, + keep_alive: false, + } + } + + pub(crate) fn route_for(&self, kernel_pid: u32) -> Option<&ManagedHostNetRoute> { + self.routes.get(&kernel_pid) + } +} /// Retains the first capability lease committed for one open socket /// description. Process-local aliases may own additional leases, but the @@ -216,8 +338,6 @@ impl VmPendingByteBudget { } } - #[cfg(test)] - #[allow(dead_code)] pub(crate) fn used(&self) -> usize { self.used.load(Ordering::Acquire) } @@ -227,6 +347,27 @@ impl VmPendingByteBudget { } } +/// Exact RAII ownership of one VM pending-budget reservation. A failed launch, +/// completed child, or process teardown all reclaim capacity through the same +/// drop path. +#[derive(Debug)] +pub(crate) struct VmPendingBudgetReservation { + budget: Arc, + amount: usize, +} + +impl VmPendingBudgetReservation { + pub(crate) fn try_new(budget: Arc, amount: usize) -> Option { + budget.try_reserve(amount).then(|| Self { budget, amount }) + } +} + +impl Drop for VmPendingBudgetReservation { + fn drop(&mut self) { + self.budget.release(self.amount); + } +} + #[derive(Debug)] pub(crate) struct HostNetTransferDescription { pub(crate) handles: Weak<()>, @@ -374,13 +515,35 @@ mod real_interval_timer_tests { /// prevents N handles from each consuming that capacity simultaneously. #[derive(Debug)] struct QueuedAsyncCompletion { - value: T, - _reservation: Reservation, + value: Option, + _count_reservation: Reservation, + _byte_reservation: Reservation, + count_gauge: Arc, + byte_gauge: Arc, + byte_depth: Arc, + retained_bytes: usize, +} + +impl Drop for QueuedAsyncCompletion { + fn drop(&mut self) { + self.count_gauge.record_dequeue(); + if self.retained_bytes != 0 { + let previous = self + .byte_depth + .fetch_sub(self.retained_bytes, Ordering::AcqRel); + self.byte_gauge + .observe_depth(previous.saturating_sub(self.retained_bytes)); + } + } } pub(crate) struct AsyncCompletionSender { inner: TokioSender>, runtime: RuntimeContext, + retained_bytes: fn(&T) -> usize, + count_gauge: Arc, + byte_gauge: Arc, + byte_depth: Arc, } impl Clone for AsyncCompletionSender { @@ -388,6 +551,10 @@ impl Clone for AsyncCompletionSender { Self { inner: self.inner.clone(), runtime: self.runtime.clone(), + retained_bytes: self.retained_bytes, + count_gauge: Arc::clone(&self.count_gauge), + byte_gauge: Arc::clone(&self.byte_gauge), + byte_depth: Arc::clone(&self.byte_depth), } } } @@ -417,81 +584,193 @@ impl fmt::Debug for AsyncCompletionReceiver { pub(crate) fn async_completion_channel( runtime: RuntimeContext, capacity: usize, + retained_bytes: fn(&T) -> usize, ) -> (AsyncCompletionSender, AsyncCompletionReceiver) { let (sender, receiver) = tokio::sync::mpsc::channel(capacity); + let resources = runtime.resources(); + let count_capacity = resources + .configured_limit(ResourceClass::AsyncCompletions) + .map_or(capacity, |limit| limit.maximum); + let byte_capacity = resources + .configured_limit(ResourceClass::AsyncCompletionBytes) + .map_or(0, |limit| limit.maximum); + let count_gauge = + queue_tracker::register_queue(TrackedLimit::AsyncCompletionCount, count_capacity); + let byte_gauge = + queue_tracker::register_queue(TrackedLimit::AsyncCompletionBytes, byte_capacity); + let byte_depth = Arc::new(AtomicUsize::new(0)); ( AsyncCompletionSender { inner: sender, runtime, + retained_bytes, + count_gauge, + byte_gauge, + byte_depth, }, AsyncCompletionReceiver { inner: receiver }, ) } impl AsyncCompletionSender { - pub(crate) async fn send(&self, value: T) -> Result<(), String> { + pub(crate) async fn send(&self, value: T) -> Result<(), HostServiceError> { let resources = Arc::clone(self.runtime.resources()); - let reservation = tokio::select! { - biased; - () = self.runtime.admission_closed() => { - return Err(String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED: VM completion admission is closed", + let retained_bytes = (self.retained_bytes)(&value); + reject_impossible_completion_size(&resources, retained_bytes)?; + let (count_reservation, byte_reservation) = loop { + if !self.runtime.admission_is_open() { + return Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED", + "VM completion admission is closed", )); } - () = self.inner.closed() => { - return Err(String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED: completion consumer disconnected", + if self.inner.is_closed() { + return Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED", + "completion consumer disconnected", )); } - reservation = resources.reserve_when_available(ResourceClass::AsyncCompletions, 1) => { - reservation.map_err(|error| error.to_string())? + match reserve_async_completion(&resources, retained_bytes) { + Ok(reservations) => break reservations, + Err(_) => tokio::select! { + biased; + () = self.runtime.admission_closed() => { + return Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED", + "VM completion admission is closed", + )); + } + () = self.inner.closed() => { + return Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED", + "completion consumer disconnected", + )); + } + () = resources.capacity_changed() => {} + }, } }; + self.count_gauge.record_enqueue(); + if retained_bytes != 0 { + let previous = self.byte_depth.fetch_add(retained_bytes, Ordering::AcqRel); + self.byte_gauge + .observe_depth(previous.saturating_add(retained_bytes)); + } let queued = QueuedAsyncCompletion { - value, - _reservation: reservation, + value: Some(value), + _count_reservation: count_reservation, + _byte_reservation: byte_reservation, + count_gauge: Arc::clone(&self.count_gauge), + byte_gauge: Arc::clone(&self.byte_gauge), + byte_depth: Arc::clone(&self.byte_depth), + retained_bytes, }; tokio::select! { biased; - () = self.runtime.admission_closed() => Err(String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED: VM completion admission closed before queue insertion", + () = self.runtime.admission_closed() => Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED", + "VM completion admission closed before queue insertion", )), - result = self.inner.send(queued) => result.map_err(|_| String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED: completion consumer disconnected", + result = self.inner.send(queued) => result.map_err(|_| async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED", + "completion consumer disconnected", )), } } - pub(crate) fn try_send(&self, value: T) -> Result<(), String> { + pub(crate) fn try_send(&self, value: T) -> Result<(), HostServiceError> { if !self.runtime.admission_is_open() { - return Err(String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED: VM completion admission is closed", + return Err(async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED", + "VM completion admission is closed", )); } - let reservation = self - .runtime - .resources() - .reserve(ResourceClass::AsyncCompletions, 1) - .map_err(|error| error.to_string())?; + let retained_bytes = (self.retained_bytes)(&value); + let resources = self.runtime.resources(); + let (count_reservation, byte_reservation) = + reserve_async_completion(resources, retained_bytes)?; + self.count_gauge.record_enqueue(); + if retained_bytes != 0 { + let previous = self.byte_depth.fetch_add(retained_bytes, Ordering::AcqRel); + self.byte_gauge + .observe_depth(previous.saturating_add(retained_bytes)); + } self.inner .try_send(QueuedAsyncCompletion { - value, - _reservation: reservation, + value: Some(value), + _count_reservation: count_reservation, + _byte_reservation: byte_reservation, + count_gauge: Arc::clone(&self.count_gauge), + byte_gauge: Arc::clone(&self.byte_gauge), + byte_depth: Arc::clone(&self.byte_depth), + retained_bytes, }) .map_err(|error| match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_LANE_LIMIT: completion lane is full; raise limits.reactor.maxAsyncCompletions", + tokio::sync::mpsc::error::TrySendError::Full(_) => async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_LANE_LIMIT", + "completion lane is full; raise limits.reactor.maxAsyncCompletions", ), - tokio::sync::mpsc::error::TrySendError::Closed(_) => String::from( - "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED: completion consumer disconnected", + tokio::sync::mpsc::error::TrySendError::Closed(_) => async_completion_error( + "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED", + "completion consumer disconnected", ), }) } } +fn reserve_async_completion( + resources: &ResourceLedger, + retained_bytes: usize, +) -> Result<(Reservation, Reservation), HostServiceError> { + let count = resources + .reserve(ResourceClass::AsyncCompletions, 1) + .map_err(async_completion_limit_error)?; + let bytes = resources + .reserve(ResourceClass::AsyncCompletionBytes, retained_bytes) + .map_err(async_completion_limit_error)?; + Ok((count, bytes)) +} + +fn reject_impossible_completion_size( + resources: &ResourceLedger, + retained_bytes: usize, +) -> Result<(), HostServiceError> { + if let Some(limit) = resources.configured_limit(ResourceClass::AsyncCompletionBytes) { + if retained_bytes > limit.maximum { + return Err(HostServiceError::limit( + "ERR_AGENTOS_RESOURCE_LIMIT", + "limits.reactor.maxAsyncCompletionBytes", + limit.maximum as u64, + retained_bytes as u64, + )); + } + } + Ok(()) +} + +fn async_completion_limit_error(error: LimitError) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_RESOURCE_LIMIT", error.to_string()).with_details(json!({ + "scope": error.scope, + "resource": error.resource.name(), + "used": error.used, + "requested": error.requested, + "limit": error.limit, + "limitName": error.config_path, + })) +} + +fn async_completion_error(code: &'static str, message: &'static str) -> HostServiceError { + HostServiceError::new(code, message) +} + impl AsyncCompletionReceiver { pub(crate) fn try_recv(&mut self) -> Result { - self.inner.try_recv().map(|queued| queued.value) + self.inner.try_recv().map(|mut queued| { + queued + .value + .take() + .expect("queued completion contains value") + }) } } @@ -507,7 +786,6 @@ pub(crate) const WASM_COMMAND: &str = "wasm"; // permissions and mount-confinement on every op, identical to what the JS/WASM // runtimes and `vm.readFile()` see), so the VFS-RPC root is `/`, not a single // workspace dir. -pub(crate) const PYTHON_VFS_RPC_GUEST_ROOT: &str = "/"; pub(crate) const EXECUTION_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT"; pub(crate) const WASM_STDIO_SYNC_RPC_ENV: &str = "AGENTOS_WASI_STDIO_SYNC_RPC"; pub(crate) const WASM_EXEC_COMMIT_RPC_ENV: &str = "AGENTOS_WASM_EXEC_COMMIT_RPC"; @@ -526,10 +804,9 @@ pub(crate) const VM_LISTEN_PORT_MIN_METADATA_KEY: &str = "network.listen.port_mi #[allow(dead_code)] pub(crate) const VM_LISTEN_PORT_MAX_METADATA_KEY: &str = "network.listen.port_max"; pub(crate) const VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY: &str = "network.listen.allow_privileged"; -pub(crate) const DEFAULT_JAVASCRIPT_NET_BACKLOG: u32 = 511; +pub(crate) const DEFAULT_NET_BACKLOG: u32 = 511; pub(crate) const LOOPBACK_EXEMPT_PORTS_ENV: &str = "AGENTOS_LOOPBACK_EXEMPT_PORTS"; pub(crate) const BINDING_DRIVER_NAME: &str = "secure-exec-host-callbacks"; -pub(crate) const MAPPED_HOST_FD_START: u32 = 1_000_000_000; // --------------------------------------------------------------------------- // Public API types @@ -561,6 +838,7 @@ impl Default for NativeSidecarConfig { #[derive(Debug, Clone, PartialEq, Eq)] pub enum SidecarError { ResourceLimit(agentos_runtime::accounting::LimitError), + Host(agentos_execution::backend::HostServiceError), InvalidState(String), ProtocolVersionMismatch(String), BridgeVersionMismatch(String), @@ -571,14 +849,52 @@ pub enum SidecarError { Kernel(String), Plugin(String), Execution(String), + ExecutionEventChannelClosed { backend: ExecutionBackendKind }, Bridge(String), Io(String), } +impl SidecarError { + pub(crate) fn host(code: impl Into, message: impl Into) -> Self { + Self::Host(agentos_execution::backend::HostServiceError::new( + code, message, + )) + } + + pub(crate) fn code(&self) -> Option<&str> { + match self { + Self::Host(error) => Some(error.code.as_str()), + Self::ResourceLimit(_) => Some("ERR_AGENTOS_RESOURCE_LIMIT"), + _ => None, + } + } + + pub(crate) fn host_resource_limit( + limit_name: &'static str, + limit: usize, + observed: usize, + message: impl Into, + ) -> Self { + Self::Host( + agentos_execution::backend::HostServiceError::new( + "ERR_AGENTOS_RESOURCE_LIMIT", + message, + ) + .with_details(serde_json::json!({ + "limitName": limit_name, + "limit": limit, + "observed": observed, + "configPath": limit_name, + })), + ) + } +} + impl fmt::Display for SidecarError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ResourceLimit(error) => error.fmt(f), + Self::Host(error) => error.fmt(f), Self::InvalidState(message) | Self::ProtocolVersionMismatch(message) | Self::BridgeVersionMismatch(message) @@ -591,12 +907,30 @@ impl fmt::Display for SidecarError { | Self::Execution(message) | Self::Bridge(message) | Self::Io(message) => f.write_str(message), + Self::ExecutionEventChannelClosed { backend } => { + write!(f, "{backend:?} execution event channel closed unexpectedly") + } } } } impl Error for SidecarError {} +impl From for SidecarError { + fn from(error: agentos_execution::backend::HostServiceError) -> Self { + Self::Host(error) + } +} + +impl From for SidecarError { + fn from(error: agentos_native_sidecar_core::SidecarCoreError) -> Self { + match error.code() { + Some(code) => Self::host(code, error.message()), + None => Self::InvalidState(error.message().to_owned()), + } + } +} + /// Format a resource-limit failure for an untrusted guest. VM-local occupancy /// is safe to expose to that VM; process occupancy includes other VMs and must /// not become a cross-tenant resource oracle. @@ -753,6 +1087,8 @@ pub(crate) struct SharedBridge { pub(crate) permissions: Arc>>, #[cfg(test)] pub(crate) set_vm_permissions_outcomes: Arc>>>, + #[cfg(test)] + pub(crate) emit_lifecycle_outcomes: Arc>>>, } impl Clone for SharedBridge { @@ -762,6 +1098,8 @@ impl Clone for SharedBridge { permissions: Arc::clone(&self.permissions), #[cfg(test)] set_vm_permissions_outcomes: Arc::clone(&self.set_vm_permissions_outcomes), + #[cfg(test)] + emit_lifecycle_outcomes: Arc::clone(&self.emit_lifecycle_outcomes), } } } @@ -837,6 +1175,8 @@ pub(crate) struct VmState { pub(crate) limits: crate::limits::VmLimits, pub(crate) pending_stdin_bytes_budget: Arc, pub(crate) pending_event_bytes_budget: Arc, + pub(crate) pending_child_sync_count_budget: Arc, + pub(crate) pending_child_sync_bytes_budget: Arc, /// Child of the one process ledger owned by RuntimeContext. pub(crate) resources: Arc, /// VM-scoped admission view over the process's single Tokio runtime and @@ -853,10 +1193,17 @@ pub(crate) struct VmState { pub(crate) requested_runtime: GuestRuntimeKind, pub(crate) root_filesystem_mode: RootFilesystemMode, pub(crate) guest_cwd: String, - pub(crate) cwd: PathBuf, + /// Private host directory for executor launch assets and host-backed Unix + /// socket implementation details. It is never a guest filesystem view; + /// mutable guest state lives only in `kernel`. + pub(crate) runtime_scratch_root: PathBuf, pub(crate) host_cwd: PathBuf, pub(crate) kernel: SidecarKernel, pub(crate) kernel_socket_readiness: KernelSocketReadinessRegistry, + /// Canonical semantic state for sidecar-backed socket descriptions. Kernel + /// description ids are VM-global, so fork, dup, SCM_RIGHTS, and spawn all + /// resolve the exact same mutable route/options/address state. + pub(crate) managed_host_net_descriptions: ManagedHostNetDescriptionRegistry, /// Sidecar-only host-network descriptions currently retained by an opaque /// SCM_RIGHTS transfer. Weak entries make queue discard/receive lifecycle /// automatic while allowing VM-wide limit accounting to see descriptions @@ -865,7 +1212,6 @@ pub(crate) struct VmState { pub(crate) loaded_snapshot: Option, pub(crate) configuration: VmConfiguration, pub(crate) layers: VmLayerStore, - pub(crate) command_guest_paths: BTreeMap, pub(crate) provided_commands: BTreeMap>, pub(crate) command_permissions: BTreeMap, pub(crate) bindings: BTreeMap, @@ -882,7 +1228,6 @@ pub(crate) struct VmState { /// child ID from monopolizing every coalesced wake. pub(crate) attached_child_event_cursor: usize, pub(crate) detached_child_event_cursor: usize, - pub(crate) signal_states: BTreeMap>, /// Legacy staging root slot retained for same-version internal state shape. /// The current `/opt/agentos` projection mounts package tars and synthetic /// symlink leaves directly, so this remains `None`. @@ -892,15 +1237,6 @@ pub(crate) struct VmState { /// packages ship no `agentos-package.json`, so agent enumeration and /// resolution read this instead of the guest filesystem. pub(crate) projected_agent_launch: BTreeMap, - /// Guest paths that were present in the VM shadow root during the last - /// shadow->kernel sync walk. The next walk diffs the current shadow tree - /// against this set so guest deletions performed directly on the shadow - /// (host-side runtimes, WASI passthrough writes) propagate into the kernel - /// VFS instead of being resurrected by the otherwise additive sync. - /// Memory is bounded by the shadow tree itself, which is capped by the - /// kernel filesystem inode/byte resource limits that bound what the walk - /// can materialize. - pub(crate) shadow_sync_inventory: BTreeMap, pub(crate) unix_address_registry: GuestUnixAddressRegistry, pub(crate) unix_socket_host_dir: PathBuf, } @@ -995,37 +1331,6 @@ pub(crate) struct ExitedProcessSnapshot { pub(crate) process: crate::protocol::ProcessSnapshotEntry, } -/// Filesystem object kind captured during a shadow-root inventory walk. -/// -/// Tracking the kind as well as the path is required for Linux replacement -/// semantics: a regular file replacing a symlink (or a directory replacing a -/// file) must replace the directory entry itself, never follow the stale -/// object that happened to occupy the same pathname in the kernel VFS. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ShadowNodeType { - Directory, - File, - Symlink, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct ShadowSyncInventoryEntry { - pub(crate) node_type: ShadowNodeType, - /// The previous kernel entry could not be removed. Keeping the tombstone - /// makes the next walk retry instead of permanently forgetting a failed - /// reconciliation. - pub(crate) deletion_pending: bool, -} - -impl ShadowSyncInventoryEntry { - pub(crate) fn present(node_type: ShadowNodeType) -> Self { - Self { - node_type, - deletion_pending: false, - } - } -} - // --------------------------------------------------------------------------- // DNS configuration // --------------------------------------------------------------------------- @@ -1037,7 +1342,7 @@ pub(crate) struct VmDnsConfig { } #[derive(Debug, Clone)] -pub(crate) struct JavascriptSocketPathContext { +pub(crate) struct SocketPathContext { pub(crate) sandbox_root: PathBuf, pub(crate) unix_abstract_namespace: [u8; 32], pub(crate) unix_socket_host_dir: PathBuf, @@ -1046,13 +1351,12 @@ pub(crate) struct JavascriptSocketPathContext { pub(crate) mounts: Vec, pub(crate) listen_policy: VmListenPolicy, pub(crate) loopback_exempt_ports: BTreeSet, - pub(crate) tcp_loopback_guest_to_host_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>, - pub(crate) http_loopback_targets: - BTreeMap<(JavascriptSocketFamily, u16), JavascriptHttpLoopbackTarget>, - pub(crate) udp_loopback_guest_to_host_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>, - pub(crate) udp_loopback_host_to_guest_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>, - pub(crate) used_tcp_guest_ports: BTreeMap>, - pub(crate) used_udp_guest_ports: BTreeMap>, + pub(crate) tcp_loopback_guest_to_host_ports: BTreeMap<(SocketFamily, u16), u16>, + pub(crate) http_loopback_targets: BTreeMap<(SocketFamily, u16), HttpLoopbackTarget>, + pub(crate) udp_loopback_guest_to_host_ports: BTreeMap<(SocketFamily, u16), u16>, + pub(crate) udp_loopback_host_to_guest_ports: BTreeMap<(SocketFamily, u16), u16>, + pub(crate) used_tcp_guest_ports: BTreeMap>, + pub(crate) used_udp_guest_ports: BTreeMap>, } #[derive(Debug, Clone, Copy, Default)] @@ -1076,6 +1380,7 @@ pub(crate) struct GuestUnixAddressRegistryEntry { pub(crate) generation: u64, pub(crate) active_bindings: usize, pub(crate) queued_by_target: BTreeMap, + pub(crate) pending_connection_limit: usize, pub(crate) pending_connections: VecDeque>, } @@ -1083,18 +1388,18 @@ pub(crate) type GuestUnixAddressRegistry = Arc>>; #[derive(Debug, Clone)] -pub(crate) struct JavascriptHttpLoopbackTarget { +pub(crate) struct HttpLoopbackTarget { pub(crate) process_id: String, pub(crate) server_id: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum JavascriptSocketFamily { +pub(crate) enum SocketFamily { Ipv4, Ipv6, } -impl JavascriptSocketFamily { +impl SocketFamily { pub(crate) fn from_ip(ip: IpAddr) -> Self { match ip { IpAddr::V4(_) => Self::Ipv4, @@ -1103,11 +1408,11 @@ impl JavascriptSocketFamily { } } -impl From for JavascriptSocketFamily { - fn from(value: JavascriptUdpFamily) -> Self { +impl From for SocketFamily { + fn from(value: UdpFamily) -> Self { match value { - JavascriptUdpFamily::Ipv4 => Self::Ipv4, - JavascriptUdpFamily::Ipv6 => Self::Ipv6, + UdpFamily::Ipv4 => Self::Ipv4, + UdpFamily::Ipv6 => Self::Ipv6, } } } @@ -1151,6 +1456,15 @@ pub(crate) struct PendingKernelStdin { pub(crate) close_requested: bool, } +/// One execute-authorized, immutable image retained while a WASM executor +/// copies it through bounded bridge replies. This is sidecar-private state: +/// it consumes no guest fd and cannot be observed through `/proc/self/fd`. +pub(crate) struct ActiveExecutableImage { + pub(crate) handle: u64, + pub(crate) bytes: Vec, + pub(crate) _retained_bytes: Reservation, +} + impl PendingKernelStdin { const CHUNK_BYTES: usize = 64 * 1024; @@ -1183,6 +1497,10 @@ impl PendingKernelStdin { pub(crate) struct ActiveProcess { pub(crate) kernel_pid: u32, pub(crate) kernel_handle: KernelProcessHandle, + /// Generation/PID-bound kernel-to-runtime controls. The endpoint producer + /// is owned by the kernel process table; only this execution owns the + /// receiver. + pub(crate) runtime_control: agentos_kernel::process_runtime::RuntimeControlReceiver, /// VM-scoped admission/accounting view over the process-owned runtime. /// Child processes inherit this exact generation-bound context; they must /// never rediscover the process context through a global lookup. @@ -1191,8 +1509,9 @@ pub(crate) struct ActiveProcess { /// their bounds from this snapshot instead of process-wide constants. pub(crate) limits: crate::limits::VmLimits, pub(crate) kernel_stdin_writer_fd: Option, - /// Whether fd 0 was installed by POSIX spawn actions and must be read - /// directly from the kernel instead of the JavaScript local-stdin bridge. + /// Whether POSIX spawn actions installed fd 0 instead of allocating a + /// sidecar-owned host-input pipe. All managed executors still read the + /// resulting descriptor through the kernel. pub(crate) direct_posix_stdin: bool, /// Kernel descriptor backing guest fd 0. POSIX spawn actions can retain /// the transported description at a sidecar-private descriptor number. @@ -1208,21 +1527,29 @@ pub(crate) struct ActiveProcess { /// host (instead of the raw guest stdout/stderr execution events). pub(crate) tty_master_fd: Option, pub(crate) runtime: GuestRuntimeKind, + /// Executor-selected transport facts consumed by common POSIX paths. + /// This is intentionally independent of `runtime`: compatibility WASM and + /// Wasmtime share a guest kind but may use different host-call transports. + pub(crate) adapter_policy: ExecutionAdapterPolicy, pub(crate) detached: bool, pub(crate) execution: ActiveExecution, pub(crate) guest_cwd: String, pub(crate) env: BTreeMap, pub(crate) host_cwd: PathBuf, - /// VM-owned host root used only for kernel/runtime shadow reconciliation. - /// This must not be inferred from a mapped host cwd or exposed through envp. - pub(crate) shadow_root: Option, - pub(crate) host_write_dirty: bool, - pub(crate) mapped_host_fds: BTreeMap, - pub(crate) next_mapped_host_fd: u32, + pub(crate) executable_image: Option, + pub(crate) next_executable_image_handle: u64, /// Wakes the shared process-event pump after durable local events are /// queued. `Notify` coalesces repeated wakes while the deque preserves all /// event data. pub(crate) process_event_notify: Arc, + /// Mutable wake destination retained by the runtime-neutral event queue. + /// Joining the VM-wide broker updates this cell without replacing the + /// generation-bound submission capability. + pub(crate) common_event_notify: Arc>>, + /// Runtime-neutral host services and their bounded common-event receiver. + /// Neither side retains an executor-specific object. + pub(crate) host_capabilities: agentos_execution::host::ProcessHostCapabilitySet, + pub(crate) common_execution_events: agentos_execution::backend::ExecutionEventReceiver, /// Durable event backlog bound inherited from /// `runtime.protocol.maxProcessEvents` when this process is admitted. pub(crate) process_event_capacity: usize, @@ -1237,19 +1564,20 @@ pub(crate) struct ActiveProcess { pub(crate) pending_execution_event_count_gauge: Arc, pub(crate) pending_execution_event_bytes_gauge: Arc, pub(crate) vm_pending_event_bytes_budget: Arc, - pub(crate) pending_javascript_net_connects: - BTreeMap>>, - pub(crate) pending_self_signal_exit: Option, + pub(crate) pending_net_connects: BTreeMap>>, + /// Deferred native connects complete off the owner thread; this binds the + /// direct call id back to the canonical description that receives the + /// resulting transport. + pub(crate) pending_managed_host_net_connects: BTreeMap, + /// Synthetic terminal event reserved outside the ordinary bounded output + /// queue so kernel termination cannot be dropped under backpressure. + pub(crate) pending_runtime_exit: Option, /// Actual terminating signal observed from the runtime process (or the /// signal used for a shared-runtime synthetic exit). This is distinct from /// a requested kill signal: handlers may catch one signal and later exit /// for another reason. pub(crate) exit_signal: Option, pub(crate) exit_core_dumped: bool, - /// Pending standard signals use a set, matching Linux's coalescing rule: - /// multiple instances of the same standard signal occupy one pending bit. - pub(crate) pending_wasm_signals: BTreeSet, - pub(crate) pending_wasm_signals_gauge: Arc, pub(crate) real_interval_timer: ActiveRealIntervalTimer, pub(crate) child_processes: BTreeMap, pub(crate) next_child_process_id: usize, @@ -1257,6 +1585,11 @@ pub(crate) struct ActiveProcess { /// Child runtime events advance these records from the shared process pump; /// no sidecar or Tokio worker blocks waiting for child output. pub(crate) pending_child_process_sync: BTreeMap, + /// The Node-compatible `child_process` bridge owns this child's stdout and + /// stderr delivery. Kernel fd 1/2 still carry the Linux process image, but + /// their inherited descriptions must not bypass JavaScript pipes, + /// spawnSync capture, or stdout-framed fork IPC. + pub(crate) child_process_bridge_owns_output: bool, pub(crate) http_servers: BTreeMap, pub(crate) pending_http_requests: BTreeMap<(u64, u64), PendingHttpRequest>, pub(crate) http2: ActiveHttp2State, @@ -1269,7 +1602,7 @@ pub(crate) struct ActiveProcess { pub(crate) next_tcp_listener_id: usize, pub(crate) tcp_sockets: BTreeMap, pub(crate) next_tcp_socket_id: usize, - pub(crate) tcp_port_reservations: BTreeMap, + pub(crate) tcp_port_reservations: BTreeMap, pub(crate) next_tcp_port_reservation_id: usize, pub(crate) unix_listeners: BTreeMap, pub(crate) next_unix_listener_id: usize, @@ -1277,11 +1610,6 @@ pub(crate) struct ActiveProcess { pub(crate) next_unix_socket_id: usize, pub(crate) udp_sockets: BTreeMap, pub(crate) next_udp_socket_id: usize, - /// Adapter handles returned to the guest Python `socket` bridge. These - /// reference the same sidecar-owned capabilities in `tcp_sockets` and - /// `udp_sockets`; Python does not own a parallel descriptor or I/O task. - pub(crate) python_sockets: BTreeMap, - pub(crate) next_python_socket_id: u64, pub(crate) hash_sessions: BTreeMap, pub(crate) next_hash_session_id: u64, pub(crate) cipher_sessions: BTreeMap, @@ -1313,8 +1641,25 @@ pub(crate) struct ActiveProcess { /// dispatch loop). At most one per process: the guest thread is blocked in /// this RPC, so it cannot issue another. The optional absolute deadline is /// `None` for a readiness-only wait with no recurring timeout. - pub(crate) deferred_kernel_wait_rpc: Option<(JavascriptSyncRpcRequest, Option)>, + pub(crate) deferred_kernel_wait_rpc: Option<(ExecutionHostCall, Option)>, + /// Preserves the one-shot 80% operation-deadline warning across readiness + /// wakes and re-parks of the same root-process `fd_write` RPC. + pub(crate) deferred_kernel_wait_deadline_warned: bool, pub(crate) deferred_child_write_timer: Option>, + /// One durable process wait or sleep owned by the sidecar. The guest is + /// synchronously parked on its direct reply lane, so one slot is the exact + /// per-process admission bound. + pub(crate) deferred_guest_wait: Option, + pub(crate) deferred_guest_wait_interrupted: bool, + /// Adapter handshake: a caught signal has been published but the guest + /// has not yet drained the checkpoint queue through `take_signal`. + pub(crate) guest_signal_checkpoint_pending: bool, + /// At most one typed kernel poll may be pending because the guest thread + /// is synchronously parked on the corresponding direct reply. + pub(crate) deferred_kernel_poll: Option, + /// At most one typed descriptor read may be pending because the guest + /// thread is synchronously parked on the corresponding direct reply. + pub(crate) deferred_kernel_read: Option, /// Per-process module resolution cache, persisted across module sync-RPCs /// (`__resolve_module` / `__load_file` / `__module_format` / /// `__batch_resolve_modules`) for the lifetime of this process so cold-start @@ -1335,11 +1680,13 @@ pub(crate) struct PendingChildProcessSync { pub(crate) timed_out: bool, pub(crate) max_buffer_exceeded: bool, pub(crate) completion: PendingChildProcessSyncCompletion, + pub(crate) _count_reservation: VmPendingBudgetReservation, + pub(crate) _bytes_reservation: VmPendingBudgetReservation, } pub(crate) enum PendingChildProcessSyncCompletion { Javascript(tokio::sync::oneshot::Sender>), - Python { request_id: u64 }, + Direct(DirectHostReplyHandle), } #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] @@ -1356,12 +1703,6 @@ pub(crate) enum NativeCapabilityKey { UdpSocket(String), } -pub(crate) struct ActiveMappedHostFd { - pub(crate) file: File, - pub(crate) path: PathBuf, - pub(crate) guest_path: Option, -} - pub(crate) struct ActiveCipherSession { pub(crate) context: crate::crypto_cipher::StreamCipherSession, } @@ -1429,7 +1770,7 @@ pub(crate) struct Http2SharedState { pub(crate) next_stream_id: u64, pub(crate) ready: Arc, pub(crate) event_capacity_notify: Arc, - pub(crate) event_session: Option, + pub(crate) event_session: Option, pub(crate) servers: BTreeMap, pub(crate) sessions: BTreeMap, pub(crate) streams: BTreeMap, @@ -1468,7 +1809,7 @@ pub(crate) struct ActiveHttp2Server { pub(crate) actual_local_addr: SocketAddr, pub(crate) guest_local_addr: SocketAddr, pub(crate) secure: bool, - pub(crate) tls: Option, + pub(crate) tls: Option, pub(crate) closed: Arc, pub(crate) close_notify: Arc, } @@ -1580,6 +1921,7 @@ impl Http2ResponseSender { .send(result.map_err(|message| DeferredRpcError { code: String::from("ERR_AGENTOS_HTTP2_COMMAND"), message, + details: None, })) .is_err() { @@ -1650,7 +1992,7 @@ pub(crate) enum Http2SessionCommand { // --------------------------------------------------------------------------- #[derive(Debug)] -pub(crate) enum JavascriptTcpListenerEvent { +pub(crate) enum TcpListenerEvent { Connection(PendingTcpSocket), Error { code: Option, @@ -1667,7 +2009,7 @@ pub(crate) struct PendingTcpSocket { } #[derive(Debug)] -pub(crate) enum JavascriptTcpSocketEvent { +pub(crate) enum TcpSocketEvent { Data { bytes: Vec, reservation: agentos_runtime::accounting::SharedReservation, @@ -1685,16 +2027,50 @@ pub(crate) enum JavascriptTcpSocketEvent { }, } +pub(crate) fn tcp_socket_event_retained_bytes(event: &TcpSocketEvent) -> usize { + match event { + TcpSocketEvent::Data { bytes, .. } => bytes.len(), + TcpSocketEvent::Error { code, message } => code + .as_ref() + .map_or(0, String::len) + .saturating_add(message.len()), + TcpSocketEvent::End | TcpSocketEvent::Close { .. } => 0, + } +} + #[derive(Clone, Debug)] -pub(crate) struct JavascriptSocketEventPusher { - pub(crate) session: V8SessionHandle, +pub(crate) struct SocketEventPusher { + pub(crate) session: ExecutionWakeHandle, pub(crate) capability_id: agentos_runtime::capability::CapabilityId, pub(crate) capability_generation: agentos_runtime::capability::CapabilityGeneration, + live: Arc, + /// Coalesced sidecar owner wake. Readiness payload remains in the socket + /// description; this only causes a parked combined poll to re-probe it. + owner_notify: Arc, +} + +impl SocketEventPusher { + pub(crate) fn is_live(&self) -> bool { + self.live.load(Ordering::Acquire) + } + + pub(crate) fn publish_readiness( + &self, + flags: agentos_runtime::readiness::ReadyFlags, + ) -> Result { + if !self.is_live() { + return Ok(false); + } + self.owner_notify.notify_one(); + self.session + .publish_readiness(self.capability_id, self.capability_generation, flags)?; + Ok(true) + } } #[derive(Debug)] -struct JavascriptSocketReadinessSubscriber { - target: JavascriptSocketEventPusher, +struct SocketReadinessSubscriber { + target: SocketEventPusher, application_read_interest: bool, } @@ -1703,7 +2079,7 @@ struct JavascriptSocketReadinessSubscriber { /// registry only coalesces level hints to each VM capability that refers to it. #[derive(Debug)] pub(crate) struct SocketReadinessSubscribers { - subscribers: Mutex>, + subscribers: Mutex>, maximum: usize, } @@ -1723,13 +2099,14 @@ impl SocketReadinessSubscribers { fn register( &self, previous: Option<(u64, u64)>, - target: JavascriptSocketEventPusher, + target: SocketEventPusher, ) -> Result { let identity = (target.capability_id, target.capability_generation); let mut subscribers = self.subscribers.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_READY_STATE_POISONED: socket readiness subscriber lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_READY_STATE_POISONED", + String::from("socket readiness subscriber lock poisoned"), + ) })?; let preserved_interest = subscribers .get(&identity) @@ -1737,22 +2114,25 @@ impl SocketReadinessSubscribers { .unwrap_or(false); if previous != Some(identity) { if let Some(previous) = previous { - subscribers.remove(&previous); + if let Some(previous) = subscribers.remove(&previous) { + previous.target.live.store(false, Ordering::Release); + } } if !subscribers.contains_key(&identity) && subscribers.len() >= self.maximum { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_SOCKET_READINESS_SUBSCRIBER_LIMIT: socket description readiness subscribers exceeded {}; raise limits.reactor.maxCapabilities", + return Err(SidecarError::host("ERR_AGENTOS_SOCKET_READINESS_SUBSCRIBER_LIMIT", format!("socket description readiness subscribers exceeded {}; raise limits.reactor.maxCapabilities", self.maximum ))); } } - subscribers.insert( + if let Some(previous) = subscribers.insert( identity, - JavascriptSocketReadinessSubscriber { + SocketReadinessSubscriber { target, application_read_interest: preserved_interest, }, - ); + ) { + previous.target.live.store(false, Ordering::Release); + } Ok(subscribers .values() .any(|subscriber| subscriber.application_read_interest)) @@ -1762,7 +2142,9 @@ impl SocketReadinessSubscribers { self.subscribers .lock() .map(|mut subscribers| { - subscribers.remove(&identity); + if let Some(subscriber) = subscribers.remove(&identity) { + subscriber.target.live.store(false, Ordering::Release); + } subscribers .values() .any(|subscriber| subscriber.application_read_interest) @@ -1782,9 +2164,10 @@ impl SocketReadinessSubscribers { ) -> Result { let target = { let subscribers = self.subscribers.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_READY_STATE_POISONED: socket readiness subscriber lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_READY_STATE_POISONED", + String::from("socket readiness subscriber lock poisoned"), + ) })?; subscribers .get(&identity) @@ -1793,6 +2176,9 @@ impl SocketReadinessSubscribers { let Some(target) = target else { return Ok(false); }; + if !target.is_live() { + return Ok(false); + } target .session .set_application_read_interest( @@ -1810,9 +2196,10 @@ impl SocketReadinessSubscribers { enabled: bool, ) -> Result { let mut subscribers = self.subscribers.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_READY_STATE_POISONED: socket readiness subscriber lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_READY_STATE_POISONED", + String::from("socket readiness subscriber lock poisoned"), + ) })?; if let Some(subscriber) = subscribers.get_mut(&identity) { subscriber.application_read_interest = enabled; @@ -1822,7 +2209,7 @@ impl SocketReadinessSubscribers { .any(|subscriber| subscriber.application_read_interest)) } - pub(crate) fn targets(&self) -> Vec { + pub(crate) fn targets(&self) -> Vec { self.subscribers .lock() .map(|subscribers| { @@ -1846,11 +2233,17 @@ impl SocketReadinessSubscribers { #[derive(Debug)] pub(crate) struct SocketReadinessRegistration { subscribers: Arc, - identity: Mutex>, + registration: Mutex>, aggregate_interest: Option>, interest_notify: Option>, } +#[derive(Debug)] +struct SocketReadinessRegistrationState { + identity: (u64, u64), + live: Arc, +} + impl SocketReadinessRegistration { pub(crate) fn new( subscribers: Arc, @@ -1859,7 +2252,7 @@ impl SocketReadinessRegistration { ) -> Self { Self { subscribers, - identity: Mutex::new(None), + registration: Mutex::new(None), aggregate_interest, interest_notify, } @@ -1867,29 +2260,33 @@ impl SocketReadinessRegistration { pub(crate) fn register( &self, - session: Option, + session: Option, identity: Option<(u64, u64)>, + owner_notify: Arc, replay_flags: agentos_runtime::readiness::ReadyFlags, ) { let (Some(session), Some((capability_id, capability_generation))) = (session, identity) else { return; }; - let target = JavascriptSocketEventPusher { + let live = Arc::new(AtomicBool::new(true)); + let target = SocketEventPusher { session, capability_id, capability_generation, + live: Arc::clone(&live), + owner_notify, }; - let previous = self - .identity - .lock() - .map(|identity| *identity) - .unwrap_or_else(|_| { - eprintln!( - "ERR_AGENTOS_READY_STATE_POISONED: socket readiness registration lock poisoned" - ); - None - }); + let mut registration = self.registration.lock().unwrap_or_else(|error| { + eprintln!( + "ERR_AGENTOS_READY_STATE_POISONED: socket readiness registration lock poisoned" + ); + error.into_inner() + }); + let previous = registration.take().map(|previous| { + previous.live.store(false, Ordering::Release); + previous.identity + }); let aggregate = match self.subscribers.register(previous, target.clone()) { Ok(aggregate) => aggregate, Err(error) => { @@ -1897,18 +2294,16 @@ impl SocketReadinessRegistration { return; } }; - if let Ok(mut registered) = self.identity.lock() { - *registered = Some((capability_id, capability_generation)); - } + *registration = Some(SocketReadinessRegistrationState { + identity: (capability_id, capability_generation), + live, + }); + drop(registration); self.update_aggregate_interest(aggregate); // Readiness is level state. Replaying one coalesced hint after // registration closes the race where data arrived before this alias // was added; the subsequent bounded poll validates the actual level. - if let Err(error) = - target - .session - .publish_readiness(capability_id, capability_generation, replay_flags) - { + if let Err(error) = target.publish_readiness(replay_flags) { eprintln!( "ERR_AGENTOS_NET_SOCKET_WAKE: capability={capability_id} generation={capability_generation} registration replay: {error}" ); @@ -1920,15 +2315,16 @@ impl SocketReadinessRegistration { enabled: bool, ) -> Result { let identity = { - let identity = self.identity.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_READY_STATE_POISONED: socket readiness registration lock poisoned", - )) + let registration = self.registration.lock().map_err(|_| { + SidecarError::host( + "ERR_AGENTOS_READY_STATE_POISONED", + String::from("socket readiness registration lock poisoned"), + ) })?; - let Some(identity) = *identity else { + let Some(registration) = registration.as_ref() else { return Ok(false); }; - identity + registration.identity }; let aggregate = self .subscribers @@ -1945,22 +2341,29 @@ impl SocketReadinessRegistration { notify.notify_waiters(); } } -} -impl Drop for SocketReadinessRegistration { - fn drop(&mut self) { - let identity = self - .identity - .get_mut() + pub(crate) fn retire(&self) { + let registration = self + .registration + .lock() .unwrap_or_else(|error| error.into_inner()) .take(); - if let Some(identity) = identity { - let aggregate = self.subscribers.unregister(identity); + if let Some(registration) = registration { + // Retire before removing the registry entry. Any publisher that + // already cloned this target will observe the same guard. + registration.live.store(false, Ordering::Release); + let aggregate = self.subscribers.unregister(registration.identity); self.update_aggregate_interest(aggregate); } } } +impl Drop for SocketReadinessRegistration { + fn drop(&mut self) { + self.retire(); + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum KernelSocketReadinessEvent { Data, @@ -1970,12 +2373,13 @@ pub(crate) enum KernelSocketReadinessEvent { #[derive(Clone, Debug)] pub(crate) struct KernelSocketReadinessTarget { - pub(crate) session: Option, + pub(crate) session: Option, pub(crate) notify: Option>, pub(crate) capability_id: agentos_runtime::capability::CapabilityId, pub(crate) capability_generation: agentos_runtime::capability::CapabilityGeneration, pub(crate) target_id: String, pub(crate) event: KernelSocketReadinessEvent, + pub(crate) live: Arc, } type KernelSocketReadinessIdentity = (u64, u64); @@ -2003,9 +2407,10 @@ impl KernelSocketReadinessRegistryState { ) -> Result<(), SidecarError> { let identity = (target.capability_id, target.capability_generation); let mut targets = self.targets.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_KERNEL_READINESS_REGISTRY_POISONED: readiness registry lock poisoned", - )) + SidecarError::host( + "ERR_AGENTOS_KERNEL_READINESS_REGISTRY_POISONED", + String::from("readiness registry lock poisoned"), + ) })?; let already_registered = targets .get(&socket_id) @@ -2013,16 +2418,18 @@ impl KernelSocketReadinessRegistryState { if !already_registered { let registered = targets.values().map(BTreeMap::len).sum::(); if registered >= self.maximum { - return Err(SidecarError::Execution(format!( - "ERR_AGENTOS_KERNEL_READINESS_TARGET_LIMIT: kernel readiness targets exceeded {}; raise limits.reactor.maxCapabilities", + return Err(SidecarError::host("ERR_AGENTOS_KERNEL_READINESS_TARGET_LIMIT", format!("kernel readiness targets exceeded {}; raise limits.reactor.maxCapabilities", self.maximum ))); } } - targets + if let Some(previous) = targets .entry(socket_id) .or_default() - .insert(identity, target); + .insert(identity, target) + { + previous.live.store(false, Ordering::Release); + } Ok(()) } @@ -2034,6 +2441,9 @@ impl KernelSocketReadinessRegistryState { return; }; if let Some(socket_targets) = targets.get_mut(&socket_id) { + if let Some(target) = socket_targets.get(&identity) { + target.live.store(false, Ordering::Release); + } socket_targets.remove(&identity); if socket_targets.is_empty() { targets.remove(&socket_id); @@ -2065,6 +2475,32 @@ impl Default for KernelSocketReadinessRegistryState { } } +/// Read-side state owned by one sidecar socket open description. +/// +/// Transport completions can be larger than one guest read and poll must be +/// observational. The bytes therefore remain here until a non-peeking read +/// consumes them. Source reservations stay live for exactly as long as any +/// retained bytes do, so moving buffering out of an executor adapter cannot +/// bypass the VM's buffered-byte accounting. +#[derive(Debug, Default)] +pub(crate) struct SocketReadState { + pub(crate) bytes: VecDeque, + pub(crate) source_reservations: Vec, + pub(crate) terminal: Option, +} + +#[derive(Debug, Clone)] +pub(crate) enum SocketReadTerminal { + End, + Closed { + had_error: bool, + }, + Error { + code: Option, + message: String, + }, +} + #[derive(Debug)] pub(crate) struct ActiveTcpSocket { pub(crate) runtime_context: agentos_runtime::RuntimeContext, @@ -2077,8 +2513,8 @@ pub(crate) struct ActiveTcpSocket { pub(crate) pending_read_stream: Option>>>, pub(crate) plain_reader_running: Arc, pub(crate) plain_reader_stopped: Arc, - pub(crate) events: Option>>>, - pub(crate) event_sender: Option>, + pub(crate) events: Option>>>, + pub(crate) event_sender: Option>, /// Durable per-operation wait source shared by adapters. Event data stays /// in `events`; this is only a coalesced readiness hint. pub(crate) read_event_notify: Arc, @@ -2103,12 +2539,12 @@ pub(crate) struct ActiveTcpSocket { /// A transport event may contain more bytes than the guest requested from /// `net.socket_read`. Retain the unread suffix on the shared socket /// description so the next read observes it before later transport events. - pub(crate) pending_read_event: Arc>>, - /// Bytes already read from the transport but not yet consumed by the - /// shared open socket description. Keeping this in the sidecar (rather - /// than per runner fd) preserves dup/SCM_RIGHTS read and MSG_PEEK - /// semantics across processes. - pub(crate) read_buffer: Arc>>, + pub(crate) pending_read_event: Arc>>, + /// Bytes and terminal state already observed from the transport but not + /// yet consumed by the shared open socket description. Keeping the source + /// reservations with the bytes makes the sidecar the durable, accounted + /// readiness owner across dup/SCM_RIGHTS aliases and executor adapters. + pub(crate) read_state: Arc>, /// One strong reference per guest-visible open socket description. This is /// separate from transport/TLS worker Arcs so SCM_RIGHTS can decide when a /// close is the final description close. @@ -2234,7 +2670,7 @@ impl fmt::Debug for LoopbackTlsEndpoint { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] -pub(crate) struct JavascriptTlsClientHello { +pub(crate) struct TlsClientHello { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) servername: Option, #[serde( @@ -2247,16 +2683,16 @@ pub(crate) struct JavascriptTlsClientHello { #[derive(Debug, Clone, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] -pub(crate) struct JavascriptTlsBridgeOptions { +pub(crate) struct TlsBridgeOptions { pub(crate) is_server: bool, pub(crate) host: Option, pub(crate) servername: Option, pub(crate) reject_unauthorized: Option, pub(crate) request_cert: Option, pub(crate) session: Option, - pub(crate) key: Option, - pub(crate) cert: Option, - pub(crate) ca: Option, + pub(crate) key: Option, + pub(crate) cert: Option, + pub(crate) ca: Option, pub(crate) passphrase: Option, pub(crate) ciphers: Option, #[serde(alias = "ALPNProtocols")] @@ -2267,21 +2703,21 @@ pub(crate) struct JavascriptTlsBridgeOptions { #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] -pub(crate) enum JavascriptTlsMaterial { - Single(JavascriptTlsDataValue), - Many(Vec), +pub(crate) enum TlsMaterial { + Single(TlsDataValue), + Many(Vec), } #[derive(Debug, Clone, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] -pub(crate) enum JavascriptTlsDataValue { +pub(crate) enum TlsDataValue { Buffer { data: String }, String { data: String }, } #[derive(Debug, Clone, Default)] pub(crate) struct ActiveTlsState { - pub(crate) client_hello: Option, + pub(crate) client_hello: Option, pub(crate) local_certificates: Vec>, pub(crate) peer_certificates: Vec>, pub(crate) protocol: Option, @@ -2309,6 +2745,8 @@ pub(crate) struct ActiveTcpListener { pub(crate) description_handles: Arc<()>, pub(crate) description_lease: Arc, pub(crate) kernel_transfer_guard: Option, + /// One accept/error event observed by poll(2) but not consumed by accept. + pub(crate) pending_event: Arc>>, } // --------------------------------------------------------------------------- @@ -2316,7 +2754,7 @@ pub(crate) struct ActiveTcpListener { // --------------------------------------------------------------------------- #[derive(Debug)] -pub(crate) enum JavascriptUnixListenerEvent { +pub(crate) enum UnixListenerEvent { Connection { socket: PendingUnixSocket, capability: agentos_runtime::capability::PendingCapability, @@ -2327,6 +2765,25 @@ pub(crate) enum JavascriptUnixListenerEvent { }, } +pub(crate) fn unix_listener_event_retained_bytes(event: &UnixListenerEvent) -> usize { + match event { + UnixListenerEvent::Connection { socket, .. } => [ + socket.local_path.as_ref(), + socket.remote_path.as_ref(), + socket.local_abstract_path_hex.as_ref(), + socket.remote_abstract_path_hex.as_ref(), + ] + .into_iter() + .flatten() + .map(String::len) + .fold(0, usize::saturating_add), + UnixListenerEvent::Error { code, message } => code + .as_ref() + .map_or(0, String::len) + .saturating_add(message.len()), + } +} + #[derive(Debug)] pub(crate) struct PendingUnixSocket { pub(crate) stream: UnixStream, @@ -2356,8 +2813,11 @@ pub(crate) struct ActiveUnixSocket { pub(crate) description_lease: Arc, pub(crate) stream: Arc>, pub(crate) plain_commands: TokioSender, - pub(crate) events: Arc>>, - pub(crate) event_sender: AsyncCompletionSender, + pub(crate) events: Arc>>, + pub(crate) event_sender: AsyncCompletionSender, + /// Coalesced wake source for blocking common host reads. Payload remains + /// in `events`/`read_state` and is consumed only on the owner thread. + pub(crate) read_event_notify: Arc, pub(crate) event_pusher: Arc, pub(crate) readiness_registration: SocketReadinessRegistration, pub(crate) application_read_interest: Arc, @@ -2374,12 +2834,11 @@ pub(crate) struct ActiveUnixSocket { pub(crate) saw_local_shutdown: Arc, pub(crate) saw_remote_end: Arc, pub(crate) close_notified: Arc, - pub(crate) pending_read_event: Arc>>, - /// Bytes already drained from the async completion lane but not yet - /// consumed by the shared Unix open description. Duplicated and - /// SCM_RIGHTS-transferred descriptors retain this same buffer so partial - /// reads and `MSG_PEEK` observe one Linux-style read cursor. - pub(crate) read_buffer: Arc>>, + pub(crate) pending_read_event: Arc>>, + /// Durable, accounted read/EOF/error state shared by every alias of this + /// Unix open description. Adapters observe this state; they never retain + /// transport payload or readiness truth themselves. + pub(crate) read_state: Arc>, pub(crate) description_handles: Arc<()>, pub(crate) listener_connection_retirement: Option>, pub(crate) resources: Arc, @@ -2389,7 +2848,7 @@ pub(crate) struct ActiveUnixSocket { pub(crate) struct ActiveUnixListener { pub(crate) listener: Option, pub(crate) bound_socket: Option, - pub(crate) events: Arc>>, + pub(crate) events: Arc>>, pub(crate) event_pusher: Arc, pub(crate) readiness_registration: SocketReadinessRegistration, pub(crate) close_notify: Arc, @@ -2404,6 +2863,8 @@ pub(crate) struct ActiveUnixListener { pub(crate) active_connection_ids: Arc>>, pub(crate) description_handles: Arc<()>, pub(crate) description_lease: Arc, + /// One accept/error event observed by poll(2) but not consumed by accept. + pub(crate) pending_event: Arc>>, } // --------------------------------------------------------------------------- @@ -2411,12 +2872,12 @@ pub(crate) struct ActiveUnixListener { // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum JavascriptUdpFamily { +pub(crate) enum UdpFamily { Ipv4, Ipv6, } -impl JavascriptUdpFamily { +impl UdpFamily { pub(crate) fn from_socket_type(value: &str) -> Result { match value { "udp4" => Ok(Self::Ipv4), @@ -2442,8 +2903,8 @@ impl JavascriptUdpFamily { } } -#[derive(Debug)] -pub(crate) enum JavascriptUdpSocketEvent { +#[derive(Clone, Debug)] +pub(crate) enum DatagramEvent { Message { data: Vec, remote_addr: SocketAddr, @@ -2497,7 +2958,7 @@ pub(crate) enum NativeUdpCommand { }, Poll { _command_reservation: SharedReservation, - completion: SyncSender, DeferredRpcError>>, + completion: SyncSender, DeferredRpcError>>, }, Connect { _command_reservation: SharedReservation, @@ -2533,28 +2994,9 @@ pub(crate) enum NativeUdpCommand { }, } -#[derive(Debug)] -pub(crate) enum PythonHostSocket { - Tcp { - socket_id: String, - pending_read: Option, - }, - Udp { - socket_id: String, - }, -} - -#[derive(Debug)] -pub(crate) struct PythonTcpReadBuffer { - pub(crate) data: Vec, - pub(crate) offset: usize, - pub(crate) _reservation: agentos_runtime::accounting::SharedReservation, - pub(crate) _source_reservations: Vec, -} - #[derive(Debug)] pub(crate) struct ActiveUdpSocket { - pub(crate) family: JavascriptUdpFamily, + pub(crate) family: UdpFamily, pub(crate) native_commands: Option>, pub(crate) kernel_socket_id: Option, pub(crate) guest_local_addr: Option, @@ -2573,6 +3015,10 @@ pub(crate) struct ActiveUdpSocket { pub(crate) fairness_retirement: Arc, pub(crate) description_lease: Arc, pub(crate) read_event_notify: Arc, + /// The next datagram observed by poll but not consumed by recv. This is + /// shared by every alias of the open description so MSG_PEEK and poll are + /// observational across dup/SCM_RIGHTS and every executor adapter. + pub(crate) pending_datagram: Arc>>, pub(crate) event_pusher: Arc, pub(crate) readiness_registration: SocketReadinessRegistration, pub(crate) native_read_wake_pending: Arc, @@ -2594,13 +3040,22 @@ pub(crate) enum ActiveExecution { #[derive(Debug, Clone)] pub(crate) struct BindingExecution { pub(crate) cancelled: Arc, + /// Durable kernel stop state. Binding callbacks may finish trusted host + /// work already in flight, but no adapter event is exposed to the process + /// while it is stopped. + pub(crate) paused: Arc, + pub(crate) pause_notify: Arc, pub(crate) pending_events: Arc>>, - pub(crate) event_overflow_reason: Arc>>, + pub(crate) event_overflow_reason: + Arc>>, pub(crate) pending_event_bytes: Arc, pub(crate) pending_event_count_limit: Arc, pub(crate) pending_event_bytes_limit: Arc, pub(crate) vm_pending_event_bytes_budget: Arc, pub(crate) event_notify: Arc, + pub(crate) host_capabilities: Option, + pub(crate) descendant_wait_ownership: DescendantWaitOwnership, + pub(crate) descendant_output_ownership: DescendantOutputOwnership, } impl Default for BindingExecution { @@ -2616,6 +3071,8 @@ impl BindingExecution { pub(crate) fn with_event_notify(event_notify: Arc, event_capacity: usize) -> Self { Self { cancelled: Arc::new(AtomicBool::new(false)), + paused: Arc::new(AtomicBool::new(false)), + pause_notify: Arc::new(Notify::new()), pending_events: Arc::new(Mutex::new(VecDeque::new())), event_overflow_reason: Arc::new(Mutex::new(None)), pending_event_bytes: Arc::new(AtomicUsize::new(0)), @@ -2628,18 +3085,22 @@ impl BindingExecution { TrackedLimit::PendingExecutionEventBytes, ), event_notify, + host_capabilities: None, + descendant_wait_ownership: DescendantWaitOwnership::Sidecar, + descendant_output_ownership: DescendantOutputOwnership::SidecarBridge, } } } #[derive(Debug)] pub(crate) enum ActiveExecutionEvent { + Common(ExecutionEvent), Stdout(Vec), Stderr(Vec), - JavascriptSyncRpcRequest(JavascriptSyncRpcRequest), - JavascriptSyncRpcCompletion(JavascriptSyncRpcCompletion), - PythonVfsRpcRequest(Box), - PythonSocketConnectCompletion(Box), + HostRpcRequest(ExecutionHostCall), + HostCallCompletion(HostCallCompletion), + ManagedStreamReadRecheck(Box), + ManagedUdpPollRecheck(Box), SignalState { signal: u32, registration: SignalHandlerRegistration, @@ -2648,27 +3109,59 @@ pub(crate) enum ActiveExecutionEvent { } #[derive(Debug)] -pub(crate) struct JavascriptSyncRpcCompletion { - pub(crate) request_id: u64, - pub(crate) result: Result, +pub(crate) struct ManagedStreamReadRecheck { + pub(crate) root_process_id: String, + pub(crate) process_path: Vec, + pub(crate) socket_id: String, + pub(crate) max_bytes: u64, + pub(crate) peek: bool, + pub(crate) deadline: Instant, + pub(crate) reply: DirectHostReplyHandle, } #[derive(Debug)] -pub(crate) struct PythonSocketConnectCompletion { - pub(crate) request_id: u64, - pub(crate) result: Result, +pub(crate) struct ManagedUdpPollRecheck { + /// VM process-map key plus a bounded descendant path. Re-entry always uses + /// the root process event lane, then resolves the generation-bound target + /// from kernel-owned process state before each readiness probe. + pub(crate) root_process_id: String, + pub(crate) process_path: Vec, + pub(crate) socket_id: String, + pub(crate) peek: bool, + pub(crate) max_bytes: Option, + pub(crate) deadline: Instant, + pub(crate) operation_deadline: Option, + pub(crate) deadline_warning_emitted: bool, + /// Native UDP owners are probed from the Tokio reactor, but guest-visible + /// completion is settled only after the result re-enters the process lane. + pub(crate) native_probe_completed: bool, + pub(crate) native_event: Option, + pub(crate) reply: DirectHostReplyHandle, + pub(crate) fair_turn: Option, +} + +#[derive(Debug, Clone)] +pub(crate) struct ExecutionHostCall { + pub(crate) request: HostRpcRequest, + pub(crate) reply: DirectHostReplyHandle, +} + +impl std::ops::Deref for ExecutionHostCall { + type Target = HostRpcRequest; + + fn deref(&self) -> &Self::Target { + &self.request + } } #[derive(Debug)] -pub(crate) struct PendingPythonTcpConnect { - pub(crate) native_socket_id: String, - pub(crate) python_socket_id: u64, - pub(crate) socket: ActiveTcpSocket, - pub(crate) pending_capability: agentos_runtime::capability::PendingCapability, +pub(crate) struct HostCallCompletion { + pub(crate) reply: DirectHostReplyHandle, + pub(crate) result: Result, } #[derive(Debug)] -pub(crate) enum PendingJavascriptNetConnect { +pub(crate) enum PendingNetConnect { Tcp { socket_id: String, socket: Box, @@ -2685,8 +3178,8 @@ pub(crate) enum PendingJavascriptNetConnect { } #[derive(Debug, Default)] -pub(crate) struct PendingJavascriptNetConnectState { - pub(crate) connected: Option, +pub(crate) struct PendingNetConnectState { + pub(crate) connected: Option, /// A bound-but-unlistened Unix socket is removed from the process table /// while its nonblocking connect is in flight. Keep the original handle /// here so a failed connect can restore the guest descriptor unchanged. @@ -2697,6 +3190,27 @@ pub(crate) struct PendingJavascriptNetConnectState { pub(crate) struct DeferredRpcError { pub(crate) code: String, pub(crate) message: String, + pub(crate) details: Option, +} + +impl From for DeferredRpcError { + fn from(error: agentos_execution::backend::HostServiceError) -> Self { + Self { + code: error.code, + message: error.message, + details: error.details, + } + } +} + +impl From for SidecarError { + fn from(error: DeferredRpcError) -> Self { + Self::Host(agentos_execution::backend::HostServiceError { + code: error.code, + message: error.message, + details: error.details, + }) + } } #[derive(Debug)] @@ -2718,6 +3232,92 @@ pub(crate) enum SocketQueryKind { // Command resolution // --------------------------------------------------------------------------- +/// Transport facts selected by an executor adapter during resolution. +/// +/// Common process/descriptor code consumes these capabilities and never +/// branches on an engine or language identity. The future Wasmtime adapter can +/// therefore choose its own transport profile even though it has the same +/// guest runtime kind as compatibility WASM. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ExecutionAdapterPolicy { + pub(crate) accepts_inherited_host_network_fds: bool, + pub(crate) materializes_direct_runtime_stdio: bool, + pub(crate) canonicalizes_runtime_stdin: bool, + pub(crate) supports_prepared_in_place_exec: bool, + pub(crate) captured_output_limit: fn(&crate::limits::VmLimits) -> usize, + pub(crate) captured_output_limit_setting: &'static str, + pub(crate) kernel_driver_command: &'static str, + pub(crate) forwards_kernel_stdin_rpc: bool, + pub(crate) encodes_inherited_fd_bootstrap: bool, + pub(crate) uses_javascript_entrypoint_projection: bool, +} + +fn javascript_captured_output_limit(limits: &crate::limits::VmLimits) -> usize { + limits.js_runtime.captured_output_limit_bytes +} + +fn python_captured_output_limit(limits: &crate::limits::VmLimits) -> usize { + limits.python.output_buffer_max_bytes +} + +fn wasm_captured_output_limit(limits: &crate::limits::VmLimits) -> usize { + limits.wasm.captured_output_limit_bytes +} + +impl ExecutionAdapterPolicy { + pub(crate) const BINDING: Self = Self { + accepts_inherited_host_network_fds: false, + materializes_direct_runtime_stdio: false, + canonicalizes_runtime_stdin: false, + supports_prepared_in_place_exec: false, + captured_output_limit: javascript_captured_output_limit, + captured_output_limit_setting: "limits.jsRuntime.capturedOutputLimitBytes", + kernel_driver_command: BINDING_DRIVER_NAME, + forwards_kernel_stdin_rpc: false, + encodes_inherited_fd_bootstrap: false, + uses_javascript_entrypoint_projection: false, + }; + + pub(crate) const DIRECT_RUNTIME: Self = Self { + accepts_inherited_host_network_fds: false, + materializes_direct_runtime_stdio: true, + canonicalizes_runtime_stdin: true, + supports_prepared_in_place_exec: false, + captured_output_limit: javascript_captured_output_limit, + captured_output_limit_setting: "limits.jsRuntime.capturedOutputLimitBytes", + kernel_driver_command: JAVASCRIPT_COMMAND, + forwards_kernel_stdin_rpc: true, + encodes_inherited_fd_bootstrap: false, + uses_javascript_entrypoint_projection: true, + }; + + pub(crate) const DIRECT_PYTHON_RUNTIME: Self = Self { + accepts_inherited_host_network_fds: false, + materializes_direct_runtime_stdio: true, + canonicalizes_runtime_stdin: true, + supports_prepared_in_place_exec: false, + captured_output_limit: python_captured_output_limit, + captured_output_limit_setting: "limits.python.outputBufferMaxBytes", + kernel_driver_command: PYTHON_COMMAND, + forwards_kernel_stdin_rpc: false, + encodes_inherited_fd_bootstrap: false, + uses_javascript_entrypoint_projection: false, + }; + + pub(crate) const KERNEL_HOST_CALL_POSIX: Self = Self { + accepts_inherited_host_network_fds: true, + materializes_direct_runtime_stdio: false, + canonicalizes_runtime_stdin: false, + supports_prepared_in_place_exec: true, + captured_output_limit: wasm_captured_output_limit, + captured_output_limit_setting: "limits.wasm.capturedOutputLimitBytes", + kernel_driver_command: WASM_COMMAND, + forwards_kernel_stdin_rpc: false, + encodes_inherited_fd_bootstrap: true, + uses_javascript_entrypoint_projection: false, + }; +} + #[derive(Debug)] pub(crate) struct ResolvedChildProcessExecution { pub(crate) command: String, @@ -2730,6 +3330,7 @@ pub(crate) struct ResolvedChildProcessExecution { pub(crate) host_cwd: PathBuf, pub(crate) wasm_permission_tier: Option, pub(crate) binding_command: bool, + pub(crate) adapter_policy: ExecutionAdapterPolicy, } // --------------------------------------------------------------------------- @@ -2752,6 +3353,14 @@ mod async_completion_tests { fn completion_runtime( maximum: usize, generation: u64, + ) -> (RuntimeContext, Arc) { + completion_runtime_with_limits(maximum, maximum * 16, generation) + } + + fn completion_runtime_with_limits( + count_maximum: usize, + byte_maximum: usize, + generation: u64, ) -> (RuntimeContext, Arc) { let process = agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) @@ -2759,10 +3368,16 @@ mod async_completion_tests { .context(); let resources = Arc::new(ResourceLedger::child( format!("completion-test-vm-{generation}"), - [( - ResourceClass::AsyncCompletions, - ResourceLimit::new(maximum, "limits.reactor.maxAsyncCompletions"), - )], + [ + ( + ResourceClass::AsyncCompletions, + ResourceLimit::new(count_maximum, "limits.reactor.maxAsyncCompletions"), + ), + ( + ResourceClass::AsyncCompletionBytes, + ResourceLimit::new(byte_maximum, "limits.reactor.maxAsyncCompletionBytes"), + ), + ], Arc::clone(process.resources()), )); ( @@ -2774,8 +3389,10 @@ mod async_completion_tests { #[test] fn completion_reservations_bound_all_lanes_in_one_vm() { let (runtime, resources) = completion_runtime(2, 91); - let (first_tx, mut first_rx) = async_completion_channel(runtime.clone(), 2); - let (second_tx, second_rx) = async_completion_channel(runtime.clone(), 2); + let (first_tx, mut first_rx) = + async_completion_channel(runtime.clone(), 2, |value: &&str| value.len()); + let (second_tx, second_rx) = + async_completion_channel(runtime.clone(), 2, |value: &&str| value.len()); first_tx.try_send("first").expect("first lane admission"); second_tx.try_send("second").expect("second lane admission"); @@ -2784,7 +3401,8 @@ mod async_completion_tests { let error = first_tx .try_send("aggregate overflow") .expect_err("per-VM completion limit must span both lanes"); - assert!(error.contains("limits.reactor.maxAsyncCompletions")); + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert!(error.message.contains("limits.reactor.maxAsyncCompletions")); assert_eq!( first_rx.try_recv().expect("release first completion"), @@ -2803,20 +3421,21 @@ mod async_completion_tests { "dropping queued lanes must release every completion reservation" ); - let (disconnected_tx, disconnected_rx) = async_completion_channel(runtime, 1); + let (disconnected_tx, disconnected_rx) = + async_completion_channel(runtime, 1, |value: &&str| value.len()); drop(disconnected_rx); let error = disconnected_tx .try_send("disconnected") .expect_err("disconnected lane rejects insertion"); - assert!(error.contains("ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED")); + assert_eq!(error.code, "ERR_AGENTOS_ASYNC_COMPLETION_DISCONNECTED"); assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 0); } #[test] fn failed_completion_send_and_vm_close_release_reservations() { let (runtime, resources) = completion_runtime(1, 92); - let (held_tx, _held_rx) = async_completion_channel(runtime.clone(), 1); - let (waiting_tx, waiting_rx) = async_completion_channel(runtime.clone(), 1); + let (held_tx, _held_rx) = async_completion_channel(runtime.clone(), 1, |_: &u8| 1); + let (waiting_tx, waiting_rx) = async_completion_channel(runtime.clone(), 1, |_: &u8| 1); held_tx.try_send(1_u8).expect("fill aggregate limit"); runtime.handle().block_on(async { @@ -2828,7 +3447,7 @@ mod async_completion_tests { .expect("VM close wakes completion admission waiter") .expect("completion waiter joins") .expect_err("closed VM rejects queued completion"); - assert!(error.contains("ERR_AGENTOS_ASYNC_COMPLETION_CLOSED")); + assert_eq!(error.code, "ERR_AGENTOS_ASYNC_COMPLETION_CLOSED"); }); drop(held_tx); @@ -2841,14 +3460,182 @@ mod async_completion_tests { drop(waiting_rx); assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 0); } + + #[test] + fn completion_byte_limit_plus_one_is_typed_and_rolls_back_count() { + let (runtime, resources) = completion_runtime_with_limits(3, 4, 93); + let (sender, receiver) = + async_completion_channel(runtime.clone(), 3, |value: &Vec| value.len()); + sender.try_send(vec![1, 2, 3, 4]).expect("fill byte limit"); + assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 1); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 4); + assert_eq!( + sender.count_gauge.name(), + TrackedLimit::AsyncCompletionCount + ); + assert_eq!(sender.byte_gauge.name(), TrackedLimit::AsyncCompletionBytes); + assert_eq!(sender.count_gauge.depth(), 1); + assert_eq!(sender.byte_gauge.depth(), 4); + + let error = sender + .try_send(vec![5]) + .expect_err("byte limit + 1 must fail atomically"); + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + error + .details + .as_ref() + .and_then(|value| value["resource"].as_str()), + Some("asyncCompletionBytes") + ); + assert_eq!( + resources.usage(ResourceClass::AsyncCompletions).used, + 1, + "failed byte admission must roll back its provisional count" + ); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 4); + + let async_error = runtime + .handle() + .block_on(sender.send(vec![0; 5])) + .expect_err( + "a completion larger than the configured byte maximum must fail without waiting", + ); + assert_eq!(async_error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + drop(receiver); + assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 0); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 0); + assert_eq!(sender.count_gauge.depth(), 0); + assert_eq!(sender.byte_gauge.depth(), 0); + } + + #[test] + fn network_completion_callers_share_byte_limit_and_drain_gauges() { + let (runtime, resources) = completion_runtime_with_limits(3, 10, 94); + let (tcp_tx, mut tcp_rx) = + async_completion_channel(runtime.clone(), 3, tcp_socket_event_retained_bytes); + let (unix_tx, mut unix_rx) = + async_completion_channel(runtime, 3, unix_listener_event_retained_bytes); + + tcp_tx + .try_send(TcpSocketEvent::Error { + code: Some(String::from("EC")), + message: String::from("123456"), + }) + .expect("TCP caller reaches the 80% near-limit warning threshold"); + assert_eq!(tcp_tx.byte_gauge.depth(), 8); + assert_eq!(tcp_tx.byte_gauge.capacity(), 10); + + unix_tx + .try_send(UnixListenerEvent::Error { + code: None, + message: String::from("12"), + }) + .expect("Unix listener caller fills the shared byte budget exactly"); + assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 2); + assert_eq!( + resources.usage(ResourceClass::AsyncCompletionBytes).used, + 10 + ); + + let error = unix_tx + .try_send(UnixListenerEvent::Error { + code: None, + message: String::from("x"), + }) + .expect_err("aggregate network completion bytes at limit + 1 must fail"); + assert_eq!(error.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + error + .details + .as_ref() + .and_then(|value| value["resource"].as_str()), + Some("asyncCompletionBytes") + ); + assert_eq!( + resources.usage(ResourceClass::AsyncCompletions).used, + 2, + "failed byte admission rolls back its provisional count reservation" + ); + assert_eq!(unix_tx.count_gauge.depth(), 1); + assert_eq!(unix_tx.byte_gauge.depth(), 2); + + assert!(matches!( + tcp_rx.try_recv().expect("drain TCP completion"), + TcpSocketEvent::Error { .. } + )); + assert!(matches!( + unix_rx.try_recv().expect("drain Unix completion"), + UnixListenerEvent::Error { .. } + )); + assert_eq!(resources.usage(ResourceClass::AsyncCompletions).used, 0); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 0); + assert_eq!(tcp_tx.count_gauge.depth(), 0); + assert_eq!(tcp_tx.byte_gauge.depth(), 0); + assert_eq!(unix_tx.count_gauge.depth(), 0); + assert_eq!(unix_tx.byte_gauge.depth(), 0); + } } #[cfg(test)] mod socket_readiness_registry_tests { use super::*; + use agentos_execution::backend::{ExecutionWakeError, ExecutionWakeTarget}; use agentos_execution::v8_host::V8RuntimeHost; use agentos_runtime::accounting::ResourceLimit; + #[derive(Default)] + struct RecordingWakeTarget { + readiness_publishes: AtomicUsize, + } + + impl ExecutionWakeTarget for RecordingWakeTarget { + fn publish_readiness( + &self, + _capability_id: u64, + _capability_generation: u64, + _flags: agentos_runtime::readiness::ReadyFlags, + ) -> Result<(), ExecutionWakeError> { + self.readiness_publishes.fetch_add(1, Ordering::AcqRel); + Ok(()) + } + + fn remove_readiness( + &self, + _capability_id: u64, + _capability_generation: u64, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + + fn set_application_read_interest( + &self, + _capability_id: u64, + _capability_generation: u64, + _enabled: bool, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + + fn publish_signal( + &self, + _signal: i32, + _delivery_token: u64, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + + fn send_adapter_event( + &self, + _event_type: &str, + _payload: &Value, + _encoded_limit_name: &'static str, + _max_encoded_bytes: usize, + ) -> Result<(), ExecutionWakeError> { + Ok(()) + } + } + fn kernel_target( capability_id: u64, capability_generation: u64, @@ -2861,17 +3648,7 @@ mod socket_readiness_registry_tests { capability_generation, target_id: target_id.to_owned(), event: KernelSocketReadinessEvent::Data, - } - } - - fn javascript_target( - session: &V8SessionHandle, - capability_id: u64, - ) -> JavascriptSocketEventPusher { - JavascriptSocketEventPusher { - session: session.clone(), - capability_id, - capability_generation: 1, + live: Arc::new(AtomicBool::new(true)), } } @@ -2920,6 +3697,44 @@ mod socket_readiness_registry_tests { .contains("ERR_AGENTOS_KERNEL_READINESS_TARGET_LIMIT")); } + #[test] + fn retired_socket_subscription_drops_cloned_late_end_wake() { + let resources = ResourceLedger::root( + "late-socket-wake-test", + [( + ResourceClass::Capabilities, + ResourceLimit::new(1, "limits.reactor.maxCapabilities"), + )], + ); + let wake_target = Arc::new(RecordingWakeTarget::default()); + let session = ExecutionWakeHandle::new( + agentos_execution::backend::ExecutionWakeIdentity { + generation: 1, + pid: 1, + }, + wake_target.clone(), + ); + let subscribers = SocketReadinessSubscribers::new(&resources); + let registration = SocketReadinessRegistration::new(Arc::clone(&subscribers), None, None); + registration.register( + Some(session), + Some((1, 1)), + Arc::new(Notify::new()), + agentos_runtime::readiness::ReadyFlags::READABLE, + ); + assert_eq!(wake_target.readiness_publishes.load(Ordering::Acquire), 1); + + // A reader task can already hold this clone when close retires the + // capability. It must not recreate readiness with a late END wake. + let late_transport_target = subscribers.targets().pop().expect("registered target"); + registration.retire(); + assert!(!late_transport_target + .publish_readiness(agentos_runtime::readiness::ReadyFlags::END) + .expect("retired readiness publish must be ignored")); + assert_eq!(wake_target.readiness_publishes.load(Ordering::Acquire), 1); + assert!(subscribers.targets().is_empty()); + } + #[test] fn native_alias_registration_is_raii_and_read_interest_is_aggregate_or() { let process_runtime = @@ -2934,30 +3749,40 @@ mod socket_readiness_registry_tests { ); let host = V8RuntimeHost::spawn(&process_runtime.context()) .expect("spawn subscriber test V8 host"); - let session = host.session_handle(String::from("socket-subscriber-test")); + let session = ExecutionWakeHandle::new( + agentos_execution::backend::ExecutionWakeIdentity { + generation: 1, + pid: 1, + }, + Arc::new(host.session_handle(String::from("socket-subscriber-test"))), + ); let subscribers = SocketReadinessSubscribers::new(&resources); let aggregate_interest = Arc::new(AtomicBool::new(false)); let interest_notify = Arc::new(Notify::new()); - let mut parent = SocketReadinessRegistration::new( + let parent = SocketReadinessRegistration::new( Arc::clone(&subscribers), Some(Arc::clone(&aggregate_interest)), Some(Arc::clone(&interest_notify)), ); - subscribers - .register(None, javascript_target(&session, 1)) - .expect("register parent alias"); - *parent.identity.get_mut().expect("parent identity") = Some((1, 1)); + parent.register( + Some(session.clone()), + Some((1, 1)), + Arc::new(Notify::new()), + agentos_runtime::readiness::ReadyFlags::READABLE, + ); - let mut child = SocketReadinessRegistration::new( + let child = SocketReadinessRegistration::new( Arc::clone(&subscribers), Some(Arc::clone(&aggregate_interest)), Some(Arc::clone(&interest_notify)), ); - subscribers - .register(None, javascript_target(&session, 2)) - .expect("register child alias"); - *child.identity.get_mut().expect("child identity") = Some((2, 1)); + child.register( + Some(session), + Some((2, 1)), + Arc::new(Notify::new()), + agentos_runtime::readiness::ReadyFlags::READABLE, + ); assert_eq!(subscribers.targets().len(), 2); let aggregate = subscribers diff --git a/crates/native-sidecar/src/stdio.rs b/crates/native-sidecar/src/stdio.rs index ae94506c71..9207ace07a 100644 --- a/crates/native-sidecar/src/stdio.rs +++ b/crates/native-sidecar/src/stdio.rs @@ -1007,6 +1007,9 @@ async fn run_async( } else { // Rivet's V8 child-process bridge cannot currently inherit fd 3. Keep // the logical lane priorities, but multiplex both lanes over stdio. + // This branch is mutually exclusive with the fd-3 reader above, so the + // process still owns exactly one constant stdio reader thread. + // AGENTOS_THREAD_SITE: constant-stdio-reader thread::spawn({ let read_error_tx = write_error_tx.clone(); move || { @@ -1069,7 +1072,17 @@ async fn run_async( biased; maybe_shutdown = shutdown_rx.recv() => { let Some(control) = maybe_shutdown else { - break 'protocol; + // The response/control reader owns the only shutdown + // sender. Its disappearance without a typed shutdown + // frame is therefore a transport failure, not a graceful + // ordinary-stdin EOF. Returning an error here also avoids + // racing the identical error notification on the lower- + // priority transport-error branch of this biased select. + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "response/control stream closed", + ) + .into()); }; match control.payload { wire::ControlPayload::ShutdownControl(shutdown) => { @@ -1821,6 +1834,7 @@ fn spawn_heartbeat_thread( write_tx: ProtocolFrameWriter, interval: Duration, ) -> thread::JoinHandle<()> { + // AGENTOS_THREAD_SITE: constant-heartbeat thread::spawn(move || { loop { thread::sleep(interval); diff --git a/crates/native-sidecar/src/vm.rs b/crates/native-sidecar/src/vm.rs index 1f45f9c312..692a6ed62a 100644 --- a/crates/native-sidecar/src/vm.rs +++ b/crates/native-sidecar/src/vm.rs @@ -4,18 +4,17 @@ //! Contains VM lifecycle methods on NativeSidecar and associated helpers. use crate::bootstrap::{ - apply_root_filesystem_entry, discover_command_guest_paths, root_snapshot_entries, - root_snapshot_entry, root_snapshot_from_entries, + apply_root_filesystem_entry, discover_kernel_commands, root_snapshot_entries, + root_snapshot_entry, root_snapshot_from_entries, KernelCommandInventory, }; use crate::bridge::{bridge_permissions, MountPluginContext}; -use crate::execution::{sync_process_host_writes_to_kernel, terminate_child_process_tree}; +use crate::execution::terminate_child_process_tree; use crate::protocol::{ AgentosProjectedAgent, ConfigureVmRequest, CreateLayerRequest, CreateOverlayRequest, DisposeReason, EventFrame, ExportSnapshotRequest, ImportSnapshotRequest, LinkPackageRequest, ListMountsRequest, MountDescriptor, MountInfo, MountPluginDescriptor, PackageCommands, ProjectedCommand, ProvidedCommandsRequest, RootFilesystemDescriptor, RootFilesystemEntry, - RootFilesystemEntryEncoding, RootFilesystemLowerDescriptor, SealLayerRequest, - SnapshotRootFilesystemRequest, VmLifecycleState, + SealLayerRequest, SnapshotRootFilesystemRequest, VmLifecycleState, }; use crate::service::{ audit_fields, dirname, emit_security_audit_event, emit_structured_event, kernel_error, @@ -38,11 +37,9 @@ use agentos_kernel::kernel::{KernelVm, KernelVmConfig}; use agentos_kernel::mount_plugin::OpenFileSystemPluginRequest; use agentos_kernel::mount_table::{MountOptions, MountTable, MountedFileSystem}; use agentos_kernel::permissions::filter_env; -use agentos_kernel::resource_accounting::ResourceLimits; use agentos_kernel::root_fs::{ - decode_snapshot_with_import_limits, encode_snapshot as encode_root_snapshot, - is_supported_root_filesystem_snapshot_format, FilesystemEntryKind as KernelFilesystemEntryKind, - RootFilesystemImportLimits, ROOT_FILESYSTEM_SNAPSHOT_FORMAT, + encode_snapshot as encode_root_snapshot, FilesystemEntryKind as KernelFilesystemEntryKind, + ROOT_FILESYSTEM_SNAPSHOT_FORMAT, }; use agentos_kernel::socket_table::{SocketReadiness, SocketReadinessKind}; use agentos_native_sidecar_core::ca::{ @@ -61,7 +58,6 @@ use agentos_native_sidecar_core::{ use agentos_runtime::accounting::{ResourceClass, ResourceLedger, ResourceLimit}; use agentos_runtime::capability::CapabilityRegistry; use agentos_vm_config as vm_config; -use base64::Engine; use openssl::rand::rand_bytes; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::fmt; @@ -69,54 +65,57 @@ use std::fs; use std::net::{IpAddr, SocketAddr}; use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -const SHADOW_ROOT_BOOTSTRAP_DIRS: &[(&str, u32)] = &[ - ("/dev", 0o755), - ("/proc", 0o755), - ("/tmp", 0o1777), - ("/bin", 0o755), - ("/lib", 0o755), - ("/sbin", 0o755), - ("/boot", 0o755), - ("/etc", 0o755), - ("/root", 0o700), - ("/run", 0o755), - ("/srv", 0o755), - ("/sys", 0o555), - ("/opt", 0o755), - ("/mnt", 0o755), - ("/media", 0o755), - ("/home", 0o755), - ("/home/agentos", 0o2755), - ("/usr", 0o755), - ("/usr/bin", 0o755), - ("/usr/games", 0o755), - ("/usr/include", 0o755), - ("/usr/lib", 0o755), - ("/usr/libexec", 0o755), - ("/usr/man", 0o755), - ("/usr/local", 0o755), - ("/usr/local/bin", 0o755), - ("/usr/sbin", 0o755), - ("/usr/share", 0o755), - ("/usr/share/man", 0o755), - ("/var", 0o755), - ("/var/cache", 0o755), - ("/var/empty", 0o555), - ("/var/lib", 0o755), - ("/var/lock", 0o777), - ("/var/log", 0o755), - ("/var/run", 0o777), - ("/var/spool", 0o755), - ("/var/tmp", 0o1777), - ("/etc/agentos", 0o755), +const ROOT_BOOTSTRAP_DIRS: &[(&str, u32, u32, u32)] = &[ + ("/dev", 0o755, 0, 0), + ("/proc", 0o755, 0, 0), + ("/tmp", 0o1777, 0, 0), + ("/bin", 0o755, 0, 0), + ("/lib", 0o755, 0, 0), + ("/sbin", 0o755, 0, 0), + ("/boot", 0o755, 0, 0), + ("/etc", 0o755, 0, 0), + // AgentOS retains `/root/node_modules` as a compatibility projection. + // Permit traversal without allowing the default guest to list `/root`. + ("/root", 0o711, 0, 0), + ("/run", 0o755, 0, 0), + ("/srv", 0o755, 0, 0), + ("/sys", 0o555, 0, 0), + ("/opt", 0o755, 0, 0), + ("/mnt", 0o755, 0, 0), + ("/media", 0o755, 0, 0), + ("/home", 0o755, 0, 0), + ("/home/agentos", 0o2755, 1000, 1000), + ("/usr", 0o755, 0, 0), + ("/usr/bin", 0o755, 0, 0), + ("/usr/games", 0o755, 0, 0), + ("/usr/include", 0o755, 0, 0), + ("/usr/lib", 0o755, 0, 0), + ("/usr/libexec", 0o755, 0, 0), + ("/usr/man", 0o755, 0, 0), + ("/usr/local", 0o755, 0, 0), + ("/usr/local/bin", 0o755, 0, 0), + ("/usr/sbin", 0o755, 0, 0), + ("/usr/share", 0o755, 0, 0), + ("/usr/share/man", 0o755, 0, 0), + ("/var", 0o755, 0, 0), + ("/var/cache", 0o755, 0, 0), + ("/var/empty", 0o555, 0, 0), + ("/var/lib", 0o755, 0, 0), + ("/var/lock", 0o777, 0, 0), + ("/var/log", 0o755, 0, 0), + ("/var/run", 0o777, 0, 0), + ("/var/spool", 0o755, 0, 0), + ("/var/tmp", 0o1777, 0, 0), + ("/etc/agentos", 0o755, 0, 0), // Non-Alpine default agent working directory (also present in the base // filesystem snapshot); scaffold it here so it exists even when the // default base layer is disabled. It is the default cwd and mount root, // kept separate from $HOME (/home/agentos). - ("/workspace", 0o755), + ("/workspace", 0o755, 1000, 1000), ]; fn create_vm_unix_socket_host_dir() -> Result { @@ -164,6 +163,9 @@ fn send_kernel_socket_readiness_event( target: KernelSocketReadinessTarget, readiness: SocketReadiness, ) { + if !target.live.load(Ordering::Acquire) { + return; + } let flags = match (target.event, readiness.kind) { (KernelSocketReadinessEvent::Accept, SocketReadinessKind::Accept) => { agentos_runtime::readiness::ReadyFlags::ACCEPT @@ -176,10 +178,15 @@ fn send_kernel_socket_readiness_event( } _ => return, }; - if let Some(notify) = target.notify { - notify.notify_one(); + if target.live.load(Ordering::Acquire) { + if let Some(notify) = &target.notify { + notify.notify_one(); + } } - if let Some(session) = target.session { + if target.live.load(Ordering::Acquire) { + let Some(session) = &target.session else { + return; + }; if let Err(error) = session.publish_readiness(target.capability_id, target.capability_generation, flags) { @@ -200,19 +207,40 @@ fn projected_command_guest_path(command: &str) -> String { format!("{}/{command}", crate::package_projection::OPT_AGENTOS_BIN) } -fn projected_commands_from_guest_paths( - command_guest_paths: &BTreeMap, +fn projected_commands_from_provided_commands( + provided_commands: &BTreeMap>, + kernel_commands: &KernelCommandInventory, ) -> Vec { - command_guest_paths - .iter() - .filter(|(_, guest_path)| { - guest_path.starts_with(crate::package_projection::OPT_AGENTOS_BIN) - }) - .map(|(name, guest_path)| ProjectedCommand { - name: name.clone(), - guest_path: guest_path.clone(), - }) - .collect() + let mut commands = BTreeMap::new(); + for command in provided_commands.values().flatten() { + if kernel_commands.names.contains(command) { + continue; + } + commands + .entry(command.clone()) + .or_insert_with(|| ProjectedCommand { + name: command.clone(), + guest_path: projected_command_guest_path(command), + }); + } + commands.into_values().collect() +} + +fn execution_driver_commands( + kernel_commands: &KernelCommandInventory, + provided_commands: &BTreeMap>, + additional_commands: impl IntoIterator, +) -> Vec { + let mut commands = BTreeSet::from([ + String::from(JAVASCRIPT_COMMAND), + String::from(PYTHON_COMMAND), + String::from("python3"), + String::from(WASM_COMMAND), + ]); + commands.extend(kernel_commands.names.iter().cloned()); + commands.extend(provided_commands.values().flatten().cloned()); + commands.extend(additional_commands); + commands.into_iter().collect() } // --------------------------------------------------------------------------- // NativeSidecar VM lifecycle methods @@ -227,17 +255,19 @@ where self.reap_reconciled_quarantined_vms(); self.ensure_vm_generation_capacity()?; let next = self.next_vm_id.checked_add(1).ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_VM_ID_EXHAUSTED: VM id counter overflowed", - )) + SidecarError::host( + "ERR_AGENTOS_VM_ID_EXHAUSTED", + String::from("VM id counter overflowed"), + ) })?; let generation = self .runtime_context .as_ref() .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: VM generation allocation requires RuntimeContext", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("VM generation allocation requires RuntimeContext"), + ) })? .allocate_vm_generation() .map_err(|error| SidecarError::InvalidState(error.to_string()))?; @@ -271,8 +301,9 @@ where validate_permissions_policy(&permissions_policy)?; let (vm_id, vm_generation) = self.allocate_vm_identity()?; - let cwd = create_vm_shadow_root(&vm_id)?; - let (guest_cwd, host_cwd) = resolve_vm_cwds(create_config.cwd.as_ref(), &cwd)?; + let runtime_scratch_root = create_vm_runtime_scratch_root(&vm_id)?; + let (guest_cwd, host_cwd) = + resolve_vm_cwds(create_config.cwd.as_ref(), &runtime_scratch_root)?; fs::create_dir_all(&host_cwd) .map_err(|error| SidecarError::Io(format!("failed to create VM cwd: {error}")))?; let limits = crate::limits::vm_limits_from_config( @@ -281,9 +312,10 @@ where )?; let resource_limits = limits.resources.clone(); let process_runtime_context = self.runtime_context.as_ref().cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: VM admission requires RuntimeContext", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("VM admission requires RuntimeContext"), + ) })?; let process_resources = Arc::clone(process_runtime_context.resources()); let vm_resources = Arc::new(vm_resource_ledger( @@ -340,8 +372,8 @@ where .set_vm_permissions(&vm_id, &permissions_policy)?; let permissions = bridge_permissions(self.bridge.clone(), &vm_id); let mut guest_env = filter_env(&vm_id, &create_config.env, &permissions); - // Sidecar-owned bootstrap work still needs to reconcile command stubs and the root - // filesystem before the guest-visible policy takes effect. + // Sidecar-owned bootstrap work still needs to install command stubs and + // the root filesystem before the guest-visible policy takes effect. self.bridge .set_vm_permissions(&vm_id, &allow_all_policy())?; let native_root = native_root_plugin_from_config(create_config.native_root.as_ref())?; @@ -354,16 +386,8 @@ where }) })? }; - if native_root.is_none() { - materialize_shadow_root_snapshot_entries( - &cwd, - &root_filesystem, - loaded_snapshot.as_ref(), - &resource_limits, - )?; - } - let mut config = KernelVmConfig::new(vm_id.clone()); + config.vm_generation = vm_generation; config.cwd = guest_cwd.clone(); config.env = guest_env.clone(); if let Some(user) = create_config.user.as_ref() { @@ -456,30 +480,22 @@ where send_kernel_socket_readiness_event(target, readiness); } })); - let command_guest_paths = discover_command_guest_paths(&mut kernel); - refresh_guest_command_path_env(&mut guest_env, &command_guest_paths); - let mut execution_commands = vec![ - String::from(JAVASCRIPT_COMMAND), - String::from(PYTHON_COMMAND), - // `python3` resolves to the same Pyodide runtime; register it so the - // guest shell can find `/bin/python3` on PATH (the command resolver - // already rewrites the alias to `python`). - String::from("python3"), - String::from(WASM_COMMAND), - ]; - if let Some(bootstrap_commands) = &create_config.bootstrap_commands { - execution_commands.extend(bootstrap_commands.iter().cloned()); - } - execution_commands.extend(command_guest_paths.keys().cloned()); + let kernel_commands = discover_kernel_commands(&mut kernel); + refresh_guest_command_path_env(&mut guest_env, &kernel_commands.search_roots); + let execution_commands = execution_driver_commands( + &kernel_commands, + &BTreeMap::new(), + create_config.bootstrap_commands.iter().flatten().cloned(), + ); kernel .register_driver(CommandDriver::new( EXECUTION_DRIVER_NAME, execution_commands, )) .map_err(kernel_error)?; - if let Some(root) = kernel.root_filesystem_mut() { - root.finish_bootstrap(); - } + kernel + .finish_root_filesystem_bootstrap() + .map_err(kernel_error)?; self.bridge .set_vm_permissions(&vm_id, &permissions_policy)?; @@ -496,10 +512,6 @@ where .expect("owned session should exist") .vm_ids .insert(vm_id.clone()); - // Seed the baseline during VM creation. Otherwise a host-side deletion - // that happens before the first shadow sync has no prior inventory and - // the deleted kernel entry is resurrected/order-dependent. - let shadow_sync_inventory = crate::execution::initial_shadow_sync_inventory(&cwd)?; let unix_socket_host_dir = create_vm_unix_socket_host_dir()?; let pending_stdin_bytes_budget = VmPendingByteBudget::new( limits.process.pending_stdin_bytes, @@ -509,6 +521,14 @@ where limits.process.pending_event_bytes, agentos_bridge::queue_tracker::TrackedLimit::PendingExecutionEventBytes, ); + let pending_child_sync_count_budget = VmPendingByteBudget::new( + limits.process.max_pending_child_sync_count, + agentos_bridge::queue_tracker::TrackedLimit::PendingChildProcessSyncCount, + ); + let pending_child_sync_bytes_budget = VmPendingByteBudget::new( + limits.process.max_pending_child_sync_bytes, + agentos_bridge::queue_tracker::TrackedLimit::PendingChildProcessSyncBytes, + ); self.vms.insert( vm_id.clone(), VmState { @@ -518,6 +538,8 @@ where limits, pending_stdin_bytes_budget, pending_event_bytes_budget, + pending_child_sync_count_budget, + pending_child_sync_bytes_budget, resources: vm_resources, runtime_context: vm_runtime_context, database, @@ -529,10 +551,11 @@ where requested_runtime: payload.runtime, root_filesystem_mode: protocol_root_filesystem_mode(root_filesystem.mode), guest_cwd, - cwd, + runtime_scratch_root, host_cwd, kernel, kernel_socket_readiness, + managed_host_net_descriptions: Arc::new(Mutex::new(BTreeMap::new())), host_net_transfer_descriptions: Arc::new(Mutex::new(BTreeMap::new())), loaded_snapshot, configuration: VmConfiguration { @@ -541,7 +564,6 @@ where ..VmConfiguration::default() }, layers: VmLayerStore::default(), - command_guest_paths, provided_commands: BTreeMap::new(), command_permissions: BTreeMap::new(), bindings: BTreeMap::new(), @@ -552,10 +574,8 @@ where detached_child_processes: BTreeSet::new(), attached_child_event_cursor: 0, detached_child_event_cursor: 0, - signal_states: BTreeMap::new(), packages_staging_root: None, projected_agent_launch: BTreeMap::new(), - shadow_sync_inventory, unix_address_registry: Arc::new(Mutex::new(BTreeMap::new())), unix_socket_host_dir, }, @@ -629,9 +649,10 @@ where let mount_plugins = &self.mount_plugins; let bridge = self.bridge.clone(); let snapshot_runtime_context = self.runtime_context.as_ref().cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "ERR_AGENTOS_RUNTIME_UNAVAILABLE: snapshot pre-warm requires RuntimeContext", - )) + SidecarError::host( + "ERR_AGENTOS_RUNTIME_UNAVAILABLE", + String::from("snapshot pre-warm requires RuntimeContext"), + ) })?; let vm = self.vms.get_mut(&vm_id).expect("owned VM should exist"); let max_pread_bytes = vm.kernel.resource_limits().max_pread_bytes; @@ -679,36 +700,23 @@ where }, ) .and_then(|()| { - vm.command_guest_paths = discover_command_guest_paths(&mut vm.kernel); - // The `{ packageDir }` projection lands each package's `bin/` at - // `/opt/agentos/bin/` (on `$PATH`) but does NOT populate - // `/__secure_exec/commands`, so `discover_command_guest_paths` alone misses - // projected commands and every projected wasm/js command resolves to - // ENOEXEC (absolute path) / ENOENT (bare name). Register each projected - // command by name -> its `/opt/agentos/bin/` entrypoint so both the - // kernel command table (via `execution_commands` below) and the sidecar - // entrypoint resolver (`resolve_guest_command_entrypoint`) can find it. - for commands in provided_commands.values() { - for command in commands { - let entrypoint = - format!("{}/{command}", crate::package_projection::OPT_AGENTOS_BIN); - vm.command_guest_paths - .entry(command.clone()) - .or_insert(entrypoint); - } - } - refresh_guest_command_path_env(&mut vm.guest_env, &vm.command_guest_paths); - let mut execution_commands = - vec![String::from(JAVASCRIPT_COMMAND), String::from(WASM_COMMAND)]; - execution_commands.extend(payload.bootstrap_commands.iter().cloned()); - execution_commands.extend(payload.binding_shim_commands.iter().cloned()); - execution_commands.extend(vm.command_guest_paths.keys().cloned()); + let kernel_commands = discover_kernel_commands(&mut vm.kernel); + let execution_commands = execution_driver_commands( + &kernel_commands, + &provided_commands, + payload + .bootstrap_commands + .iter() + .chain(payload.binding_shim_commands.iter()) + .cloned(), + ); vm.kernel - .register_driver(CommandDriver::new( + .replace_driver(CommandDriver::new( EXECUTION_DRIVER_NAME, execution_commands, )) .map_err(kernel_error)?; + refresh_guest_command_path_env(&mut vm.guest_env, &kernel_commands.search_roots); vm.command_permissions = payload.command_permissions.clone().into_iter().collect(); let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone(); loopback_exempt_ports.extend(payload.loopback_exempt_ports.iter().copied()); @@ -756,7 +764,9 @@ where let applied_mounts = effective_mounts.len() as u32; let configured_software = payload.software.len() as u32; - let projected_commands = projected_commands_from_guest_paths(&vm.command_guest_paths); + let kernel_commands = discover_kernel_commands(&mut vm.kernel); + let projected_commands = + projected_commands_from_provided_commands(&vm.provided_commands, &kernel_commands); let agents = projected_agents_from_descriptors(&package_descriptors); vm.projected_agent_launch = projected_agent_launch_from_descriptors(&package_descriptors); let _ = vm; @@ -880,22 +890,25 @@ where vm.configuration .provided_commands .insert(descriptor.name.clone(), commands.clone()); - for command in &commands { - let entrypoint = projected_command_guest_path(command); - vm.command_guest_paths - .entry(command.clone()) - .or_insert(entrypoint); - } - refresh_guest_command_path_env(&mut vm.guest_env, &vm.command_guest_paths); - let mut execution_commands = - vec![String::from(JAVASCRIPT_COMMAND), String::from(WASM_COMMAND)]; - execution_commands.extend(vm.command_guest_paths.keys().cloned()); + let retained_execution_commands = vm + .kernel + .commands() + .into_iter() + .filter_map(|(command, driver)| (driver == EXECUTION_DRIVER_NAME).then_some(command)) + .collect::>(); + let kernel_commands = discover_kernel_commands(&mut vm.kernel); + let execution_commands = execution_driver_commands( + &kernel_commands, + &vm.provided_commands, + retained_execution_commands, + ); vm.kernel - .register_driver(CommandDriver::new( + .replace_driver(CommandDriver::new( EXECUTION_DRIVER_NAME, execution_commands, )) .map_err(kernel_error)?; + refresh_guest_command_path_env(&mut vm.guest_env, &kernel_commands.search_roots); let projected_commands = commands .iter() .map(|command| ProjectedCommand { @@ -925,19 +938,18 @@ where let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - let packages = self + let vm = self .vms .get(&vm_id) - .map(|vm| { - vm.provided_commands - .iter() - .map(|(package_name, commands)| PackageCommands { - package_name: package_name.clone(), - commands: commands.clone(), - }) - .collect() + .ok_or_else(|| SidecarError::host("ESTALE", "VM disappeared during command lookup"))?; + let packages = vm + .provided_commands + .iter() + .map(|(package_name, commands)| PackageCommands { + package_name: package_name.clone(), + commands: commands.clone(), }) - .unwrap_or_default(); + .collect(); Ok(DispatchResult { response: provided_commands_response(request, packages), @@ -1170,7 +1182,12 @@ where database: vm.database.clone(), max_pread_bytes: vm.kernel.resource_limits().max_pread_bytes, }; - let _ = shutdown_configured_mounts(&mut vm, &mount_context, "dispose_vm", true); + if let Err(error) = shutdown_configured_mounts(&mut vm, &mount_context, "dispose_vm", true) + { + eprintln!( + "ERR_AGENTOS_MOUNT_TEARDOWN: mount shutdown returned an unexpected error for VM {vm_id}: {error}" + ); + } // Snapshot/flush/kernel-dispose/permission-reset can each fail; run them // in a helper whose result is captured so cleanup below is unconditional. @@ -1181,9 +1198,23 @@ where // steps' `?`, so any failure stranded the engine/extension maps (H1) and // the output-buffer map was never reclaimed at all (M6). self.reclaim_vm_tracking(session_id, vm_id); - let _ = fs::remove_dir_all(&vm.cwd); + if let Err(error) = fs::remove_dir_all(&vm.runtime_scratch_root) { + if error.kind() != std::io::ErrorKind::NotFound { + eprintln!( + "ERR_AGENTOS_VM_SCRATCH_CLEANUP: failed to remove {}: {error}", + vm.runtime_scratch_root.display() + ); + } + } if let Some(staging_root) = vm.packages_staging_root.take() { - let _ = fs::remove_dir_all(&staging_root); + if let Err(error) = fs::remove_dir_all(&staging_root) { + if error.kind() != std::io::ErrorKind::NotFound { + eprintln!( + "ERR_AGENTOS_PACKAGE_STAGING_CLEANUP: failed to remove {}: {error}", + staging_root.display() + ); + } + } } if let Err(error) = fs::remove_dir_all(&vm.unix_socket_host_dir) { if error.kind() != std::io::ErrorKind::NotFound { @@ -1395,11 +1426,6 @@ where remaining.len() ); for (process_id, mut process) in remaining { - let should_sync_host_writes = process.host_write_dirty_recursive() - || !process.clean_host_writes_are_observable_recursive(); - if should_sync_host_writes { - sync_process_host_writes_to_kernel(vm, &process)?; - } terminate_child_process_tree( &mut vm.kernel, &mut process, @@ -1407,8 +1433,12 @@ where &unix_address_registry, ); process.kernel_handle.finish(137); - let _ = vm.kernel.wait_and_reap(process.kernel_pid); - vm.signal_states.remove(&process_id); + if let Err(error) = vm.kernel.wait_and_reap(process.kernel_pid) { + eprintln!( + "ERR_AGENTOS_VM_FORCED_PROCESS_REAP: vm_id={vm_id} process_id={process_id} pid={} error={error}", + process.kernel_pid + ); + } } } @@ -1480,9 +1510,10 @@ fn retire_vm_fairness( .retire_vm(vm_generation) .map(|_| ()) .map_err(|error| { - SidecarError::Execution(format!( - "ERR_AGENTOS_FAIRNESS_RETIRE_VM: generation={vm_generation}: {error}" - )) + SidecarError::host( + "ERR_AGENTOS_FAIRNESS_RETIRE_VM", + format!("generation={vm_generation}: {error}"), + ) }) } @@ -1947,14 +1978,12 @@ fn bootstrap_native_root_filesystem( filesystem: &mut dyn MountedFileSystem, descriptor: &RootFilesystemDescriptor, ) -> Result<(), SidecarError> { - for (guest_path, mode) in SHADOW_ROOT_BOOTSTRAP_DIRS { + for (guest_path, mode, uid, gid) in ROOT_BOOTSTRAP_DIRS { filesystem.mkdir(guest_path, true).map_err(vfs_error)?; - let (uid, gid) = match *guest_path { - "/home/agentos" | "/workspace" => (1000, 1000), - _ => (0, 0), - }; - filesystem.chown(guest_path, uid, gid).map_err(vfs_error)?; filesystem.chmod(guest_path, *mode).map_err(vfs_error)?; + filesystem + .chown(guest_path, *uid, *gid) + .map_err(vfs_error)?; } seed_native_ca_certificates_bundle(filesystem)?; @@ -2200,7 +2229,7 @@ where ), Err(error) if error.code() == "EINVAL" => {} Err(error) => { - let _ = emit_structured_event( + if let Err(emit_error) = emit_structured_event( &context.bridge, &context.vm_id, "filesystem.mount.shutdown_failed", @@ -2212,7 +2241,12 @@ where (String::from("error_code"), String::from(error.code())), (String::from("error"), error.to_string()), ]), - ); + ) { + eprintln!( + "ERR_AGENTOS_DIAGNOSTIC_EMIT: failed to emit mount shutdown failure for VM {} at {}: {emit_error:?}", + context.vm_id, existing.guest_path + ); + } if !continue_on_error { return Err(kernel_error(error)); @@ -2562,7 +2596,7 @@ fn resolve_guest_cwd(value: Option<&String>) -> String { fn resolve_vm_cwds( metadata_cwd: Option<&String>, - shadow_root: &Path, + runtime_scratch_root: &Path, ) -> Result<(String, PathBuf), SidecarError> { if let Some(raw_cwd) = metadata_cwd { let candidate = PathBuf::from(raw_cwd); @@ -2573,7 +2607,7 @@ fn resolve_vm_cwds( } let guest_cwd = resolve_guest_cwd(metadata_cwd); - let host_cwd = shadow_path_for_guest(shadow_root, &guest_cwd); + let host_cwd = runtime_scratch_path_for_guest(runtime_scratch_root, &guest_cwd); Ok((guest_cwd, host_cwd)) } @@ -2598,32 +2632,30 @@ fn resolve_host_path(value: Option<&String>) -> Result { } } -fn create_vm_shadow_root(vm_id: &str) -> Result { +fn create_vm_runtime_scratch_root(vm_id: &str) -> Result { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) - .map_err(|error| SidecarError::Io(format!("failed to compute shadow-root nonce: {error}")))? + .map_err(|error| { + SidecarError::Io(format!("failed to compute scratch-root nonce: {error}")) + })? .as_nanos(); - let root = std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-{vm_id}-{nonce}")); + let root = std::env::temp_dir().join(format!("agentos-native-sidecar-runtime-{vm_id}-{nonce}")); fs::create_dir_all(&root) - .map_err(|error| SidecarError::Io(format!("failed to create VM shadow root: {error}")))?; - initialize_vm_shadow_root(root) + .map_err(|error| SidecarError::Io(format!("failed to create VM runtime root: {error}")))?; + initialize_vm_runtime_scratch_root(root) } -fn initialize_vm_shadow_root(root: PathBuf) -> Result { +fn initialize_vm_runtime_scratch_root(root: PathBuf) -> Result { let cleanup_root = root.clone(); // macOS: `std::env::temp_dir()` lives under `/var/folders/…`, but `/var` is a // symlink to `/private/var`, and macOS fd→path recovery (`fcntl(F_GETPATH)`) - // reports the resolved `/private/var/…` form. Canonicalize the shadow root up - // front so the stored host-root matches those resolved paths; otherwise the - // mapped-runtime confinement prefix checks (`strip_prefix(host_root)`) reject - // every child and guest `readdir` of a populated dir returns empty. host_dir - // mounts already canonicalize their root for the same reason. + // reports the resolved `/private/var/…` form. Canonicalize the private + // runtime root so executor confinement compares the same host path form. let initialized = (|| { #[cfg(target_os = "macos")] let root = fs::canonicalize(&root).map_err(|error| { - SidecarError::Io(format!("failed to canonicalize VM shadow root: {error}")) + SidecarError::Io(format!("failed to canonicalize VM runtime root: {error}")) })?; - bootstrap_shadow_root(&root)?; Ok(root) })(); @@ -2632,413 +2664,21 @@ fn initialize_vm_shadow_root(root: PathBuf) -> Result { Err(error) => match fs::remove_dir_all(&cleanup_root) { Ok(()) => Err(error), Err(cleanup_error) => Err(SidecarError::Io(format!( - "{error}; additionally failed to clean shadow root {}: {cleanup_error}", + "{error}; additionally failed to clean runtime root {}: {cleanup_error}", cleanup_root.display() ))), }, } } -fn bootstrap_shadow_root(root: &Path) -> Result<(), SidecarError> { - for (guest_path, mode) in SHADOW_ROOT_BOOTSTRAP_DIRS { - let host_path = shadow_path_for_guest(root, guest_path); - fs::create_dir_all(&host_path).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow directory {}: {error}", - host_path.display() - )) - })?; - fs::set_permissions(&host_path, fs::Permissions::from_mode(*mode)).map_err(|error| { - SidecarError::Io(format!( - "failed to set shadow directory mode {mode:o} on {}: {error}", - host_path.display() - )) - })?; - } - seed_ca_certificates_bundle(root)?; - Ok(()) -} - -/// Seed the Mozilla CA bundle into the shadow root at -/// `/etc/ssl/certs/ca-certificates.crt` (plus the conventional -/// `/etc/ssl/cert.pem` symlink) so guest TLS clients resolve trust the standard -/// Linux way. -fn seed_ca_certificates_bundle(root: &Path) -> Result<(), SidecarError> { - if CA_CERTIFICATES_BUNDLE.is_empty() { - return Err(SidecarError::Io( - "embedded Mozilla CA certificate bundle is empty".to_string(), - )); - } - - let bundle_path = shadow_path_for_guest(root, CA_CERTIFICATES_GUEST_PATH); - if let Some(parent) = bundle_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow CA certs directory {}: {error}", - parent.display() - )) - })?; - } - match fs::symlink_metadata(&bundle_path) { - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - fs::write(&bundle_path, CA_CERTIFICATES_BUNDLE).map_err(|error| { - SidecarError::Io(format!( - "failed to seed CA bundle {}: {error}", - bundle_path.display() - )) - })?; - fs::set_permissions(&bundle_path, fs::Permissions::from_mode(0o644)).map_err( - |error| { - SidecarError::Io(format!( - "failed to set CA bundle mode on {}: {error}", - bundle_path.display() - )) - }, - )?; - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow CA bundle {}: {error}", - bundle_path.display() - ))); - } - } - - let symlink_path = shadow_path_for_guest(root, CA_CERTIFICATES_SYMLINK_PATH); - match fs::symlink_metadata(&symlink_path) { - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - std::os::unix::fs::symlink(CA_CERTIFICATES_SYMLINK_TARGET, &symlink_path).map_err( - |error| { - SidecarError::Io(format!( - "failed to seed CA bundle symlink {}: {error}", - symlink_path.display() - )) - }, - )?; - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow CA bundle symlink {}: {error}", - symlink_path.display() - ))); - } - } - Ok(()) -} - -fn materialize_shadow_root_snapshot_entries( - shadow_root: &Path, - descriptor: &RootFilesystemDescriptor, - loaded_snapshot: Option<&FilesystemSnapshot>, - resource_limits: &ResourceLimits, -) -> Result<(), SidecarError> { - let import_limits = RootFilesystemImportLimits::from_resource_limits(resource_limits); - if let Some(snapshot) = loaded_snapshot - .filter(|snapshot| is_supported_root_filesystem_snapshot_format(&snapshot.format)) - .map(|snapshot| { - decode_snapshot_with_import_limits(&snapshot.bytes, &import_limits) - .map_err(root_filesystem_error) - }) - .transpose()? - { - materialize_shadow_entries(shadow_root, &root_snapshot_entries(&snapshot))?; - materialize_shadow_entries(shadow_root, &descriptor.bootstrap_entries)?; - return Ok(()); - } - - validate_shadow_descriptor_import_limits(descriptor, &import_limits)?; - for lower in &descriptor.lowers { - if let RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(inner) = lower { - materialize_shadow_entries(shadow_root, &inner.entries)?; - } - } - materialize_shadow_entries(shadow_root, &descriptor.bootstrap_entries)?; - Ok(()) -} - -fn validate_shadow_descriptor_import_limits( - descriptor: &RootFilesystemDescriptor, - limits: &RootFilesystemImportLimits, -) -> Result<(), SidecarError> { - let mut explicit_entry_count = descriptor.bootstrap_entries.len(); - let mut inode_paths = BTreeSet::new(); - collect_root_protocol_entry_paths(&descriptor.bootstrap_entries, &mut inode_paths); - let mut bytes = root_protocol_entry_content_bytes(&descriptor.bootstrap_entries)?; - - for lower in &descriptor.lowers { - match lower { - RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(inner) => { - let entries = &inner.entries; - explicit_entry_count = explicit_entry_count.saturating_add(entries.len()); - collect_root_protocol_entry_paths(entries, &mut inode_paths); - bytes = bytes.saturating_add(root_protocol_entry_content_bytes(entries)?); - } - RootFilesystemLowerDescriptor::BundledBaseFilesystemLower => {} - } - } - - if let Some(limit) = limits.max_inode_count { - if explicit_entry_count > limit { - return Err(root_filesystem_error(format!( - "root filesystem descriptor contains {explicit_entry_count} entries, exceeding limit {limit}" - ))); - } - - let entry_count = inode_paths.len(); - if entry_count > limit { - return Err(root_filesystem_error(format!( - "root filesystem descriptor contains {entry_count} entries, exceeding limit {limit}" - ))); - } - } - - if let Some(limit) = limits.max_filesystem_bytes { - if bytes > limit { - return Err(root_filesystem_error(format!( - "root filesystem descriptor contains {bytes} bytes, exceeding limit {limit}" - ))); - } - } - - Ok(()) -} - -fn collect_root_protocol_entry_paths( - entries: &[RootFilesystemEntry], - paths: &mut BTreeSet, -) { - for entry in entries { - collect_root_protocol_path(&entry.path, paths); - } -} - -fn collect_root_protocol_path(path: &str, paths: &mut BTreeSet) { - let normalized = normalize_guest_path(path); - paths.insert(normalized.clone()); - - let mut parent = String::new(); - let segments = normalized - .split('/') - .filter(|segment| !segment.is_empty()) - .collect::>(); - for segment in segments.iter().take(segments.len().saturating_sub(1)) { - parent.push('/'); - parent.push_str(segment); - paths.insert(parent.clone()); - } -} - -fn root_protocol_entry_content_bytes(entries: &[RootFilesystemEntry]) -> Result { - entries.iter().try_fold(0_u64, |total, entry| { - let bytes = match entry.kind { - crate::protocol::RootFilesystemEntryKind::Directory => 0, - crate::protocol::RootFilesystemEntryKind::File => { - root_protocol_file_content_bytes(entry)? - } - crate::protocol::RootFilesystemEntryKind::Symlink => entry - .target - .as_ref() - .map(|target| usize_to_u64(target.len())) - .unwrap_or(0), - }; - Ok(total.saturating_add(bytes)) - }) -} - -fn root_protocol_file_content_bytes(entry: &RootFilesystemEntry) -> Result { - let Some(content) = entry.content.as_deref() else { - return Ok(0); - }; - - let bytes = match entry - .encoding - .clone() - .unwrap_or(RootFilesystemEntryEncoding::Utf8) - { - RootFilesystemEntryEncoding::Utf8 => content.len(), - RootFilesystemEntryEncoding::Base64 => estimated_base64_decoded_len(content), - }; - Ok(usize_to_u64(bytes)) -} - -fn estimated_base64_decoded_len(content: &str) -> usize { - let padding = content - .as_bytes() - .iter() - .rev() - .take_while(|byte| **byte == b'=') - .count() - .min(2); - content - .len() - .div_ceil(4) - .saturating_mul(3) - .saturating_sub(padding) -} - -fn usize_to_u64(value: usize) -> u64 { - u64::try_from(value).unwrap_or(u64::MAX) -} - -fn materialize_shadow_entries( - shadow_root: &Path, - entries: &[RootFilesystemEntry], -) -> Result<(), SidecarError> { - let mut ordered = entries.iter().collect::>(); - ordered.sort_by_key(|entry| { - let depth = entry.path.matches('/').count(); - let kind_rank = match entry.kind { - crate::protocol::RootFilesystemEntryKind::Directory => 0, - crate::protocol::RootFilesystemEntryKind::File => 1, - crate::protocol::RootFilesystemEntryKind::Symlink => 2, - }; - (kind_rank, depth, entry.path.as_str()) - }); - - for entry in ordered { - let shadow_path = shadow_path_for_guest(shadow_root, &entry.path); - if let Some(parent) = shadow_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - SidecarError::Io(format!( - "failed to create shadow parent for {}: {error}", - entry.path - )) - })?; - } - prepare_shadow_destination(&shadow_path, &entry.kind, &entry.path)?; - - match entry.kind { - crate::protocol::RootFilesystemEntryKind::Directory => { - fs::create_dir_all(&shadow_path).map_err(|error| { - SidecarError::Io(format!( - "failed to materialize shadow directory {}: {error}", - entry.path - )) - })?; - } - crate::protocol::RootFilesystemEntryKind::File => { - let bytes = decode_root_entry_content(entry)?; - fs::write(&shadow_path, bytes).map_err(|error| { - SidecarError::Io(format!( - "failed to materialize shadow file {}: {error}", - entry.path - )) - })?; - } - crate::protocol::RootFilesystemEntryKind::Symlink => { - std::os::unix::fs::symlink( - entry.target.as_deref().ok_or_else(|| { - SidecarError::InvalidState(format!( - "root filesystem symlink {} requires a target", - entry.path - )) - })?, - &shadow_path, - ) - .map_err(|error| { - SidecarError::Io(format!( - "failed to materialize shadow symlink {}: {error}", - entry.path - )) - })?; - continue; - } - } - - let mode = entry.mode.unwrap_or(match entry.kind { - crate::protocol::RootFilesystemEntryKind::Directory => 0o755, - crate::protocol::RootFilesystemEntryKind::File => { - if entry.executable { - 0o755 - } else { - 0o644 - } - } - crate::protocol::RootFilesystemEntryKind::Symlink => 0o777, - }); - fs::set_permissions(&shadow_path, fs::Permissions::from_mode(mode & 0o7777)).map_err( - |error| { - SidecarError::Io(format!( - "failed to set shadow mode on {}: {error}", - entry.path - )) - }, - )?; - } - - Ok(()) -} - -fn prepare_shadow_destination( - path: &Path, - desired_kind: &crate::protocol::RootFilesystemEntryKind, - guest_path: &str, -) -> Result<(), SidecarError> { - let existing = match fs::symlink_metadata(path) { - Ok(existing) => existing, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to inspect shadow entry {guest_path}: {error}" - ))); - } - }; - let file_type = existing.file_type(); - let already_compatible = match desired_kind { - crate::protocol::RootFilesystemEntryKind::Directory => { - file_type.is_dir() && !file_type.is_symlink() - } - crate::protocol::RootFilesystemEntryKind::File => { - file_type.is_file() && !file_type.is_symlink() - } - crate::protocol::RootFilesystemEntryKind::Symlink => false, - }; - if already_compatible { - return Ok(()); - } - - let result = if file_type.is_dir() && !file_type.is_symlink() { - fs::remove_dir_all(path) - } else { - fs::remove_file(path) - }; - result.map_err(|error| { - SidecarError::Io(format!( - "failed to replace incompatible shadow entry {guest_path}: {error}" - )) - }) -} - -fn decode_root_entry_content(entry: &RootFilesystemEntry) -> Result, SidecarError> { - let content = entry.content.as_deref().unwrap_or_default(); - match entry - .encoding - .clone() - .unwrap_or(crate::protocol::RootFilesystemEntryEncoding::Utf8) - { - crate::protocol::RootFilesystemEntryEncoding::Utf8 => Ok(content.as_bytes().to_vec()), - crate::protocol::RootFilesystemEntryEncoding::Base64 => { - base64::engine::general_purpose::STANDARD - .decode(content) - .map_err(|error| { - SidecarError::InvalidState(format!( - "invalid base64 root filesystem content for {}: {error}", - entry.path - )) - }) - } - } -} - -fn shadow_path_for_guest(shadow_root: &std::path::Path, guest_path: &str) -> PathBuf { - let normalized = normalize_guest_path(guest_path); - let relative = normalized.trim_start_matches('/'); +fn runtime_scratch_path_for_guest(runtime_root: &Path, guest_path: &str) -> PathBuf { + let relative = normalize_guest_path(guest_path); + let relative = relative.trim_start_matches('/'); if relative.is_empty() { - return shadow_root.to_path_buf(); + runtime_root.to_path_buf() + } else { + runtime_root.join(relative) } - shadow_root.join(relative) } fn normalize_guest_path(path: &str) -> String { @@ -3081,19 +2721,13 @@ fn parse_vm_dns_nameserver(value: &str) -> Result { fn refresh_guest_command_path_env( guest_env: &mut BTreeMap, - command_guest_paths: &BTreeMap, + command_search_roots: &[String], ) { let mut merged = Vec::new(); let mut seen = BTreeSet::new(); - for guest_path in command_guest_paths.values() { - let Some(parent) = Path::new(guest_path) - .parent() - .and_then(|path| path.to_str()) - else { - continue; - }; - let normalized = normalize_path(parent); + for root in command_search_roots { + let normalized = normalize_path(root); if normalized == "/" { continue; } @@ -3109,6 +2743,10 @@ fn refresh_guest_command_path_env( } } + // PATH is derived state. Strip roots managed by the command projection + // before preserving caller-supplied extras, so a removed numeric legacy + // mount cannot survive forever merely because it appeared in the previous + // synthesized PATH value. if let Some(existing_path) = guest_env.get("PATH") { for segment in existing_path.split(':') { let trimmed = segment.trim(); @@ -3120,6 +2758,9 @@ fn refresh_guest_command_path_env( } else { trimmed.to_owned() }; + if is_managed_guest_command_path_segment(&normalized) { + continue; + } if seen.insert(normalized.clone()) { merged.push(normalized); } @@ -3129,6 +2770,25 @@ fn refresh_guest_command_path_env( guest_env.insert(String::from("PATH"), merged.join(":")); } +fn is_managed_guest_command_path_segment(segment: &str) -> bool { + let normalized = if segment.starts_with('/') { + normalize_path(segment) + } else { + segment.to_owned() + }; + if DEFAULT_GUEST_PATH_ENV + .split(':') + .any(|default| normalize_path(default) == normalized) + { + return true; + } + normalized + .strip_prefix("/__secure_exec/commands/") + .is_some_and(|root| { + !root.is_empty() && !root.contains('/') && root.chars().all(|ch| ch.is_ascii_digit()) + }) +} + pub(crate) fn normalize_dns_hostname(hostname: &str) -> Result { let normalized = hostname.trim().trim_end_matches('.').to_ascii_lowercase(); if normalized.is_empty() { @@ -3161,33 +2821,28 @@ fn prune_kernel_command_stub( #[cfg(test)] mod tests { use super::{ - bootstrap_native_root_filesystem, bootstrap_shadow_root, close_vm_admission, - create_vm_unix_socket_host_dir, initialize_vm_shadow_root, - materialize_shadow_root_snapshot_entries, native_root_plugin_from_config, - prune_kernel_command_stub, retire_vm_fairness, shadow_path_for_guest, vm_quarantine_reason, + bootstrap_native_root_filesystem, close_vm_admission, create_vm_unix_socket_host_dir, + execution_driver_commands, native_root_plugin_from_config, + projected_commands_from_provided_commands, prune_kernel_command_stub, + refresh_guest_command_path_env, retire_vm_fairness, vm_quarantine_reason, vm_resource_ledger, wait_for_vm_reconciliation, CA_CERTIFICATES_BUNDLE, - CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, CA_CERTIFICATES_SYMLINK_TARGET, + CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, DEFAULT_GUEST_PATH_ENV, KERNEL_COMMAND_STUB, }; + use crate::bootstrap::KernelCommandInventory; use crate::bridge::MountPluginContext; use crate::plugins::chunked_local::ChunkedLocalMountPlugin; - use crate::protocol::{ - RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryKind, - RootFilesystemLowerDescriptor, - }; + use crate::protocol::{RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryKind}; use crate::service::NativeSidecar; use crate::state::{ ConnectionState, QuarantinedVmGeneration, SessionState, VmQuarantineReason, VmReconciliationSnapshot, }; use crate::stdio::LocalBridge; - use agentos_bridge::FilesystemSnapshot; use agentos_kernel::kernel::{KernelVm, KernelVmConfig}; use agentos_kernel::mount_plugin::{FileSystemPluginFactory, OpenFileSystemPluginRequest}; use agentos_kernel::mount_table::{MountOptions, MountTable}; use agentos_kernel::permissions::Permissions; - use agentos_kernel::resource_accounting::ResourceLimits; - use agentos_kernel::root_fs::{encode_snapshot, FilesystemEntry, RootFilesystemSnapshot}; use agentos_kernel::vfs::VirtualFileSystem; use agentos_runtime::accounting::{ResourceClass, ResourceLedger, ResourceLimit}; use agentos_runtime::capability::{CapabilityKind, CapabilityRegistry}; @@ -3197,7 +2852,6 @@ mod tests { use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::os::unix::fs::PermissionsExt; - use std::path::Path; use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; @@ -3231,6 +2885,73 @@ mod tests { (resources, runtime_context, capabilities) } + #[test] + fn guest_command_path_rebuild_drops_removed_managed_roots() { + let mut guest_env = BTreeMap::from([( + String::from("PATH"), + format!( + "/__secure_exec/commands/001:{DEFAULT_GUEST_PATH_ENV}:/custom/bin:relative:/__secure_exec/commands/custom" + ), + )]); + + refresh_guest_command_path_env( + &mut guest_env, + &[String::from("/__secure_exec/commands/002")], + ); + + assert_eq!( + guest_env.get("PATH").map(String::as_str), + Some( + "/__secure_exec/commands/002:/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin:/custom/bin:relative:/__secure_exec/commands/custom" + ) + ); + } + + #[test] + fn transient_inventory_drives_registration_and_projection_reporting() { + let kernel_commands = KernelCommandInventory { + names: BTreeSet::from([String::from("legacy"), String::from("shadowed")]), + search_roots: vec![String::from("/__secure_exec/commands/001")], + }; + let provided_commands = BTreeMap::from([ + ( + String::from("pkg-a"), + vec![String::from("visible"), String::from("shadowed")], + ), + (String::from("pkg-b"), vec![String::from("second")]), + ]); + + let registered = execution_driver_commands( + &kernel_commands, + &provided_commands, + [String::from("binding"), String::from("visible")], + ); + assert_eq!( + registered.iter().collect::>().len(), + registered.len() + ); + for expected in [ + "binding", "legacy", "node", "python", "python3", "second", "shadowed", "visible", + "wasm", + ] { + assert!(registered.iter().any(|command| command == expected)); + } + + assert_eq!( + projected_commands_from_provided_commands(&provided_commands, &kernel_commands), + vec![ + crate::protocol::ProjectedCommand { + name: String::from("second"), + guest_path: String::from("/opt/agentos/bin/second"), + }, + crate::protocol::ProjectedCommand { + name: String::from("visible"), + guest_path: String::from("/opt/agentos/bin/visible"), + }, + ] + ); + } + #[test] fn vm_runtime_bounds_every_resource_class_by_default() { let process = SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) @@ -3576,95 +3297,6 @@ mod tests { ); } } - - #[test] - fn bootstrap_shadow_root_seeds_standard_directories() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-test-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let tmp = shadow_path_for_guest(&root, "/tmp"); - let etc_agentos = shadow_path_for_guest(&root, "/etc/agentos"); - let usr_local_bin = shadow_path_for_guest(&root, "/usr/local/bin"); - - assert!(tmp.is_dir(), "/tmp should exist in the shadow root"); - assert!( - etc_agentos.is_dir(), - "/etc/agentos should exist in the shadow root" - ); - assert!( - usr_local_bin.is_dir(), - "/usr/local/bin should exist in the shadow root" - ); - assert_eq!( - fs::metadata(&tmp) - .expect("/tmp metadata should be readable") - .permissions() - .mode() - & 0o7777, - 0o1777, - "/tmp should preserve its sticky-bit mode in the shadow root" - ); - - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn bootstrap_shadow_root_seeds_ca_bundle_when_present() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = std::env::temp_dir().join(format!("agentos-native-sidecar-ca-test-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let bundle = shadow_path_for_guest(&root, CA_CERTIFICATES_GUEST_PATH); - let symlink = shadow_path_for_guest(&root, CA_CERTIFICATES_SYMLINK_PATH); - - assert!(!CA_CERTIFICATES_BUNDLE.is_empty()); - let seeded = fs::read(&bundle).expect("CA bundle should be seeded"); - assert_eq!( - seeded, CA_CERTIFICATES_BUNDLE, - "seeded CA bundle should match the embedded asset" - ); - let target = fs::read_link(&symlink).expect("cert.pem symlink should be seeded"); - assert_eq!( - target, - Path::new(CA_CERTIFICATES_SYMLINK_TARGET), - "cert.pem should point at certs/ca-certificates.crt" - ); - - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn failed_shadow_bootstrap_removes_temporary_root() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-failure-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - fs::write(root.join("dev"), b"blocks directory creation") - .expect("blocking file should be created"); - - initialize_vm_shadow_root(root.clone()) - .expect_err("invalid shadow scaffold should fail bootstrap"); - assert!( - !root.exists(), - "failed bootstrap must not leak its temporary shadow root" - ); - } - #[test] fn native_root_config_opens_chunked_local_as_persistent_root() { let unique = SystemTime::now() @@ -3740,17 +3372,15 @@ mod tests { filesystem, MountOptions::new(native_root.plugin.id.clone()), ); - assert!(mount_table.exists("/home/agentos")); - let home = mount_table - .stat("/home/agentos") - .expect("native AgentOS home metadata should be readable"); - assert_eq!((home.uid, home.gid), (1000, 1000)); + let home = mount_table.stat("/home/agentos").expect("stat guest home"); assert_eq!(home.mode & 0o7777, 0o2755); - let workspace = mount_table - .stat("/workspace") - .expect("native workspace metadata should be readable"); - assert_eq!((workspace.uid, workspace.gid), (1000, 1000)); + assert_eq!((home.uid, home.gid), (1000, 1000)); + let workspace = mount_table.stat("/workspace").expect("stat workspace"); assert_eq!(workspace.mode & 0o7777, 0o755); + assert_eq!((workspace.uid, workspace.gid), (1000, 1000)); + let root_home = mount_table.stat("/root").expect("stat root home"); + assert_eq!(root_home.mode & 0o7777, 0o711); + assert_eq!((root_home.uid, root_home.gid), (0, 0)); assert_eq!( mount_table .read_file("/etc/agentos/boot.txt") @@ -3812,307 +3442,4 @@ mod tests { let _ = fs::remove_file(database_path); let _ = fs::remove_dir_all(block_root); } - - #[test] - fn custom_shadow_ca_files_replace_seeded_defaults_without_following_symlinks() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-custom-ca-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let descriptor = RootFilesystemDescriptor { - bootstrap_entries: vec![ - RootFilesystemEntry { - path: "/custom/ca.pem".to_string(), - kind: RootFilesystemEntryKind::File, - content: Some("custom bundle\n".to_string()), - ..Default::default() - }, - RootFilesystemEntry { - path: CA_CERTIFICATES_GUEST_PATH.to_string(), - kind: RootFilesystemEntryKind::Symlink, - target: Some("../../../custom/ca.pem".to_string()), - ..Default::default() - }, - RootFilesystemEntry { - path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), - kind: RootFilesystemEntryKind::File, - content: Some("custom cert.pem\n".to_string()), - ..Default::default() - }, - ], - ..RootFilesystemDescriptor::default() - }; - - materialize_shadow_root_snapshot_entries( - &root, - &descriptor, - None, - &ResourceLimits::default(), - ) - .expect("custom CA entries should materialize"); - - let bundle = shadow_path_for_guest(&root, CA_CERTIFICATES_GUEST_PATH); - let cert_pem = shadow_path_for_guest(&root, CA_CERTIFICATES_SYMLINK_PATH); - assert_eq!( - fs::read(&bundle).expect("read custom bundle through custom symlink"), - b"custom bundle\n" - ); - assert_eq!( - fs::read_link(&bundle).expect("read custom CA bundle symlink"), - Path::new("../../../custom/ca.pem"), - "a custom symlink must replace the seeded regular bundle" - ); - assert_eq!( - fs::read(&cert_pem).expect("read custom regular cert.pem"), - b"custom cert.pem\n" - ); - assert!( - !fs::symlink_metadata(cert_pem) - .expect("lstat custom cert.pem") - .file_type() - .is_symlink(), - "custom cert.pem must replace rather than follow the seeded symlink" - ); - - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn materialize_shadow_root_snapshot_entries_rejects_oversized_legacy_restored_snapshots() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-limit-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let snapshot = RootFilesystemSnapshot { - entries: vec![FilesystemEntry::file("/large.txt", b"four".to_vec())], - }; - let loaded_snapshot = FilesystemSnapshot { - format: String::from("agentos_filesystem_snapshot_v1"), - bytes: encode_snapshot(&snapshot).expect("encode restored snapshot"), - }; - let resource_limits = ResourceLimits { - max_filesystem_bytes: Some(3), - ..ResourceLimits::default() - }; - - let error = materialize_shadow_root_snapshot_entries( - &root, - &RootFilesystemDescriptor::default(), - Some(&loaded_snapshot), - &resource_limits, - ) - .expect_err("oversized restored snapshot should be rejected"); - - assert!(error.to_string().contains("exceeding limit 3")); - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn materialize_shadow_root_snapshot_entries_rejects_oversized_descriptor_before_writes() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-descriptor-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let descriptor = RootFilesystemDescriptor { - lowers: vec![RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower( - crate::protocol::SnapshotRootFilesystemLower { - entries: vec![RootFilesystemEntry { - path: String::from("/large.txt"), - kind: RootFilesystemEntryKind::File, - mode: Some(0o644), - uid: Some(0), - gid: Some(0), - content: Some(String::from("four")), - encoding: Some(crate::protocol::RootFilesystemEntryEncoding::Utf8), - target: None, - executable: false, - }], - }, - )], - ..RootFilesystemDescriptor::default() - }; - let resource_limits = ResourceLimits { - max_filesystem_bytes: Some(3), - ..ResourceLimits::default() - }; - - let error = - materialize_shadow_root_snapshot_entries(&root, &descriptor, None, &resource_limits) - .expect_err("oversized descriptor should be rejected"); - - assert!(error.to_string().contains("exceeding limit 3")); - assert!( - !shadow_path_for_guest(&root, "/large.txt").exists(), - "oversized descriptor must be rejected before materializing files" - ); - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn materialize_shadow_root_snapshot_entries_counts_implicit_parent_directories() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-parents-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let descriptor = RootFilesystemDescriptor { - lowers: vec![RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower( - crate::protocol::SnapshotRootFilesystemLower { - entries: vec![RootFilesystemEntry { - path: String::from("/deep/nested/file.txt"), - kind: RootFilesystemEntryKind::File, - mode: Some(0o644), - uid: Some(0), - gid: Some(0), - content: Some(String::from("x")), - encoding: Some(crate::protocol::RootFilesystemEntryEncoding::Utf8), - target: None, - executable: false, - }], - }, - )], - ..RootFilesystemDescriptor::default() - }; - let resource_limits = ResourceLimits { - max_inode_count: Some(1), - ..ResourceLimits::default() - }; - - let error = - materialize_shadow_root_snapshot_entries(&root, &descriptor, None, &resource_limits) - .expect_err("implicit parents should be rejected"); - - assert!(error.to_string().contains("exceeding limit 1")); - assert!( - !shadow_path_for_guest(&root, "/deep").exists(), - "implicit parents must not be materialized after rejection" - ); - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn materialize_shadow_root_snapshot_entries_rejects_duplicate_descriptor_entries() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-duplicates-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let duplicate_entry = RootFilesystemEntry { - path: String::from("/dup.txt"), - kind: RootFilesystemEntryKind::File, - mode: Some(0o644), - uid: Some(0), - gid: Some(0), - content: Some(String::new()), - encoding: Some(crate::protocol::RootFilesystemEntryEncoding::Utf8), - target: None, - executable: false, - }; - let descriptor = RootFilesystemDescriptor { - lowers: vec![RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower( - crate::protocol::SnapshotRootFilesystemLower { - entries: vec![duplicate_entry.clone(), duplicate_entry], - }, - )], - ..RootFilesystemDescriptor::default() - }; - let resource_limits = ResourceLimits { - max_inode_count: Some(1), - ..ResourceLimits::default() - }; - - let error = - materialize_shadow_root_snapshot_entries(&root, &descriptor, None, &resource_limits) - .expect_err("duplicate descriptor entries should be rejected"); - - assert!(error.to_string().contains("exceeding limit 1")); - assert!( - !shadow_path_for_guest(&root, "/dup.txt").exists(), - "duplicate descriptor must be rejected before materializing files" - ); - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } - - #[test] - fn materialize_shadow_root_snapshot_entries_copies_custom_snapshot_files() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let root = - std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-snapshot-{unique}")); - fs::create_dir_all(&root).expect("temp shadow root should be created"); - bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); - - let descriptor = RootFilesystemDescriptor { - lowers: vec![RootFilesystemLowerDescriptor::SnapshotRootFilesystemLower( - crate::protocol::SnapshotRootFilesystemLower { - entries: vec![ - RootFilesystemEntry { - path: String::from("/"), - kind: RootFilesystemEntryKind::Directory, - mode: Some(0o755), - uid: Some(0), - gid: Some(0), - content: None, - encoding: None, - target: None, - executable: false, - }, - RootFilesystemEntry { - path: String::from("/hello.txt"), - kind: RootFilesystemEntryKind::File, - mode: Some(0o644), - uid: Some(0), - gid: Some(0), - content: Some(String::from("hello from snapshot\n")), - encoding: Some(crate::protocol::RootFilesystemEntryEncoding::Utf8), - target: None, - executable: false, - }, - ], - }, - )], - ..RootFilesystemDescriptor::default() - }; - - materialize_shadow_root_snapshot_entries( - &root, - &descriptor, - None, - &ResourceLimits::default(), - ) - .expect("snapshot entries should materialize into the shadow root"); - - assert_eq!( - fs::read_to_string(shadow_path_for_guest(&root, "/hello.txt")) - .expect("shadow file should be readable"), - "hello from snapshot\n" - ); - - fs::remove_dir_all(&root).expect("temp shadow root should be removed"); - } } diff --git a/crates/native-sidecar/tests/architecture_guards.rs b/crates/native-sidecar/tests/architecture_guards.rs index 09039f098d..b26652db23 100644 --- a/crates/native-sidecar/tests/architecture_guards.rs +++ b/crates/native-sidecar/tests/architecture_guards.rs @@ -57,20 +57,869 @@ fn repo_root() -> PathBuf { .to_path_buf() } +#[test] +fn managed_v8_filesystem_state_is_kernel_authoritative() { + let root = repo_root(); + let runner = + std::fs::read_to_string(root.join("crates/execution/assets/runners/wasm-runner.mjs")) + .expect("read managed WASM runner"); + let wasi = std::fs::read_to_string(root.join("crates/execution/assets/runners/wasi-module.js")) + .expect("read WASI module"); + let filesystem = std::fs::read_to_string(root.join("crates/native-sidecar/src/filesystem.rs")) + .expect("read sidecar filesystem service"); + let rpc = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/javascript/rpc.rs")) + .expect("read JavaScript process RPC service"); + + for stale_shadow in [ + "hostFsSizeByGuestPath", + "rememberHostFsSize", + "rememberedHostFsSize", + "forgetHostFsSize", + ] { + assert!( + !runner.contains(stale_shadow), + "managed runner must not retain mutable path shadow {stale_shadow}" + ); + } + assert!( + runner.contains( + "if (!Number.isFinite(nextSize) || nextSize < 0) {\n return WASI_ERRNO_INVAL;" + ), + "host ftruncate must return the typed EINVAL value, never a numeric sentinel" + ); + + let path_open = runner + .find(" wasiImport.path_open = (") + .expect("managed path_open wrapper"); + let path_open = &runner[path_open..]; + let kernel_open = path_open + .find("callSyncRpc('process.path_open_at'") + .expect("dirfd-aware kernel path_open"); + let kernel_registration = path_open + .find("registerKernelDelegateFd(kernelFd)") + .expect("kernel descriptor registration"); + let ambient_delegate = path_open + .find("() => delegatePathOpen(") + .expect("standalone WASI path_open fallback"); + assert!( + kernel_open < kernel_registration && kernel_registration < ambient_delegate, + "managed path_open must return a kernel open description before standalone WASI fallback" + ); + + assert!( + path_open.contains( + "const procFdResult = SIDECAR_MANAGED_PROCESS\n ? null\n : openProcSelfFdAlias(" + ), + "managed /proc/self/fd aliases must flow through capability-aware kernel path_open" + ); + + for (start, end) in [ + (" path_owner(", " fd_owner("), + (" path_mode(", " path_size("), + (" path_size(", " path_blocks("), + (" path_blocks(", " path_rdev("), + (" path_rdev(", " chmod("), + ] { + let start = runner + .find(start) + .unwrap_or_else(|| panic!("missing {start}")); + let section = &runner[start..]; + let end = section.find(end).unwrap_or_else(|| panic!("missing {end}")); + assert!( + section[..end].contains("callSyncRpc('process.path_stat_at'"), + "{start} must read dirfd-relative metadata from the kernel" + ); + } + + let wasi_path_open = wasi.find(" _pathOpen(").expect("WASI path_open"); + let wasi_path_open = &wasi[wasi_path_open..]; + assert!( + wasi_path_open + .find("if (this._sidecarManagedProcess())") + .expect("managed ambient-open rejection") + < wasi_path_open + .find("__agentOSFs().openSync") + .expect("standalone Node-fs open"), + "managed WASI must reject ambient Node-fs path_open fallback" + ); + + for field in ["mode", "uid", "gid", "blocks", "rdev"] { + assert!( + rpc.contains(&format!("\"{field}\": stat.{field}")), + "kernel stat RPC must expose {field} without a runner-side metadata shadow" + ); + } + assert!( + filesystem.contains("struct ProcessModuleFsReader") + && filesystem.contains("read_file_for_process(") + && filesystem.contains("let reader = ProcessModuleFsReader"), + "production JavaScript module loading must resolve against the live kernel VFS" + ); +} + +#[test] +fn managed_wasm_uses_one_sidecar_owned_posix_poll() { + let root = repo_root(); + let runner = + std::fs::read_to_string(root.join("crates/execution/assets/runners/wasm-runner.mjs")) + .expect("read managed WASM runner"); + let sidecar = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/network_compat.rs"), + ) + .expect("read POSIX poll dispatcher"); + + let net_poll = runner + .split(" net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr, temporarySignalMask = null) {") + .nth(1) + .expect("managed net_poll implementation"); + let managed = net_poll + .split(" const startedAt = Date.now();") + .next() + .expect("managed poll fast path"); + assert!( + managed.contains("callSyncRpc('process.posix_poll'") + && managed.contains("temporarySignalMask"), + "managed poll and ppoll must use one typed sidecar RPC carrying the optional mask" + ); + for split_wait in [ + "callSyncRpc('__kernel_poll'", + "callSyncRpc('process.hostnet_poll'", + "pumpSpawnedChildren(", + "Atomics.wait(", + ] { + assert!( + !managed.contains(split_wait), + "managed poll must not regress to a split/pumped wait: {split_wait}" + ); + } + + let ppoll = runner + .split(" proc_ppoll_v1(") + .nth(1) + .expect("ppoll ABI implementation") + .split(" },") + .next() + .expect("ppoll ABI body"); + assert!(ppoll.contains("hostNetImport.net_poll(")); + assert!(!ppoll.contains("signal_mask_scope_begin")); + assert!(!ppoll.contains("signal_mask_scope_end")); + + assert!( + sidecar.contains("pub(in crate::execution) fn service_deferred_posix_poll") + && sidecar.contains("task_notify.notified()") + && sidecar.contains("wait_handle.wait_for_change_async(observed)"), + "the sidecar must own one coalesced managed/kernel/deadline wait task" + ); +} + +#[test] +fn managed_blocking_socket_operations_wait_through_posix_poll() { + let root = repo_root(); + let runner = + std::fs::read_to_string(root.join("crates/execution/assets/runners/wasm-runner.mjs")) + .expect("read managed WASM runner"); + let helper = runner + .split("function waitManagedHostNetReadable(") + .nth(1) + .expect("managed socket wait helper") + .split("\n}") + .next() + .expect("managed socket wait body"); + assert!(helper.contains("callSyncRpc('process.posix_poll'")); + assert!(helper.contains("dispatchPendingWasmSignals(restartableOperation === true)")); + assert!(helper.contains("pumpSpawnedChildren(0)")); + assert!(helper.contains("Math.min(SPAWNED_CHILD_WAIT_SLICE_MS")); + assert!(helper.contains("deadline - Date.now()")); + + for (operation, managed_end) in [ + ("net_accept", "\n if (!socket.serverId"), + ("net_recv", "\n if (hostNetSocketBaseType"), + ("net_recvfrom", "\n const udpSocketId"), + ] { + let body = runner + .split(&format!(" {operation}(")) + .nth(1) + .unwrap_or_else(|| panic!("{operation} implementation")); + let body = body + .split("\n },") + .next() + .expect("operation body boundary"); + let managed = body + .split("managed === true") + .nth(1) + .unwrap_or_else(|| panic!("{operation} managed path")); + let managed = managed + .split(managed_end) + .next() + .unwrap_or_else(|| panic!("{operation} managed path boundary")); + assert!( + managed.contains("waitManagedHostNetReadable"), + "managed {operation} must use the interruptible combined wait" + ); + assert!( + !managed.contains("pumpSpawnedChildrenOrWaitRestartable"), + "managed {operation} must not poll/pump on a timer" + ); + } +} + +#[test] +fn sidecar_publishes_only_one_signal_delivery_scope_at_a_time() { + let root = repo_root(); + let process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read process owner"); + let published = process + .split("Ok(SignalCheckpointOutcome::Published) => {") + .nth(1) + .expect("published signal branch") + .split("Ok(SignalCheckpointOutcome::ForwardToProcess") + .next() + .expect("published signal branch end"); + assert!( + published.contains("break;") && !published.contains("continue;"), + "kernel signal delivery tokens are strict LIFO; publish one and wait for signal_end" + ); +} + +#[test] +fn phase_one_contains_no_wasmtime_implementation_or_dependency() { + let root = repo_root(); + let lock = std::fs::read_to_string(root.join("Cargo.lock")).expect("read Cargo.lock"); + assert!( + !lock.contains("name = \"wasmtime") && !lock.contains("name = \"wasmtime-"), + "Wasmtime dependencies belong to the later executor revision, not the prerequisite refactor" + ); + + let mut manifests = vec![root.join("Cargo.toml")]; + for entry in std::fs::read_dir(root.join("crates")).expect("read workspace crates") { + let path = entry + .expect("read workspace crate entry") + .path() + .join("Cargo.toml"); + if path.is_file() { + manifests.push(path); + } + } + for manifest in manifests { + let source = std::fs::read_to_string(&manifest) + .unwrap_or_else(|error| panic!("read {}: {error}", manifest.display())); + for line in source.lines().map(strip_line_comment) { + let compact = line + .chars() + .filter(|ch| !ch.is_whitespace()) + .collect::(); + assert!( + !compact.starts_with("wasmtime=") + && !compact.starts_with("wasmtime-") + && !compact.contains("package=\"wasmtime"), + "Wasmtime dependency appeared in {}: {}", + manifest.display(), + line.trim() + ); + } + } + + for relative in production_source_files(&root) { + let source = std::fs::read_to_string(root.join(&relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + let production = production_source_text(&source); + assert!( + !production.contains("wasmtime::") && !production.contains("extern crate wasmtime"), + "Wasmtime implementation code appeared before Phase 1 closed in {}", + relative.display() + ); + } +} + +#[test] +fn kernel_process_table_is_the_only_durable_signal_state_owner() { + let root = repo_root(); + let process_table = std::fs::read_to_string(root.join("crates/kernel/src/process_table.rs")) + .expect("read kernel process table"); + let record = rust_braced_item(&process_table, "struct ProcessRecord {"); + for field in [ + "blocked_signals: SignalSet", + "pending_signals: SignalSet", + "signal_actions: [SignalAction", + "signal_deliveries: Vec", + "temporary_signal_masks: Vec", + ] { + assert!( + record.contains(field), + "kernel ProcessRecord is missing {field}" + ); + } + let schedule = rust_braced_item(&process_table, "fn queue_or_schedule_signal("); + assert!( + schedule.contains("record.pending_signals.insert(signal)?") + && schedule.contains("ProcessControlRequest::Checkpoint") + && schedule.contains("ProcessControlRequest::Stop") + && schedule.contains("ProcessControlRequest::Terminate"), + "one kernel path must decide pending, caught, stop, and terminating signal behavior" + ); + let delivery = rust_braced_item(&process_table, "fn deliver_signals("); + assert!( + delivery.contains("delivery.runtime_endpoint.request_control(*request)"), + "kernel signal decisions must reach adapters only through the runtime endpoint" + ); + + let signals = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/signals.rs")) + .expect("read signal adapter"); + let registration = + rust_braced_item(&signals, "pub(crate) fn apply_kernel_signal_registration("); + assert!( + registration.contains("process") + && registration.contains(".kernel_handle") + && registration.contains(".signal_action(signal, Some(action))"), + "adapter registrations must update the authoritative kernel record directly" + ); + + let non_kernel_files = production_source_files(&root) + .into_iter() + .filter(|path| { + path.starts_with("crates/execution/src") + || path.starts_with("crates/native-sidecar/src") + }) + .collect::>(); + let duplicated_owner_fields = production_matches( + &root, + &non_kernel_files, + &[ + "blocked_signals:", + "pending_signals:", + "signal_actions:", + "signal_deliveries:", + "temporary_signal_masks:", + ], + ); + assert!( + duplicated_owner_fields.is_empty(), + "durable signal state escaped the kernel process table:\n{}", + duplicated_owner_fields.join("\n") + ); +} + +#[test] +fn common_posix_semantics_do_not_switch_on_executor_variants() { + let root = repo_root(); + + // Capability owners and reactors are engine-blind without an allowlist. + // Any future occurrence in these directories is a hard architecture + // regression, not a line to append to a sampled list. + for relative in production_source_files(&root).into_iter().filter(|path| { + path.starts_with("crates/native-sidecar/src/execution/host_dispatch") + || path.starts_with("crates/native-sidecar/src/execution/network") + || matches!( + path.to_string_lossy().as_ref(), + "crates/native-sidecar/src/execution/signals.rs" + | "crates/native-sidecar/src/execution/stdio.rs" + | "crates/native-sidecar/src/execution/process_events.rs" + | "crates/native-sidecar/src/execution/coordinator.rs" + | "crates/native-sidecar/src/filesystem.rs" + ) + }) { + let source = std::fs::read_to_string(root.join(&relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + let production = production_source_text(&source); + for forbidden in [ + "GuestRuntimeKind", + "ActiveExecution::", + "ExecutionBackendKind", + ] { + assert!( + !production.contains(forbidden), + "common POSIX owner {} contains executor switch token {forbidden}", + relative.display() + ); + } + } + + let child = production_source_text( + &std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/child_process.rs")) + .expect("read child process service"), + ); + for forbidden in [ + "process.runtime == GuestRuntimeKind", + "process.runtime != GuestRuntimeKind", + "resolved.runtime == GuestRuntimeKind", + "resolved.runtime != GuestRuntimeKind", + "current_runtime == GuestRuntimeKind", + "current_runtime != GuestRuntimeKind", + ] { + assert!( + !child.contains(forbidden), + "child process semantics switched on executor identity: {forbidden}" + ); + } + assert_eq!( + child.matches("match resolved.runtime {").count(), + 3, + "resolved-runtime matches are confined to direct spawn, exec replacement, and nested spawn adapter construction" + ); + assert!( + child.contains("resolved.adapter_policy.accepts_inherited_host_network_fds") + && child.contains("resolved.adapter_policy.materializes_direct_runtime_stdio") + && child.contains("resolved.adapter_policy.canonicalizes_runtime_stdin") + && child.contains("supports_prepared_in_place_exec"), + "common process code must consume explicit adapter capabilities" + ); + + let process = production_source_text( + &std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read ActiveProcess implementation"), + ); + let adapter_impl = process + .find("impl ActiveExecution {") + .expect("ActiveExecution adapter implementation"); + assert!( + !process[..adapter_impl].contains("ActiveExecution::"), + "ActiveProcess common lifecycle must call the backend contract instead of matching its storage enum" + ); + + let rpc = production_source_text( + &std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/javascript/rpc.rs"), + ) + .expect("read compatibility RPC decoder"), + ); + assert!( + !rpc.contains("process.runtime == GuestRuntimeKind") + && rpc.contains("process.execution.synchronous_fd_write_policy()"), + "descriptor write semantics must use an explicit backend policy" + ); +} + +#[test] +fn production_backends_route_typed_host_calls_through_family_capabilities() { + let root = repo_root(); + let process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read active execution adapter"); + let events = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process_events.rs")) + .expect("read execution event service"); + let dispatcher = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/mod.rs"), + ) + .expect("read shared host dispatcher"); + + // JavaScript, Python's embedded-JavaScript bridge, and compatibility WASM + // all use the same decoder before a sidecar semantic operation is chosen. + assert_eq!( + process + .matches("return route_compatibility_host_call(") + .count(), + 3, + "every production V8-backed adapter must use the bound common host-call submission path" + ); + assert!( + process.contains("ExecutionBackend::configure_host_services") + && process.contains("poll_event_with_host(") + && process.contains("try_poll_event_with_host("), + "production backends must receive host services before start and submit through them" + ); + assert!( + events.contains("ActiveExecutionEvent::Common(ExecutionEvent::HostCall"), + "the production event pump must consume common host-call events" + ); + assert!( + events.contains("dispatch_host_operation(generation, kernel, process, operation, reply)"), + "common host calls must reach the shared sidecar dispatcher" + ); + + for family in [ + "FilesystemCapability", + "NetworkCapability", + "ProcessCapability", + "TerminalCapability", + "SignalCapability", + "IdentityCapability", + "ClockCapability", + "EntropyCapability", + ] { + assert!( + dispatcher.contains(family), + "shared dispatcher is missing production {family} routing" + ); + } + for family in [ + "filesystem", + "network", + "process", + "terminal", + "signal", + "identity", + "clock", + "entropy", + ] { + let path = root.join(format!( + "crates/native-sidecar/src/execution/host_dispatch/{family}.rs" + )); + let source = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("read {}: {error}", path.display())); + assert!( + source.contains("impl SidecarHostCapability<"), + "{family} must have a production capability implementation" + ); + } +} + +#[test] +fn loopback_vm_fetch_uses_the_vm_scoped_event_pump() { + let coordinator = include_str!("../src/execution/coordinator.rs"); + let http = include_str!("../src/execution/javascript/http.rs"); + let vm_fetch = coordinator + .split_once("pub(crate) async fn vm_fetch(") + .expect("vm.fetch coordinator") + .1 + .split_once("pub(crate) async fn get_signal_state(") + .expect("end of vm.fetch coordinator") + .0; + + for required in [ + "begin_loopback_http_request", + "self.pump_process_events(&ownership).await", + "process_event_notify.notified()", + "take_loopback_http_response", + ] { + assert!( + vm_fetch.contains(required), + "loopback vm.fetch must retain main event-pump step {required}" + ); + } + assert!( + !http.contains("dispatch_host_operation"), + "loopback HTTP must not bypass VM-scoped context dispatch through the kernel-only fallback" + ); +} + +#[test] +fn shared_tcp_connect_has_no_blocking_native_fallback() { + let root = repo_root(); + let tcp = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/network/tcp.rs")) + .expect("read shared TCP implementation"); + + assert!( + !tcp.contains("TcpStream::connect_timeout"), + "native TCP connects must be deferred to the shared Tokio reactor" + ); + assert!( + tcp.contains( + "native TCP connect reached the synchronous constructor without reactor deferral" + ), + "the synchronous constructor must fail closed when a caller misses reactor deferral" + ); +} + +#[test] +fn compatibility_wasm_rpc_inventory_matches_runner_literals() { + let root = repo_root(); + let runner = + std::fs::read_to_string(root.join("crates/execution/assets/runners/wasm-runner.mjs")) + .expect("read compatibility WASM runner"); + let inventory = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/inventory.rs"), + ) + .expect("read reviewed compatibility WASM RPC inventory"); + + fn quoted_values(section: &str) -> BTreeSet { + section + .lines() + .filter_map(|line| { + let line = line.trim(); + line.strip_prefix('"') + .and_then(|line| line.strip_suffix(",")) + .and_then(|line| line.strip_suffix('"')) + .map(str::to_owned) + }) + .collect() + } + + let mut dynamic_targets = Vec::new(); + let runner_methods = runner + .match_indices("callSyncRpc(") + .filter_map(|(offset, marker)| { + let tail = runner[offset + marker.len()..].trim_start(); + let quote = tail.chars().next()?; + if quote != '\'' && quote != '"' { + if runner[..offset].ends_with("function ") { + return None; + } + let line = runner[..offset] + .rsplit_once('\n') + .map(|(_, line)| line) + .unwrap_or(&runner[..offset]); + dynamic_targets.push(format!( + "{}{}", + line.trim(), + tail.lines().next().unwrap_or("") + )); + return None; + } + let tail = &tail[quote.len_utf8()..]; + let end = tail.find(quote)?; + Some(tail[..end].to_owned()) + }) + .collect::>(); + assert!(dynamic_targets.is_empty(), + "compatibility WASM runner callSyncRpc targets must be literal; add every target to the frozen inventory: {dynamic_targets:?}" + ); + let inventory_section = inventory + .split_once("WASM_RUNNER_RPC_INVENTORY: &[&str] = &[") + .expect("inventory declaration") + .1 + .split_once("\n];") + .expect("inventory terminator") + .0; + let frozen_methods = quoted_values(inventory_section); + assert_eq!( + frozen_methods, runner_methods, + "update and review the typed/adapter-only WASM RPC inventory whenever the runner changes" + ); + + // The compatibility bootstrap hides a second semantic surface behind one + // generic `process.wasm_sync_rpc` method. Freeze both the wrapper switch + // and its delta from the literal runner inventory; otherwise a Linux call + // can bypass the typed decoder without changing wasm-runner.mjs. + let bootstrap = std::fs::read_to_string(root.join("crates/execution/src/wasm.rs")) + .expect("read compatibility WASM bootstrap"); + let rpc = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/javascript/rpc.rs")) + .expect("read compatibility RPC allowlist"); + let allowed_section = rpc + .split_once("const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[") + .expect("wrapped RPC allowlist") + .1 + .split_once("\n];") + .expect("wrapped RPC allowlist terminator") + .0; + let allowed = quoted_values(allowed_section); + let wrapped_switch = bootstrap + .split_once("case \"process.exec_image_open\":") + .expect("wrapped RPC switch") + .1 + .split_once("_processWasmSyncRpc.applySync") + .expect("wrapped RPC dispatch") + .0; + let mut emitted_wrapped = wrapped_switch + .lines() + .filter_map(|line| { + line.trim() + .strip_prefix("case \"") + .and_then(|line| line.strip_suffix("\":")) + .map(str::to_owned) + }) + .collect::>(); + emitted_wrapped.insert(String::from("process.exec_image_open")); + assert_eq!( + allowed, emitted_wrapped, + "the generic WASM wrapper switch and sidecar allowlist must remain exact" + ); + + let wrapped_only_section = inventory + .split_once("WASM_WRAPPED_ONLY_RPC_INVENTORY: &[&str] = &[") + .expect("wrapped-only inventory declaration") + .1 + .split_once("\n];") + .expect("wrapped-only inventory terminator") + .0; + let frozen_wrapped_only = quoted_values(wrapped_only_section); + let actual_wrapped_only = allowed + .difference(&runner_methods) + .cloned() + .collect::>(); + assert_eq!( + frozen_wrapped_only, actual_wrapped_only, + "review every semantic RPC hidden behind process.wasm_sync_rpc" + ); + + let adapter_only_section = inventory + .split_once("WASM_ADAPTER_ONLY_RPCS: &[&str] = &[") + .expect("adapter-only inventory declaration") + .1 + .split_once("\n];") + .expect("adapter-only inventory terminator") + .0; + assert_eq!( + quoted_values(adapter_only_section), + BTreeSet::from([ + String::from("fs.blockingIoTimeoutMsSync"), + String::from("process.fd_description_alias_count"), + String::from("process.fd_description_identity"), + String::from("process.fd_snapshot"), + ]), + "only read-only V8 projection/configuration queries may bypass typed Linux operations" + ); + + let filesystem = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/filesystem.rs"), + ) + .expect("read typed filesystem decoder"); + assert!( + filesystem.contains("for method in semantic_rpc_inventory()") + && filesystem.contains("must not fall through to the legacy bridge"), + "the typed decoder proof must cover the union of literal and wrapped-only RPCs" + ); + + let legacy_exec_routes = production_matches( + &root, + &[ + PathBuf::from("crates/native-sidecar/src/execution/child_process.rs"), + PathBuf::from("crates/native-sidecar/src/service.rs"), + ], + &[ + "request.method == \"process.exec\"", + "request.method == \"process.exec_fd_image_commit\"", + "\"process.exec\" =>", + "\"process.exec_fd_image_commit\" =>", + ], + ); + assert!( + legacy_exec_routes.is_empty(), + "execve/fexecve must enter as typed ProcessOperation::Exec, never a legacy HostRpcRequest:\n{}", + legacy_exec_routes.join("\n") + ); +} + +#[test] +fn compatibility_wasm_filesystem_inventory_uses_only_typed_kernel_dispatch() { + let root = repo_root(); + let router = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/mod.rs"), + ) + .expect("read host dispatcher"); + let filesystem = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/host_dispatch/filesystem.rs"), + ) + .expect("read filesystem capability"); + let process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read execution event mapper"); + + assert!( + router.contains("_ if full_filesystem =>") + && router.contains( + "filesystem::decode(request, full_filesystem, max_reply_bytes)?" + ), + "the compatibility-WASM router must delegate unmatched calls to the bounded typed filesystem decoder" + ); + let wasm_mapper = process + .split_once("fn map_wasm_execution_event_with_host(") + .expect("WASM mapper") + .1 + .split_once("pub(super) fn find_socket_state_entry") + .expect("end WASM mapper") + .0; + assert!( + wasm_mapper.contains("route_compatibility_host_call(") + && wasm_mapper.contains("true,") + && wasm_mapper.contains("max_reply_bytes,"), + "compatibility WASM must enable complete typed filesystem decoding" + ); + for forbidden in [ + "service_javascript_fs_sync_rpc", + "service_javascript_sync_rpc", + "JavascriptSyncRpcServiceRequest", + "guest_filesystem_call", + "handle_guest_filesystem_call", + "javascript::rpc", + ] { + assert!( + !filesystem.contains(forbidden), + "typed filesystem capability must not delegate to legacy RPC service {forbidden}" + ); + } +} + +#[test] +fn typed_process_dispatch_cannot_reconstruct_javascript_launch_protocol_types() { + let dispatcher = include_str!("../src/execution/host_dispatch/mod.rs"); + let process_capability = include_str!("../src/execution/host_dispatch/process.rs"); + for (label, source) in [ + ("host dispatcher", dispatcher), + ("process capability", process_capability), + ] { + for forbidden in [ + "JavascriptChildProcessSpawnRequest", + "JavascriptChildProcessSpawnOptions", + "JavascriptPosixSpawnFileAction", + "JavascriptSpawnHostNetFd", + ] { + assert!( + !source.contains(forbidden), + "{label} reconstructs compatibility-only {forbidden} below the adapter decoder" + ); + } + } + + let contract = include_str!("../../execution/src/host/process.rs"); + assert!( + contract.contains("Spawn(BoundedProcessLaunchRequest)") + && contract.contains("Exec(BoundedProcessLaunchRequest)"), + "queued process launch operations must retain their payload admission proof" + ); +} + +#[test] +fn neutral_capability_execution_files_do_not_depend_on_executor_protocol_types() { + let network = include_str!("../src/execution/host_dispatch/network.rs"); + let filesystem = include_str!("../src/execution/host_dispatch/filesystem.rs"); + let filesystem_execution = filesystem + .split_once("pub(super) struct FilesystemCapability") + .expect("filesystem capability marker") + .1 + .split_once("#[cfg(test)]") + .expect("filesystem capability test marker") + .0; + for (label, source) in [ + ("network", network), + ("filesystem execution", filesystem_execution), + ] { + for forbidden in [ + "Javascript", + "V8", + "Python", + "Wasmtime", + "HostRpcRequest", + "HostRpcServiceResponse", + ] { + assert!( + !source.contains(forbidden), + "neutral {label} capability depends on adapter type {forbidden}" + ); + } + } + assert!( + !filesystem.contains("process.runtime == GuestRuntimeKind"), + "filesystem write semantics must be explicit in the typed operation" + ); + let compatibility = include_str!("../src/execution/host_dispatch/network_compat.rs"); + assert!( + compatibility.contains("HostRpcRequest") + && compatibility.contains("dispatch_context_managed_network_operation"), + "compatibility payload adaptation must stay in the explicitly named adapter module" + ); +} + #[test] fn unix_listener_close_is_lossless_and_acknowledged() { let root = repo_root(); let unix = std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/network/unix.rs")) .expect("read Unix reactor source"); - let rpc = - std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/javascript/rpc.rs")) - .expect("read JavaScript RPC source"); + let managed = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/network/managed.rs"), + ) + .expect("read shared managed-network source"); let compact_unix: String = unix .chars() .filter(|character| !character.is_whitespace()) .collect(); - let compact_rpc: String = rpc + let compact_managed: String = managed .chars() .filter(|character| !character.is_whitespace()) .collect(); @@ -87,9 +936,10 @@ fn unix_listener_close_is_lossless_and_acknowledged() { "the Unix listener owner must acknowledge every terminal path after dropping its FD" ); assert!( - compact_rpc.contains("tokio::time::timeout(operation_deadline,close_completion).await") - && compact_rpc.contains("JavascriptSyncRpcServiceResponse::Deferred"), - "the listener close bridge response must await bounded owner-task completion" + compact_managed.contains( + "operation_deadline_timeout(\"Unixlistenerclose\",deadline,completion,).await" + ) && compact_managed.contains("HostServiceResponse::Deferred"), + "the shared listener-close operation must await bounded owner-task completion" ); } @@ -225,6 +1075,16 @@ impl CfgTestTracker { } } +fn production_source_text(source: &str) -> String { + let mut tracker = CfgTestTracker::new(); + source + .lines() + .filter(|line| !tracker.in_test(line)) + .map(strip_line_comment) + .collect::>() + .join("\n") +} + fn count_open(s: &str) -> u32 { s.bytes().filter(|&b| b == b'{').count() as u32 } @@ -352,13 +1212,14 @@ const FS_ALLOW: &[&str] = &[ /// net: host network access. /// -/// Sanctioned surface: the kernel DNS resolver plane, the sidecar host-net +/// Sanctioned surface: the kernel DNS/socket contract plane, the sidecar host-net /// chokepoint (`execution.rs`, which owns all guest TCP/UDP/Unix sockets), the /// host-backed storage/agent plugins (which open egress to S3 / Google Drive / /// the sandbox-agent control plane), the embedded V8 runtime IPC socketpair, /// and the client transport that talks to the spawned sidecar. const NET_ALLOW: &[&str] = &[ - // kernel network plane + // Kernel DNS contract: address/config/result values only; host DNS + // transport lives in the native-sidecar resolver module below. "crates/kernel/src/dns.rs", // Shared IP classifier only; no host sockets are opened here. "crates/kernel/src/network_policy.rs", @@ -537,32 +1398,717 @@ crates/native-sidecar/tests/architecture_guards.rs with a justifying comment.\n\ ); } -#[test] -fn fs_access_confined_to_chokepoints() { - let root = repo_root(); - let files = production_source_files(&root); - assert_green(&root, &files, fs_class()); +#[test] +fn fs_access_confined_to_chokepoints() { + let root = repo_root(); + let files = production_source_files(&root); + assert_green(&root, &files, fs_class()); +} + +#[test] +fn net_access_confined_to_chokepoints() { + let root = repo_root(); + let files = production_source_files(&root); + assert_green(&root, &files, net_class()); +} + +#[test] +fn process_spawn_confined_to_chokepoints() { + let root = repo_root(); + let files = production_source_files(&root); + assert_green(&root, &files, process_class()); +} + +#[test] +fn env_reads_confined_to_chokepoints() { + let root = repo_root(); + let files = production_source_files(&root); + assert_green(&root, &files, env_class()); +} + +#[test] +fn production_execution_lifecycle_is_runtime_neutral_and_delegated() { + let root = repo_root(); + let lifecycle = std::fs::read_to_string(root.join("crates/execution/src/backend/lifecycle.rs")) + .expect("read runtime-neutral lifecycle contract"); + for engine_type in [ + "JavascriptExecution", + "PythonExecution", + "WasmExecution", + "BindingExecution", + "V8SessionHandle", + ] { + assert!( + !lifecycle.contains(engine_type), + "common lifecycle contract must not name adapter type {engine_type}" + ); + } + + let process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read ActiveExecution adapter"); + let compact: String = process + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + assert!( + compact.contains("implExecutionBackendforActiveExecution") + && compact.contains("self.backend().kind()") + && compact.contains("self.backend().native_process_id()") + && compact.contains("self.backend().is_prepared_for_start()") + && compact.contains("self.backend_mut().start_prepared()") + && compact.contains("self.backend_mut().begin_shutdown(reason)") + && compact.contains("self.backend().set_paused(paused)") + && compact.contains("self.backend_mut().write_stdin(bytes)") + && compact.contains("self.backend_mut().close_stdin()") + && compact.contains( + "self.backend().deliver_signal_checkpoint(identity,signal,delivery_token,flags)" + ), + "ActiveExecution must delegate every common lifecycle method through ExecutionBackend" + ); + + for method in [ + "fn native_process_id(", + "fn set_paused(", + "fn write_stdin(", + "fn close_stdin(", + "fn deliver_signal_checkpoint(", + ] { + assert!( + lifecycle.contains(method), + "the runtime-neutral lifecycle contract must own {method}" + ); + } + + let controls_start = process + .find("pub(crate) fn apply_runtime_controls") + .expect("find ActiveProcess runtime controls"); + let controls_end = process[controls_start..] + .find("fn take_pending_runtime_exit_event") + .map(|offset| controls_start + offset) + .expect("find end of ActiveProcess runtime controls"); + let controls = &process[controls_start..controls_end]; + for executor_semantic in [ + "ActiveExecution::", + "matches!(self.execution", + "uses_shared_v8_runtime", + "child_pid()", + ] { + assert!( + !controls.contains(executor_semantic), + "common runtime controls must not branch on executor semantic {executor_semantic}" + ); + } + assert!( + controls.contains("if controls.checkpoint {") + && controls.contains("deliver_signal_checkpoint("), + "every backend, including compatibility WASM, must enter the kernel-owned signal checkpoint path" + ); + + let wasm = std::fs::read_to_string(root.join("crates/execution/src/wasm.rs")) + .expect("read compatibility WASM adapter"); + let wasm_backend_start = wasm + .find("impl ExecutionBackend for WasmExecution") + .expect("find compatibility WASM backend adapter"); + let wasm_backend: String = wasm[wasm_backend_start..] + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + assert!( + wasm_backend.contains("fndeliver_signal_checkpoint(") + && wasm_backend.contains("self.wake_handle(identity)") + && wasm_backend.contains("wake.publish_signal(signal,delivery_token)"), + "compatibility WASM checkpoints must publish through the runtime-neutral kernel wake capability" + ); +} + +#[test] +fn common_execution_lifecycle_has_no_backend_specific_signal_or_process_residence_debt() { + let root = repo_root(); + let files = [ + "crates/execution/src", + "crates/execution/tests", + "crates/native-sidecar/src", + "crates/native-sidecar/tests", + ] + .into_iter() + .flat_map(|relative| { + production_source_files(&root) + .into_iter() + .filter(move |path| path.starts_with(relative)) + }) + .collect::>(); + let violations = production_matches( + &root, + &files, + &[ + "NodeSignalDispositionAction", + "NodeSignalHandlerRegistration", + "WasmSignalDispositionAction", + "WasmSignalHandlerRegistration", + "JavascriptHostCall", + "javascript_host_call", + "map_node_signal_registration", + "map_wasm_signal_registration", + "closed_javascript_event_channel", + "closed_python_event_channel", + "closed_wasm_event_channel", + "EventChannelClosed.to_string()", + ], + ); + assert!( + violations.is_empty(), + "common lifecycle naming or string-matched channel errors reappeared:\n{}", + violations.join("\n") + ); + + let sidecar_files = production_source_files(&root) + .into_iter() + .filter(|path| path.starts_with("crates/native-sidecar/src")) + .collect::>(); + let residence_violations = production_matches( + &root, + &sidecar_files, + &["uses_shared_v8_runtime", ".child_pid()"], + ); + assert!( + residence_violations.is_empty(), + "common sidecar lifecycle must use ExecutionBackend::native_process_id:\n{}", + residence_violations.join("\n") + ); + + let signals = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/signals.rs")) + .expect("read native signal mapper"); + assert_eq!( + signals + .matches("fn map_execution_signal_registration(") + .count(), + 1, + "native sidecar must have exactly one runtime-neutral execution signal mapper" + ); + + let active_execution = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read ActiveExecution error adapters"); + for exact_mapper in [ + ".map_err(javascript_error)?", + ".map_err(python_error)?", + ".map_err(wasm_error)?", + ] { + assert!( + active_execution.contains(exact_mapper), + "ActiveExecution must preserve typed engine errors through {exact_mapper}" + ); + } + + let process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process_events.rs")) + .expect("read root process-event recovery"); + let recovery = process + .find("fn recover_closed_root_runtime_process_event(") + .map(|offset| &process[offset..]) + .expect("find root channel-close recovery"); + let recovery = recovery + .split("pub(super) fn active_process_by_path") + .next() + .expect("bound root channel-close recovery"); + for backend_branch in [ + "GuestRuntimeKind::", + "uses_shared_v8_runtime", + "child_pid()", + ] { + assert!( + !recovery.contains(backend_branch), + "root channel-close recovery must use native_process_id, not {backend_branch}" + ); + } + assert!(recovery.contains("execution.native_process_id()")); + + let child = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/child_process.rs")) + .expect("read descendant process-event recovery"); + let recovery = child + .find("fn recover_descendant_runtime_child_process_event(") + .map(|offset| &child[offset..]) + .expect("find descendant channel-close recovery"); + let recovery = recovery + .split("fn write_descendant_process_stdin(") + .next() + .expect("bound descendant channel-close recovery"); + for backend_branch in [ + "GuestRuntimeKind::", + "uses_shared_v8_runtime", + "child_pid()", + ] { + assert!( + !recovery.contains(backend_branch), + "descendant channel-close recovery must use native_process_id, not {backend_branch}" + ); + } + assert!(recovery.contains("execution.native_process_id()")); + + let state = std::fs::read_to_string(root.join("crates/native-sidecar/src/state.rs")) + .expect("read typed sidecar errors"); + assert!(state.contains("ExecutionEventChannelClosed { backend: ExecutionBackendKind }")); +} + +#[test] +fn every_production_active_process_attaches_the_real_kernel_runtime_endpoint() { + let root = repo_root(); + + // The old stub must not remain available for a production call site to + // select accidentally. Deliberately virtual kernel processes use the same + // durable RuntimeControlCell without attaching its consumer; once a real + // backend is installed it is wrapped by ActiveProcess below. + for relative in [ + "crates/kernel/src", + "crates/execution/src", + "crates/native-sidecar/src", + ] { + let files = production_source_files(&root) + .into_iter() + .filter(|path| path.starts_with(relative)) + .collect::>(); + for path in files { + let source = std::fs::read_to_string(root.join(&path)) + .unwrap_or_else(|error| panic!("read {}: {error}", path.display())); + assert!( + !source.contains("StubDriverProcess"), + "production runtime endpoint stub reappeared in {}", + path.display() + ); + } + } + + let process_relative = PathBuf::from("crates/native-sidecar/src/execution/process.rs"); + let process = std::fs::read_to_string(root.join(&process_relative)) + .expect("read ActiveProcess implementation"); + let constructor = rust_braced_item(&process, "pub(crate) fn new("); + let compact_constructor = constructor + .chars() + .filter(|character| !character.is_whitespace()) + .collect::(); + assert!( + compact_constructor.contains("Self::attach_runtime_control_before_start(") + && compact_constructor.contains("Self::new_with_attached_runtime_control("), + "ordinary ActiveProcess construction must attach and retain the real generation-bound kernel runtime endpoint" + ); + let preattached_constructor = + rust_braced_item(&process, "pub(crate) fn new_with_attached_runtime_control("); + assert!( + preattached_constructor + .contains("runtime_control: agentos_kernel::process_runtime::RuntimeControlReceiver") + && preattached_constructor.contains("runtime_control.identity()") + && preattached_constructor.contains("kernel_handle.runtime_identity()"), + "startup construction must accept and validate the receiver attached before engine start" + ); + + let launch = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/launch.rs")) + .expect("read top-level launch implementation"); + let execute = launch + .split_once(" pub(crate) async fn execute(") + .expect("top-level execute implementation") + .1; + let allocated = execute + .find("let kernel_handle = vm\n .kernel\n .spawn_process(") + .expect("top-level kernel process allocation"); + let startup = &execute[allocated..]; + let attach = startup + .find("ActiveProcess::attach_runtime_control_before_start(") + .expect("pre-start runtime endpoint attachment"); + let pty_setup = startup + .find("let tty_master_fd = if requested_tty") + .expect("top-level PTY setup"); + let engine_start = startup + .find("let (execution, process_env, started_context) = match resolved.runtime") + .expect("top-level engine start"); + let publish = startup + .find("new_with_attached_runtime_control(") + .expect("pre-attached ActiveProcess publication"); + assert!( + attach < pty_setup && pty_setup < engine_start && engine_start < publish, + "the real endpoint must attach before any fallible setup or engine start" + ); + let fallible_startup = &startup[attach..publish]; + assert!( + startup[..publish].contains("macro_rules! top_level_start_step") + && startup[..publish].contains("rollback_failed_top_level_process_start(") + && !fallible_startup.contains("?;"), + "every fallible post-allocation setup/start step must use the common rollback funnel" + ); + let rollback = rust_braced_item(&launch, "fn rollback_failed_top_level_process_start("); + assert!( + rollback.contains("execution.terminate()") + && rollback.contains("kernel_handle.finish(127)") + && rollback.contains("kernel.waitpid(kernel_handle.pid())"), + "failed top-level setup must terminate the engine, mark exit, and reap kernel resources" + ); + let published_rollback = + rust_braced_item(&launch, "fn rollback_published_top_level_process_start("); + assert!( + published_rollback.contains("vm.active_processes.remove(process_id)") + && published_rollback.contains("rollback_failed_top_level_process_start("), + "a failure after active-process publication must remove, terminate, and reap the process" + ); + assert_eq!( + launch + .matches("if let Err(error) = self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)") + .count(), + 2, + "both binding and engine lifecycle publications must handle bridge failure" + ); + assert_eq!( + launch + .matches("rollback_published_top_level_process_start(") + .count(), + 3, + "the helper declaration and both lifecycle failure paths must remain wired" + ); + + // Every production backend-installation path attaches its generation-bound + // runtime endpoint before engine or binding-producer start, then transfers + // that receiver into ActiveProcess. Test fixtures are ignored. + // A direct struct literal would bypass endpoint attachment, so it is + // forbidden outside the declaration and impl header. + let mut constructors = Vec::new(); + let mut preattached_constructors = Vec::new(); + let mut direct_literals = Vec::new(); + for relative in production_source_files(&root) + .into_iter() + .filter(|path| path.starts_with("crates/native-sidecar/src/")) + { + let source = std::fs::read_to_string(root.join(&relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + let mut tracker = CfgTestTracker::new(); + for (index, raw) in source.lines().enumerate() { + if tracker.in_test(raw) { + continue; + } + let code = strip_line_comment(raw); + if code.contains("ActiveProcess::new(") { + constructors.push(relative.clone()); + } + if code.contains("ActiveProcess::new_with_attached_runtime_control(") { + preattached_constructors.push(relative.clone()); + } + if code.contains("ActiveProcess {") + && !code.contains("struct ActiveProcess {") + && !code.trim_start().starts_with("impl ") + { + direct_literals.push(format!( + "{}:{}: {}", + relative.display(), + index + 1, + code.trim() + )); + } + } + } + constructors.sort(); + let top_level_launch = PathBuf::from("crates/native-sidecar/src/execution/launch.rs"); + let top_level_source = std::fs::read_to_string(root.join(&top_level_launch)) + .expect("read top-level launch implementation"); + if top_level_source.contains("ActiveProcess::new_with_attached_runtime_control(") { + preattached_constructors.push(top_level_launch); + } + preattached_constructors.sort(); + preattached_constructors.dedup(); + assert!( + constructors.is_empty(), + "production startup must never attach the endpoint after an executor may already be running: {constructors:?}" + ); + assert_eq!( + preattached_constructors, + [ + PathBuf::from("crates/native-sidecar/src/execution/child_process.rs"), + PathBuf::from("crates/native-sidecar/src/execution/launch.rs"), + ], + "only reviewed startup paths may transfer a receiver attached before engine start" + ); + assert!( + direct_literals.is_empty(), + "production ActiveProcess literals bypass endpoint attachment:\n{}", + direct_literals.join("\n") + ); +} + +#[test] +fn neutral_host_contracts_and_shared_capabilities_have_no_engine_types() { + let root = repo_root(); + let contract_files = production_source_files(&root) + .into_iter() + .filter(|path| { + path.starts_with("crates/execution/src/backend/") + || path.starts_with("crates/execution/src/host/") + }) + .collect::>(); + + let mut violations = engine_type_identifier_matches(&root, &contract_files, &[]); + + // These lower-layer host-service models are consumed by every executor. + // They are deliberately scanned as complete production files because no + // engine adapter belongs in native-sidecar-core. + let core_host_files = [ + "crates/native-sidecar-core/src/guest_fs.rs", + "crates/native-sidecar-core/src/guest_net.rs", + "crates/native-sidecar-core/src/guest_pty.rs", + "crates/native-sidecar-core/src/identity.rs", + "crates/native-sidecar-core/src/signals.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + violations.extend(engine_type_identifier_matches( + &root, + &core_host_files, + &["GuestRuntimeKind", "ExecutionBackendKind"], + )); + + // host_dispatch/mod.rs starts with the compatibility-wire decoder. Scan + // the semantic dispatcher half so the adapter may mention its source + // engine while capability routing and kernel effects may not. + let dispatcher_relative = + PathBuf::from("crates/native-sidecar/src/execution/host_dispatch/mod.rs"); + let dispatcher = std::fs::read_to_string(root.join(&dispatcher_relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", dispatcher_relative.display())); + let semantic_dispatcher = dispatcher + .find("pub(super) fn dispatch_host_operation(") + .map(|offset| &dispatcher[offset..]) + .expect("shared host semantic dispatcher marker"); + violations.extend(engine_type_identifiers_in_source( + &dispatcher_relative, + semantic_dispatcher, + &["GuestRuntimeKind", "ExecutionBackendKind"], + )); + + // Domain files contain compatibility-wire decoders before their shared + // capability implementation. Scan only the execution half: JavaScript + // request types are permitted in the explicit adapter decoder, never in + // the semantic capability that touches kernel state. + for family in [ + "clock", + "entropy", + "filesystem", + "identity", + "network", + "process", + "signal", + "terminal", + ] { + let relative = PathBuf::from(format!( + "crates/native-sidecar/src/execution/host_dispatch/{family}.rs" + )); + let source = std::fs::read_to_string(root.join(&relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + let marker = format!("pub(super) struct {}Capability", to_pascal_case(family)); + let capability = source + .find(&marker) + .map(|offset| &source[offset..]) + .unwrap_or_else(|| panic!("missing capability marker {marker}")); + violations.extend(engine_type_identifiers_in_source( + &relative, + capability, + &["GuestRuntimeKind", "ExecutionBackendKind"], + )); + } + + // These files are semantic sidecar reactor owners. Engine-specific request + // decoding remains allowed only in the explicitly named javascript/* and + // host_dispatch/network_compat.rs adapters, never in a shared reactor file. + for relative in [ + "crates/native-sidecar/src/execution/network/tcp.rs", + "crates/native-sidecar/src/execution/network/udp.rs", + "crates/native-sidecar/src/execution/network/unix.rs", + "crates/native-sidecar/src/execution/network/tls.rs", + "crates/native-sidecar/src/execution/network/dns.rs", + "crates/native-sidecar/src/execution/network/resolver.rs", + "crates/native-sidecar/src/execution/network/managed.rs", + "crates/native-sidecar/src/execution/network/managed_endpoint.rs", + "crates/native-sidecar/src/execution/network/http_client.rs", + "crates/native-sidecar/src/execution/network/http2.rs", + ] { + let relative = PathBuf::from(relative); + let source = std::fs::read_to_string(root.join(&relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + violations.extend(engine_type_identifiers_in_source( + &relative, + &source, + &["GuestRuntimeKind", "ExecutionBackendKind"], + )); + } + + // state.rs also owns executor sessions, so scan the reviewed shared + // networking model declarations rather than granting or denying the whole + // mixed-domain file. A newly added field on one of these types is covered + // automatically by balanced-brace extraction. + let state_relative = PathBuf::from("crates/native-sidecar/src/state.rs"); + let state = std::fs::read_to_string(root.join(&state_relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", state_relative.display())); + for marker in [ + "pub(crate) struct SocketDescriptionLease", + "pub(crate) struct SocketFairnessRetirement", + "pub(crate) struct ListenerConnectionRetirement", + "pub(crate) struct HostNetTransferDescription", + "pub(crate) struct VmDnsConfig", + "pub(crate) struct SocketPathContext", + "pub(crate) struct NetworkResourceCounts", + "pub(crate) struct GuestUnixAddress", + "pub(crate) struct GuestUnixAddressRegistryEntry", + "pub(crate) struct HttpLoopbackTarget", + "pub(crate) enum SocketFamily", + "pub(crate) struct VmListenPolicy", + "pub(crate) struct ActiveHttpServer", + "pub(crate) enum PendingHttpRequest", + "pub(crate) struct ActiveHttp2State", + "pub(crate) struct Http2SharedState", + "pub(crate) struct ActiveHttp2Server", + "pub(crate) struct ActiveHttp2Session", + "pub(crate) struct ActiveHttp2Stream", + "pub(crate) struct QueuedHttp2Event", + "pub(crate) struct QueuedHttp2Command", + "pub(crate) struct Http2SocketSnapshot", + "pub(crate) struct Http2RuntimeSnapshot", + "pub(crate) struct Http2SessionSnapshot", + "pub(crate) struct Http2BridgeEvent", + "pub(crate) enum Http2SessionCommand", + "pub(crate) enum TcpListenerEvent", + "pub(crate) struct PendingTcpSocket", + "pub(crate) enum TcpSocketEvent", + "pub(crate) struct SocketEventPusher", + "struct SocketReadinessSubscriber", + "pub(crate) struct SocketReadinessSubscribers", + "pub(crate) struct SocketReadinessRegistration", + "pub(crate) enum KernelSocketReadinessEvent", + "pub(crate) struct KernelSocketReadinessTarget", + "pub(crate) struct KernelSocketReadinessRegistryState", + "pub(crate) struct ActiveTcpSocket", + "pub(crate) enum NativeTlsCommand", + "pub(crate) enum NativePlainSocketCommand", + "pub(crate) struct PlainSocketWritePayload", + "pub(crate) struct TlsWritePayload", + "pub(crate) struct ReactorIoLimits", + "pub(crate) struct LoopbackTlsTransportPair", + "pub(crate) struct LoopbackTlsTransportPairState", + "pub(crate) struct LoopbackTlsEndpoint", + "pub(crate) struct TlsClientHello", + "pub(crate) struct TlsBridgeOptions", + "pub(crate) enum TlsMaterial", + "pub(crate) enum TlsDataValue", + "pub(crate) struct ActiveTlsState", + "pub(crate) struct ResolvedTcpConnectAddr", + "pub(crate) struct ActiveTcpListener", + "pub(crate) enum UnixListenerEvent", + "pub(crate) struct PendingUnixSocket", + "pub(crate) struct GuestUnixConnectionState", + "pub(crate) struct PendingUnixConnectionGuard", + "pub(crate) struct ActiveUnixSocket", + "pub(crate) struct ActiveUnixListener", + "pub(crate) enum UdpFamily", + "pub(crate) enum DatagramEvent", + "pub(crate) struct NativeUdpSendPayload", + "pub(crate) enum NativeUdpSocketOption", + "pub(crate) enum NativeUdpCommand", + "pub(crate) struct ActiveUdpSocket", + "pub(crate) struct ManagedUdpPollRecheck", + "pub(crate) enum PendingNetConnect", + "pub(crate) struct PendingNetConnectState", + "pub(crate) enum SocketQueryKind", + "pub(crate) struct ProcNetEntry", + ] { + let declaration = rust_braced_item(&state, marker); + violations.extend(engine_type_identifiers_in_source( + &state_relative, + declaration, + &["GuestRuntimeKind", "ExecutionBackendKind"], + )); + } + + assert!( + violations.is_empty(), + "runtime-neutral host contracts and capability execution must not contain engine-specific types or executor switchboards; keep those in explicit adapter decoders:\n{}", + violations.join("\n") + ); +} + +fn rust_braced_item<'a>(source: &'a str, marker: &str) -> &'a str { + let start = source + .find(marker) + .unwrap_or_else(|| panic!("missing shared model declaration {marker}")); + let item = &source[start..]; + let open = item + .find('{') + .unwrap_or_else(|| panic!("shared model declaration {marker} has no body")); + let mut depth = 0_usize; + for (offset, byte) in item[open..].bytes().enumerate() { + match byte { + b'{' => depth = depth.saturating_add(1), + b'}' => { + depth = depth + .checked_sub(1) + .unwrap_or_else(|| panic!("unbalanced shared model declaration {marker}")); + if depth == 0 { + return &item[..open + offset + 1]; + } + } + _ => {} + } + } + panic!("unterminated shared model declaration {marker}") } -#[test] -fn net_access_confined_to_chokepoints() { - let root = repo_root(); - let files = production_source_files(&root); - assert_green(&root, &files, net_class()); +fn to_pascal_case(value: &str) -> String { + let mut characters = value.chars(); + characters + .next() + .map(|first| first.to_ascii_uppercase().to_string() + characters.as_str()) + .unwrap_or_default() } -#[test] -fn process_spawn_confined_to_chokepoints() { - let root = repo_root(); - let files = production_source_files(&root); - assert_green(&root, &files, process_class()); +fn engine_type_identifier_matches( + root: &Path, + files: &[PathBuf], + forbidden_exact: &[&str], +) -> Vec { + files + .iter() + .flat_map(|relative| { + let source = std::fs::read_to_string(root.join(relative)) + .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); + engine_type_identifiers_in_source(relative, &source, forbidden_exact) + }) + .collect() } -#[test] -fn env_reads_confined_to_chokepoints() { - let root = repo_root(); - let files = production_source_files(&root); - assert_green(&root, &files, env_class()); +fn engine_type_identifiers_in_source( + relative: &Path, + source: &str, + forbidden_exact: &[&str], +) -> Vec { + let mut violations = Vec::new(); + let mut tracker = CfgTestTracker::new(); + for (index, raw) in source.lines().enumerate() { + if tracker.in_test(raw) { + continue; + } + let code = strip_line_comment(raw); + for identifier in + code.split(|character: char| !character.is_ascii_alphanumeric() && character != '_') + { + let engine_type = ["Javascript", "V8", "Python", "Wasmtime"] + .iter() + .any(|prefix| identifier.starts_with(prefix) && identifier.len() > prefix.len()); + if engine_type || forbidden_exact.contains(&identifier) { + violations.push(format!( + "{}:{}: {identifier}", + relative.display(), + index + 1 + )); + } + } + } + violations } /// Sanity: the scan actually sees source files and the allowlisted files exist. @@ -634,6 +2180,7 @@ fn dependency_keys(manifest: &Path) -> BTreeSet { fn generic_runtime_layers_do_not_depend_on_product_or_acp_layers() { let root = repo_root(); let lower_layers = [ + "resource", "runtime", "kernel", "vfs", @@ -665,6 +2212,95 @@ fn generic_runtime_layers_do_not_depend_on_product_or_acp_layers() { ); } +#[test] +fn kernel_dns_contract_has_no_native_resolver_or_runtime_dependency() { + let root = repo_root(); + let manifest = root.join("crates/kernel/Cargo.toml"); + let dependencies = dependency_keys(&manifest); + for forbidden in ["hickory-resolver", "tokio"] { + assert!( + !dependencies.contains(forbidden), + "kernel must not depend on native DNS transport crate {forbidden}" + ); + } + + let kernel_dns = std::fs::read_to_string(root.join("crates/kernel/src/dns.rs")) + .expect("read kernel DNS contract"); + for forbidden in [ + "agentos_runtime", + "BlockingJobError", + "RuntimeContext", + "hickory_resolver", + "TokioResolver", + "tokio::", + ] { + assert!( + !kernel_dns.contains(forbidden), + "kernel DNS contract contains native resolver/runtime symbol {forbidden}" + ); + } + + let native_resolver = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/network/resolver.rs"), + ) + .expect("read native DNS resolver"); + assert!( + native_resolver.contains("pub(crate) struct HickoryDnsResolver") + && native_resolver.contains("runtime: RuntimeContext") + && native_resolver.contains("TokioResolver"), + "native sidecar must own Hickory/Tokio DNS transport with an injected RuntimeContext" + ); + let service = std::fs::read_to_string(root.join("crates/native-sidecar/src/service.rs")) + .expect("read native sidecar service"); + assert!( + service.contains("HickoryDnsResolver::new(runtime_context.clone())"), + "native sidecar must inject its one process RuntimeContext into DNS transport" + ); +} + +#[test] +fn kernel_resource_accounting_has_no_runtime_or_tokio_dependency_cycle() { + let root = repo_root(); + let kernel_dependencies = dependency_keys(&root.join("crates/kernel/Cargo.toml")); + for forbidden in ["agentos-runtime", "tokio"] { + assert!( + !kernel_dependencies.contains(forbidden), + "kernel resource authority must not depend on {forbidden}" + ); + } + + let runtime_dependencies = dependency_keys(&root.join("crates/runtime/Cargo.toml")); + assert!( + !runtime_dependencies.contains("agentos-kernel"), + "process runtime must not create a runtime -> kernel -> VFS -> runtime cycle" + ); + assert!( + runtime_dependencies.contains("agentos-resource") + && kernel_dependencies.contains("agentos-resource"), + "kernel and process runtime must share the runtime-neutral accounting layer" + ); + + let resource_dependencies = dependency_keys(&root.join("crates/resource/Cargo.toml")); + assert_eq!( + resource_dependencies, + BTreeSet::from([String::from("event-listener")]), + "resource accounting must remain independent of executors, Tokio, VFS, and product layers" + ); + + for path in [ + "crates/kernel/src/kernel.rs", + "crates/kernel/src/socket_table.rs", + ] { + let source = std::fs::read_to_string(root.join(path)).expect("read kernel resource owner"); + for forbidden in ["agentos_runtime", "tokio::"] { + assert!( + !source.contains(forbidden), + "{path} contains concrete runtime symbol {forbidden}" + ); + } + } +} + #[test] fn shared_acp_runtime_has_no_adapter_name_policy() { let root = repo_root(); @@ -816,15 +2452,12 @@ fn native_execution_is_split_by_domain() { "crates/native-sidecar/src/execution/network/tls.rs", "crates/native-sidecar/src/execution/network/http2.rs", "crates/native-sidecar/src/execution/network/dns.rs", + "crates/native-sidecar/src/execution/network/resolver.rs", "crates/native-sidecar/src/execution/javascript/mod.rs", "crates/native-sidecar/src/execution/javascript/rpc.rs", "crates/native-sidecar/src/execution/javascript/crypto.rs", "crates/native-sidecar/src/execution/javascript/sqlite.rs", "crates/native-sidecar/src/execution/javascript/http.rs", - "crates/native-sidecar/src/execution/python/mod.rs", - "crates/native-sidecar/src/execution/python/rpc.rs", - "crates/native-sidecar/src/execution/python/sockets.rs", - "crates/native-sidecar/src/execution/python/subprocess.rs", ]; for path in expected { @@ -836,6 +2469,169 @@ fn native_execution_is_split_by_domain() { ); } +#[test] +fn python_filesystem_and_process_calls_use_common_host_operations() { + let root = repo_root(); + let adapter = std::fs::read_to_string(root.join("crates/execution/src/python.rs")) + .expect("read Python execution adapter"); + let mapper = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/process.rs")) + .expect("read common execution event mapper"); + let filesystem = std::fs::read_to_string(root.join("crates/native-sidecar/src/filesystem.rs")) + .expect("read sidecar filesystem helpers"); + let state = std::fs::read_to_string(root.join("crates/native-sidecar/src/state.rs")) + .expect("read shared sidecar state"); + + assert!( + adapter.contains("pub fn try_host_call(") + && adapter.contains("HostOperation::Filesystem(") + && adapter.contains("HostOperation::Process(ProcessOperation::RunCaptured"), + "the Python wire adapter must translate filesystem and captured-process calls into runtime-neutral host operations" + ); + assert!( + mapper.contains("python_responder.try_host_call(") + && mapper.contains("host.submit(call.operation, call.reply.clone(), admission)"), + "Python filesystem/process requests must enter the same admitted host-operation lane as other executors" + ); + for removed in [ + "crates/native-sidecar/src/execution/python/mod.rs", + "crates/native-sidecar/src/execution/python/rpc.rs", + "crates/native-sidecar/src/execution/python/sockets.rs", + ] { + assert!( + !root.join(removed).exists(), + "Python semantics must not return to the deleted sidecar dispatcher ({removed})" + ); + } + for removed_state in [ + "PythonVfsRpcRequest(Box<", + "PythonSocketConnectCompletion", + "python_sockets:", + "next_python_socket_id:", + ] { + assert!( + !state.contains(removed_state), + "shared sidecar state must not retain Python-specific semantic state ({removed_state})" + ); + } + assert!( + !filesystem.contains("PythonVfsRpc") + && !filesystem.contains("handle_python_vfs_rpc_request"), + "the filesystem source of truth must not contain a Python-specific semantic switch" + ); + assert!( + !root + .join("crates/native-sidecar/src/execution/python/subprocess.rs") + .exists(), + "captured subprocess execution belongs to the common process capability, not a Python implementation" + ); +} + +#[test] +fn python_common_host_replies_preserve_typed_error_details() { + let adapter = include_str!("../../execution/src/python.rs"); + let target = adapter + .split_once("impl DirectHostReplyTarget for PythonHostReplyTarget") + .expect("Python direct host reply target") + .1 + .split_once("fn python_host_reply_adapter_error") + .expect("end Python direct host reply target") + .0; + + assert!( + target.contains("respond_claimed_host_error(call_id, error)") + && target.contains("respond_host_error(call_id, error)"), + "the Python adapter must forward the complete HostServiceError, including structured details" + ); + assert!( + !target.contains("error.code") && !target.contains("error.message"), + "the Python common reply target must not flatten typed host errors into code/message pairs" + ); +} + +#[test] +fn javascript_timer_and_direct_reply_errors_use_typed_classification() { + let javascript = include_str!("../../execution/src/javascript.rs"); + let timer = javascript + .split_once("fn timer_dispatch_error(") + .expect("JavaScript timer error adapter") + .1 + .split_once("fn javascript_timer_error(") + .expect("end JavaScript timer error adapter") + .0; + assert!( + timer.contains("error.code") + && timer.contains("error.message") + && timer.contains("error.details"), + "JavaScript timer dispatch must preserve the typed HostServiceError payload" + ); + + let direct_reply = javascript + .split_once("pub(crate) fn map_host_reply_adapter_response(") + .expect("JavaScript direct reply adapter") + .1 + .split_once("fn encode_host_service_error_payload(") + .expect("end JavaScript direct reply adapter") + .0; + assert!( + direct_reply.contains("BridgeSettlementErrorKind::StaleCompletion"), + "stale direct replies must be classified by the typed V8 settlement kind" + ); + + for (label, source) in [ + ("timer dispatch", timer), + ("direct host reply", direct_reply), + ] { + for forbidden in [ + ".split(", + ".split_once(", + ".starts_with(", + ".strip_prefix(", + ".contains(", + ] { + assert!( + !source.contains(forbidden), + "{label} errors must not infer behavior from diagnostic strings ({forbidden})" + ); + } + } +} + +#[test] +fn pending_process_event_limits_are_typed_and_actionable() { + let service = include_str!("../src/service.rs"); + let state = include_str!("../src/state.rs"); + let check = service + .split_once("fn check_pending_process_event_capacity(") + .expect("pending process-event capacity check") + .1 + .split_once("fn ") + .expect("end pending process-event capacity check") + .0; + + assert!( + check.matches("SidecarError::host_resource_limit(").count() >= 2, + "pending event count and byte limits must return typed resource-limit errors" + ); + assert!( + !check.contains("SidecarError::InvalidState"), + "bounded queue admission failures are resource limits, not invalid internal state" + ); + let typed_details = state + .split_once("pub(crate) fn host_resource_limit(") + .expect("typed host resource-limit helper") + .1 + .split_once("impl fmt::Display for SidecarError") + .expect("end typed host resource-limit helper") + .0; + for field in ["limitName", "observed", "limit", "configPath"] { + assert!( + typed_details.contains(field), + "pending process-event resource errors must preserve {field} details" + ); + } +} + fn production_matches(root: &Path, files: &[PathBuf], needles: &[&str]) -> Vec { let mut matches = Vec::new(); for rel in files { @@ -946,10 +2742,12 @@ fn top_level_python_start_uses_the_async_runtime_adapter() { let path = repo_root().join("crates/native-sidecar/src/execution/launch.rs"); let source = std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("read {path:?}: {error}")); + let compact = source + .chars() + .filter(|character| !character.is_whitespace()) + .collect::(); assert!( - source.contains( - ".python_engine\n .start_execution_with_runtime_async(" - ), + compact.contains(".python_engine.start_execution_with_runtime_async("), "top-level Python startup must await cache materialization and prewarm instead of blocking a Tokio worker" ); assert!( @@ -963,11 +2761,11 @@ fn nested_child_start_never_blocks_the_shared_runtime_worker() { let source = native_execution_source(&repo_root()); assert!( - source.contains("pub(crate) async fn spawn_javascript_child_process("), + source.contains("pub(crate) async fn spawn_child_process("), "root child startup must be an async sidecar dispatch path" ); assert!( - source.contains("async fn spawn_descendant_javascript_child_process("), + source.contains("async fn spawn_descendant_process("), "descendant child startup must be an async sidecar dispatch path" ); assert!( @@ -1087,6 +2885,74 @@ fn native_reactor_has_no_unbounded_channels_or_per_io_thread_names() { ); } +#[test] +fn common_network_reactor_has_no_executor_specific_control_or_encoding() { + let root = repo_root(); + let network_files = native_execution_source_files(&root) + .into_iter() + .filter(|path| path.starts_with("crates/native-sidecar/src/execution/network/")) + .collect::>(); + let violations = production_matches( + &root, + &network_files, + &[ + "ActiveExecution::", + "ExecutionBackendKind::", + "V8SessionHandle", + "v8_session_handle(", + "v8_runtime::", + ], + ); + assert!( + violations.is_empty(), + "shared network ownership must use runtime-neutral wake/reply capabilities; executor selection and engine wire encoding belong in adapters:\n{}", + violations.join("\n") + ); + + let process_file = vec![PathBuf::from( + "crates/native-sidecar/src/execution/process.rs", + )]; + let wake_leaks = production_matches( + &root, + &process_file, + &[ + "V8SessionHandle", + "ExecutionWakeTarget", + "v8_session_handle(", + ], + ); + assert!( + wake_leaks.is_empty(), + "common process orchestration must obtain ExecutionWakeHandle from the backend contract instead of constructing an engine session target:\n{}", + wake_leaks.join("\n") + ); + + let lifecycle = std::fs::read_to_string(root.join("crates/execution/src/backend/lifecycle.rs")) + .expect("read execution backend lifecycle contract"); + assert!( + lifecycle.contains( + "fn wake_handle(&self, _identity: ExecutionWakeIdentity) -> Option" + ), + "the executor backend contract must own construction of its runtime-neutral wake capability" + ); + assert!( + lifecycle.contains("WebAssembly") + && !lifecycle.contains("CompatibilityWasm") + && !lifecycle.contains("Wasmtime"), + "common lifecycle kinds must identify the WebAssembly language backend without predeclaring an engine" + ); + + let unix = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/network/unix.rs")) + .expect("read Unix reactor source"); + assert!( + unix.contains( + "target.pending_connections.len() >= target.pending_connection_limit" + ) && unix.contains("listener_accept_capacity(backlog, reactor_limits)"), + "Unix pre-accept metadata must be admitted against the same bounded listener capacity as its completion lane" + ); +} + #[test] fn native_reactor_tasks_enter_through_task_supervision() { let root = repo_root(); @@ -1160,6 +3026,7 @@ fn production_threads_match_the_reviewed_topology_manifest() { "constant-stdio-reader", "crates/native-sidecar/src/stdio.rs", ), + ("constant-heartbeat", "crates/native-sidecar/src/stdio.rs"), ]; let root = repo_root(); @@ -1531,6 +3398,7 @@ fn reactor_completion_paths_do_not_silently_drop_settlement() { "limits.javascript.sessionCommandQueue", "runtime.protocol.maxEgressFrames", "let _ = entry.shutdown_tx.try_send", + "let _ = crate::bridge::resolve_pending_promise", ][..], ), ( @@ -1538,8 +3406,56 @@ fn reactor_completion_paths_do_not_silently_drop_settlement() { &[ "let _ = v8_session.send_bridge_response", "let _ = self.v8_session.send_stream_event", + "cbor_payload_to_json_args(&payload).unwrap_or_default", + "json_to_cbor_payload(&response).unwrap_or_default", + "encode_host_service_error_payload(&error).unwrap_or_else", + "getrandom(&mut bytes).is_err", + "timers.lock().ok()", + ][..], + ), + ( + "crates/execution/src/backend/submission.rs", + &["let _ = reply.fail"][..], + ), + ( + "crates/execution/src/host/mod.rs", + &["let _ = reply.fail"][..], + ), + ( + "crates/native-sidecar-core/src/guest_net.rs", + &["let _ = kernel.socket_close"][..], + ), + ( + "crates/native-sidecar/src/execution/javascript/sqlite.rs", + &[ + "let _ = connection.pragma_update", + "let _ = database\n .connection\n .execute_batch", ][..], ), + ( + "crates/native-sidecar/src/execution/javascript/rpc.rs", + &["let _ = socket.close", "let _ = listener.close"][..], + ), + ( + "crates/native-sidecar/src/execution/network/udp.rs", + &["let _ = close_kernel_socket_idempotent"][..], + ), + ( + "crates/native-sidecar/src/execution/network/unix.rs", + &["let _ = stream.shutdown"][..], + ), + ( + "crates/native-sidecar/src/execution/process_events.rs", + &["let _ = fs::write", "let _ = vm.kernel.wait_and_reap"][..], + ), + ( + "crates/native-sidecar/src/filesystem.rs", + &["let _ = fs::write"][..], + ), + ( + "crates/native-sidecar/src/service.rs", + &["self.permissions.lock().ok()?"][..], + ), ] { let path = root.join(relative_path); let source = @@ -1582,47 +3498,28 @@ fn structured_audit_delivery_failures_have_a_non_recursive_stderr_fallback() { } #[test] -fn python_native_tcp_connect_is_deferred_through_the_shared_runtime() { - let source = std::fs::read_to_string( - repo_root().join("crates/native-sidecar/src/execution/python/sockets.rs"), - ) - .expect("read native-sidecar Python sockets source"); +fn python_native_tcp_connect_uses_the_common_managed_network_operation() { + let root = repo_root(); + let source = std::fs::read_to_string(root.join("crates/execution/src/python.rs")) + .expect("read Python execution adapter"); let socket_connect_arm = source .split("PythonVfsRpcMethod::SocketConnect =>") .nth(1) .and_then(|tail| tail.split("PythonVfsRpcMethod::SocketSend =>").next()) .expect("locate Python SocketConnect arm"); assert!( - socket_connect_arm.contains("defer_python_native_tcp_connect"), - "Python external TCP connect must leave the dispatcher as deferred shared-runtime work" + socket_connect_arm.contains("HostOperation::Network(NetworkOperation::ManagedConnect"), + "Python TCP connect must normalize to the common managed-network operation" ); assert!( - socket_connect_arm.contains("connect_kernel_loopback"), - "VM-local kernel connect may remain immediate only through its explicit nonblocking path" + socket_connect_arm.contains("PythonHostReplyKind::SocketCreated(reservation)"), + "the Python adapter must retain only its bounded guest-handle reservation" ); assert!( - !socket_connect_arm.contains("ActiveTcpSocket::connect("), - "Python SocketConnect must not reach the synchronous native TCP constructor" - ); - - let deferred_connect = source - .split("fn defer_python_native_tcp_connect") - .nth(1) - .and_then(|tail| tail.split("fn python_socket_async_context").next()) - .expect("locate deferred Python TCP connect helper"); - for required in [ - "tokio::net::TcpStream::connect", - "ProcessEventEnvelope", - "PythonSocketConnectCompletion", - ] { - assert!( - deferred_connect.contains(required), - "deferred Python TCP connect is missing {required}" - ); - } - assert!( - !deferred_connect.contains("connect_timeout"), - "deferred Python TCP connect must not call a blocking std socket API" + !root + .join("crates/native-sidecar/src/execution/python/sockets.rs") + .exists(), + "Python must not retain a parallel native TCP dispatcher" ); } @@ -1742,3 +3639,132 @@ fn native_udp_has_one_descriptor_owner_and_no_readiness_clone() { "kernel UDP connect must be table state only, with no task or runtime" ); } + +#[test] +fn python_bridge_error_classification_uses_typed_codes_only() { + let runner = std::fs::read_to_string( + repo_root().join("crates/execution/assets/runners/python-runner.mjs"), + ) + .expect("read Python runner"); + let classifier = runner + .split_once("def _agentos_raise_from_error(error):") + .expect("Python bridge error classifier") + .1 + .split_once("def _agentos_bridge_error(error):") + .expect("end of Python bridge error classifier") + .0; + + assert!(classifier.contains("code in (\"EACCES\", \"EPERM\")")); + assert!(classifier.contains("code == \"ENOENT\"")); + assert!(classifier.contains("exception.code = code")); + assert!(classifier.contains("exception.details = details")); + assert!( + !classifier.contains(" in message") && !classifier.contains("message.lower("), + "Python exception classes must never be inferred from engine-specific error strings" + ); + + let bridge_normalizer = runner + .split_once("function normalizePythonBridgeError(error) {") + .expect("Python bridge error normalizer") + .1 + .split_once("function createPythonBridgeRpcBridge() {") + .expect("end of Python bridge error normalizer") + .0; + assert!(bridge_normalizer.contains("typeof error?.code === 'string'")); + assert!(bridge_normalizer.contains("normalized.code = structuredCode")); + assert!(bridge_normalizer.contains(": 'EIO'")); + assert!(bridge_normalizer.contains("normalized.details = error.details")); + for forbidden in [ + "separatorIndex", + "message.indexOf(", + "message.slice(", + ".test(code)", + ] { + assert!( + !bridge_normalizer.contains(forbidden), + "Python bridge errno must come from error.code, not diagnostic parsing ({forbidden})" + ); + } + + let socket_classifier = runner + .split_once("def _agentos_socket_oserror(exc):") + .expect("Python socket error classifier") + .1 + .split_once("def _agentos_socket_rpc(call):") + .expect("end of Python socket error classifier") + .0; + assert!(socket_classifier.contains("code_name = getattr(exc, \"code\", None)")); + assert!(socket_classifier.contains("_agentos_errno.EIO")); + assert!(socket_classifier.contains("mapped = OSError(errno_value, message)")); + assert!(socket_classifier.contains("mapped.details = details")); + for forbidden in [ + "message.split(", + "message.lower(", + "message.upper(", + " in message", + "head =", + ] { + assert!( + !socket_classifier.contains(forbidden), + "Python socket errno must come from exc.code, not diagnostic parsing ({forbidden})" + ); + } + + let filesystem_classifier = runner + .split_once(" function createFsError(error) {") + .expect("Python filesystem error classifier") + .1 + .split_once(" function withFsErrors(operation) {") + .expect("end of Python filesystem error classifier") + .0; + assert!(filesystem_classifier.contains("typeof error?.code === 'string'")); + assert!(filesystem_classifier.contains("ERRNO_CODES[code]")); + assert!(filesystem_classifier.contains("ERRNO_CODES.EIO")); + assert!(filesystem_classifier.contains("mapped.code = code || 'EIO'")); + assert!(filesystem_classifier.contains("mapped.message =")); + assert!(filesystem_classifier.contains("mapped.details = error.details")); + for forbidden in [ + ".toLowerCase(", + ".toUpperCase(", + ".test(message)", + "message.includes(", + ] { + assert!( + !filesystem_classifier.contains(forbidden), + "Python filesystem errno must come from error.code, not diagnostic parsing ({forbidden})" + ); + } +} + +#[test] +fn reactor_event_errors_preserve_typed_codes() { + let root = repo_root(); + for relative in [ + "crates/native-sidecar/src/execution/network/managed.rs", + "crates/native-sidecar/src/execution/javascript/rpc.rs", + "crates/native-sidecar/src/execution/network/tcp.rs", + ] { + let source = std::fs::read_to_string(root.join(relative)).expect("read reactor adapter"); + for forbidden in [ + "SidecarError::Execution(format!(\"{code}: {message}\"))", + "SidecarError::Execution(format!(\"{detail}: {message}\"))", + ] { + assert!( + !source.contains(forbidden), + "{relative} must carry reactor error codes structurally, not encode them into diagnostics" + ); + } + } + + let managed = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/network/managed.rs"), + ) + .expect("read runtime-neutral network adapter"); + assert!( + managed + .matches("code.as_deref().unwrap_or(\"EIO\")") + .count() + >= 3, + "runtime-neutral read and accept errors need stable typed codes" + ); +} diff --git a/crates/native-sidecar/tests/bidirectional_frames.rs b/crates/native-sidecar/tests/bidirectional_frames.rs index 22dca38294..339ba3419a 100644 --- a/crates/native-sidecar/tests/bidirectional_frames.rs +++ b/crates/native-sidecar/tests/bidirectional_frames.rs @@ -132,9 +132,9 @@ fn native_sidecar_bounds_popped_unanswered_sidecar_requests() { #[test] fn native_sidecar_bounds_completed_sidecar_responses() { let (mut sidecar, ownership) = new_vm_scope("bidirectional-completed-bound"); - let mut latest_request_id = 0; + let mut oldest_request_id = None; - for index in 0..=SIDECAR_CALLBACK_LIMIT { + for index in 0..SIDECAR_CALLBACK_LIMIT { let request_id = sidecar .queue_wire_sidecar_request(ownership.clone(), host_callback(index)) .expect("queue wire sidecar request"); @@ -151,22 +151,47 @@ fn native_sidecar_bounds_completed_sidecar_responses() { payload: host_callback_response(index), }) .expect("accept wire sidecar response"); - latest_request_id = request_id; + oldest_request_id.get_or_insert(request_id); } + let rejected_request_id = sidecar + .queue_wire_sidecar_request(ownership.clone(), host_callback(SIDECAR_CALLBACK_LIMIT)) + .expect("queue completion-pressure request"); + sidecar + .pop_wire_sidecar_request() + .expect("pop completion-pressure request") + .expect("completion-pressure request must be queued"); + let error = sidecar + .accept_wire_sidecar_response(SidecarResponseFrame { + schema: agentos_native_sidecar::wire::protocol_schema(), + request_id: rejected_request_id, + ownership: ownership.clone(), + payload: host_callback_response(SIDECAR_CALLBACK_LIMIT), + }) + .expect_err("completed response pressure must reject without eviction"); assert!( - sidecar - .take_wire_sidecar_response(-1) - .expect("take evicted wire sidecar response") - .is_none(), - "oldest completed response should be evicted" + error.to_string().contains("ERR_AGENTOS_RESOURCE_LIMIT"), + "completion pressure must return the typed resource limit: {error}" ); - assert_eq!( + + let oldest_request_id = oldest_request_id.expect("at least one accepted response"); + assert!( sidecar - .take_wire_sidecar_response(latest_request_id) - .expect("take latest wire sidecar response") - .expect("latest completed response should remain") - .request_id, - latest_request_id + .take_wire_sidecar_response(oldest_request_id) + .expect("take retained oldest wire sidecar response") + .is_some(), + "completion pressure must not evict the oldest response" ); + sidecar + .accept_wire_sidecar_response(SidecarResponseFrame { + schema: agentos_native_sidecar::wire::protocol_schema(), + request_id: rejected_request_id, + ownership, + payload: host_callback_response(SIDECAR_CALLBACK_LIMIT), + }) + .expect("draining one completion permits retry"); + assert!(sidecar + .take_wire_sidecar_response(rejected_request_id) + .expect("take retried wire sidecar response") + .is_some()); } diff --git a/crates/native-sidecar/tests/builtin_completeness.rs b/crates/native-sidecar/tests/builtin_completeness.rs index 030cb4d78f..8f52fc94b1 100644 --- a/crates/native-sidecar/tests/builtin_completeness.rs +++ b/crates/native-sidecar/tests/builtin_completeness.rs @@ -138,6 +138,10 @@ const BUILTIN_EXPECTATIONS: &[BuiltinExpectation] = &[ name: "assert", status: BuiltinStatus::Polyfilled, }, + BuiltinExpectation { + name: "assert/strict", + status: BuiltinStatus::Polyfilled, + }, BuiltinExpectation { name: "constants", status: BuiltinStatus::Polyfilled, @@ -196,7 +200,7 @@ const BUILTIN_EXPECTATIONS: &[BuiltinExpectation] = &[ }, BuiltinExpectation { name: "inspector", - status: BuiltinStatus::Denied, + status: BuiltinStatus::StubOk, }, BuiltinExpectation { name: "v8", @@ -238,6 +242,7 @@ const BUILTIN_EXPECTATIONS: &[BuiltinExpectation] = &[ const EXPECTED_RUNTIME_BUILTINS: &[&str] = &[ "assert", + "assert/strict", "async_hooks", "buffer", "child_process", diff --git a/crates/native-sidecar/tests/builtin_conformance.rs b/crates/native-sidecar/tests/builtin_conformance.rs index aea2370d7a..79140607c4 100644 --- a/crates/native-sidecar/tests/builtin_conformance.rs +++ b/crates/native-sidecar/tests/builtin_conformance.rs @@ -2,8 +2,8 @@ mod support; use agentos_native_sidecar::wire::{ CloseStdinRequest, CreateVmRequest, DisposeReason, DisposeVmRequest, EventPayload, - GuestRuntimeKind, PatternPermissionScope, PermissionMode, PermissionsPolicy, RequestPayload, - ResponsePayload, RootFilesystemDescriptor, RootFilesystemMode, StreamChannel, + ExecuteRequest, GuestRuntimeKind, PatternPermissionScope, PermissionMode, PermissionsPolicy, + RequestPayload, ResponsePayload, RootFilesystemDescriptor, RootFilesystemMode, StreamChannel, WriteStdinRequest, }; use hickory_resolver::proto::op::{Message, Query}; @@ -1483,6 +1483,181 @@ fn readline_question_reads_real_stdin() { run_isolated_builtin_conformance_test("readline-question"); } +fn tty_stdin_uses_kernel_canonical_and_raw_discipline_impl() { + assert_node_available(); + + let cwd = temp_dir("builtin-tty-stdin-discipline"); + let entrypoint = cwd.join("entry.mjs"); + write_fixture( + &entrypoint, + r#" +process.stdin.setEncoding("utf8"); +process.stdout.write("__READY_CANON__\n"); + +process.stdin.once("data", (canonical) => { + process.stdout.write(`__CANON__${JSON.stringify(canonical)}\n`); + process.stdin.setRawMode(true); + process.stdout.write("__READY_RAW__\n"); + process.stdin.once("data", (raw) => { + process.stdout.write(`__RAW__${JSON.stringify(raw)}\n`); + }); +}); +"#, + ); + + let mut sidecar = new_sidecar("builtin-tty-stdin-discipline"); + let connection_id = authenticate_wire(&mut sidecar, "conn-tty-stdin-discipline"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let vm_id = create_vm_with_metadata_and_permissions( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + HashMap::new(), + wire_permissions_allow_all(), + ); + let process_id = "proc-tty-stdin-discipline"; + let start = sidecar + .dispatch_wire_blocking(wire_request( + 4, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.to_owned(), + command: None, + runtime: Some(GuestRuntimeKind::JavaScript), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::from([(String::from("AGENTOS_EXEC_TTY"), String::from("1"))]), + cwd: None, + wasm_permission_tier: None, + }), + )) + .expect("start TTY stdin discipline probe"); + assert!(matches!( + start.response.payload, + ResponsePayload::ProcessStartedResponse(_) + )); + + let ownership = wire_session(&connection_id, &session_id); + let deadline = Instant::now() + Duration::from_secs(10); + let mut stdout = String::new(); + let mut stderr = String::new(); + let mut canonical_partial_sent_at = None; + let mut canonical_line_sent = false; + let mut raw_byte_sent = false; + let mut stdin_closed = false; + let mut exit = None; + + loop { + if let Some(event) = sidecar + .poll_event_wire_blocking(&ownership, Duration::from_millis(25)) + .expect("poll TTY stdin discipline event") + { + match event.payload { + EventPayload::ProcessOutputEvent(output) if output.process_id == process_id => { + match output.channel { + StreamChannel::Stdout => { + append_probe_output(&mut stdout, &output.chunk, process_id, "stdout") + } + StreamChannel::Stderr => { + append_probe_output(&mut stderr, &output.chunk, process_id, "stderr") + } + } + } + EventPayload::ProcessExitedEvent(exited) if exited.process_id == process_id => { + exit = Some((exited.exit_code, Instant::now())); + } + _ => {} + } + } + + if canonical_partial_sent_at.is_none() && stdout.contains("__READY_CANON__") { + write_process_stdin( + &mut sidecar, + 5, + &connection_id, + &session_id, + &vm_id, + process_id, + "alpha", + ); + canonical_partial_sent_at = Some(Instant::now()); + } + if !canonical_line_sent + && canonical_partial_sent_at + .is_some_and(|sent_at| sent_at.elapsed() >= Duration::from_millis(100)) + { + assert!( + !stdout.contains("__CANON__"), + "canonical TTY input became readable before newline: {stdout:?}" + ); + write_process_stdin( + &mut sidecar, + 6, + &connection_id, + &session_id, + &vm_id, + process_id, + "\n", + ); + canonical_line_sent = true; + } + if !raw_byte_sent && stdout.contains("__READY_RAW__") { + write_process_stdin( + &mut sidecar, + 7, + &connection_id, + &session_id, + &vm_id, + process_id, + "z", + ); + raw_byte_sent = true; + } + if !stdin_closed && stdout.contains("__RAW__") { + close_process_stdin( + &mut sidecar, + 8, + &connection_id, + &session_id, + &vm_id, + process_id, + ); + stdin_closed = true; + } + + if let Some((exit_code, seen_at)) = exit { + if seen_at.elapsed() >= Duration::from_millis(200) { + dispose_vm_and_close_session_wire( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + ); + assert_eq!(exit_code, 0, "TTY probe failed: {stderr}"); + assert!(stderr.trim().is_empty(), "unexpected TTY stderr: {stderr}"); + assert_eq!(stdout.matches("__CANON__").count(), 1, "{stdout:?}"); + assert_eq!(stdout.matches("__RAW__").count(), 1, "{stdout:?}"); + assert!(stdout.contains(r#"__CANON__"alpha\n""#), "{stdout:?}"); + assert!(stdout.contains(r#"__RAW__"z""#), "{stdout:?}"); + return; + } + } + + assert!( + Instant::now() < deadline, + "timed out waiting for TTY stdin discipline probe\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + } +} + +#[test] +fn tty_stdin_uses_kernel_canonical_and_raw_discipline() { + run_isolated_builtin_conformance_test("tty-stdin-discipline"); +} + fn vm_is_context_only_accepts_create_context_tagged_sandboxes_impl() { assert_conformance( "vm-is-context", @@ -2464,10 +2639,14 @@ console.log(JSON.stringify({ fn child_process_fork_supports_basic_ipc_impl() { let cwd = temp_dir("builtin-child-process-fork-ipc"); let entrypoint = cwd.join("entry.mjs"); - let worker = cwd.join("worker.mjs"); write_fixture( - &worker, + &entrypoint, r#" +import childProcess from "node:child_process"; +import { Buffer } from "node:buffer"; +import fs from "node:fs"; + +fs.writeFileSync("./worker.mjs", ` process.send({ type: "ready", connected: process.connected, @@ -2482,13 +2661,7 @@ process.on("message", (message) => { }); process.exit(0); }); -"#, - ); - write_fixture( - &entrypoint, - r#" -import childProcess from "node:child_process"; -import { Buffer } from "node:buffer"; +`); const child = childProcess.fork("./worker.mjs", ["worker-arg"]); const stdout = []; @@ -2509,9 +2682,21 @@ child.on("message", (message) => { } }); +const diagnosticTimer = setTimeout(() => { + console.error(JSON.stringify({ + marker: "fork-ipc-timeout", + connected: child.connected, + sendReturn, + messages, + errors, + stdoutBase64: Buffer.concat(stdout).toString("base64"), + })); + child.kill("SIGTERM"); +}, 8000); const exit = await new Promise((resolve) => { child.on("close", (code, signal) => resolve({ code, signal })); }); +clearTimeout(diagnosticTimer); console.log(JSON.stringify({ connectedAfterFork: child.connected, @@ -4944,6 +5129,7 @@ fn __builtin_conformance_extra_test_runner() { readable_on_data_respects_explicit_pause_matches_host_node_impl() } "readline-question" => readline_question_reads_real_stdin_impl(), + "tty-stdin-discipline" => tty_stdin_uses_kernel_canonical_and_raw_discipline_impl(), "vm-is-context" => vm_is_context_only_accepts_create_context_tagged_sandboxes_impl(), "vm-context-isolation" => vm_context_isolation_and_script_options_match_host_node_impl(), "vm-optional-surface" => { diff --git a/crates/native-sidecar/tests/filesystem.rs b/crates/native-sidecar/tests/filesystem.rs index b6816ab862..8f5b0548ac 100644 --- a/crates/native-sidecar/tests/filesystem.rs +++ b/crates/native-sidecar/tests/filesystem.rs @@ -347,7 +347,7 @@ mod host_dir { } } -mod shadow_root { +mod kernel_authority { use agentos_native_sidecar::wire::{ ConfigureVmRequest, DisposeReason, DisposeVmRequest, EventPayload, ExecuteRequest, GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, MountDescriptor, @@ -357,8 +357,6 @@ mod shadow_root { use serde_json::json; use std::collections::HashMap; use std::fs; - use std::os::unix::fs::PermissionsExt; - use std::path::{Path, PathBuf}; use std::time::Duration; use crate::support::{ @@ -559,330 +557,11 @@ mod shadow_root { .expect("guest lstat response should include stat") } - fn guest_read_text( - sidecar: &mut agentos_native_sidecar::NativeSidecar, - connection_id: &str, - session_id: &str, - vm_id: &str, - request_id: i64, - path: &str, - ) -> String { - let response = guest_filesystem_call( - sidecar, - connection_id, - session_id, - vm_id, - request_id, - base_guest_filesystem_request(GuestFilesystemOperation::ReadFile, path), - ); - assert_eq!( - response.encoding, - Some(RootFilesystemEntryEncoding::Utf8), - "test fixture should remain UTF-8" - ); - response - .content - .expect("read response should include content") - } - - fn locate_shadow_root(marker_guest_path: &str) -> PathBuf { - let marker_relative = marker_guest_path.trim_start_matches('/'); - fs::read_dir(std::env::temp_dir()) - .expect("list temp dir") - .filter_map(|entry| entry.ok().map(|entry| entry.path())) - .find(|candidate| { - candidate - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("agentos-native-sidecar-shadow-")) - && fs::symlink_metadata(candidate.join(marker_relative)).is_ok() - }) - .expect("locate VM shadow root through unique marker") - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - enum TestNodeType { - File, - Symlink, - Directory, - } - - fn create_guest_test_node( - sidecar: &mut agentos_native_sidecar::NativeSidecar, - connection_id: &str, - session_id: &str, - vm_id: &str, - request_id: &mut i64, - path: &str, - node_type: TestNodeType, - ) { - let request = match node_type { - TestNodeType::File => { - let mut request = - base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, path); - request.content = Some(String::from("replacement file\n")); - request.encoding = Some(RootFilesystemEntryEncoding::Utf8); - request - } - TestNodeType::Symlink => { - let mut request = - base_guest_filesystem_request(GuestFilesystemOperation::Symlink, path); - request.target = Some(String::from("target.txt")); - request - } - TestNodeType::Directory => { - let mut request = - base_guest_filesystem_request(GuestFilesystemOperation::Mkdir, path); - request.recursive = true; - request - } - }; - guest_filesystem_call( - sidecar, - connection_id, - session_id, - vm_id, - *request_id, - request, - ); - *request_id += 1; - } - - fn replace_shadow_test_node(path: &Path, node_type: TestNodeType) { - if let Ok(metadata) = fs::symlink_metadata(path) { - if metadata.is_dir() && !metadata.file_type().is_symlink() { - fs::remove_dir_all(path).expect("remove old shadow directory"); - } else { - fs::remove_file(path).expect("remove old shadow file or symlink"); - } - } - match node_type { - TestNodeType::File => { - fs::write(path, b"replacement file\n").expect("write replacement shadow file") - } - TestNodeType::Symlink => std::os::unix::fs::symlink("target.txt", path) - .expect("create replacement shadow symlink"), - TestNodeType::Directory => { - fs::create_dir(path).expect("create replacement shadow directory") - } - } - } - - /// Deleting a path directly from the VM shadow root (the way host-side - /// guest runtimes delete files, without a kernel-direct unlink) must - /// propagate into the kernel VFS on the next shadow sync walk instead of - /// being resurrected by the additive copy-in. - #[test] - fn shadow_direct_deletions_before_first_sync_propagate_into_kernel_vfs() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - // Guest filesystem calls need no command mounts; create the VM bare so - // this regression test runs even without built registry commands. - let cwd = temp_dir("filesystem-shadow-reconcile-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let doomed_root = format!("/workspace/reconcile-{nonce}"); - let doomed_dir = format!("{doomed_root}/nested"); - let doomed_file = format!("{doomed_dir}/probe.txt"); - let survivor_file = format!("/workspace/reconcile-survivor-{nonce}.txt"); - - let mut mkdir = - base_guest_filesystem_request(GuestFilesystemOperation::CreateDir, &doomed_dir); - mkdir.recursive = true; - guest_filesystem_call(&mut sidecar, &connection_id, &session_id, &vm_id, 20, mkdir); - - let mut write = - base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, &doomed_file); - write.content = Some(String::from("doomed\n")); - write.encoding = Some(RootFilesystemEntryEncoding::Utf8); - guest_filesystem_call(&mut sidecar, &connection_id, &session_id, &vm_id, 21, write); - - let mut survivor = - base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, &survivor_file); - survivor.content = Some(String::from("survivor\n")); - survivor.encoding = Some(RootFilesystemEntryEncoding::Utf8); - guest_filesystem_call( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - 22, - survivor, - ); - - // Locate this VM's shadow root through the mirrored unique file, then - // delete the subtree before any read-side sync primes the inventory. - // This is exactly how a short-lived host-side runtime file can be - // created and deleted between reconciliation walks. - let marker_rel = format!("workspace/reconcile-{nonce}/nested/probe.txt"); - let shadow_root = std::fs::read_dir(std::env::temp_dir()) - .expect("list temp dir") - .filter_map(|entry| entry.ok().map(|entry| entry.path())) - .find(|candidate| { - candidate - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("agentos-native-sidecar-shadow-")) - && candidate.join(&marker_rel).is_file() - }) - .expect("locate VM shadow root through mirrored probe file"); - fs::remove_dir_all(shadow_root.join(format!("workspace/reconcile-{nonce}"))) - .expect("delete subtree from the shadow root"); - - // The next host filesystem call re-walks the shadow; the kernel must - // drop the deleted subtree instead of resurrecting it forever. - assert!( - !guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - 24, - &doomed_file, - ), - "kernel resurrected a file deleted from the shadow root" - ); - assert!( - !guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - 25, - &doomed_root, - ), - "kernel resurrected a directory deleted from the shadow root" - ); - assert!( - guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - 26, - &survivor_file, - ), - "deletion reconcile must not remove shadow-backed paths that still exist" - ); - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - #[test] - fn shadow_type_replacements_cover_file_symlink_directory_matrix() { + fn kernel_rename_moves_a_broken_symlink_without_recreating_its_source() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-replacement-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let base = format!("/workspace/replacement-matrix-{nonce}"); - let target = format!("{base}/target.txt"); - let mut request_id = 20; - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &target, - TestNodeType::File, - ); - let shadow_root = locate_shadow_root(&target); - let transitions = [ - (TestNodeType::File, TestNodeType::Symlink), - (TestNodeType::File, TestNodeType::Directory), - (TestNodeType::Symlink, TestNodeType::File), - (TestNodeType::Symlink, TestNodeType::Directory), - (TestNodeType::Directory, TestNodeType::File), - (TestNodeType::Directory, TestNodeType::Symlink), - ]; - - for (index, (initial, replacement)) in transitions.into_iter().enumerate() { - let path = format!("{base}/node-{index}"); - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &path, - initial, - ); - // Prime this node's initial type so reconciliation must explicitly - // unlink the stale kernel node before copying the replacement. - guest_lstat( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &path, - ); - request_id += 1; - - replace_shadow_test_node(&shadow_root.join(path.trim_start_matches('/')), replacement); - let stat = guest_lstat( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &path, - ); - request_id += 1; - match replacement { - TestNodeType::File => { - assert!(!stat.is_directory && !stat.is_symbolic_link) - } - TestNodeType::Symlink => assert!(stat.is_symbolic_link), - TestNodeType::Directory => { - assert!(stat.is_directory && !stat.is_symbolic_link) - } - } - assert_eq!( - guest_read_text( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &target, - ), - "replacement file\n", - "replacing {initial:?} with {replacement:?} followed and overwrote the stale symlink target" - ); - request_id += 1; - } - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - - #[test] - fn guest_rename_moves_a_broken_symlink_without_resurrecting_its_source() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-broken-symlink-rename-cwd"); + let cwd = temp_dir("filesystem-kernel-broken-symlink-rename-cwd"); let (vm_id, _) = create_vm_wire( &mut sidecar, 3, @@ -910,17 +589,6 @@ mod shadow_root { symlink_request, ); - let shadow_root = locate_shadow_root(&source); - let shadow_source = shadow_root.join(source.trim_start_matches('/')); - assert!(fs::symlink_metadata(&shadow_source) - .expect("lstat broken shadow symlink") - .file_type() - .is_symlink()); - assert!( - fs::metadata(&shadow_source).is_err(), - "test symlink must remain dangling" - ); - let mut rename_request = base_guest_filesystem_request(GuestFilesystemOperation::Rename, &source); rename_request.destination_path = Some(destination.clone()); @@ -942,7 +610,7 @@ mod shadow_root { 22, &source, ), - "reconciliation resurrected the renamed broken symlink at its source" + "rename recreated the broken symlink at its source" ); assert!( guest_lstat( @@ -972,447 +640,6 @@ mod shadow_root { dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); } - #[test] - fn shadow_mode_change_updates_an_existing_kernel_directory() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-existing-directory-mode-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let directory = format!("/workspace/mode-change-{nonce}"); - let mut request_id = 20; - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &directory, - TestNodeType::Directory, - ); - let shadow_root = locate_shadow_root(&directory); - let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o710)) - .expect("change existing shadow directory mode"); - - let stat = guest_lstat( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &directory, - ); - assert_eq!( - stat.mode & 0o7777, - 0o710, - "shadow reconciliation did not import the existing directory mode" - ); - - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o755)) - .expect("restore shadow directory mode"); - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - - #[test] - fn guest_chmod_zero_preserves_descendant_deletion_inventory() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-chmod-zero-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let directory = format!("/workspace/chmod-zero-{nonce}"); - let child = format!("{directory}/tracked-child.txt"); - let mut request_id = 20; - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &child, - TestNodeType::File, - ); - let shadow_root = locate_shadow_root(&child); - let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); - let shadow_child = shadow_root.join(child.trim_start_matches('/')); - - let mut chmod_request = - base_guest_filesystem_request(GuestFilesystemOperation::Chmod, &directory); - chmod_request.mode = Some(0); - guest_filesystem_call( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - chmod_request, - ); - request_id += 1; - - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o755)) - .expect("restore shadow directory access for direct deletion"); - fs::remove_file(&shadow_child).expect("delete tracked child from shadow"); - assert!( - !guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &child, - ), - "chmod 000 discarded descendant inventory and resurrected a deleted child" - ); - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - - #[test] - fn unreadable_shadow_subtree_does_not_delete_kernel_children() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-unreadable-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let directory = format!("/workspace/unreadable-{nonce}"); - let child = format!("{directory}/preserved.txt"); - let mut request_id = 20; - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &child, - TestNodeType::File, - ); - assert!(guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &child, - )); - - let shadow_root = locate_shadow_root(&child); - let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); - let original_mode = fs::metadata(&shadow_directory) - .expect("stat shadow directory") - .permissions() - .mode(); - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o000)) - .expect("make shadow directory unreadable"); - if fs::read_dir(&shadow_directory).is_ok() { - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(original_mode)) - .expect("restore readable shadow directory"); - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - return; - } - - let preserved = guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id + 1, - &child, - ); - fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(original_mode)) - .expect("restore readable shadow directory"); - assert!( - preserved, - "an unreadable shadow subtree was mistaken for a deleted subtree" - ); - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - - #[test] - fn shadow_sync_skips_every_live_normalized_mount_boundary() { - let host_mount = temp_dir("filesystem-shadow-mount-boundary-host"); - fs::write(host_mount.join("value.txt"), b"host plugin\n").expect("seed host mount file"); - - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-mount-boundary-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let locator = format!("/workspace/mount-boundary-locator-{nonce}.txt"); - let mut request_id = 20; - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &locator, - TestNodeType::File, - ); - let shadow_root = locate_shadow_root(&locator); - - configure_vm_mounts( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - vec![ - MountDescriptor { - guest_path: String::from("/mount-boundaries/./memory//"), - guest_source: String::from("memory"), - guest_fstype: String::from("memory"), - read_only: false, - plugin: MountPluginDescriptor { - id: String::from("memory"), - config: String::from("{}"), - }, - }, - MountDescriptor { - guest_path: String::from("/mount-boundaries/host/../host//"), - guest_source: String::from("host_dir"), - guest_fstype: String::from("host_dir"), - read_only: false, - plugin: MountPluginDescriptor { - id: String::from("host_dir"), - config: serde_json::to_string(&json!({ - "hostPath": host_mount.to_string_lossy().into_owned(), - "readOnly": false, - })) - .expect("serialize host mount config"), - }, - }, - ], - ); - - let memory_path = "/mount-boundaries/memory/value.txt"; - let mut write_memory = - base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, memory_path); - write_memory.content = Some(String::from("memory plugin\n")); - write_memory.encoding = Some(RootFilesystemEntryEncoding::Utf8); - guest_filesystem_call( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - write_memory, - ); - request_id += 1; - - let shadow_memory = shadow_root.join("mount-boundaries/memory/value.txt"); - let shadow_host = shadow_root.join("mount-boundaries/host/value.txt"); - fs::create_dir_all(shadow_host.parent().expect("shadow host parent")) - .expect("create stale host mount shadow"); - fs::write(&shadow_memory, b"stale shadow\n").expect("overwrite memory mount shadow"); - fs::write(&shadow_host, b"stale shadow\n").expect("write host mount shadow"); - - assert_eq!( - guest_read_text( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - memory_path, - ), - "memory plugin\n" - ); - request_id += 1; - assert_eq!( - guest_read_text( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - "/mount-boundaries/host/value.txt", - ), - "host plugin\n" - ); - request_id += 1; - - fs::remove_dir_all(shadow_root.join("mount-boundaries/memory")) - .expect("delete memory mount shadow subtree"); - fs::remove_dir_all(shadow_root.join("mount-boundaries/host")) - .expect("delete host mount shadow subtree"); - assert_eq!( - guest_read_text( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - memory_path, - ), - "memory plugin\n", - "shadow deletion crossed the normalized memory mount boundary" - ); - request_id += 1; - assert_eq!( - guest_read_text( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - "/mount-boundaries/host/value.txt", - ), - "host plugin\n", - "shadow deletion crossed the normalized host_dir mount boundary" - ); - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - fs::remove_dir_all(host_mount).expect("remove host mount temp dir"); - } - - #[test] - fn failed_shadow_directory_deletion_retries_after_unmounted_mountpoint_is_removed() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); - let cwd = temp_dir("filesystem-shadow-delete-retry-cwd"); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::JavaScript, - &cwd, - ); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let directory = format!("/workspace/delete-retry-{nonce}"); - let mountpoint = format!("{directory}/mounted"); - let mut request_id = 20; - - create_guest_test_node( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - &mut request_id, - &directory, - TestNodeType::Directory, - ); - assert!(guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &directory, - )); - request_id += 1; - - // Mounting creates the mountpoint in the kernel VFS but not in the host - // shadow. After unmounting, that kernel-only directory makes the first - // tracked parent removal fail ENOTEMPTY. - configure_vm_mounts( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - vec![MountDescriptor { - guest_path: mountpoint.clone(), - guest_source: String::from("memory"), - guest_fstype: String::from("memory"), - read_only: false, - plugin: MountPluginDescriptor { - id: String::from("memory"), - config: String::from("{}"), - }, - }], - ); - configure_vm_mounts( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - Vec::new(), - ); - let shadow_root = locate_shadow_root(&directory); - fs::remove_dir_all(shadow_root.join(directory.trim_start_matches('/'))) - .expect("remove retry directory from shadow"); - assert!( - guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &directory, - ), - "first non-empty directory deletion should leave the kernel directory for retry" - ); - request_id += 1; - - guest_filesystem_call( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - base_guest_filesystem_request(GuestFilesystemOperation::RemoveDir, &mountpoint), - ); - request_id += 1; - assert!( - !guest_path_exists( - &mut sidecar, - &connection_id, - &session_id, - &vm_id, - request_id, - &directory, - ), - "pending directory deletion was not retried after its blocker disappeared" - ); - - dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); - } - - #[allow(clippy::too_many_arguments)] fn execute_command( sidecar: &mut agentos_native_sidecar::NativeSidecar, connection_id: &str, diff --git a/crates/native-sidecar/tests/fixtures/limits-inventory.json b/crates/native-sidecar/tests/fixtures/limits-inventory.json index f9fca61aa7..062cd5c607 100644 --- a/crates/native-sidecar/tests/fixtures/limits-inventory.json +++ b/crates/native-sidecar/tests/fixtures/limits-inventory.json @@ -267,7 +267,7 @@ "name": "MAX_FDS_PER_PROCESS", "path": "crates/kernel/src/fd_table.rs", "class": "invariant", - "rationale": "FD table layout fixed at 0-255; max_open_fds is the policy knob above it." + "rationale": "Bounded standalone/test fallback aligned with the default max_open_fds policy; production tables use the configured policy value." }, { "name": "DEFAULT_MAX_RECORD_LOCKS", @@ -323,6 +323,30 @@ "class": "invariant", "rationale": "Mirrors Linux PTY buffer semantics." }, + { + "name": "MAX_PTY_READ_BYTES", + "path": "crates/kernel/src/pty.rs", + "class": "invariant", + "rationale": "A single PTY read cannot usefully exceed the bounded PTY buffer it drains." + }, + { + "name": "MAX_PTY_WRITE_BYTES", + "path": "crates/kernel/src/pty.rs", + "class": "invariant", + "rationale": "A single PTY write is capped at the bounded PTY buffer before line-discipline processing." + }, + { + "name": "MAX_PTY_READ_WAITERS", + "path": "crates/kernel/src/pty.rs", + "class": "policy-deferred", + "rationale": "Bounds parked PTY read state; make this VM-configurable if workloads demonstrate a legitimate need above the conservative default." + }, + { + "name": "MAX_PTY_RETAINED_READ_BYTES", + "path": "crates/kernel/src/pty.rs", + "class": "policy-deferred", + "rationale": "Bounds read results retained between PTY delivery and waiter wakeup; future VM policy may tune the global per-kernel budget." + }, { "name": "DEFAULT_BLOCKING_READ_TIMEOUT_MS", "path": "crates/kernel/src/resource_accounting.rs", @@ -919,6 +943,13 @@ "rationale": "Default JavaScript wall-clock backstop; zero keeps it disabled.", "wired": "VmLimits.js_runtime.wall_clock_limit_ms" }, + { + "name": "DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS", + "path": "crates/native-sidecar-core/src/limits.rs", + "class": "policy", + "rationale": "Default active CPU runaway safeguard for standalone WASM execution.", + "wired": "VmLimits.wasm.active_cpu_time_limit_ms" + }, { "name": "DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES", "path": "crates/native-sidecar-core/src/limits.rs", @@ -1176,12 +1207,6 @@ "class": "policy-deferred", "rationale": "Crypto/state handle table cap tunable in principle; low demand, wire later." }, - { - "name": "PYTHON_SOCKET_MAX_RECV", - "path": "crates/native-sidecar/src/execution/python/sockets.rs", - "class": "policy-deferred", - "rationale": "Guest Python socket recv request clamp for one synchronous host read; bounded by default, fold into Python/socket VmLimits if operators need to tune it." - }, { "name": "SQLITE_JS_SAFE_INTEGER_MAX", "path": "crates/native-sidecar/src/execution/javascript/sqlite.rs", @@ -1194,18 +1219,6 @@ "class": "invariant", "rationale": "UDP payload ceiling imposed by datagram protocol semantics; buffering remains separately policy-bounded." }, - { - "name": "MAX_CROSS_DEVICE_MOVE_DEPTH", - "path": "crates/native-sidecar/src/filesystem.rs", - "class": "policy-deferred", - "rationale": "Host filesystem safety bound; expose through typed filesystem policy if operators need to tune it." - }, - { - "name": "MAX_MAPPED_TRUNCATE_BYTES", - "path": "crates/native-sidecar/src/filesystem.rs", - "class": "policy-deferred", - "rationale": "Host filesystem safety bound; expose through typed filesystem policy if operators need to tune it." - }, { "name": "MAX_AGENTOS_PACKAGE_MOUNTS", "path": "crates/native-sidecar/src/package_projection.rs", @@ -2043,5 +2056,405 @@ "path": "packages/core/src/bindings.ts", "class": "policy-deferred", "rationale": "Cross-boundary contract with native-sidecar-core bindings validation; both sides must change together." + }, + { + "name": "APPEND_WIRE_BYTE_LIMIT", + "path": "crates/agentos-sidecar/src/session_store/performance_tests.rs", + "class": "invariant", + "rationale": "Performance-test assertion for bounded append response encoding; not production policy." + }, + { + "name": "HISTORY_COMPLEXITY_LIMIT", + "path": "crates/agentos-sidecar/src/session_store/performance_tests.rs", + "class": "invariant", + "rationale": "Performance-test workload size; production history policy is configured separately." + }, + { + "name": "SESSION_LIST_WIRE_BYTE_LIMIT", + "path": "crates/agentos-sidecar/src/session_store/performance_tests.rs", + "class": "invariant", + "rationale": "Performance-test assertion for bounded session-list response encoding; not production policy." + }, + { + "name": "DEFAULT_BLOCK_CACHE_BYTES", + "path": "crates/vfs-store/src/local/file_block_store.rs", + "class": "policy-deferred", + "rationale": "Bounded local block-cache default; expose through storage configuration if operators need to tune it." + }, + { + "name": "DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_BYTES", + "path": "crates/native-sidecar-core/src/limits.rs", + "class": "policy", + "rationale": "Default retained-byte budget for pending child sync calls.", + "wired": "VmLimits.process.max_pending_child_sync_bytes" + }, + { + "name": "DEFAULT_PROCESS_MAX_PENDING_CHILD_SYNC_COUNT", + "path": "crates/native-sidecar-core/src/limits.rs", + "class": "policy", + "rationale": "Default count budget for pending child sync calls.", + "wired": "VmLimits.process.max_pending_child_sync_count" + }, + { + "name": "DEFAULT_WASM_PENDING_EVENT_BYTES", + "path": "crates/execution/src/wasm.rs", + "class": "policy", + "rationale": "Standalone WASM pending-event byte fallback; production launches receive the typed process limit.", + "wired": "WasmExecutionLimits.pending_event_bytes" + }, + { + "name": "DEFAULT_WASM_SYNC_RPC_RESPONSE_LINE_BYTES", + "path": "crates/execution/src/wasm.rs", + "class": "policy", + "rationale": "Standalone WASM sync-response framing fallback; production launches receive the typed bridge limit.", + "wired": "WasmExecutionLimits.max_sync_rpc_response_line_bytes" + }, + { + "name": "MAX_ACCOUNTS", + "path": "crates/vm-config/src/lib.rs", + "class": "policy-deferred", + "rationale": "Bounded configured guest account count; expose separately only if larger account databases are required." + }, + { + "name": "MAX_ACCOUNT_RECORD_BYTES", + "path": "crates/vm-config/src/lib.rs", + "class": "invariant", + "rationale": "Owned account ABI record ceiling, leaving one byte for the terminating NUL." + }, + { + "name": "MAX_GROUPS", + "path": "crates/vm-config/src/lib.rs", + "class": "policy-deferred", + "rationale": "Bounded configured guest group count; expose separately only if larger account databases are required." + }, + { + "name": "MAX_GROUP_MEMBERS", + "path": "crates/vm-config/src/lib.rs", + "class": "policy-deferred", + "rationale": "Bounded member count per configured guest group; expose separately if operators need to tune it." + }, + { + "name": "MAX_SUPPLEMENTARY_GIDS", + "path": "crates/vm-config/src/lib.rs", + "class": "invariant", + "rationale": "Owned Linux identity ABI supports a bounded 64-entry supplementary group set." + }, + { + "name": "MAX_ACCOUNT_NAME_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Owned account lookup ABI input ceiling." + }, + { + "name": "MAX_ACCOUNT_RECORD_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Owned account lookup ABI output ceiling." + }, + { + "name": "MAX_CHILD_PROCESS_ID_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Internal child-process correlation identifier framing ceiling." + }, + { + "name": "MAX_DEFERRED_GUEST_WAIT_MS", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Largest wait representable by the owned u32-millisecond bridge ABI." + }, + { + "name": "MAX_ENTROPY_CHUNK_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Fixed per-import entropy work quantum; aggregate guest consumption remains separately accounted." + }, + { + "name": "MAX_SIGNAL_SET_ENTRIES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Owned signal ABI represents signals 1 through 64." + }, + { + "name": "MAX_SIGNAL_STATE_MASK_JSON_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Bounded encoding ceiling for the fixed 64-entry signal set." + }, + { + "name": "MAX_SUPPLEMENTARY_GROUPS", + "path": "crates/native-sidecar/src/execution/host_dispatch/mod.rs", + "class": "invariant", + "rationale": "Owned Linux identity ABI supports a bounded 64-entry supplementary group set." + }, + { + "name": "MAX_CLOSEFROM_TARGETS", + "path": "crates/native-sidecar/src/execution/host_dispatch/filesystem.rs", + "class": "invariant", + "rationale": "Defensive work bound above every supported open-fd limit for one closefrom operation." + }, + { + "name": "MAX_PATH_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/filesystem.rs", + "class": "invariant", + "rationale": "Linux PATH_MAX-compatible owned ABI ceiling." + }, + { + "name": "MAX_READDIR_ENTRIES", + "path": "crates/native-sidecar/src/execution/host_dispatch/filesystem.rs", + "class": "policy-deferred", + "rationale": "Bounded directory batch size; expose through filesystem limits if operators need to tune it." + }, + { + "name": "MAX_XATTR_NAME_BYTES", + "path": "crates/native-sidecar/src/execution/host_dispatch/filesystem.rs", + "class": "invariant", + "rationale": "Linux XATTR_NAME_MAX-compatible owned ABI ceiling." + }, + { + "name": "MAX_ADAPTIVE_CHUNK_SIZE", + "path": "crates/vfs/src/engine/engines/chunked.rs", + "class": "invariant", + "rationale": "Storage-format chunk-selection ceiling preventing unbounded adaptive chunks." + }, + { + "name": "MAX_PENDING_WRITE_COMMITS", + "path": "crates/vfs-store/src/local/sqlite_metadata_store.rs", + "class": "policy-deferred", + "rationale": "Bounded SQLite write-coalescing queue; expose through storage configuration if tuning is needed." + }, + { + "name": "MAX_DNS_RESULTS", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Bounded Python DNS result batch; expose through networking limits if operators need to tune it." + }, + { + "name": "MAX_HOST_BYTES", + "path": "crates/execution/src/python.rs", + "class": "invariant", + "rationale": "DNS host-name wire-format ceiling." + }, + { + "name": "MAX_HTTP_HEADERS", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Bounded Python HTTP header count; expose through HTTP limits if tuning is needed." + }, + { + "name": "MAX_HTTP_HEADER_BYTES", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Bounded Python HTTP header bytes; expose through HTTP limits if tuning is needed." + }, + { + "name": "MAX_HTTP_METHOD_BYTES", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Bounded Python HTTP method bytes; expose through HTTP limits if tuning is needed." + }, + { + "name": "MAX_HTTP_URL_BYTES", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Bounded Python HTTP URL bytes; expose through HTTP limits if tuning is needed." + }, + { + "name": "MAX_PATH_BYTES", + "path": "crates/execution/src/python.rs", + "class": "invariant", + "rationale": "Linux PATH_MAX-compatible Python bridge ceiling." + }, + { + "name": "PYTHON_SOCKET_MAX_RECV", + "path": "crates/execution/src/python.rs", + "class": "policy-deferred", + "rationale": "Per-call Python socket receive clamp; expose through Python networking limits if operators need to tune it." + }, + { + "name": "MAX_FD_WRITE_BYTES_LIMIT", + "path": "crates/kernel/src/resource_accounting.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "MAX_PREAD_BYTES_LIMIT", + "path": "crates/kernel/src/resource_accounting.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "MAX_PROCESS_ARGV_BYTES_LIMIT", + "path": "crates/kernel/src/resource_accounting.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "MAX_PROCESS_ENV_BYTES_LIMIT", + "path": "crates/kernel/src/resource_accounting.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "MAX_READDIR_ENTRIES_LIMIT", + "path": "crates/kernel/src/resource_accounting.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "MAX_RUNTIME_FAULT_CODE_BYTES", + "path": "crates/kernel/src/process_runtime.rs", + "class": "invariant", + "rationale": "Bounded runtime-control error-code encoding ceiling." + }, + { + "name": "MAX_RUNTIME_FAULT_DETAILS_BYTES", + "path": "crates/kernel/src/process_runtime.rs", + "class": "invariant", + "rationale": "Bounded runtime-control structured-details encoding ceiling." + }, + { + "name": "MAX_RUNTIME_FAULT_MESSAGE_BYTES", + "path": "crates/kernel/src/process_runtime.rs", + "class": "invariant", + "rationale": "Bounded runtime-control error-message encoding ceiling." + }, + { + "name": "MAX_SIGNAL_HANDLER_DEPTH", + "path": "crates/kernel/src/process_table.rs", + "class": "invariant", + "rationale": "Kernel recursion guard for nested caught-signal delivery." + }, + { + "name": "MAX_SUPPLEMENTARY_GROUPS", + "path": "crates/kernel/src/kernel.rs", + "class": "invariant", + "rationale": "Owned Linux identity ABI supports a bounded 64-entry supplementary group set." + }, + { + "name": "POSIX_ACL_ENTRY_LIMIT", + "path": "crates/kernel/src/kernel.rs", + "class": "invariant", + "rationale": "Owned POSIX ACL xattr representation ceiling." + }, + { + "name": "XATTR_NAME_MAX", + "path": "crates/kernel/src/kernel.rs", + "class": "invariant", + "rationale": "Linux XATTR_NAME_MAX compatibility constant." + }, + { + "name": "MAX_SCRIPT_PREVIEW_BYTES", + "path": "crates/native-sidecar/src/execution/launch.rs", + "class": "invariant", + "rationale": "Bounded diagnostic-only script preview; not guest execution policy." + }, + { + "name": "NEAR_LIMIT_PERCENT", + "path": "crates/execution/src/backend/payload.rs", + "class": "invariant", + "rationale": "Shared 80-percent warning contract required by the runtime safety model." + }, + { + "name": "TEST_PROCESS_VM_EXECUTOR_LIMIT", + "path": "crates/v8-runtime/src/lib.rs", + "class": "invariant", + "rationale": "Test-only process runtime admission bound." + }, + { + "name": "VM_EXECUTOR_LIMIT_CONFIG_PATH", + "path": "crates/runtime/src/executor.rs", + "class": "invariant", + "rationale": "Stable configuration-path identifier, not a limit value." + }, + { + "name": "XATTR_LIST_MAX", + "path": "crates/vfs/src/engine/types.rs", + "class": "invariant", + "rationale": "Linux xattr list-size compatibility ceiling." + }, + { + "name": "XATTR_NAME_MAX", + "path": "crates/vfs/src/engine/types.rs", + "class": "invariant", + "rationale": "Linux XATTR_NAME_MAX compatibility constant." + }, + { + "name": "XATTR_SIZE_MAX", + "path": "crates/vfs/src/engine/types.rs", + "class": "invariant", + "rationale": "Linux xattr value-size compatibility ceiling." + }, + { + "name": "XATTR_LIST_MAX", + "path": "crates/vfs/src/posix/vfs.rs", + "class": "invariant", + "rationale": "Linux xattr list-size compatibility ceiling." + }, + { + "name": "XATTR_NAME_MAX", + "path": "crates/vfs/src/posix/vfs.rs", + "class": "invariant", + "rationale": "Linux XATTR_NAME_MAX compatibility constant." + }, + { + "name": "XATTR_SIZE_MAX", + "path": "crates/vfs/src/posix/vfs.rs", + "class": "invariant", + "rationale": "Linux xattr value-size compatibility ceiling." + }, + { + "name": "MAX_ACP_ADDITIONAL_DIRECTORIES", + "path": "crates/agentos-sidecar-core/src/engine.rs", + "class": "policy-deferred", + "rationale": "Bounded trusted ACP request collection; expose through VM configuration if operators need to tune it." + }, + { + "name": "MAX_ACP_GUEST_PATH_BYTES", + "path": "crates/agentos-sidecar-core/src/engine.rs", + "class": "policy-deferred", + "rationale": "Bounded trusted ACP guest-path payload; expose through VM configuration if operators need to tune it." + }, + { + "name": "CHILD_PROCESS_EXIT_DRAIN_MAX_MS", + "path": "packages/build-tools/bridge-src/builtins/child-process.ts", + "class": "policy-deferred", + "rationale": "Bounded child stdio drain grace period; expose through process configuration if operators need to tune it." + }, + { + "name": "CHILD_PROCESS_IPC_MAX_GRAPH_DEPTH", + "path": "packages/build-tools/bridge-src/builtins/child-process.ts", + "class": "invariant", + "rationale": "Parser recursion guard for the private child-process IPC serialization format." + }, + { + "name": "CHILD_PROCESS_IPC_MAX_GRAPH_NODES", + "path": "packages/build-tools/bridge-src/builtins/child-process.ts", + "class": "policy-deferred", + "rationale": "Bounded retained graph for child-process IPC messages; expose through process configuration if operators need to tune it." + }, + { + "name": "MAX_EARLY_CHILD_PROCESS_EVENTS", + "path": "packages/build-tools/bridge-src/builtins/child-process.ts", + "class": "policy-deferred", + "rationale": "Bounded events retained while child-process startup races handler registration; expose through process configuration if operators need to tune it." + }, + { + "name": "MAX_EARLY_CHILD_PROCESS_IDS", + "path": "packages/build-tools/bridge-src/builtins/child-process.ts", + "class": "policy-deferred", + "rationale": "Bounded child identifiers retained while startup races handler registration; expose through process configuration if operators need to tune it." + }, + { + "name": "MAX_NODE_CLI_SHIM_BYTES", + "path": "crates/native-sidecar/src/execution/launch.rs", + "class": "invariant", + "rationale": "Parser safety ceiling for the small trusted Node CLI redirect shim format." + }, + { + "name": "MAX_TAR_REALPATH_CACHE_ENTRIES", + "path": "crates/vfs/src/posix/tar_fs.rs", + "class": "invariant", + "rationale": "Process-local memoization cap; clearing the cache changes performance only, not guest behavior." } ] diff --git a/crates/native-sidecar/tests/fs_watch_and_streams.rs b/crates/native-sidecar/tests/fs_watch_and_streams.rs index 8281a22b8f..0f8fe8f643 100644 --- a/crates/native-sidecar/tests/fs_watch_and_streams.rs +++ b/crates/native-sidecar/tests/fs_watch_and_streams.rs @@ -33,8 +33,8 @@ fn root_file(path: &str, content: &str) -> RootFilesystemEntry { path: path.to_owned(), kind: RootFilesystemEntryKind::File, mode: None, - uid: None, - gid: None, + uid: Some(1000), + gid: Some(1000), content: Some(content.to_owned()), encoding: Some(RootFilesystemEntryEncoding::Utf8), target: None, diff --git a/crates/native-sidecar/tests/guest_identity.rs b/crates/native-sidecar/tests/guest_identity.rs index 9e59ee4589..3dc852256f 100644 --- a/crates/native-sidecar/tests/guest_identity.rs +++ b/crates/native-sidecar/tests/guest_identity.rs @@ -5,6 +5,7 @@ use agentos_native_sidecar::wire::{ RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemEntryKind, RootFilesystemMode, }; +use base64::Engine as _; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fs; @@ -18,7 +19,14 @@ use support::{ const DEFAULT_GUEST_PATH_ENV: &str = "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin"; -const GUEST_IDENTITY_CASES: &[&str] = &["javascript", "python", "wasm_identity", "wasm_env"]; +const GUEST_IDENTITY_CASES: &[&str] = &[ + "javascript", + "python", + "wasm_identity", + "wasm_pty", + "wasm_env", + "wasm_preopen", +]; fn create_vm_with_root_filesystem( sidecar: &mut agentos_native_sidecar::NativeSidecar, @@ -29,13 +37,39 @@ fn create_vm_with_root_filesystem( cwd: &std::path::Path, root_filesystem: RootFilesystemDescriptor, ) -> String { + create_vm_with_root_filesystem_and_metadata( + sidecar, + request_id, + connection_id, + session_id, + runtime, + cwd, + root_filesystem, + HashMap::new(), + ) +} + +#[allow(clippy::too_many_arguments)] +fn create_vm_with_root_filesystem_and_metadata( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + request_id: RequestId, + connection_id: &str, + session_id: &str, + runtime: GuestRuntimeKind, + cwd: &std::path::Path, + root_filesystem: RootFilesystemDescriptor, + mut metadata: HashMap, +) -> String { + metadata + .entry(String::from("cwd")) + .or_insert_with(|| cwd.to_string_lossy().into_owned()); let result = sidecar .dispatch_wire_blocking(wire_request( request_id, wire_session(connection_id, session_id), RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config( runtime, - HashMap::from([(String::from("cwd"), cwd.to_string_lossy().into_owned())]), + metadata, root_filesystem, Some(wire_permissions_allow_all()), )), @@ -60,6 +94,20 @@ fn parse_env_stdout(stdout: &str) -> BTreeMap { .collect() } +fn executable_wasm_root_entry(path: &str, bytes: &[u8]) -> RootFilesystemEntry { + RootFilesystemEntry { + path: path.to_owned(), + kind: RootFilesystemEntryKind::File, + mode: None, + uid: None, + gid: None, + content: Some(base64::engine::general_purpose::STANDARD.encode(bytes)), + encoding: Some(RootFilesystemEntryEncoding::Base64), + target: None, + executable: true, + } +} + fn javascript_guest_identity_uses_kernel_owned_defaults() { let mut sidecar = new_sidecar("guest-identity-js"); let cwd = temp_dir("guest-identity-js-cwd"); @@ -256,20 +304,8 @@ fn wasm_guest_identity_commands_use_kernel_owned_defaults() { let cwd = temp_dir("guest-identity-wasm-cwd"); let connection_id = authenticate_wire(&mut sidecar, "conn-guest-identity-wasm"); let session_id = open_session_wire(&mut sidecar, 2, &connection_id); - let (vm_id, _) = create_vm_wire( - &mut sidecar, - 3, - &connection_id, - &session_id, - GuestRuntimeKind::WebAssembly, - &cwd, - ); - - let wasm_path = cwd.join("identity.wasm"); - fs::write( - &wasm_path, - wat::parse_str( - r#" + let wasm_bytes = wat::parse_str( + r#" (module (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) (type $getid_t (func (param i32) (result i32))) @@ -324,6 +360,19 @@ fn wasm_guest_identity_commands_use_kernel_owned_defaults() { i32.const 1000 call $assert_value + i32.const 0 + i32.load + i32.const 128 + i32.const 1 + i32.const 8 + call $getpwuid + i32.const 68 + call $assert_value + i32.const 8 + i32.load + i32.const 42 + call $assert_value + i32.const 0 i32.load i32.const 128 @@ -338,10 +387,26 @@ fn wasm_guest_identity_commands_use_kernel_owned_defaults() { call $write_stdout )) "#, - ) - .expect("compile wasm identity fixture"), ) - .expect("write wasm identity fixture"); + .expect("compile wasm identity fixture"); + let wasm_path = std::path::Path::new("/identity.wasm"); + let vm_id = create_vm_with_root_filesystem( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: vec![executable_wasm_root_entry( + wasm_path.to_str().unwrap(), + &wasm_bytes, + )], + }, + ); execute_wire( &mut sidecar, @@ -371,26 +436,224 @@ fn wasm_guest_identity_commands_use_kernel_owned_defaults() { assert_eq!(stdout, "agentos:x:1000:1000::/home/agentos:/bin/sh"); } -fn wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults() { +fn wasm_guest_created_pty_uses_live_bounded_kernel_state() { assert_node_available(); - let mut sidecar = new_sidecar("guest-env-wasm"); - let cwd = temp_dir("guest-env-wasm-cwd"); - let connection_id = authenticate_wire(&mut sidecar, "conn-guest-env-wasm"); + let mut sidecar = new_sidecar("guest-created-pty-wasm"); + let cwd = temp_dir("guest-created-pty-wasm-cwd"); + let connection_id = authenticate_wire(&mut sidecar, "conn-guest-created-pty-wasm"); let session_id = open_session_wire(&mut sidecar, 2, &connection_id); - let (vm_id, _) = create_vm_wire( + let wasm_bytes = wat::parse_str( + r#" +(module + (type $pty_open_t (func (param i32 i32) (result i32))) + (type $isatty_t (func (param i32) (result i32))) + (type $get_size_t (func (param i32 i32 i32) (result i32))) + (type $fd_close_t (func (param i32) (result i32))) + (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) + (type $proc_exit_t (func (param i32))) + (import "host_process" "pty_open" (func $pty_open (type $pty_open_t))) + (import "host_tty" "isatty" (func $isatty (type $isatty_t))) + (import "host_tty" "get_size" (func $get_size (type $get_size_t))) + (import "wasi_snapshot_preview1" "fd_close" (func $fd_close (type $fd_close_t))) + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (type $proc_exit_t))) + (memory (export "memory") 1) + (data (i32.const 64) "pty:kernel-bounded\n") + (func $fail (param $code i32) + local.get $code + call $proc_exit + unreachable) + (func $_start (export "_start") + ;; The first guest-created PTY must be backed by live kernel descriptors. + i32.const 0 + i32.const 4 + call $pty_open + i32.eqz + if + else + i32.const 41 + call $fail + end + i32.const 0 + i32.load + i32.const 4 + i32.load + i32.eq + if + i32.const 42 + call $fail + end + i32.const 0 + i32.load + call $isatty + i32.const 1 + i32.ne + if + i32.const 43 + call $fail + end + i32.const 4 + i32.load + call $isatty + i32.const 1 + i32.ne + if + i32.const 44 + call $fail + end + i32.const 0 + i32.load + i32.const 8 + i32.const 10 + call $get_size + i32.eqz + if + else + i32.const 45 + call $fail + end + i32.const 8 + i32.load16_u + i32.const 80 + i32.ne + if + i32.const 46 + call $fail + end + i32.const 10 + i32.load16_u + i32.const 24 + i32.ne + if + i32.const 47 + call $fail + end + + ;; This VM admits exactly one PTY. A second request must fail atomically + ;; with EAGAIN and leave both guest output pointers untouched. + i32.const 12 + i32.const 287454020 + i32.store + i32.const 16 + i32.const 1432778632 + i32.store + i32.const 12 + i32.const 16 + call $pty_open + i32.const 6 + i32.ne + if + i32.const 48 + call $fail + end + i32.const 12 + i32.load + i32.const 287454020 + i32.ne + if + i32.const 49 + call $fail + end + i32.const 16 + i32.load + i32.const 1432778632 + i32.ne + if + i32.const 50 + call $fail + end + + i32.const 0 + i32.load + call $fd_close + i32.eqz + if + else + i32.const 51 + call $fail + end + i32.const 4 + i32.load + call $fd_close + i32.eqz + if + else + i32.const 52 + call $fail + end + i32.const 24 + i32.const 64 + i32.store + i32.const 28 + i32.const 19 + i32.store + i32.const 1 + i32.const 24 + i32.const 1 + i32.const 32 + call $fd_write + i32.eqz + if + else + i32.const 53 + call $fail + end)) +"#, + ) + .expect("compile guest-created PTY fixture"); + let wasm_path = std::path::Path::new("/guest-pty.wasm"); + let vm_id = create_vm_with_root_filesystem_and_metadata( &mut sidecar, 3, &connection_id, &session_id, GuestRuntimeKind::WebAssembly, &cwd, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: vec![executable_wasm_root_entry( + wasm_path.to_str().unwrap(), + &wasm_bytes, + )], + }, + HashMap::from([(String::from("resource.max_ptys"), String::from("1"))]), ); - let wasm_path = cwd.join("env.wasm"); - fs::write( + execute_wire( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "proc-wasm-guest-pty", + GuestRuntimeKind::WebAssembly, &wasm_path, - wat::parse_str( + Vec::new(), + ); + let (stdout, stderr, exit_code) = collect_guest_identity_process_output( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "proc-wasm-guest-pty", + ); + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + assert_eq!(exit_code, 0, "stderr:\n{stderr}"); + assert!(stderr.is_empty(), "unexpected PTY stderr: {stderr}"); + assert_eq!(stdout, "pty:kernel-bounded\n"); +} + +fn wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults() { + assert_node_available(); + + let mut sidecar = new_sidecar("guest-env-wasm"); + let cwd = temp_dir("guest-env-wasm-cwd"); + let connection_id = authenticate_wire(&mut sidecar, "conn-guest-env-wasm"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let wasm_bytes = wat::parse_str( r#" (module (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) @@ -481,9 +744,25 @@ fn wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults() { end))) "#, ) - .expect("compile wasm env fixture"), - ) - .expect("write wasm env fixture"); + .expect("compile wasm env fixture"); + let wasm_path = std::path::Path::new("/env.wasm"); + let vm_id = create_vm_with_root_filesystem( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: vec![executable_wasm_root_entry( + wasm_path.to_str().unwrap(), + &wasm_bytes, + )], + }, + ); execute_wire( &mut sidecar, @@ -530,12 +809,150 @@ fn wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults() { ); } +fn wasm_preopens_and_rights_are_kernel_authoritative() { + assert_node_available(); + + let mut sidecar = new_sidecar("guest-preopen-wasm"); + let cwd = temp_dir("guest-preopen-wasm-cwd"); + let connection_id = authenticate_wire(&mut sidecar, "conn-guest-preopen-wasm"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let wasm_bytes = wat::parse_str( + r#" +(module + (type $fd_prestat_get_t (func (param i32 i32) (result i32))) + (type $fd_prestat_dir_name_t (func (param i32 i32 i32) (result i32))) + (type $fd_fdstat_get_t (func (param i32 i32) (result i32))) + (type $fd_close_t (func (param i32) (result i32))) + (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "fd_prestat_get" (func $fd_prestat_get (type $fd_prestat_get_t))) + (import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $fd_prestat_dir_name (type $fd_prestat_dir_name_t))) + (import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fd_fdstat_get (type $fd_fdstat_get_t))) + (import "wasi_snapshot_preview1" "fd_close" (func $fd_close (type $fd_close_t))) + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t))) + (memory (export "memory") 1) + (data (i32.const 128) "preopen:kernel\n") + (func $assert_zero (param $value i32) + local.get $value + i32.eqz + if + else + unreachable + end) + (func $_start (export "_start") + i32.const 3 + i32.const 0 + call $fd_prestat_get + call $assert_zero + i32.const 4 + i32.load + i32.eqz + if unreachable end + + i32.const 3 + i32.const 16 + i32.const 1 + call $fd_prestat_dir_name + call $assert_zero + i32.const 16 + i32.load8_u + i32.const 47 + i32.ne + if unreachable end + + i32.const 3 + i32.const 32 + call $fd_fdstat_get + call $assert_zero + i32.const 40 + i64.load + i64.const 8192 + i64.and + i64.eqz + if unreachable end + i32.const 40 + i64.load + i64.const 64 + i64.and + i64.eqz + if unreachable end + + i32.const 3 + call $fd_close + call $assert_zero + ;; Closing the public Linux descriptor must not revoke wasi-libc's private + ;; tagged capability root, but the public descriptor itself is now bad. + i32.const 3 + i32.const 0 + call $fd_prestat_get + i32.const 8 + i32.ne + if unreachable end + + i32.const 96 + i32.const 128 + i32.store + i32.const 100 + i32.const 15 + i32.store + i32.const 1 + i32.const 96 + i32.const 1 + i32.const 104 + call $fd_write + call $assert_zero)) +"#, + ) + .expect("compile hostile WASI preopen fixture"); + let wasm_path = std::path::Path::new("/preopen.wasm"); + let vm_id = create_vm_with_root_filesystem( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: vec![executable_wasm_root_entry( + wasm_path.to_str().unwrap(), + &wasm_bytes, + )], + }, + ); + + execute_wire( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "proc-wasm-preopen", + GuestRuntimeKind::WebAssembly, + &wasm_path, + Vec::new(), + ); + let (stdout, stderr, exit_code) = collect_guest_identity_process_output( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "proc-wasm-preopen", + ); + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + assert_eq!(exit_code, 0, "stderr:\n{stderr}"); + assert_eq!(stdout, "preopen:kernel\n"); +} + fn run_named_case(case_name: &str) { match case_name { "javascript" => javascript_guest_identity_uses_kernel_owned_defaults(), "python" => python_guest_identity_uses_kernel_owned_defaults(), "wasm_identity" => wasm_guest_identity_commands_use_kernel_owned_defaults(), + "wasm_pty" => wasm_guest_created_pty_uses_live_bounded_kernel_state(), "wasm_env" => wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults(), + "wasm_preopen" => wasm_preopens_and_rights_are_kernel_authoritative(), other => panic!("unknown guest_identity case: {other}"), } } diff --git a/crates/native-sidecar/tests/host_dir.rs b/crates/native-sidecar/tests/host_dir.rs index 26c110e1c1..b7b255871d 100644 --- a/crates/native-sidecar/tests/host_dir.rs +++ b/crates/native-sidecar/tests/host_dir.rs @@ -213,6 +213,22 @@ mod host_dir { fs::remove_dir_all(outside_dir).expect("remove outside temp dir"); } + #[test] + fn filesystem_utimes_supports_mount_root() { + let host_dir = temp_dir("host-dir-plugin-root-utimes"); + let mut filesystem = HostDirFilesystem::new(&host_dir).expect("create host dir fs"); + + filesystem + .utimes("/", 1_700_000_123_000, 1_700_000_456_000) + .expect("utimes should update the mount root through its anchored fd"); + + let metadata = fs::metadata(&host_dir).expect("read mount root metadata"); + assert_eq!(metadata.atime(), 1_700_000_123); + assert_eq!(metadata.mtime(), 1_700_000_456); + + fs::remove_dir_all(host_dir).expect("remove temp dir"); + } + // Regression: metadata reads must not require READ permission on the // target under a non-root sidecar. POSIX `stat`/`exists` need only search // on the parent, but the pre-fix code opened the leaf `O_RDONLY`, which diff --git a/crates/native-sidecar/tests/limits.rs b/crates/native-sidecar/tests/limits.rs index 1cdfe31ed9..5a591e7f6a 100644 --- a/crates/native-sidecar/tests/limits.rs +++ b/crates/native-sidecar/tests/limits.rs @@ -26,6 +26,15 @@ fn defaults_match_struct_default() { Some(128 * 1024 * 1024), "WASM memory must be bounded by default" ); + assert_eq!( + parsed.wasm.active_cpu_time_limit_ms, 30_000, + "WASM active CPU must have a default runaway safeguard" + ); + assert_eq!( + parsed.wasm.wall_clock_limit_ms, None, + "WASM wall-clock cutoff must remain opt-in" + ); + assert_eq!(parsed.wasm.deterministic_fuel, None); } #[test] @@ -37,7 +46,9 @@ fn overrides_only_present_keys() { }), wasm: Some(WasmLimitsConfig { max_module_file_bytes: Some(1_048_576), - runner_cpu_time_limit_ms: Some(90_000), + active_cpu_time_limit_ms: Some(90_000), + wall_clock_limit_ms: Some(120_000), + deterministic_fuel: Some(1_000_000), ..Default::default() }), js_runtime: Some(JsRuntimeLimitsConfig { @@ -57,7 +68,9 @@ fn overrides_only_present_keys() { assert_eq!(parsed.bindings.max_binding_schema_bytes, 4096); assert_eq!(parsed.wasm.max_module_file_bytes, 1_048_576); - assert_eq!(parsed.wasm.runner_cpu_time_limit_ms, 90_000); + assert_eq!(parsed.wasm.active_cpu_time_limit_ms, 90_000); + assert_eq!(parsed.wasm.wall_clock_limit_ms, Some(120_000)); + assert_eq!(parsed.wasm.deterministic_fuel, Some(1_000_000)); assert_eq!(parsed.js_runtime.v8_heap_limit_mb, Some(256)); assert_eq!(parsed.python.execution_timeout_ms, 1000); assert_eq!(parsed.http.max_fetch_response_bytes, 65536); diff --git a/crates/native-sidecar/tests/limits_audit.rs b/crates/native-sidecar/tests/limits_audit.rs index 0806806dc8..7e3fa9735f 100644 --- a/crates/native-sidecar/tests/limits_audit.rs +++ b/crates/native-sidecar/tests/limits_audit.rs @@ -353,7 +353,6 @@ fn match_rule_unit_assertions() { assert!(!name_qualifies("EXECUTION_DRIVER_NAME")); assert!(!name_qualifies("DEFAULT_VIRTUAL_CPU_COUNT")); // Exclusions. - assert!(!name_qualifies("AGENTOS_WASM_MAX_FUEL_ENV")); assert!(!name_qualifies("ERR_SESSION_DEFERRED_COMMAND_ERROR_CODE")); // Declaration extraction. diff --git a/crates/native-sidecar/tests/permission_flags.rs b/crates/native-sidecar/tests/permission_flags.rs index 1741f51296..7a9b52f651 100644 --- a/crates/native-sidecar/tests/permission_flags.rs +++ b/crates/native-sidecar/tests/permission_flags.rs @@ -380,7 +380,7 @@ fn permission_flags_single_star_paths_do_not_cross_path_separators() { .expect("attempt nested child directory create"); match deny_nested_child.response.payload { ResponsePayload::RejectedResponse(rejected) => { - assert_eq!(rejected.code, "kernel_error"); + assert_eq!(rejected.code, "EACCES"); assert!(rejected.message.contains("EACCES")); } other => panic!("expected rejected nested mkdir response, got {other:?}"), diff --git a/crates/native-sidecar/tests/posix_compliance.rs b/crates/native-sidecar/tests/posix_compliance.rs index 283f68a118..981de30bbe 100644 --- a/crates/native-sidecar/tests/posix_compliance.rs +++ b/crates/native-sidecar/tests/posix_compliance.rs @@ -4,9 +4,13 @@ use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::fd_table::O_RDWR; use agentos_kernel::kernel::{KernelVm, KernelVmConfig, SpawnOptions}; use agentos_kernel::permissions::Permissions; +use agentos_kernel::process_runtime::{ + ProcessControlRequest, ProcessExit, ProcessRuntimeEndpoint, ProcessRuntimeEndpointError, + ProcessRuntimeIdentity, ProcessTermination, +}; use agentos_kernel::process_table::{ - DriverProcess, ProcessContext, ProcessExitCallback, ProcessResult, ProcessTable, - ProcessWaitEvent, WaitPidFlags, SIGCHLD, SIGTERM, + ProcessContext, ProcessEntry, ProcessResult, ProcessTable, ProcessWaitEvent, SignalAction, + SignalDisposition, WaitPidFlags, SIGCHLD, SIGTERM, }; use agentos_kernel::vfs::MemoryFileSystem; use agentos_native_sidecar::wire::{ @@ -16,7 +20,7 @@ use agentos_native_sidecar::wire::{ use nix::libc; use std::collections::BTreeMap; use std::fmt::Debug; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use support::{ assert_node_available, authenticate_wire, create_vm_wire, execute_wire, new_sidecar, @@ -120,13 +124,12 @@ fn wait_for_process_output( struct MockProcessState { kills: Vec, exit_code: Option, - on_exit: Option, + binding: Option<(ProcessTable, u32)>, } #[derive(Default)] struct MockDriverProcess { state: Mutex, - exited: Condvar, } impl MockDriverProcess { @@ -142,55 +145,89 @@ impl MockDriverProcess { .clone() } + fn bind(&self, table: &ProcessTable, pid: u32) { + self.state + .lock() + .expect("mock process lock poisoned") + .binding = Some((table.clone(), pid)); + } + fn exit(&self, exit_code: i32) { - let callback = { + let binding = { let mut state = self.state.lock().expect("mock process lock poisoned"); if state.exit_code.is_some() { return; } state.exit_code = Some(exit_code); - self.exited.notify_all(); - state.on_exit.clone() + state.binding.clone() }; - if let Some(callback) = callback { - callback(exit_code); + if let Some((table, pid)) = binding { + table + .report_exit(pid, ProcessExit::Exited(exit_code)) + .expect("mock process exit must reach the bound kernel process"); } } } -impl DriverProcess for MockDriverProcess { - fn kill(&self, signal: i32) { - let should_exit = { +impl ProcessRuntimeEndpoint for MockDriverProcess { + fn identity(&self) -> Option { + None + } + + fn request_control( + &self, + request: ProcessControlRequest, + ) -> Result<(), ProcessRuntimeEndpointError> { + let (binding, termination) = { let mut state = self.state.lock().expect("mock process lock poisoned"); - state.kills.push(signal); - signal == SIGTERM + let signal = match request { + ProcessControlRequest::Checkpoint => state + .binding + .as_ref() + .and_then(|(table, pid)| table.sigpending(*pid).ok()) + .and_then(|pending| pending.signals().into_iter().next()), + ProcessControlRequest::Terminate(ProcessTermination::Signal { signal, .. }) => { + Some(signal) + } + _ => None, + }; + if let Some(signal) = signal { + state.kills.push(signal); + } + let termination = match request { + ProcessControlRequest::Terminate(ProcessTermination::Signal { signal, .. }) => { + Some(ProcessExit::Signaled { + signal, + core_dumped: false, + }) + } + ProcessControlRequest::Terminate(ProcessTermination::RuntimeFault) + | ProcessControlRequest::Cancel(_) => Some(ProcessExit::Exited(1)), + _ => None, + }; + (state.binding.clone(), termination) }; - if should_exit { - self.exit(128 + signal); + if let (Some((table, pid)), Some(termination)) = (binding, termination) { + table + .report_exit(pid, termination) + .expect("mock termination must reach the bound kernel process"); } + Ok(()) } +} - fn wait(&self, timeout: Duration) -> Option { - let state = self.state.lock().expect("mock process lock poisoned"); - if state.exit_code.is_some() { - return state.exit_code; - } - - let (state, _) = self - .exited - .wait_timeout(state, timeout) - .expect("mock process wait lock poisoned"); - state.exit_code - } - - fn set_on_exit(&self, callback: ProcessExitCallback) { - self.state - .lock() - .expect("mock process lock poisoned") - .on_exit = Some(callback); - } +fn register_mock_process( + table: &ProcessTable, + pid: u32, + command: &str, + context: ProcessContext, + process: Arc, +) -> ProcessEntry { + let entry = table.register(pid, "wasmvm", command, Vec::new(), context, process.clone()); + process.bind(table, pid); + entry } fn create_context(ppid: u32) -> ProcessContext { @@ -473,22 +510,30 @@ fn process_table_delivers_sigchld_and_reaps_zombies_via_waitpid() { let parent_pid = allocate_pid(&table); let child_pid = allocate_pid(&table); - table.register( + register_mock_process( + &table, parent_pid, - "wasmvm", "parent", - Vec::new(), create_context(0), parent.clone(), ); - table.register( + register_mock_process( + &table, child_pid, - "wasmvm", "child", - Vec::new(), create_context(parent_pid), child.clone(), ); + table + .signal_action( + parent_pid, + SIGCHLD, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("catch SIGCHLD"); assert_eq!( table @@ -526,19 +571,17 @@ fn process_table_negative_pid_kill_targets_entire_process_groups() { let leader_pid = allocate_pid(&table); let peer_pid = allocate_pid(&table); - table.register( + register_mock_process( + &table, leader_pid, - "wasmvm", "leader", - Vec::new(), create_context(0), leader.clone(), ); - table.register( + register_mock_process( + &table, peer_pid, - "wasmvm", "peer", - Vec::new(), create_context(leader_pid), peer.clone(), ); diff --git a/crates/native-sidecar/tests/python.rs b/crates/native-sidecar/tests/python.rs index fbdbd9c667..691a003d80 100644 --- a/crates/native-sidecar/tests/python.rs +++ b/crates/native-sidecar/tests/python.rs @@ -54,7 +54,10 @@ fn chunk_contains(chunk: &[u8], needle: &str) -> bool { } fn root_dir(path: impl Into) -> RootFilesystemEntry { - root_entry(path, RootFilesystemEntryKind::Directory, None, None) + let mut entry = root_entry(path, RootFilesystemEntryKind::Directory, None, None); + entry.uid = Some(1000); + entry.gid = Some(1000); + entry } fn root_file( @@ -940,7 +943,6 @@ fn python_runtime_executes_code_end_to_end() { GuestRuntimeKind::Python, &cwd, ); - execute_inline_python( &mut sidecar, 4, @@ -3109,7 +3111,8 @@ print(json.dumps(result)) ); assert_eq!( parsed["http"]["type"], - Value::String(String::from("PermissionError")) + Value::String(String::from("PermissionError")), + "stdout: {stdout}" ); } @@ -3118,7 +3121,6 @@ fn python_runtime_runs_node_subprocesses_through_sidecar_bridge() { let mut sidecar = new_sidecar("python-subprocess-bridge"); let cwd = temp_dir("python-subprocess-bridge-cwd"); - write_fixture(&cwd.join("child.mjs"), "console.log('child-ready')\n"); let connection_id = authenticate_wire(&mut sidecar, "conn-python"); let session_id = open_session_wire(&mut sidecar, 2, &connection_id); let (vm_id, _) = create_vm_wire( @@ -3129,10 +3131,22 @@ fn python_runtime_runs_node_subprocesses_through_sidecar_bridge() { GuestRuntimeKind::Python, &cwd, ); + bootstrap_root_filesystem( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + vec![root_file( + "/child.mjs", + "console.log('child-ready')\n", + None, + )], + ); execute_inline_python( &mut sidecar, - 4, + 5, &connection_id, &session_id, &vm_id, @@ -3849,6 +3863,18 @@ fn python_pip_installs_persist_across_invocations() { stdout1.contains("Successfully installed"), "stdout: {stdout1}\nstderr: {stderr1}" ); + let home_install_exists = guest_exists( + &mut sidecar, + 50, + &connection_id, + &session_id, + &vm_id, + "/home/agentos/.agentos/site-packages/click/__init__.py", + ); + assert!( + home_install_exists, + "pip must persist the installed module under the kernel-owned guest home directory" + ); // Process 2: a FRESH Python interpreter imports the package from the VFS // site-packages — proving the install persisted across invocations. diff --git a/crates/native-sidecar/tests/security_audit.rs b/crates/native-sidecar/tests/security_audit.rs index 7cd787d6e7..6fc67c8a29 100644 --- a/crates/native-sidecar/tests/security_audit.rs +++ b/crates/native-sidecar/tests/security_audit.rs @@ -185,14 +185,7 @@ fn filesystem_permission_denials_emit_security_audit_events() { .expect("dispatch denied read"); match read.response.payload { ResponsePayload::RejectedResponse(rejected) => { - // Which layer surfaces the denial (a POSIX-coded kernel error -> - // "kernel_error", any other message -> "invalid_state") depends on - // the host environment; the audit event below is the contract. - assert!( - rejected.code == "invalid_state" || rejected.code == "kernel_error", - "unexpected rejection code: {}", - rejected.code - ); + assert_eq!(rejected.code, "EACCES"); assert!(rejected.message.contains("EACCES")); } other => panic!("unexpected read response: {other:?}"), diff --git a/crates/native-sidecar/tests/security_hardening.rs b/crates/native-sidecar/tests/security_hardening.rs index 7512636cea..a2379e12f0 100644 --- a/crates/native-sidecar/tests/security_hardening.rs +++ b/crates/native-sidecar/tests/security_hardening.rs @@ -387,7 +387,7 @@ fn vm_resource_limits_cap_active_processes_without_poisoning_followup_execs() { .expect("dispatch second execute"); match second.response.payload { ResponsePayload::RejectedResponse(rejected) => { - assert_eq!(rejected.code, "kernel_error"); + assert_eq!(rejected.code, "EAGAIN"); assert!(rejected.message.contains("maximum process limit reached")); } other => panic!("unexpected resource-limit response: {other:?}"), diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index db0b9ccbde..32fecc8423 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -85,11 +85,11 @@ mod service { use super::*; use crate::bridge::{bridge_permissions, HostFilesystem, ScopedHostFilesystem}; use crate::execution::{ - clamp_javascript_net_poll_wait, finalize_javascript_net_connect, format_dns_resource, + clamp_javascript_net_poll_wait, finalize_net_connect, format_dns_resource, format_tcp_resource, runtime_child_is_alive, service_javascript_net_sync_rpc as service_javascript_net_sync_rpc_inner, - signal_runtime_process, JavascriptNetSyncRpcServiceRequest, - JavascriptSyncRpcServiceRequest, JavascriptSyncRpcServiceResponse, + signal_runtime_process, HostServiceResponse, JavascriptSyncRpcServiceRequest, + NetServiceRequest, }; use crate::filesystem::service_javascript_fs_sync_rpc; use crate::plugins::s3_common::test_support::MockS3Server; @@ -114,14 +114,19 @@ mod service { use crate::state::{ ActiveCipherSession, ActiveDiffieHellmanSession, ActiveEcdhSession, ActiveExecution, ActiveExecutionEvent, ActiveProcess, ActiveSqliteDatabase, ActiveSqliteStatement, - ActiveTcpListener, ActiveUdpSocket, BindingExecution, PendingHttpRequest, - ProcessEventEnvelope, SidecarKernel, VmPendingByteBudget, EXECUTION_SANDBOX_ROOT_ENV, - JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, PYTHON_COMMAND, - VM_DNS_SERVERS_METADATA_KEY, VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, - VM_LISTEN_PORT_MAX_METADATA_KEY, VM_LISTEN_PORT_MIN_METADATA_KEY, WASM_COMMAND, - WASM_STDIO_SYNC_RPC_ENV, + ActiveTcpListener, ActiveUdpSocket, BindingExecution, ExecutionAdapterPolicy, + ExecutionHostCall, PendingHttpRequest, ProcessEventEnvelope, SidecarKernel, + VmPendingByteBudget, EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, + LOOPBACK_EXEMPT_PORTS_ENV, PYTHON_COMMAND, VM_DNS_SERVERS_METADATA_KEY, + VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, VM_LISTEN_PORT_MAX_METADATA_KEY, + VM_LISTEN_PORT_MIN_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV, }; use agentos_bridge::SymlinkRequest; + use agentos_execution::backend::{ + DirectHostReplyHandle, DirectHostReplyTarget, HostCallIdentity, HostCallReply, + HostServiceError, + }; + use agentos_kernel::process_runtime::ProcessRuntimeIdentity; macro_rules! block_on_sidecar { ($sidecar:expr, $future:expr) => {{ @@ -140,14 +145,92 @@ mod service { timeout: Duration, ) -> Result, SidecarError> { let handle = process.runtime_context.handle().clone(); - handle.block_on(process.execution.poll_event(timeout)) + handle.block_on(process.poll_execution_event_for_test(timeout)) + } + + struct TestReplyTarget; + + impl DirectHostReplyTarget for TestReplyTarget { + fn claim(&self, _call_id: u64) -> Result { + Ok(true) + } + + fn respond( + &self, + _call_id: u64, + _claimed: bool, + _result: Result, + ) -> Result<(), HostServiceError> { + Ok(()) + } + } + + fn dispatch_test_host_operation( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + call_id: u64, + operation: agentos_execution::host::HostOperation, + ) -> Result<(), SidecarError> { + let (generation, pid) = { + let vm = sidecar.vms.get(vm_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown test VM {vm_id}")) + })?; + let process = vm.active_processes.get(process_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown test process {process_id} in VM {vm_id}" + )) + })?; + (vm.generation, process.kernel_pid) + }; + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation, + pid, + call_id, + }, + std::sync::Arc::new(TestReplyTarget), + 1024 * 1024, + ) + .map_err(SidecarError::from)?; + let event = ActiveExecutionEvent::Common( + agentos_execution::backend::ExecutionEvent::HostCall { operation, reply }, + ); + block_on_sidecar!( + sidecar, + sidecar.handle_execution_event(vm_id, process_id, event) + )?; + Ok(()) + } + + fn bounded_test_host_path(path: &str) -> agentos_execution::host::BoundedString { + agentos_execution::host::BoundedString::try_new( + path.to_owned(), + &agentos_execution::backend::PayloadLimit::new("test.maxPathBytes", 4096) + .expect("test path limit"), + ) + .expect("bounded test path") + } + + fn test_execution_host_call(request: HostRpcRequest) -> ExecutionHostCall { + let reply = DirectHostReplyHandle::new( + HostCallIdentity { + generation: 1, + pid: 1, + call_id: request.id, + }, + std::sync::Arc::new(TestReplyTarget), + 1024, + ) + .expect("create test reply handle"); + ExecutionHostCall { request, reply } } - fn spawn_javascript_child_process_sync_for_test( + fn spawn_child_process_sync_for_test( sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, - request: crate::protocol::JavascriptChildProcessSpawnRequest, + request: agentos_execution::host::ProcessLaunchRequest, max_buffer: Option, ) -> Result { let handle = sidecar @@ -157,7 +240,7 @@ mod service { .runtime_context .handle() .clone(); - let JavascriptSyncRpcServiceResponse::Deferred { mut receiver, .. } = handle.block_on( + let HostServiceResponse::Deferred { mut receiver, .. } = handle.block_on( sidecar.defer_javascript_child_process_sync(vm_id, process_id, request, max_buffer), )? else { @@ -188,11 +271,11 @@ mod service { } } - fn spawn_javascript_child_process_for_test( + fn spawn_child_process_for_test( sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, - request: crate::protocol::JavascriptChildProcessSpawnRequest, + request: agentos_execution::host::ProcessLaunchRequest, ) -> Result { let handle = sidecar .vms @@ -201,10 +284,10 @@ mod service { .runtime_context .handle() .clone(); - handle.block_on(sidecar.spawn_javascript_child_process(vm_id, process_id, request)) + handle.block_on(sidecar.spawn_child_process(vm_id, process_id, request)) } - fn poll_javascript_child_process_for_test( + fn poll_child_process_for_test( sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, @@ -220,7 +303,7 @@ mod service { .clone(); let deadline = Instant::now() + Duration::from_millis(timeout_ms); loop { - let event = handle.block_on(sidecar.poll_javascript_child_process( + let event = handle.block_on(sidecar.poll_child_process( vm_id, process_id, child_process_id, @@ -243,12 +326,12 @@ mod service { } } - fn spawn_descendant_javascript_child_process_for_test( + fn spawn_descendant_process_for_test( sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, current_process_path: &[&str], - request: crate::protocol::JavascriptChildProcessSpawnRequest, + request: agentos_execution::host::ProcessLaunchRequest, ) -> Result { let handle = sidecar .vms @@ -257,7 +340,7 @@ mod service { .runtime_context .handle() .clone(); - handle.block_on(sidecar.spawn_descendant_javascript_child_process_for_test( + handle.block_on(sidecar.spawn_descendant_process_for_test( vm_id, process_id, current_process_path, @@ -266,8 +349,7 @@ mod service { } use agentos_execution::{ CreateJavascriptContextRequest, CreatePythonContextRequest, CreateWasmContextRequest, - JavascriptSyncRpcRequest, PythonVfsRpcMethod, PythonVfsRpcRequest, - StartJavascriptExecutionRequest, StartPythonExecutionRequest, + HostRpcRequest, StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest, WasmPermissionTier, }; use agentos_kernel::command_registry::CommandDriver; @@ -277,7 +359,7 @@ mod service { CommandAccessRequest, EnvAccessRequest, EnvironmentOperation, FsAccessRequest, FsOperation, NetworkAccessRequest, NetworkOperation, Permissions, }; - use agentos_kernel::poll::{PollTargetEntry, POLLIN}; + use agentos_kernel::poll::{PollTargetEntry, POLLHUP, POLLIN}; use agentos_kernel::process_table::{SIGKILL, SIGTERM}; use agentos_kernel::vfs::{ MemoryFileSystem, VirtualDirEntry, VirtualFileSystem, VirtualStat, @@ -480,6 +562,44 @@ ykAheWCsAteSEWVc0w==\n\ .insert(process_id.to_owned(), process); } + fn spawn_vm_wasm_binding_process( + sidecar: &mut NativeSidecar, + vm_id: &str, + ) -> ActiveProcess { + let vm = sidecar.vms.get_mut(vm_id).expect("test vm"); + let kernel_handle = vm + .kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn VM-owned WASM binding process"); + active_process_for_vm_tests( + kernel_handle.pid(), + kernel_handle, + vm.runtime_context.clone(), + vm.limits.clone(), + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding( + BindingExecution::default() + .with_descendant_wait_ownership( + agentos_execution::backend::DescendantWaitOwnership::Guest, + ) + .with_descendant_output_ownership( + agentos_execution::backend::DescendantOutputOwnership::GuestDescriptors, + ), + ), + ) + .with_vm_pending_byte_budgets( + Arc::clone(&vm.pending_stdin_bytes_budget), + Arc::clone(&vm.pending_event_bytes_budget), + ) + } + fn ext_sidecar_request_payload() -> SidecarRequestPayload { SidecarRequestPayload::Ext(crate::protocol::ExtEnvelope { namespace: String::from("test.completion.evict"), @@ -521,21 +641,44 @@ ykAheWCsAteSEWVc0w==\n\ request_id } - // The completed-response map is bounded: once more responses complete - // than the cap, the oldest *unretrieved* response is evicted (and the - // host can no longer fetch it) so the map cannot grow without bound. - fn completed_sidecar_responses_evict_oldest_beyond_cap() { + // The completed-response map is bounded and non-lossy: pressure rejects + // the new completion while every previously accepted response remains + // retrievable. The rejected response stays pending so a caller can + // drain capacity and retry it instead of losing a waiter settlement. + fn completed_sidecar_responses_reject_beyond_cap_without_eviction() { let mut sidecar = create_test_sidecar(); let ownership = OwnershipScope::connection("conn-completion-evict"); let cap = crate::service::MAX_COMPLETED_SIDECAR_RESPONSES; - // The first completion is the oldest; everything after it pushes the - // map past the cap and must evict from the front. let oldest_request_id = complete_one_sidecar_response(&mut sidecar, &ownership); - for _ in 1..(cap + 5) { + for _ in 1..cap { complete_one_sidecar_response(&mut sidecar, &ownership); } + let rejected_request_id = sidecar + .queue_sidecar_request(ownership.clone(), ext_sidecar_request_payload()) + .expect("queue response that reaches completed-response pressure"); + sidecar + .pop_sidecar_request() + .expect("pressure response should reach the host"); + let error = sidecar + .accept_sidecar_response(ext_sidecar_response_frame( + rejected_request_id, + &ownership, + )) + .expect_err("completed-response pressure must reject without eviction"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + let SidecarError::Host(host_error) = &error else { + panic!("completed response pressure must be typed: {error}"); + }; + let details = host_error.details.as_ref().expect("limit details"); + assert_eq!( + details["limitName"], + "runtime.protocol.maxCompletedResponses" + ); + assert_eq!(details["limit"], cap); + assert_eq!(details["observed"], cap + 1); + assert_eq!( sidecar.completed_sidecar_responses.len(), cap, @@ -547,17 +690,22 @@ ykAheWCsAteSEWVc0w==\n\ "the completion gauge must track the bounded map depth" ); assert!( - sidecar.take_sidecar_response(oldest_request_id).is_none(), - "the oldest unretrieved response should be evicted once the cap is exceeded" + sidecar.take_sidecar_response(oldest_request_id).is_some(), + "completed-response pressure must not evict the oldest waiter result" ); - - // A response completed after the cap was reached is still retrievable, - // proving eviction drops the front and keeps the most recent entries. - let recent_request_id = complete_one_sidecar_response(&mut sidecar, &ownership); - assert!( - sidecar.take_sidecar_response(recent_request_id).is_some(), - "a freshly completed response should remain retrievable after eviction" + assert_eq!( + sidecar.pending_sidecar_responses.pending_count(), + 1, + "rejected completion must remain registered for a lossless retry" ); + + sidecar + .accept_sidecar_response(ext_sidecar_response_frame( + rejected_request_id, + &ownership, + )) + .expect("draining one completion permits retry"); + assert!(sidecar.take_sidecar_response(rejected_request_id).is_some()); } // Retrieving completed responses must keep the gauge in sync so the @@ -674,11 +822,18 @@ ykAheWCsAteSEWVc0w==\n\ sidecar .accept_sidecar_response(ext_sidecar_response_frame(first, &ownership)) .expect("accept first configured completion"); - sidecar + let completed_error = sidecar .accept_sidecar_response(ext_sidecar_response_frame(second, &ownership)) - .expect("accept second configured completion"); + .expect_err("configured completion overflow must reject without eviction"); + assert_eq!(completed_error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); assert_eq!(sidecar.completed_sidecar_responses.len(), 1); assert_eq!(sidecar.completed_sidecar_responses_gauge.depth(), 1); + assert_eq!(sidecar.pending_sidecar_responses.pending_count(), 1); + assert!(sidecar.take_sidecar_response(first).is_some()); + sidecar + .accept_sidecar_response(ext_sidecar_response_frame(second, &ownership)) + .expect("draining configured completion capacity permits retry"); + assert!(sidecar.take_sidecar_response(second).is_some()); } fn pending_process_events_are_bounded() { @@ -766,15 +921,37 @@ ykAheWCsAteSEWVc0w==\n\ let mut execution = ActiveExecution::Binding(binding_execution); assert!(matches!( execution - .poll_event(Duration::ZERO) + .poll_event( + ProcessRuntimeIdentity { + generation: 1, + pid: 1, + }, + 1024, + Duration::ZERO, + ) .await .expect("poll queued binding event"), Some(ActiveExecutionEvent::Stdout(_)) )); let error = execution - .poll_event(Duration::ZERO) + .poll_event( + ProcessRuntimeIdentity { + generation: 1, + pid: 1, + }, + 1024, + Duration::ZERO, + ) .await .expect_err("binding event overflow should be reported"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + let SidecarError::Host(host_error) = &error else { + panic!("binding count overflow must be a typed host error: {error}"); + }; + let details = host_error.details.as_ref().expect("limit details"); + assert_eq!(details["limitName"], "limits.process.pendingEventCount"); + assert_eq!(details["limit"], 1); + assert_eq!(details["observed"], 2); assert!( error .to_string() @@ -804,9 +981,24 @@ ykAheWCsAteSEWVc0w==\n\ runtime.block_on(async move { let mut execution = ActiveExecution::Binding(binding_execution); let error = execution - .poll_event(Duration::ZERO) + .poll_event( + ProcessRuntimeIdentity { + generation: 1, + pid: 1, + }, + 1024, + Duration::ZERO, + ) .await .expect_err("binding byte overflow should be reported"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + let SidecarError::Host(host_error) = &error else { + panic!("binding byte overflow must be a typed host error: {error}"); + }; + let details = host_error.details.as_ref().expect("limit details"); + assert_eq!(details["limitName"], "limits.process.pendingEventBytes"); + assert_eq!(details["limit"], 8); + assert!(details["observed"].as_u64().is_some_and(|value| value > 8)); assert!( error .to_string() @@ -864,6 +1056,19 @@ ykAheWCsAteSEWVc0w==\n\ let error = child_two .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![3])) .expect_err("aggregate event bytes must reject a third enqueue"); + assert_eq!(error.code(), Some("ERR_AGENTOS_RESOURCE_LIMIT")); + let SidecarError::Host(host_error) = &error else { + panic!("aggregate event overflow must be a typed host error: {error}"); + }; + let details = host_error.details.as_ref().expect("limit details"); + assert_eq!(details["limitName"], "limits.process.pendingEventBytes"); + assert_eq!(details["limit"], aggregate_limit); + assert!( + details["observed"] + .as_u64() + .is_some_and(|value| value > aggregate_limit as u64), + "limit+1 admission must report the rejected occupancy" + ); assert!( error .to_string() @@ -1056,6 +1261,17 @@ ykAheWCsAteSEWVc0w==\n\ &vec![3; 135_000], ) .expect_err("combined child backlogs must obey the VM byte budget"); + assert_eq!( + error.code(), + Some("ERR_AGENTOS_VM_PENDING_STDIN_BYTES_LIMIT") + ); + let SidecarError::Host(host_error) = &error else { + panic!("VM pending stdin overflow must be a typed host error: {error}"); + }; + let details = host_error.details.as_ref().expect("limit details"); + assert_eq!(details["limitName"], "limits.process.pendingStdinBytes"); + assert_eq!(details["limit"], 150_000); + assert_eq!(details["observed"], 155_000); assert!( error .to_string() @@ -1097,25 +1313,61 @@ ykAheWCsAteSEWVc0w==\n\ } fn wasm_signal_queue_is_bounded() { - let kernel_handle = create_kernel_process_handle_for_tests(); - let mut process = active_process_for_tests( + // KernelVm owns process lifetime. Retain it while asserting the + // process table's coalesced pending-signal state. + let (_kernel, kernel_handle) = create_live_kernel_process_for_tests(); + let process = active_process_for_tests( kernel_handle.pid(), kernel_handle, GuestRuntimeKind::WebAssembly, ActiveExecution::Binding(BindingExecution::default()), ); + process + .kernel_handle + .signal_action( + nix::libc::SIGUSR1, + Some(agentos_kernel::process_table::SignalAction { + disposition: agentos_kernel::process_table::SignalDisposition::User, + ..agentos_kernel::process_table::SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); for _ in 0..(MAX_PROCESS_EVENT_QUEUE * 2) { - process - .queue_pending_wasm_signal(nix::libc::SIGUSR1) - .expect("repeated standard signals should coalesce"); + process.kernel_handle.kill(nix::libc::SIGUSR1); } - assert_eq!(process.pending_wasm_signals.len(), 1); + assert_eq!( + process + .kernel_handle + .sigpending() + .expect("pending signals") + .signals(), + vec![nix::libc::SIGUSR1] + ); for signal in 1..=64 { + if matches!(signal, nix::libc::SIGKILL | nix::libc::SIGSTOP) { + continue; + } process - .queue_pending_wasm_signal(signal) - .expect("distinct supported signals fit the finite signal set"); + .kernel_handle + .signal_action( + signal, + Some(agentos_kernel::process_table::SignalAction { + disposition: agentos_kernel::process_table::SignalDisposition::User, + ..agentos_kernel::process_table::SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); + process.kernel_handle.kill(signal); } - assert!(process.pending_wasm_signals.len() <= 64); + assert!( + process + .kernel_handle + .sigpending() + .expect("pending signals") + .signals() + .len() + <= 62 + ); } fn poll_event_rechecks_durable_queue_after_pump() { @@ -1422,7 +1674,7 @@ ykAheWCsAteSEWVc0w==\n\ let error = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("crypto.cipherivCreate"), @@ -1455,7 +1707,7 @@ ykAheWCsAteSEWVc0w==\n\ let error = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("crypto.diffieHellmanSessionCreate"), @@ -1467,7 +1719,7 @@ ykAheWCsAteSEWVc0w==\n\ crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 20, method: String::from("crypto.diffieHellmanSessionDestroy"), @@ -1477,7 +1729,7 @@ ykAheWCsAteSEWVc0w==\n\ .expect("destroy diffie-hellman session"); let session_id = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 21, method: String::from("crypto.diffieHellmanSessionCreate"), @@ -1536,7 +1788,7 @@ ykAheWCsAteSEWVc0w==\n\ &mut sidecar, &vm_id, "proc-sqlite-handles", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("sqlite.open"), @@ -1588,7 +1840,7 @@ ykAheWCsAteSEWVc0w==\n\ &mut sidecar, &vm_id, "proc-sqlite-handles", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("sqlite.prepare"), @@ -1599,7 +1851,8 @@ ykAheWCsAteSEWVc0w==\n\ assert_handle_limit_error(error); } - fn create_kernel_process_handle_for_tests() -> agentos_kernel::kernel::KernelProcessHandle { + fn create_live_kernel_process_for_tests( + ) -> (SidecarKernel, agentos_kernel::kernel::KernelProcessHandle) { let mut config = KernelVmConfig::new("vm-js-crypto-rpc"); config.permissions = Permissions::allow_all(); let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); @@ -1609,7 +1862,7 @@ ykAheWCsAteSEWVc0w==\n\ [JAVASCRIPT_COMMAND], )) .expect("register execution driver"); - kernel + let handle = kernel .spawn_process( JAVASCRIPT_COMMAND, Vec::new(), @@ -1618,7 +1871,12 @@ ykAheWCsAteSEWVc0w==\n\ ..SpawnOptions::default() }, ) - .expect("spawn javascript kernel process") + .expect("spawn javascript kernel process"); + (kernel, handle) + } + + fn create_kernel_process_handle_for_tests() -> agentos_kernel::kernel::KernelProcessHandle { + create_live_kernel_process_for_tests().1 } fn install_kernel_stdin_pipe_for_tests(kernel: &mut SidecarKernel, pid: u32) -> u32 { @@ -1632,6 +1890,11 @@ ykAheWCsAteSEWVc0w==\n\ runtime: GuestRuntimeKind, execution: ActiveExecution, ) -> ActiveProcess { + let adapter_policy = match runtime { + GuestRuntimeKind::JavaScript => ExecutionAdapterPolicy::DIRECT_RUNTIME, + GuestRuntimeKind::Python => ExecutionAdapterPolicy::DIRECT_PYTHON_RUNTIME, + GuestRuntimeKind::WebAssembly => ExecutionAdapterPolicy::KERNEL_HOST_CALL_POSIX, + }; let runtime_context = agentos_runtime::SidecarRuntime::process( &agentos_runtime::RuntimeConfig::default(), ) @@ -1646,6 +1909,7 @@ ykAheWCsAteSEWVc0w==\n\ runtime, execution, ) + .with_adapter_policy(adapter_policy) } fn active_process_for_vm_tests( @@ -1656,6 +1920,11 @@ ykAheWCsAteSEWVc0w==\n\ runtime: GuestRuntimeKind, execution: ActiveExecution, ) -> ActiveProcess { + let adapter_policy = match runtime { + GuestRuntimeKind::JavaScript => ExecutionAdapterPolicy::DIRECT_RUNTIME, + GuestRuntimeKind::Python => ExecutionAdapterPolicy::DIRECT_PYTHON_RUNTIME, + GuestRuntimeKind::WebAssembly => ExecutionAdapterPolicy::KERNEL_HOST_CALL_POSIX, + }; ActiveProcess::new( kernel_pid, kernel_handle, @@ -1665,6 +1934,7 @@ ykAheWCsAteSEWVc0w==\n\ runtime, execution, ) + .with_adapter_policy(adapter_policy) } #[allow(dead_code)] @@ -1764,6 +2034,29 @@ ykAheWCsAteSEWVc0w==\n\ }) } + fn js_bridge_fixture_file_stat(size: u64) -> Value { + stat_json(VirtualStat { + mode: 0o644, + size, + blocks: size.div_ceil(512), + dev: 1, + rdev: 0, + is_directory: false, + is_symbolic_link: false, + atime_ms: 0, + atime_nsec: 0, + mtime_ms: 0, + mtime_nsec: 0, + ctime_ms: 0, + ctime_nsec: 0, + birthtime_ms: 0, + ino: 1, + nlink: 1, + uid: 0, + gid: 0, + }) + } + fn dir_entry_json(entry: VirtualDirEntry) -> Value { json!({ "name": entry.name, @@ -2326,7 +2619,8 @@ ykAheWCsAteSEWVc0w==\n\ fn run_isolated_service_test(test_name: &str) { let _guard = isolated_service_test_spawn_lock(); let current_exe = std::env::current_exe().expect("current service test binary path"); - let status = Command::new(¤t_exe) + let mut command = Command::new(¤t_exe); + command .arg("--exact") .arg("service::tests::__service_isolated_runner") .arg("--nocapture") @@ -2334,7 +2628,15 @@ ykAheWCsAteSEWVc0w==\n\ .env( ISOLATED_SERVICE_CACHE_SUFFIX_ENV, format!("{}-{}", std::process::id(), test_name.replace('-', "_")), - ) + ); + // The reconciliation fixture warms two concurrent VMs before its + // thread census. Match the warm-pool target to that fixture so a + // later generation cannot legitimately finish an asynchronous + // four-worker refill after the baseline was captured. + if test_name.starts_with("multi-vm-protocol-fault-") { + command.env("AGENTOS_V8_WARM_ISOLATES", "2"); + } + let status = command .status() .unwrap_or_else(|error| panic!("spawn isolated service test {test_name}: {error}")); @@ -2642,10 +2944,11 @@ console.log(JSON.stringify({ status: "ok", summary })); "stderr", ), ActiveExecutionEvent::Exited(code) => output.exit_code = Some(*code), - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) + ActiveExecutionEvent::Common(_) + | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } => {} } block_on_sidecar!( @@ -2682,6 +2985,7 @@ console.log(JSON.stringify({ status: "ok", summary })); } fn stable_linux_thread_count() -> Option { + const REQUIRED_STABLE_SAMPLES: usize = 50; let mut previous = linux_thread_count()?; let mut stable_samples = 0; for _ in 0..100 { @@ -2689,7 +2993,7 @@ console.log(JSON.stringify({ status: "ok", summary })); let current = linux_thread_count()?; if current == previous { stable_samples += 1; - if stable_samples == 3 { + if stable_samples == REQUIRED_STABLE_SAMPLES { return Some(current); } } else { @@ -3116,38 +3420,13 @@ console.log(JSON.stringify({ status: "ok", summary })); } fn cleanup_fake_runtime_process(process: ActiveProcess) { - let child_pid = process.execution.child_pid(); - let uses_shared_v8_runtime = match &process.execution { - ActiveExecution::Javascript(execution) => execution.uses_shared_v8_runtime(), - ActiveExecution::Python(execution) => execution.uses_shared_v8_runtime(), - ActiveExecution::Wasm(_) => false, - ActiveExecution::Binding(_) => false, - }; - if !uses_shared_v8_runtime { - let _ = signal_runtime_process(child_pid, SIGTERM); - } - } - - fn allow_synthetic_python_vfs_reply_drop(result: Result<(), SidecarError>, context: &str) { - match result { - Ok(()) => {} - Err(SidecarError::Execution(message)) - if message - .contains("failed to reply to guest Python VFS RPC request: session ") - && message.contains(" does not exist") => {} - // These filesystem tests inject a Python RPC directly into the - // sidecar without first registering a V8 bridge call waiter. - // The direct-response lane must reject that synthetic reply; - // only the filesystem side effect is under test here. - Err(SidecarError::Execution(message)) - if message.contains( - "ERR_AGENTOS_BRIDGE_UNKNOWN_CALL_ID: response for unknown bridge call_id", - ) => {} - Err(SidecarError::Execution(message)) - if message.starts_with( - "failed to reply to guest Python VFS RPC request: VFS RPC request ", - ) && message.ends_with(" is no longer pending") => {} - Err(error) => panic!("{context}: {error}"), + if let Some(native_process_id) = process.execution.native_process_id() { + if let Err(error) = signal_runtime_process(native_process_id, SIGTERM) { + eprintln!( + "[agentos-test] failed to terminate fake runtime process \ + {native_process_id}: {error}" + ); + } } } @@ -3197,8 +3476,41 @@ console.log(JSON.stringify({ status: "ok", summary })); vm_id: &str, cwd: &Path, process_id: &str, - env: BTreeMap, + mut env: BTreeMap, ) { + let fixture_stem = process_id + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') { + character + } else { + '_' + } + }) + .collect::(); + let guest_fixture_name = format!(".agentos-test-{fixture_stem}.mjs"); + let guest_entrypoint = format!("/workspace/{guest_fixture_name}"); + env.entry(String::from("PWD")) + .or_insert_with(|| String::from("/workspace")); + env.entry(String::from("HOME")) + .or_insert_with(|| String::from("/home/agentos")); + env.insert( + String::from("AGENTOS_GUEST_ENTRYPOINT"), + guest_entrypoint.clone(), + ); + { + let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let source = + fs::read(cwd.join("entry.mjs")).expect("read JavaScript entry fixture"); + vm.kernel + .admit_trusted_initial_runtime_image( + &guest_entrypoint, + source, + 0o644, + vm.limits.wasm.max_module_file_bytes, + ) + .expect("stage JavaScript entry fixture in the kernel VFS"); + } let context = sidecar .javascript_engine @@ -3228,10 +3540,10 @@ console.log(JSON.stringify({ status: "ok", summary })); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], + vec![format!("./{guest_fixture_name}")], SpawnOptions { requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), + cwd: Some(String::from("/workspace")), ..SpawnOptions::default() }, ) @@ -3251,6 +3563,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ActiveExecution::Javascript(execution), ) .with_env(env) + .with_guest_cwd(String::from("/workspace")) .with_host_cwd(cwd.to_path_buf()), ); } @@ -3504,10 +3817,11 @@ console.log(JSON.stringify({ status: "ok", summary })); ActiveExecutionEvent::Exited(code) => { exit_code = Some(*code); } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) + ActiveExecutionEvent::Common(_) + | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } => {} } @@ -3565,9 +3879,12 @@ console.log(JSON.stringify({ status: "ok", summary })); if matches!( event, - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) + ActiveExecutionEvent::Common( + agentos_execution::backend::ExecutionEvent::HostCall { .. }, + ) | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } ) { block_on_sidecar!( @@ -3665,84 +3982,146 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("compile wasm stdout fixture") } - fn wat_escape_ascii(input: &str) -> String { - let mut escaped = String::new(); - for ch in input.chars() { - match ch { - '\\' => escaped.push_str("\\\\"), - '"' => escaped.push_str("\\\""), - '\n' => escaped.push_str("\\n"), - '\r' => escaped.push_str("\\0d"), - _ => escaped.push(ch), - } - } - escaped - } - - fn wasm_expect_read_errno_module(path: &str, expected_errno: u32) -> Vec { + fn wasm_kernel_pipe_probe_module() -> Vec { + const READY_MARKER: &str = "kernel-pipe-ready\n"; wat::parse_str(format!( r#" (module - (type $path_open_t (func (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) + (type $fd_pipe_t (func (param i32 i32) (result i32))) + (type $fd_fdstat_get_t (func (param i32 i32) (result i32))) (type $fd_read_t (func (param i32 i32 i32 i32) (result i32))) - (type $fd_close_t (func (param i32) (result i32))) - (import "wasi_snapshot_preview1" "path_open" (func $path_open (type $path_open_t))) + (type $fd_write_t (func (param i32 i32 i32 i32) (result i32))) + (import "host_process" "fd_pipe" (func $fd_pipe (type $fd_pipe_t))) + (import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fd_fdstat_get (type $fd_fdstat_get_t))) (import "wasi_snapshot_preview1" "fd_read" (func $fd_read (type $fd_read_t))) - (import "wasi_snapshot_preview1" "fd_close" (func $fd_close (type $fd_close_t))) + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t))) (memory (export "memory") 1) - (data (i32.const 64) "{path}") + (data (i32.const 64) "x") + (data (i32.const 80) "kernel-pipe-ready\n") (func $_start (export "_start") - (local $errno i32) - (local $fd i32) - (local.set $errno - (call $path_open - (i32.const 3) - (i32.const 0) - (i32.const 64) - (i32.const {path_len}) + (if (i32.ne (call $fd_pipe (i32.const 0) (i32.const 4)) (i32.const 0)) + (then unreachable) + ) + + (i32.store (i32.const 124) (i32.const 0x13579bdf)) + (i32.store (i32.const 152) (i32.const 0x2468ace0)) + (if + (i32.ne + (call $fd_fdstat_get (i32.load (i32.const 4)) (i32.const 128)) (i32.const 0) - (i64.const 2) - (i64.const 2) + ) + (then unreachable) + ) + (if (i32.ne (i32.load (i32.const 124)) (i32.const 0x13579bdf)) + (then unreachable) + ) + (if (i32.ne (i32.load (i32.const 152)) (i32.const 0x2468ace0)) + (then unreachable) + ) + (if (i32.ne (i32.load8_u (i32.const 128)) (i32.const 0)) + (then unreachable) + ) + + (i32.store (i32.const 16) (i32.const 64)) + (i32.store (i32.const 20) (i32.const 1)) + (if + (i32.ne + (call $fd_write + (i32.load (i32.const 4)) + (i32.const 16) + (i32.const 1) + (i32.const 32) + ) (i32.const 0) - (i32.const 8) ) + (then unreachable) + ) + (if (i32.ne (i32.load (i32.const 32)) (i32.const 1)) + (then unreachable) ) + + (i32.store (i32.const 156) (i32.const 0x3579bdf1)) + (i32.store (i32.const 184) (i32.const 0x468ace02)) (if (i32.ne - (local.get $errno) + (call $fd_fdstat_get (i32.load (i32.const 0)) (i32.const 160)) (i32.const 0) ) (then unreachable) ) - (local.set $fd (i32.load (i32.const 8))) - (i32.store (i32.const 16) (i32.const 128)) - (i32.store (i32.const 20) (i32.const 8)) - (local.set $errno - (call $fd_read - (local.get $fd) - (i32.const 16) - (i32.const 1) - (i32.const 24) + (if (i32.ne (i32.load (i32.const 156)) (i32.const 0x3579bdf1)) + (then unreachable) + ) + (if (i32.ne (i32.load (i32.const 184)) (i32.const 0x468ace02)) + (then unreachable) + ) + (if (i32.ne (i32.load8_u (i32.const 160)) (i32.const 0)) + (then unreachable) + ) + + (i32.store (i32.const 24) (i32.const 65)) + (i32.store (i32.const 28) (i32.const 1)) + (if + (i32.ne + (call $fd_read + (i32.load (i32.const 0)) + (i32.const 24) + (i32.const 1) + (i32.const 36) + ) + (i32.const 0) ) + (then unreachable) + ) + (if (i32.ne (i32.load (i32.const 36)) (i32.const 1)) + (then unreachable) + ) + (if (i32.ne (i32.load8_u (i32.const 65)) (i32.const 120)) + (then unreachable) ) + + (i32.store (i32.const 40) (i32.const 80)) + (i32.store (i32.const 44) (i32.const {ready_len})) (if (i32.ne - (local.get $errno) - (i32.const {expected_errno}) + (call $fd_write + (i32.const 1) + (i32.const 40) + (i32.const 1) + (i32.const 56) + ) + (i32.const 0) ) (then unreachable) ) - (drop (call $fd_close (local.get $fd))) ) ) "#, - path = wat_escape_ascii(path), - path_len = path.len(), + ready_len = READY_MARKER.len(), )) - .expect("compile wasm read errno fixture") + .expect("compile managed WASM kernel-pipe probe") + } + + fn wat_escape_ascii(input: &str) -> String { + let mut escaped = String::new(); + for ch in input.chars() { + match ch { + '\\' => escaped.push_str("\\\\"), + '"' => escaped.push_str("\\\""), + '\n' => escaped.push_str("\\n"), + '\r' => escaped.push_str("\\0d"), + _ => escaped.push(ch), + } + } + escaped } - fn wasm_expect_write_open_errno_module(path: &str, expected_errno: u32) -> Vec { + fn wasm_expect_path_open_errno_module( + path: &str, + oflags: u32, + rights: u64, + expected_errno: u32, + ) -> Vec { wat::parse_str(format!( r#" (module @@ -3760,9 +4139,9 @@ console.log(JSON.stringify({ status: "ok", summary })); (i32.const 0) (i32.const 64) (i32.const {path_len}) - (i32.const 1) - (i64.const 64) - (i64.const 64) + (i32.const {oflags}) + (i64.const {rights}) + (i64.const {rights}) (i32.const 0) (i32.const 8) ) @@ -3786,7 +4165,7 @@ console.log(JSON.stringify({ status: "ok", summary })); path = wat_escape_ascii(path), path_len = path.len(), )) - .expect("compile wasm write-open errno fixture") + .expect("compile wasm path_open errno fixture") } fn start_fake_wasm_process( @@ -3808,7 +4187,9 @@ console.log(JSON.stringify({ status: "ok", summary })); BTreeMap::from([ ( String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + normalize_host_path(&vm.runtime_scratch_root) + .to_string_lossy() + .into_owned(), ), (String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")), ]) @@ -3821,6 +4202,7 @@ console.log(JSON.stringify({ status: "ok", summary })); limits: Default::default(), vm_id: vm_id.to_owned(), context_id: context.context_id, + managed_kernel_host: false, argv: vec![String::from("./guest.wasm")], env: env.clone(), cwd: cwd.to_path_buf(), @@ -3885,59 +4267,8 @@ console.log(JSON.stringify({ status: "ok", summary })); cwd: &Path, process_id: &str, ) { - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.to_owned(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.to_owned(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::new(), - cwd: cwd.to_path_buf(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); - vm.active_processes.insert( - process_id.to_owned(), - active_process_for_vm_tests( - kernel_handle.pid(), - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.to_path_buf()), - ); - } + start_javascript_entry_with_env(sidecar, vm_id, cwd, process_id, BTreeMap::new()); + } fn insert_fake_javascript_parent_process( sidecar: &mut NativeSidecar, @@ -3982,8 +4313,8 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, - request: JavascriptSyncRpcRequest, - ) -> Result { + request: HostRpcRequest, + ) -> Result { let runtime_handle = sidecar .runtime_context .as_ref() @@ -3991,13 +4322,14 @@ console.log(JSON.stringify({ status: "ok", summary })); .handle() .clone(); let bridge = sidecar.bridge.clone(); - let (dns, socket_paths, capabilities, kernel_readiness) = { + let (dns, socket_paths, capabilities, kernel_readiness, managed_descriptions) = { let vm = sidecar.vms.get(vm_id).expect("javascript vm"); ( vm.dns.clone(), - build_javascript_socket_path_context(vm).expect("build socket path context"), + build_socket_path_context(vm).expect("build socket path context"), vm.capabilities.clone(), vm.kernel_socket_readiness.clone(), + Arc::clone(&vm.managed_host_net_descriptions), ) }; @@ -4017,6 +4349,7 @@ console.log(JSON.stringify({ status: "ok", summary })); process, sync_request: &request, capabilities, + managed_descriptions: Some(managed_descriptions), }, )) } @@ -4025,16 +4358,17 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, - request: JavascriptSyncRpcRequest, - ) -> Result { + request: HostRpcRequest, + ) -> Result { let bridge = sidecar.bridge.clone(); - let (dns, socket_paths, capabilities, kernel_readiness) = { + let (dns, socket_paths, capabilities, kernel_readiness, managed_descriptions) = { let vm = sidecar.vms.get(vm_id).expect("javascript vm"); ( vm.dns.clone(), - build_javascript_socket_path_context(vm).expect("build socket path context"), + build_socket_path_context(vm).expect("build socket path context"), vm.capabilities.clone(), vm.kernel_socket_readiness.clone(), + Arc::clone(&vm.managed_host_net_descriptions), ) }; let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); @@ -4052,6 +4386,7 @@ console.log(JSON.stringify({ status: "ok", summary })); process, sync_request: &request, capabilities, + managed_descriptions: Some(managed_descriptions), }) .await } @@ -4060,19 +4395,18 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar: &mut NativeSidecar, vm_id: &str, process_id: &str, - request: JavascriptSyncRpcRequest, + request: HostRpcRequest, ) -> Result { let request_id = request.id; let method = request.method.clone(); let response = call_javascript_sync_rpc_response(sidecar, vm_id, process_id, request)?; match response { - JavascriptSyncRpcServiceResponse::Json(value) => Ok(value), - JavascriptSyncRpcServiceResponse::SourceBackedJson { value, .. } => Ok(value), - JavascriptSyncRpcServiceResponse::Raw(_) - | JavascriptSyncRpcServiceResponse::SourceBackedRaw { .. } => Err( + HostServiceResponse::Json(value) => Ok(value), + HostServiceResponse::SourceBackedJson { value, .. } => Ok(value), + HostServiceResponse::Raw(_) | HostServiceResponse::SourceBackedRaw { .. } => Err( SidecarError::Execution(String::from("expected JSON sync RPC response")), ), - JavascriptSyncRpcServiceResponse::Deferred { receiver, .. } => { + HostServiceResponse::Deferred { receiver, .. } => { let result = block_on_sidecar!(sidecar, receiver) .map_err(|_| { SidecarError::Execution(String::from( @@ -4092,14 +4426,14 @@ console.log(JSON.stringify({ status: "ok", summary })); .get_mut(process_id) .expect("javascript process"); let connected = process - .pending_javascript_net_connects + .pending_net_connects .remove(&request_id) .ok_or_else(|| { SidecarError::InvalidState(format!( "missing deferred net.connect state for request {request_id}" )) })?; - finalize_javascript_net_connect(process, &kernel_readiness, connected) + finalize_net_connect(process, &kernel_readiness, connected) } } } @@ -4118,7 +4452,7 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar, vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: request_id_start + attempt, method: String::from("net.socket_read"), @@ -4127,24 +4461,20 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .unwrap_or_else(|error| panic!("{context}: {error}")); match response { - JavascriptSyncRpcServiceResponse::Raw(chunk) - | JavascriptSyncRpcServiceResponse::SourceBackedRaw { - payload: chunk, .. - } => { + HostServiceResponse::Raw(chunk) + | HostServiceResponse::SourceBackedRaw { payload: chunk, .. } => { return chunk; } - JavascriptSyncRpcServiceResponse::Json(value) - if value == "__agentos_net_timeout__" => - { + HostServiceResponse::Json(value) if value == "__agentos_net_timeout__" => { thread::sleep(std::time::Duration::from_millis(10)); } - JavascriptSyncRpcServiceResponse::Json(value) => { + HostServiceResponse::Json(value) => { panic!("{context}: expected socket data chunk, got {value}"); } - JavascriptSyncRpcServiceResponse::SourceBackedJson { value, .. } => { + HostServiceResponse::SourceBackedJson { value, .. } => { panic!("{context}: expected socket data chunk, got {value}"); } - JavascriptSyncRpcServiceResponse::Deferred { .. } => { + HostServiceResponse::Deferred { .. } => { panic!("{context}: unexpected deferred socket read response"); } } @@ -4167,7 +4497,7 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar, vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: request_id_start + attempt, method: String::from("net.poll"), @@ -4188,17 +4518,17 @@ console.log(JSON.stringify({ status: "ok", summary })); bridge: &SharedBridge, vm_id: &str, dns: &VmDnsConfig, - socket_paths: &JavascriptSocketPathContext, + socket_paths: &SocketPathContext, kernel: &mut SidecarKernel, process: &mut ActiveProcess, - request: &JavascriptSyncRpcRequest, + request: &HostRpcRequest, capabilities: CapabilityRegistry, ) -> Result where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { - service_javascript_net_sync_rpc_inner(JavascriptNetSyncRpcServiceRequest { + service_javascript_net_sync_rpc_inner(NetServiceRequest { bridge, vm_id, dns, @@ -4232,7 +4562,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-query", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -4252,7 +4582,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-query", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("dgram.createSocket"), @@ -4268,7 +4598,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-query", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("dgram.bind"), @@ -4370,7 +4700,7 @@ console.log(JSON.stringify({ status: "ok", summary })); match response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "execution_error"); + assert_eq!(rejected.code, "EACCES"); assert!( rejected .message @@ -4402,7 +4732,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-inspect-listener", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -4461,7 +4791,7 @@ console.log(JSON.stringify({ status: "ok", summary })); match response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "execution_error"); + assert_eq!(rejected.code, "EACCES"); assert!( rejected .message @@ -4493,7 +4823,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-inspect-udp", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("dgram.createSocket"), @@ -4509,7 +4839,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-inspect-udp", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("dgram.bind"), @@ -4567,7 +4897,7 @@ console.log(JSON.stringify({ status: "ok", summary })); match response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "execution_error"); + assert_eq!(rejected.code, "EACCES"); assert!( rejected .message @@ -4639,7 +4969,7 @@ console.log(JSON.stringify({ status: "ok", summary })); match response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "execution_error"); + assert_eq!(rejected.code, "EACCES"); assert!( rejected .message @@ -4681,6 +5011,10 @@ console.log(JSON.stringify({ status: "ok", summary })); snapshot.running_processes >= 1, "expected running kernel process in snapshot: {snapshot:?}" ); + assert_eq!( + snapshot.stopped_processes, 0, + "running process must not be reported as stopped: {snapshot:?}" + ); assert!( snapshot.fd_tables >= 1, "expected fd table accounting in snapshot: {snapshot:?}" @@ -4717,7 +5051,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-counts", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -4737,7 +5071,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-counts", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("dgram.createSocket"), @@ -4753,7 +5087,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-kernel-counts", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("dgram.bind"), @@ -4794,6 +5128,7 @@ console.log(JSON.stringify({ status: "ok", summary })); description_handles: std::sync::Arc::clone(&listener.description_handles), description_lease: Arc::clone(&listener.description_lease), kernel_transfer_guard: listener.kernel_transfer_guard.clone(), + pending_event: Arc::clone(&listener.pending_event), } }; process @@ -4824,6 +5159,7 @@ console.log(JSON.stringify({ status: "ok", summary })); fairness_retirement: Arc::clone(&socket.fairness_retirement), description_lease: Arc::clone(&socket.description_lease), read_event_notify: Arc::clone(&socket.read_event_notify), + pending_datagram: Arc::clone(&socket.pending_datagram), event_pusher: Arc::clone(&socket.event_pusher), readiness_registration: crate::state::SocketReadinessRegistration::new( Arc::clone(&socket.event_pusher), @@ -4858,7 +5194,7 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar, vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 9_000, method: String::from(method), @@ -5458,7 +5794,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -5480,7 +5816,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -5497,7 +5833,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.socket_wait_connect"), @@ -5522,7 +5858,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4 + attempt, method: String::from("net.server_accept"), @@ -5545,7 +5881,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 50, method: String::from("net.destroy"), @@ -5557,7 +5893,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 51, method: String::from("net.destroy"), @@ -5569,7 +5905,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-wait-connect", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 52, method: String::from("net.server_close"), @@ -5599,7 +5935,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -5621,7 +5957,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -5638,7 +5974,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.socket_set_no_delay"), @@ -5650,7 +5986,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.socket_set_keep_alive"), @@ -5665,7 +6001,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5 + attempt, method: String::from("net.server_accept"), @@ -5711,7 +6047,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 60, method: String::from("net.write"), @@ -5729,7 +6065,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 61, method: String::from("net.shutdown"), @@ -5744,7 +6080,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10 + attempt, method: String::from("net.socket_read"), @@ -5753,22 +6089,19 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("read bridged socket chunk"); match response { - JavascriptSyncRpcServiceResponse::Raw(chunk) - | JavascriptSyncRpcServiceResponse::SourceBackedRaw { - payload: chunk, .. - } => { + HostServiceResponse::Raw(chunk) + | HostServiceResponse::SourceBackedRaw { payload: chunk, .. } => { payload = Some(chunk); break; } - JavascriptSyncRpcServiceResponse::Json(value) - if value == "__agentos_net_timeout__" => {} - JavascriptSyncRpcServiceResponse::Json(value) => { + HostServiceResponse::Json(value) if value == "__agentos_net_timeout__" => {} + HostServiceResponse::Json(value) => { panic!("expected bridged socket data chunk, got {value}"); } - JavascriptSyncRpcServiceResponse::SourceBackedJson { value, .. } => { + HostServiceResponse::SourceBackedJson { value, .. } => { panic!("expected bridged socket data chunk, got {value}"); } - JavascriptSyncRpcServiceResponse::Deferred { .. } => { + HostServiceResponse::Deferred { .. } => { panic!("unexpected deferred socket read response"); } } @@ -5783,7 +6116,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 40 + attempt, method: String::from("net.socket_read"), @@ -5804,7 +6137,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 99, method: String::from("net.destroy"), @@ -5816,7 +6149,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 100, method: String::from("net.destroy"), @@ -5828,7 +6161,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-net-read", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 101, method: String::from("net.server_close"), @@ -5840,7 +6173,7 @@ console.log(JSON.stringify({ status: "ok", summary })); // Regression for #88: a server in one guest exec process and a client in a // *different* guest exec process inside the SAME VM must talk over loopback. // The fix builds the per-VM socket-path context from every concurrent exec's - // listeners (`build_javascript_socket_path_context` iterates all + // listeners (`build_socket_path_context` iterates all // `active_processes`), so the client's `net.connect` resolves the server // process's listener and routes through the shared kernel socket table. fn javascript_net_cross_exec_loopback_routes_through_kernel_socket_table() { @@ -5877,7 +6210,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -5900,7 +6233,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -5941,7 +6274,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10 + attempt, method: String::from("net.server_accept"), @@ -5970,7 +6303,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 100, method: String::from("net.write"), @@ -5988,7 +6321,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 101, method: String::from("net.shutdown"), @@ -6016,7 +6349,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 300, method: String::from("net.destroy"), @@ -6028,7 +6361,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 301, method: String::from("net.destroy"), @@ -6040,7 +6373,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 302, method: String::from("net.server_close"), @@ -6070,7 +6403,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -6092,7 +6425,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -6111,7 +6444,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10 + attempt, method: String::from("net.server_accept"), @@ -6134,7 +6467,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 50, method: String::from("net.upgrade_socket_write"), @@ -6162,7 +6495,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 80, method: String::from("net.upgrade_socket_end"), @@ -6177,7 +6510,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 90 + attempt, method: String::from("net.socket_read"), @@ -6198,7 +6531,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 120, method: String::from("net.upgrade_socket_destroy"), @@ -6210,7 +6543,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 121, method: String::from("net.upgrade_socket_destroy"), @@ -6222,7 +6555,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-upgrade-socket", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 122, method: String::from("net.server_close"), @@ -6252,7 +6585,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("dgram.createSocket"), @@ -6269,7 +6602,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("dgram.bind"), @@ -6288,7 +6621,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("dgram.address"), @@ -6310,7 +6643,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("dgram.setBufferSize"), @@ -6322,7 +6655,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("dgram.setBufferSize"), @@ -6335,7 +6668,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("dgram.getBufferSize"), @@ -6352,7 +6685,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("dgram.getBufferSize"), @@ -6418,7 +6751,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id, method: String::from("dgram.setOption"), @@ -6432,7 +6765,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-dgram-options", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 17, method: String::from("dgram.close"), @@ -6495,7 +6828,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("tls.get_ciphers"), @@ -6516,7 +6849,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -6533,7 +6866,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.socket_upgrade_tls"), @@ -6555,7 +6888,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.socket_tls_query"), @@ -6576,7 +6909,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.socket_tls_query"), @@ -6593,7 +6926,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.socket_tls_query"), @@ -6617,7 +6950,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.write"), @@ -6647,7 +6980,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-client", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 99, method: String::from("net.destroy"), @@ -6680,7 +7013,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -6701,7 +7034,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -6723,7 +7056,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10 + attempt, method: String::from("net.server_accept"), @@ -6751,7 +7084,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 40, method: String::from("net.socket_upgrade_tls"), @@ -6773,7 +7106,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 45, method: String::from("net.write"), @@ -6794,7 +7127,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 50 + attempt, method: String::from("net.socket_get_tls_client_hello"), @@ -6825,7 +7158,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 80, method: String::from("net.socket_upgrade_tls"), @@ -6847,7 +7180,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 81, method: String::from("net.socket_tls_query"), @@ -6866,7 +7199,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 82 + attempt * 2, method: String::from("net.socket_tls_query"), @@ -6883,7 +7216,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 83 + attempt * 2, method: String::from("net.socket_tls_query"), @@ -6922,7 +7255,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 190 + attempt, method: String::from("net.socket_tls_query"), @@ -6951,7 +7284,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 120, method: String::from("net.write"), @@ -6981,7 +7314,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 121, method: String::from("net.destroy"), @@ -6993,7 +7326,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 122, method: String::from("net.destroy"), @@ -7005,7 +7338,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-tls-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 123, method: String::from("net.server_close"), @@ -7035,7 +7368,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -7056,7 +7389,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.server_accept"), @@ -7070,7 +7403,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.connect"), @@ -7092,7 +7425,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10 + attempt, method: String::from("net.server_accept"), @@ -7129,7 +7462,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 40, method: String::from("net.destroy"), @@ -7141,7 +7474,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-server-accept", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 41, method: String::from("net.destroy"), @@ -7242,7 +7575,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-stdin", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("__kernel_stdin_read"), @@ -7274,7 +7607,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-stdin", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("__kernel_stdin_read"), @@ -7290,6 +7623,33 @@ console.log(JSON.stringify({ status: "ok", summary })); }) ); + // The managed coordinator must not mirror the same bytes into the + // execution crate's standalone local-stdin bridge. A second source + // would make an adapter-route change double-deliver this payload. + let adapter_local = { + let process = sidecar + .vms + .get(&vm_id) + .and_then(|vm| vm.active_processes.get("proc-js-stdin")) + .expect("managed JavaScript process"); + let ActiveExecution::Javascript(execution) = &process.execution else { + panic!("expected JavaScript execution"); + }; + execution + .read_kernel_stdin_sync_rpc(&HostRpcRequest { + raw_bytes_args: std::collections::HashMap::new(), + id: 20, + method: String::from("__kernel_stdin_read"), + args: vec![json!(1024), json!(0)], + }) + .expect("probe standalone local stdin bridge") + }; + assert_eq!( + adapter_local, + Value::Null, + "managed stdin must have exactly one byte source" + ); + let close = sidecar .dispatch_blocking(request( 12, @@ -7310,7 +7670,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-stdin", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("__kernel_stdin_read"), @@ -7444,7 +7804,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-pty", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("__pty_set_raw_mode"), @@ -7477,7 +7837,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-pty", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("__pty_set_raw_mode"), @@ -7508,7 +7868,7 @@ console.log(JSON.stringify({ status: "ok", summary })); &mut sidecar, &vm_id, "proc-js-pty", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("__pty_set_raw_mode"), @@ -7712,7 +8072,7 @@ console.log(JSON.stringify({ status: "ok", summary })); let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); - let request = JavascriptSyncRpcRequest { + let request = HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("process.kill"), @@ -7739,7 +8099,7 @@ console.log(JSON.stringify({ status: "ok", summary })); sidecar.handle_javascript_sync_rpc_request( &disposed_vm_id, "proc-js-race", - request.clone(), + test_execution_host_call(request.clone()), ) ) .expect("ignore stale vm javascript sync rpc"); @@ -7753,7 +8113,11 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("create live vm"); block_on_sidecar!( sidecar, - sidecar.handle_javascript_sync_rpc_request(&live_vm_id, "proc-js-race", request) + sidecar.handle_javascript_sync_rpc_request( + &live_vm_id, + "proc-js-race", + test_execution_host_call(request), + ) ) .expect("ignore stale process javascript sync rpc"); } @@ -7960,104 +8324,6 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("create live vm"); - { - let vm = sidecar.vms.get(&live_vm_id).expect("live vm"); - assert!( - !vm.kernel - .exists("/tmp/stale-python-rpc") - .expect("check missing workspace before stale python rpc"), - "stale python request precondition failed" - ); - } - - block_on_sidecar!( - sidecar, - sidecar.handle_python_vfs_rpc_request( - &live_vm_id, - "proc-stale-python", - PythonVfsRpcRequest { - id: 1, - method: PythonVfsRpcMethod::Mkdir, - path: String::from("/tmp/stale-python-rpc"), - destination: None, - target: None, - mode: None, - uid: None, - gid: None, - atime_ms: None, - mtime_ms: None, - content_base64: None, - recursive: false, - url: None, - http_method: None, - headers: BTreeMap::new(), - body_base64: None, - hostname: None, - family: None, - port: None, - socket_id: None, - command: None, - args: Vec::new(), - argv0: None, - cwd: None, - env: BTreeMap::new(), - shell: false, - max_buffer: None, - timeout_ms: None, - }, - ) - ) - .expect("ignore stale python vfs process"); - - { - let vm = sidecar.vms.get(&live_vm_id).expect("live vm"); - assert!( - !vm.kernel - .exists("/tmp/stale-python-rpc") - .expect("check stale python rpc did not mutate kernel"), - "stale python VFS request should not mutate the kernel" - ); - } - - block_on_sidecar!( - sidecar, - sidecar.handle_python_vfs_rpc_request( - &disposed_vm_id, - "proc-stale-python", - PythonVfsRpcRequest { - id: 2, - method: PythonVfsRpcMethod::Mkdir, - path: String::from("/tmp/stale-python-rpc"), - destination: None, - target: None, - mode: None, - uid: None, - gid: None, - atime_ms: None, - mtime_ms: None, - content_base64: None, - recursive: false, - url: None, - http_method: None, - headers: BTreeMap::new(), - body_base64: None, - hostname: None, - family: None, - port: None, - socket_id: None, - command: None, - args: Vec::new(), - argv0: None, - cwd: None, - env: BTreeMap::new(), - shell: false, - max_buffer: None, - timeout_ms: None, - }, - ) - ) - .expect("ignore stale python vfs vm"); - let write_response = sidecar .dispatch_blocking(request( 5, @@ -8702,100 +8968,8 @@ console.log(JSON.stringify({ status: "ok", summary })); configure_vm_passes_resource_read_limits_to_module_access_mounts(); } - // Regression guard for the read-side shadow-walk fix. - // - // Every read-side guest fs op (Exists/Stat/Lstat/ReadFile) reconciles the host - // shadow tree into the kernel VFS first. The reconciliation walks the whole tree - // from `vm.cwd`, but it must now SKIP files the kernel already holds an identical - // copy of (same size/mode/mtime) instead of unconditionally re-reading every - // file's bytes and re-writing them into the kernel. Without the skip a single - // `exists("/anything")` costs O(whole tree) and is super-linear as the shadow - // grows -- the session-creation/runtime latency this fixes. - // - // We prove two things: - // 1. A warm read op over an UNCHANGED tree is far cheaper than the first - // (cold) one, i.e. unchanged files are skipped, not re-copied. - // 2. The skip is self-correcting: after a file's content changes, a read still - // observes the new bytes (no stale skip). - fn read_side_ops_skip_unchanged_shadow_files_repro() { - use std::time::{Duration, Instant}; - - fn fs_payload( - operation: GuestFilesystemOperation, - path: &str, - content: Option, - ) -> RequestPayload { - RequestPayload::GuestFilesystemCall(GuestFilesystemCallRequest { - operation, - path: String::from(path), - destination_path: None, - target: None, - content, - encoding: Some(RootFilesystemEntryEncoding::Utf8), - recursive: true, - max_depth: None, - mode: None, - uid: None, - gid: None, - atime_ms: None, - mtime_ms: None, - len: None, - offset: None, - }) - } - - fn dispatch( - sidecar: &mut NativeSidecar, - ownership: &OwnershipScope, - next_id: &mut i64, - payload: RequestPayload, - ) -> ResponsePayload { - *next_id += 1; - sidecar - .dispatch_blocking(request(*next_id, ownership.clone(), payload)) - .expect("dispatch guest fs op") - .response - .payload - } - - // Seed flat files `from..to` via guest WriteFile (mirrors into the host - // shadow root). Write-side ops do not walk, so seeding is O(count). - fn seed_to( - sidecar: &mut NativeSidecar, - ownership: &OwnershipScope, - next_id: &mut i64, - body: &str, - from: usize, - to: usize, - ) { - for i in from..to { - let path = format!("/seed-{i:05}.txt"); - let payload = fs_payload( - GuestFilesystemOperation::WriteFile, - &path, - Some(String::from(body)), - ); - match dispatch(sidecar, ownership, next_id, payload) { - ResponsePayload::GuestFilesystemResult(_) => {} - other => panic!("seed write failed: {other:?}"), - } - } - } - - fn time_exists( - sidecar: &mut NativeSidecar, - ownership: &OwnershipScope, - next_id: &mut i64, - ) -> Duration { - let payload = fs_payload(GuestFilesystemOperation::Exists, "/zzz-not-here", None); - let start = Instant::now(); - match dispatch(sidecar, ownership, next_id, payload) { - ResponsePayload::GuestFilesystemResult(r) => assert_eq!(r.exists, Some(false)), - other => panic!("exists failed: {other:?}"), - } - start.elapsed() - } - + #[test] + fn guest_filesystem_calls_leave_runtime_scratch_private() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); @@ -8807,73 +8981,79 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("create vm"); let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id); - let mut next_id: i64 = 1000; - - let file_body = "a".repeat(8 * 1024); - const COUNT: usize = 800; - seed_to(&mut sidecar, &ownership, &mut next_id, &file_body, 0, COUNT); - - // Cold: first read op reconciles the whole tree (reads + writes every file). - let cold = time_exists(&mut sidecar, &ownership, &mut next_id); - // Warm: tree is unchanged, so every file must be skipped. - let warm = time_exists(&mut sidecar, &ownership, &mut next_id); - - eprintln!("[shadow-skip] cold={cold:?} warm={warm:?}"); + let scratch_file = sidecar + .vms + .get(&vm_id) + .expect("created vm") + .runtime_scratch_root + .join("kernel-authority.txt"); + assert!(!scratch_file.exists()); - // Symptom-1 guard: the warm walk skips unchanged files, so it is far cheaper - // than the cold walk that copied them all. (Lenient 4x; observed >>10x.) + let write = sidecar + .dispatch_blocking(request( + 1001, + ownership.clone(), + RequestPayload::GuestFilesystemCall(GuestFilesystemCallRequest { + operation: GuestFilesystemOperation::WriteFile, + path: String::from("/kernel-authority.txt"), + content: Some(String::from("kernel-only\n")), + encoding: Some(RootFilesystemEntryEncoding::Utf8), + recursive: true, + max_depth: None, + mode: None, + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + destination_path: None, + target: None, + }), + )) + .expect("write kernel file"); + assert!(matches!( + write.response.payload, + ResponsePayload::GuestFilesystemResult(_) + )); assert!( - cold >= warm * 4, - "warm read op over an unchanged shadow tree should skip re-copying files: \ - cold={cold:?} warm={warm:?}" - ); - - // End-to-end smoke: overwrite a seeded file (different length) then read it - // back and observe the new bytes. NOTE: this is a guest WriteFile, which - // updates the kernel directly, so it does not exercise the host-shadow->kernel - // skip predicate itself -- it only guards that overwrite-then-read is coherent. - // A true stale-skip test (host-side rewrite that keeps size+mode+mtime) is not - // reachable through the public wire API and would need an in-crate unit test - // with direct shadow-root access; see the skip-limitation note in - // sync_host_directory_tree_to_kernel_inner. - let changed_path = "/seed-00042.txt"; - let new_body = "b".repeat(16 * 1024); - match dispatch( - &mut sidecar, - &ownership, - &mut next_id, - fs_payload( - GuestFilesystemOperation::WriteFile, - changed_path, - Some(new_body.clone()), - ), - ) { - ResponsePayload::GuestFilesystemResult(_) => {} - other => panic!("overwrite failed: {other:?}"), - } - match dispatch( - &mut sidecar, - &ownership, - &mut next_id, - fs_payload(GuestFilesystemOperation::ReadFile, changed_path, None), - ) { - ResponsePayload::GuestFilesystemResult(r) => { - assert_eq!( - r.content.as_deref(), - Some(new_body.as_str()), - "changed shadow file must not be served stale by the skip" - ); + !scratch_file.exists(), + "guest writes must not materialize into executor scratch" + ); + + let read = sidecar + .dispatch_blocking(request( + 1002, + ownership, + RequestPayload::GuestFilesystemCall(GuestFilesystemCallRequest { + operation: GuestFilesystemOperation::ReadFile, + path: String::from("/kernel-authority.txt"), + encoding: Some(RootFilesystemEntryEncoding::Utf8), + recursive: false, + max_depth: None, + mode: None, + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + destination_path: None, + target: None, + content: None, + }), + )) + .expect("read kernel file"); + match read.response.payload { + ResponsePayload::GuestFilesystemResult(result) => { + assert_eq!(result.content.as_deref(), Some("kernel-only\n")); } - other => panic!("read after overwrite failed: {other:?}"), + other => panic!("unexpected read response: {other:?}"), } - } - - // Expensive: seeds hundreds of files and pays one cold full-tree reconciliation - // (seconds in debug). Gated out of the default suite; run with `--ignored`. - #[test] - #[ignore = "expensive: cold shadow-tree reconciliation; run with --ignored"] - fn read_side_ops_skip_unchanged_shadow_files() { - read_side_ops_skip_unchanged_shadow_files_repro(); + assert!( + !scratch_file.exists(), + "guest reads must not reconcile executor scratch" + ); } fn configure_vm_rejects_module_access_root_symlink_to_non_node_modules() { @@ -9065,6 +9245,9 @@ console.log(JSON.stringify({ status: "ok", summary })); serde_json::from_str(&call.args).expect("js bridge args json"); match call.operation.as_str() { "exists" => js_bridge_result(request, Some(Value::Bool(true)), None), + "stat" | "lstat" => { + js_bridge_result(request, Some(js_bridge_fixture_file_stat(5)), None) + } "realpath" => { let path = call_args .get("path") @@ -9157,6 +9340,9 @@ console.log(JSON.stringify({ status: "ok", summary })); serde_json::from_str(&call.args).expect("js bridge args json"); match call.operation.as_str() { "exists" => js_bridge_result(request, Some(Value::Bool(true)), None), + "stat" | "lstat" => { + js_bridge_result(request, Some(js_bridge_fixture_file_stat(5)), None) + } "realpath" => { let path = call_args .get("path") @@ -10240,7 +10426,6 @@ console.log(JSON.stringify({ status: "ok", summary })); max_process_argv_bytes: Some(2048), max_process_env_bytes: Some(1024), max_readdir_entries: Some(32), - max_wasm_fuel: Some(5000), max_wasm_memory_bytes: Some(131_072), max_wasm_stack_bytes: Some(262_144), ..Default::default() @@ -10266,7 +10451,6 @@ console.log(JSON.stringify({ status: "ok", summary })); assert_eq!(limits.max_process_argv_bytes, Some(2048)); assert_eq!(limits.max_process_env_bytes, Some(1024)); assert_eq!(limits.max_readdir_entries, Some(32)); - assert_eq!(limits.max_wasm_fuel, Some(5000)); assert_eq!(limits.max_wasm_memory_bytes, Some(131072)); assert_eq!(limits.max_wasm_stack_bytes, Some(262144)); } @@ -10467,9 +10651,9 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("register original binding collection"); - let (bindings_before, command_paths_before) = { + let (bindings_before, commands_before) = { let vm = sidecar.vms.get(&vm_id).expect("configured vm"); - (vm.bindings.clone(), vm.command_guest_paths.clone()) + (vm.bindings.clone(), vm.kernel.commands()) }; sidecar @@ -10499,9 +10683,18 @@ console.log(JSON.stringify({ status: "ok", summary })); ResponsePayload::Rejected(rejected) => { assert_eq!(rejected.code, "invalid_state"); let message = rejected.message; - assert!(message.contains("binding collection registration rollback failed")); - assert!(message.contains("injected restore failure")); - assert!(message.contains("applied deny-all fallback")); + assert!( + message.contains("collection registration rollback failed"), + "unexpected rejection message: {message}" + ); + assert!( + message.contains("injected restore failure"), + "unexpected rejection message: {message}" + ); + assert!( + message.contains("applied deny-all fallback"), + "unexpected rejection message: {message}" + ); } other => panic!("expected rejected response, got {other:?}"), } @@ -10525,7 +10718,7 @@ console.log(JSON.stringify({ status: "ok", summary })); agentos_native_sidecar_core::permissions::deny_all_policy() ); assert_eq!(vm.bindings, bindings_before); - assert_eq!(vm.command_guest_paths, command_paths_before); + assert_eq!(vm.kernel.commands(), commands_before); } fn create_vm_rejects_permission_rules_with_empty_operations() { let mut sidecar = create_test_sidecar(); @@ -11193,6 +11386,86 @@ console.log(JSON.stringify({ status: "ok", summary })); fs::remove_dir_all(host_dir).expect("remove temp dir"); } + + #[test] + fn top_level_lifecycle_publication_failure_reaps_started_runtime() { + assert_node_available(); + + let cwd = temp_dir("agentos-native-sidecar-lifecycle-publication-rollback"); + let module_path = cwd.join("guest.wasm"); + write_fixture( + &module_path, + wat::parse_str("(module (func (export \"_start\")))") + .expect("compile lifecycle rollback fixture"), + ); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let baseline = sidecar + .vms + .get(&vm_id) + .expect("created vm") + .kernel + .resource_snapshot(); + let context_baseline = sidecar.wasm_engine.context_count_for_test(); + sidecar + .bridge + .queue_emit_lifecycle_result(Err(SidecarError::Bridge(String::from( + "injected Busy lifecycle publication failure", + )))) + .expect("queue lifecycle failure"); + + let result = sidecar + .dispatch_blocking(request( + 4, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::Execute(crate::protocol::ExecuteRequest { + process_id: String::from("proc-lifecycle-failure"), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(module_path.to_string_lossy().into_owned()), + args: Vec::new(), + env: std::collections::HashMap::new(), + cwd: None, + wasm_permission_tier: None, + }), + )) + .expect("dispatch lifecycle publication failure"); + match result.response.payload { + ResponsePayload::Rejected(response) => { + assert_eq!(response.code, "bridge_error"); + assert!(response + .message + .contains("injected Busy lifecycle publication failure")); + } + other => panic!("expected lifecycle rejection, got {other:?}"), + } + + let vm = sidecar.vms.get(&vm_id).expect("created vm"); + assert!( + !vm.active_processes.contains_key("proc-lifecycle-failure"), + "failed response publication must remove the active process" + ); + assert_eq!( + vm.kernel.resource_snapshot(), + baseline, + "failed response publication must reap the PID and descriptors" + ); + assert_eq!( + sidecar.wasm_engine.context_count_for_test(), + context_baseline, + "failed response publication must drop the started adapter context" + ); + } + fn execute_starts_python_runtime_instead_of_rejecting_it() { assert_node_available(); @@ -11364,9 +11637,8 @@ console.log(JSON.stringify({ status: "ok", summary })); } fn wasm_command_timeout_is_enforced_by_sidecar_poll_path() { - // Timeout-dependent: an infinite-loop wasm module whose termination is - // enforced by the sidecar poll path only after ~30s. Gate it to the - // nightly timing lane rather than pay ~30s per PR. See CLAUDE.md > Testing. + // Timing-dependent: an infinite-loop module whose configured + // wall-clock deadline is enforced by the sidecar poll path. if !run_timing_sensitive_tests() { return; } @@ -11396,7 +11668,10 @@ console.log(JSON.stringify({ status: "ok", summary })); &connection_id, &session_id, PermissionsPolicy::allow_all(), - BTreeMap::from([(String::from("resource.max_wasm_fuel"), String::from("25"))]), + BTreeMap::from([( + String::from("limits.wasm.wall_clock_limit_ms"), + String::from("25"), + )]), ) .expect("create vm"); @@ -11463,7 +11738,7 @@ console.log(JSON.stringify({ status: "ok", summary })); assert_eq!(exit_code, Some(124), "stdout: {stdout} stderr: {stderr}"); assert!( - stderr.contains("fuel budget exhausted"), + stderr.contains("wall-clock limit exceeded"), "stderr should mention timeout: {stderr}" ); } @@ -11547,64 +11822,359 @@ console.log(JSON.stringify({ status: "ok", summary })); "stdout B leaked A marker: {stdout_b:?}" ); } - fn wasm_path_open_read_goes_through_kernel_filesystem_permissions() { - let cwd = temp_dir("agentos-native-sidecar-wasm-fs-permissions"); - write_fixture( - &cwd.join("guest.wasm"), - wasm_expect_read_errno_module("secret.txt", 2), - ); + + fn managed_wasm_pipe_is_kernel_owned_despite_guest_env_spoofing() { + let cwd = temp_dir("agentos-native-sidecar-managed-wasm-kernel-pipe"); + write_fixture(&cwd.join("guest.wasm"), wasm_kernel_pipe_probe_module()); let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = - authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); let vm_id = create_vm( &mut sidecar, &connection_id, &session_id, - capability_permissions(&[ - ("fs", PermissionMode::Allow), - ("fs.read", PermissionMode::Deny), - ("child_process.spawn", PermissionMode::Allow), - ]), + PermissionsPolicy::allow_all(), ) .expect("create vm"); - - sidecar + let baseline = sidecar .vms - .get_mut(&vm_id) - .expect("wasm vm") + .get(&vm_id) + .expect("created vm") .kernel - .filesystem_mut() - .write_file("/secret.txt", b"should-not-read".to_vec()) - .expect("seed denied-read fixture"); + .resource_snapshot(); let response = sidecar .dispatch_blocking(request( 6, OwnershipScope::vm(&connection_id, &session_id, &vm_id), RequestPayload::Execute(crate::protocol::ExecuteRequest { - process_id: String::from("proc-wasm-fs-permission"), + process_id: String::from("proc-managed-wasm-kernel-pipe"), command: None, runtime: Some(GuestRuntimeKind::WebAssembly), entrypoint: Some(cwd.join("guest.wasm").to_string_lossy().into_owned()), args: Vec::new(), - env: std::collections::HashMap::new(), + // These values selected the standalone descriptor path + // when managed mode was inferred from guest-visible env. + // Trusted launch state must keep this production + // execution kernel-backed regardless of either value. + env: std::collections::HashMap::from([ + (String::from("AGENTOS_SANDBOX_ROOT"), String::new()), + ( + String::from("AGENTOS_WASI_STDIO_SYNC_RPC"), + String::from("0"), + ), + ]), cwd: Some(String::from("/")), wasm_permission_tier: None, }), )) - .expect("dispatch wasm execute"); - + .expect("dispatch managed WASM pipe probe"); match response.response.payload { ResponsePayload::ProcessStarted(response) => { - assert_eq!(response.process_id, "proc-wasm-fs-permission"); + assert_eq!(response.process_id, "proc-managed-wasm-kernel-pipe"); } other => panic!("unexpected execute response: {other:?}"), } - let (stdout, stderr, exit_code) = - drain_process_output(&mut sidecar, &vm_id, "proc-wasm-fs-permission"); - + wait_for_process_stdout_contains( + &mut sidecar, + &vm_id, + "proc-managed-wasm-kernel-pipe", + "kernel-pipe-ready", + ); + + { + let vm = sidecar.vms.get(&vm_id).expect("active vm"); + let process = vm + .active_processes + .get("proc-managed-wasm-kernel-pipe") + .expect("active managed WASM process"); + let snapshot = vm.kernel.resource_snapshot(); + let pipe_fds = vm + .kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, process.kernel_pid) + .expect("snapshot managed WASM descriptors") + .into_iter() + .filter(|entry| entry.is_pipe) + .collect::>(); + + assert!( + snapshot.pipes >= baseline.pipes + 1, + "fd_pipe must allocate in the authoritative kernel pipe table" + ); + assert_eq!( + snapshot.pipe_buffered_bytes, baseline.pipe_buffered_bytes, + "the guest read must drain the byte written through the kernel pipe" + ); + assert!( + pipe_fds.len() >= 2, + "both pipe ends must be visible in the managed process kernel fd table" + ); + } + + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "proc-managed-wasm-kernel-pipe"); + assert_eq!(exit_code, Some(0), "stdout: {stdout} stderr: {stderr}"); + assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); + assert_eq!( + sidecar + .vms + .get(&vm_id) + .expect("active vm") + .kernel + .resource_snapshot(), + baseline, + "managed WASM exit must release the kernel pipe and descriptors" + ); + } + + fn managed_wasm_descendant_pipe_publishes_data_eof_hup_exit_and_cleanup() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + write_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + "/pipe-child.wasm", + wasm_stdout_module("child-pipe-payload"), + 0o755, + ); + + let (parent_pid, read_fd, write_fd, baseline) = { + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let handle = vm + .kernel + .spawn_process( + WASM_COMMAND, + vec![String::from("parent.wasm")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + cwd: Some(String::from("/")), + ..SpawnOptions::default() + }, + ) + .expect("spawn managed WASM parent"); + let parent_pid = handle.pid(); + vm.active_processes.insert( + String::from("managed-wasm-pipe-parent"), + active_process_for_vm_tests( + parent_pid, + handle, + vm.runtime_context.clone(), + vm.limits.clone(), + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(vm.guest_env.clone()) + .with_host_cwd(vm.runtime_scratch_root.clone()), + ); + let baseline = vm.kernel.resource_snapshot(); + let (read_fd, write_fd) = vm + .kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent_pid) + .expect("open parent-owned kernel pipe"); + (parent_pid, read_fd, write_fd, baseline) + }; + + let spawned = spawn_child_process_for_test( + &mut sidecar, + &vm_id, + "managed-wasm-pipe-parent", + agentos_execution::host::ProcessLaunchRequest { + command: String::from("/pipe-child.wasm"), + args: Vec::new(), + options: agentos_execution::host::ProcessLaunchOptions { + spawn_exact_path: true, + spawn_fd_mappings: vec![[read_fd, read_fd], [write_fd, write_fd]], + spawn_file_actions: vec![ + agentos_execution::host::ProcessSpawnFileAction { + command: 2, + guest_fd: Some(1), + fd: 1, + source_fd: write_fd as i32, + guest_source_fd: Some(write_fd as i32), + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + agentos_execution::host::ProcessSpawnFileAction { + command: 1, + guest_fd: Some(read_fd as i32), + fd: read_fd as i32, + source_fd: -1, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + agentos_execution::host::ProcessSpawnFileAction { + command: 1, + guest_fd: Some(write_fd as i32), + fd: write_fd as i32, + source_fd: -1, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ], + ..Default::default() + }, + }, + ) + .expect("spawn managed WASM pipe child"); + let child_id = spawned["childId"] + .as_str() + .expect("spawned child id") + .to_owned(); + + sidecar + .vms + .get_mut(&vm_id) + .expect("active vm") + .kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, write_fd) + .expect("close parent writer after spawn"); + + let deadline = Instant::now() + Duration::from_secs(10); + let exit_event = loop { + let event = poll_child_process_for_test( + &mut sidecar, + &vm_id, + "managed-wasm-pipe-parent", + &child_id, + 250, + ) + .expect("poll managed pipe child"); + if event.get("type").and_then(Value::as_str) == Some("exit") { + break event; + } + assert!( + Instant::now() < deadline, + "managed pipe child did not publish exit; last event: {event}" + ); + }; + assert_eq!(exit_event["exitCode"].as_i64(), Some(0), "{exit_event}"); + + let vm = sidecar.vms.get_mut(&vm_id).expect("active vm"); + let ready = vm + .kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + parent_pid, + vec![PollTargetEntry::fd(read_fd, POLLIN | POLLHUP)], + 0, + ) + .expect("poll buffered child output plus EOF"); + assert_eq!(ready.ready_count, 1); + assert!(ready.targets[0].revents.intersects(POLLIN)); + assert!(ready.targets[0].revents.intersects(POLLHUP)); + + assert_eq!( + vm.kernel + .fd_read(EXECUTION_DRIVER_NAME, parent_pid, read_fd, 64) + .expect("read child pipe payload"), + b"child-pipe-payload\n" + ); + assert_eq!( + vm.kernel + .fd_read(EXECUTION_DRIVER_NAME, parent_pid, read_fd, 64) + .expect("read child pipe EOF"), + b"" + ); + let eof = vm + .kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + parent_pid, + vec![PollTargetEntry::fd(read_fd, POLLIN | POLLHUP)], + 0, + ) + .expect("poll drained child pipe EOF"); + assert_eq!(eof.ready_count, 1); + assert!(!eof.targets[0].revents.intersects(POLLIN)); + assert!(eof.targets[0].revents.intersects(POLLHUP)); + + vm.kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, read_fd) + .expect("close parent pipe reader"); + assert_eq!( + vm.kernel.resource_snapshot(), + baseline, + "child wait completion and final reader close must restore kernel resources" + ); + } + + fn wasm_path_open_read_goes_through_kernel_filesystem_permissions() { + let cwd = temp_dir("agentos-native-sidecar-wasm-fs-permissions"); + write_fixture( + &cwd.join("guest.wasm"), + wasm_expect_path_open_errno_module("secret.txt", 0, 2, 2), + ); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + capability_permissions(&[ + ("fs", PermissionMode::Allow), + ("fs.read", PermissionMode::Deny), + ("child_process.spawn", PermissionMode::Allow), + ]), + ) + .expect("create vm"); + + sidecar + .vms + .get_mut(&vm_id) + .expect("wasm vm") + .kernel + .filesystem_mut() + .write_file("/secret.txt", b"should-not-read".to_vec()) + .expect("seed denied-read fixture"); + + let response = sidecar + .dispatch_blocking(request( + 6, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::Execute(crate::protocol::ExecuteRequest { + process_id: String::from("proc-wasm-fs-permission"), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(cwd.join("guest.wasm").to_string_lossy().into_owned()), + args: Vec::new(), + env: std::collections::HashMap::new(), + cwd: Some(String::from("/")), + wasm_permission_tier: None, + }), + )) + .expect("dispatch wasm execute"); + + match response.response.payload { + ResponsePayload::ProcessStarted(response) => { + assert_eq!(response.process_id, "proc-wasm-fs-permission"); + } + other => panic!("unexpected execute response: {other:?}"), + } + + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "proc-wasm-fs-permission"); + assert_eq!(exit_code, Some(0), "stdout: {stdout} stderr: {stderr}"); assert!(stdout.is_empty(), "unexpected stdout: {stdout}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); @@ -11614,7 +12184,7 @@ console.log(JSON.stringify({ status: "ok", summary })); let cwd = temp_dir("agentos-native-sidecar-wasm-fs-write-permissions"); write_fixture( &cwd.join("guest.wasm"), - wasm_expect_write_open_errno_module("created.txt", 2), + wasm_expect_path_open_errno_module("created.txt", 1, 64, 2), ); let mut sidecar = create_test_sidecar(); @@ -11832,7 +12402,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure command-path mounts"); - let vm = sidecar.vms.get(&vm_id).expect("configured vm"); + let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let path = vm .guest_env .get("PATH") @@ -11848,14 +12418,17 @@ console.log(JSON.stringify({ status: "ok", summary })); path_entries.contains(&"/__secure_exec/commands/0"), "PATH should include mounted command root: {path}" ); + let parent_env = vm.guest_env.clone(); + let parent_guest_cwd = vm.guest_cwd.clone(); + let parent_host_cwd = vm.host_cwd.clone(); for (command, request, expected_process_args) in [ ( "sh", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("sh"), args: vec![String::from("-c"), String::from("echo hello")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![ String::from("sh"), @@ -11865,28 +12438,28 @@ console.log(JSON.stringify({ status: "ok", summary })); ), ( "ls", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("ls"), args: vec![String::from("/")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![String::from("ls"), String::from("/")], ), ( "cat", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("cat"), args: vec![String::from("/tmp/file")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![String::from("cat"), String::from("/tmp/file")], ), ( "grep", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("grep"), args: vec![String::from("pattern"), String::from("/tmp/file")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![ String::from("grep"), @@ -11896,19 +12469,19 @@ console.log(JSON.stringify({ status: "ok", summary })); ), ( "echo", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("echo"), args: vec![String::from("hello")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![String::from("echo"), String::from("hello")], ), ( "sed", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("sed"), args: vec![String::from("s/a/b/"), String::from("/tmp/file")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, vec![ String::from("sed"), @@ -11917,13 +12490,15 @@ console.log(JSON.stringify({ status: "ok", summary })); ], ), ] { - let resolved = sidecar - .resolve_javascript_child_process_execution( + let resolved = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, &request, + false, + None, ) .unwrap_or_else(|error| panic!("failed to resolve {command}: {error}")); assert_eq!( @@ -11942,16 +12517,19 @@ console.log(JSON.stringify({ status: "ok", summary })); ); } - let missing = sidecar.resolve_javascript_child_process_execution( + let missing = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, - &crate::protocol::JavascriptChildProcessSpawnRequest { + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &agentos_execution::host::ProcessLaunchRequest { command: String::from("definitely-not-a-command"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, + false, + None, ); let error = missing.expect_err("missing command should fail"); assert!( @@ -11964,15 +12542,16 @@ console.log(JSON.stringify({ status: "ok", summary })); // execve resolves a literal relative/absolute pathname and must // not reuse spawnp's basename fallback. `/workspace/echo` does not // exist even though an `echo` command is installed on PATH. - let exact_missing = sidecar.resolve_javascript_child_process_execution_with_mode( + let exact_missing = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, &BTreeMap::new(), - &vm.guest_cwd, - &vm.host_cwd, - &crate::protocol::JavascriptChildProcessSpawnRequest { + &parent_guest_cwd, + &parent_host_cwd, + &agentos_execution::host::ProcessLaunchRequest { command: String::from("/workspace/echo"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, true, None, @@ -12045,11 +12624,17 @@ console.log(JSON.stringify({ status: "ok", summary })); String::from("STALE"), String::from("old"), )])); - process.host_write_dirty = true; - process.pending_self_signal_exit = Some(nix::libc::SIGTERM); process - .queue_pending_wasm_signal(nix::libc::SIGUSR1) - .expect("queue pending signal"); + .kernel_handle + .signal_action( + nix::libc::SIGUSR1, + Some(agentos_kernel::process_table::SignalAction { + disposition: agentos_kernel::process_table::SignalDisposition::User, + ..agentos_kernel::process_table::SignalAction::DEFAULT + }), + ) + .expect("install caught signal"); + process.kernel_handle.kill(nix::libc::SIGUSR1); process .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"before".to_vec())) .expect("queue pre-exec output"); @@ -12068,18 +12653,18 @@ console.log(JSON.stringify({ status: "ok", summary })); // Native commit must validate the interpreter chain rather than // reject the script merely because its own header is not WASM. sidecar - .exec_javascript_process_image( + .exec_process_image( &vm_id, "exec-process", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/script"), args: vec![ String::from("one optional argument"), String::from("/script"), String::from("script-argument"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::from("/interpreter.wasm")), env: BTreeMap::from([( String::from("SCRIPT_ONLY"), @@ -12108,14 +12693,14 @@ console.log(JSON.stringify({ status: "ok", summary })); let replacement_env = BTreeMap::from([(String::from("ONLY"), String::from("new"))]); sidecar - .exec_javascript_process_image( + .exec_process_image( &vm_id, "exec-process", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("replacement.wasm"), args: vec![String::from("argument")], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::new()), env: replacement_env.clone(), local_replacement: true, @@ -12139,12 +12724,11 @@ console.log(JSON.stringify({ status: "ok", summary })); "executor bootstrap env must not leak into guest envp: {:?}", process.env ); - assert!( - process.host_write_dirty, - "exec must preserve dirty VFS state" - ); - assert_eq!(process.pending_self_signal_exit, Some(nix::libc::SIGTERM)); - assert!(process.pending_wasm_signals.contains(&nix::libc::SIGUSR1)); + assert!(process + .kernel_handle + .sigpending() + .expect("pending signals") + .contains(nix::libc::SIGUSR1)); assert_eq!(process.pending_execution_events.len(), 1); assert!(matches!( process.pending_execution_events.front(), @@ -12188,13 +12772,13 @@ console.log(JSON.stringify({ status: "ok", summary })); &vm_id, "exec-process", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { // This path deliberately does not exist in the VFS. // The trusted runner owns and prevalidates the live FD // image, so commit must never reopen this display path. command: String::from("/proc/self/fd/1048576"), args: vec![String::from("fd-argument")], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::from("fd-custom-argv0")), executable_fd: Some(1_048_576), env: fd_replacement_env.clone(), @@ -12298,7 +12882,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, opened_fd) .expect("close extra inherited fd"); } - (kernel_handle, vm.cwd.join("work")) + (kernel_handle, vm.runtime_scratch_root.join("work")) }; let kernel_pid = kernel_handle.pid(); let mut process = active_process_for_tests( @@ -12336,14 +12920,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let replacement_env = BTreeMap::from([(String::from("ONLY"), String::from("new"))]); sidecar - .exec_javascript_process_image( + .exec_process_image( &vm_id, "exec-process", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("replacement.js"), args: vec![String::from("arg-one")], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::from("replacement-argv0")), env: replacement_env.clone(), ..Default::default() @@ -12369,8 +12953,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .all(|event| !matches!( event, ActiveExecutionEvent::Exited(91) - | ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) + | ActiveExecutionEvent::HostRpcRequest(_) | ActiveExecutionEvent::SignalState { .. } )), "old-image continuation events must not survive exec" @@ -12418,14 +13001,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); sidecar.fail_next_exec_start_after_commit = true; sidecar - .exec_javascript_process_image( + .exec_process_image( &vm_id, "exec-process", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("replacement.js"), args: vec![String::from("fatal-argument")], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::from("fatal-argv0")), env: BTreeMap::from([( String::from("FATAL_ONLY"), @@ -12500,7 +13083,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("mark exact script executable"); } let open_action = - |guest_fd, path: &str, oflag| crate::protocol::JavascriptPosixSpawnFileAction { + |guest_fd, path: &str, oflag| agentos_execution::host::ProcessSpawnFileAction { command: 3, guest_fd: Some(guest_fd), fd: guest_fd, @@ -12512,14 +13095,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); close_from_guest_fds: Vec::new(), }; - let error = spawn_javascript_child_process_for_test( + let error = spawn_child_process_for_test( &mut sidecar, &vm_id, "posix-spawn-parent", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/missing-executable"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { spawn_exact_path: true, spawn_file_actions: vec![open_action( 40, @@ -12543,14 +13126,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "Linux keeps successful O_CREAT actions when the later exec fails" ); - let error = spawn_javascript_child_process_for_test( + let error = spawn_child_process_for_test( &mut sidecar, &vm_id, "posix-spawn-parent", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/truncated-script"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { spawn_exact_path: true, spawn_file_actions: vec![open_action( 41, @@ -12575,207 +13158,34 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ); } - fn dirty_host_shadow_sync_precedes_top_level_and_nested_spawn_actions() { - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = - authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); - let vm_id = create_vm( - &mut sidecar, - &connection_id, - &session_id, - PermissionsPolicy::allow_all(), - ) - .expect("create vm"); - let top_host_cwd = temp_dir("agentos-native-sidecar-posix-spawn-shadow-top"); - insert_fake_javascript_parent_process( - &mut sidecar, - &vm_id, - &top_host_cwd, - "posix-spawn-shadow-parent", - ); - write_fixture( - &top_host_cwd.join("truncate-before-failed-exec"), - b"host-dirty", - ); - sidecar - .vms - .get_mut(&vm_id) - .expect("created vm") - .active_processes - .get_mut("posix-spawn-shadow-parent") - .expect("top-level parent") - .host_write_dirty = true; - - let open_action = - |guest_fd, path: &str, oflag| crate::protocol::JavascriptPosixSpawnFileAction { - command: 3, - guest_fd: Some(guest_fd), - fd: guest_fd, - source_fd: -1, - guest_source_fd: None, - oflag, - mode: 0o600, - path: path.to_owned(), - close_from_guest_fds: Vec::new(), - }; - let exact_request = - |command: &str, action| crate::protocol::JavascriptChildProcessSpawnRequest { - command: command.to_owned(), - args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { - spawn_exact_path: true, - spawn_file_actions: vec![action], - ..Default::default() - }, - }; - - let error = spawn_javascript_child_process_for_test( - &mut sidecar, - &vm_id, - "posix-spawn-shadow-parent", - exact_request( - "/missing-after-shadow-truncate", - open_action(50, "/truncate-before-failed-exec", 0x1000_0000 | (8 << 12)), - ), - ) - .expect_err("the later exact exec must fail"); - assert!(error.to_string().contains("ENOENT"), "{error}"); - assert_eq!( - sidecar - .vms - .get_mut(&vm_id) - .expect("created vm") - .kernel - .read_file("/truncate-before-failed-exec") - .expect("read file-action target after failed exec"), - b"", - "the pre-exec host sync must not run again after O_TRUNC" - ); - - let nested_host_cwd = temp_dir("agentos-native-sidecar-posix-spawn-shadow-nested"); - fs::create_dir(nested_host_cwd.join("host-only-directory")) - .expect("create host-only nested directory"); - let nested_module = nested_host_cwd.join("success.wasm"); - let nested_module_bytes = wat::parse_str(r#"(module (func (export "_start")))"#) - .expect("compile successful nested module"); - write_fixture(&nested_module, &nested_module_bytes); - let mut module_permissions = fs::metadata(&nested_module) - .expect("stat successful nested module") - .permissions(); - module_permissions.set_mode(0o755); - fs::set_permissions(&nested_module, module_permissions) - .expect("mark successful nested module executable"); - let staged_nested_module = sidecar - .vms - .get(&vm_id) - .expect("created vm") - .cwd - .join("success.wasm"); - write_fixture(&staged_nested_module, &nested_module_bytes); - let mut staged_module_permissions = fs::metadata(&staged_nested_module) - .expect("stat staged successful nested module") - .permissions(); - staged_module_permissions.set_mode(0o755); - fs::set_permissions(&staged_nested_module, staged_module_permissions) - .expect("mark staged successful nested module executable"); - - let (nested_handle, nested_env) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); - let root_pid = vm - .active_processes - .get("posix-spawn-shadow-parent") - .expect("root parent") - .kernel_pid; - let handle = vm - .kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("nested-shadow-parent")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - parent_pid: Some(root_pid), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn nested kernel parent"); - (handle, vm.guest_env.clone()) - }; - let nested_pid = nested_handle.pid(); - let mut nested_parent = active_process_for_tests( - nested_pid, - nested_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Binding(BindingExecution::default()), - ) - .with_guest_cwd(String::from("/")) - .with_env(nested_env) - .with_host_cwd(nested_host_cwd); - nested_parent.host_write_dirty = true; - sidecar - .vms - .get_mut(&vm_id) - .expect("created vm") - .active_processes - .get_mut("posix-spawn-shadow-parent") - .expect("root parent") - .child_processes - .insert(String::from("nested-shadow-parent"), nested_parent); - - spawn_descendant_javascript_child_process_for_test( - &mut sidecar, - &vm_id, - "posix-spawn-shadow-parent", - &["nested-shadow-parent"], - exact_request( - "/success.wasm", - open_action( - 51, - "/host-only-directory/created-before-successful-exec", - 0x1000_0000 | (1 << 12) | (4 << 12), - ), - ), - ) - .expect("nested exact spawn succeeds after syncing its dirty host shadow"); - assert!( - sidecar - .vms - .get(&vm_id) - .expect("created vm") - .kernel - .exists("/host-only-directory/created-before-successful-exec") - .expect("query nested O_CREAT side effect"), - "the O_CREAT side effect must survive successful exec" - ); - } - - fn write_posix_spawnp_fixture( - sidecar: &mut NativeSidecar, - vm_id: &str, - guest_path: &str, - contents: impl AsRef<[u8]>, - mode: u32, - ) { - let contents = contents.as_ref(); - let host_path = { - let vm = sidecar.vms.get_mut(vm_id).expect("created vm"); - let parent = Path::new(guest_path) - .parent() - .and_then(Path::to_str) - .expect("fixture parent path"); - if parent != "/" { - vm.kernel - .mkdir(parent, true) - .expect("create fixture guest parent"); - } - vm.kernel - .write_file(guest_path, contents.to_vec()) - .expect("write guest fixture"); - vm.kernel - .chmod(guest_path, mode) - .expect("chmod guest fixture"); - vm.cwd.join(guest_path.trim_start_matches('/')) - }; + fn write_posix_spawnp_fixture( + sidecar: &mut NativeSidecar, + vm_id: &str, + guest_path: &str, + contents: impl AsRef<[u8]>, + mode: u32, + ) { + let contents = contents.as_ref(); + let host_path = { + let vm = sidecar.vms.get_mut(vm_id).expect("created vm"); + let parent = Path::new(guest_path) + .parent() + .and_then(Path::to_str) + .expect("fixture parent path"); + if parent != "/" { + vm.kernel + .mkdir(parent, true) + .expect("create fixture guest parent"); + } + vm.kernel + .write_file(guest_path, contents.to_vec()) + .expect("write guest fixture"); + vm.kernel + .chmod(guest_path, mode) + .expect("chmod guest fixture"); + vm.runtime_scratch_root + .join(guest_path.trim_start_matches('/')) + }; fs::create_dir_all(host_path.parent().expect("fixture host parent")) .expect("create fixture host parent"); @@ -12808,11 +13218,15 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .symlink(target, link_path) .expect("create guest symlink fixture"); let host_target = if target.starts_with('/') { - vm.cwd.join(target.trim_start_matches('/')) + vm.runtime_scratch_root.join(target.trim_start_matches('/')) } else { PathBuf::from(target) }; - (vm.cwd.join(link_path.trim_start_matches('/')), host_target) + ( + vm.runtime_scratch_root + .join(link_path.trim_start_matches('/')), + host_target, + ) }; fs::create_dir_all(host_path.parent().expect("symlink fixture host parent")) @@ -12821,15 +13235,54 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("create host symlink fixture"); } + fn guest_exact_wasm_exec_rejects_non_executable_kernel_image_with_eacces() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let cwd = temp_dir("agentos-native-sidecar-guest-exec-dac"); + insert_fake_javascript_parent_process( + &mut sidecar, + &vm_id, + &cwd, + "guest-exec-dac-parent", + ); + let wasm = wat::parse_str("(module (func (export \"_start\")))") + .expect("compile guest exec fixture"); + write_posix_spawnp_fixture(&mut sidecar, &vm_id, "/non-executable.wasm", wasm, 0o644); + + let error = spawn_child_process_for_test( + &mut sidecar, + &vm_id, + "guest-exec-dac-parent", + agentos_execution::host::ProcessLaunchRequest { + command: String::from("/non-executable.wasm"), + args: Vec::new(), + options: agentos_execution::host::ProcessLaunchOptions { + spawn_exact_path: true, + ..Default::default() + }, + }, + ) + .expect_err("guest exact exec must enforce kernel execute DAC"); + assert_eq!(error.code(), Some("EACCES"), "unexpected error: {error}"); + } + fn posix_spawnp_request( command: &str, search_path: &str, args: &[&str], - ) -> crate::protocol::JavascriptChildProcessSpawnRequest { - crate::protocol::JavascriptChildProcessSpawnRequest { + ) -> agentos_execution::host::ProcessLaunchRequest { + agentos_execution::host::ProcessLaunchRequest { command: command.to_owned(), args: args.iter().map(|arg| (*arg).to_owned()).collect(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { argv0: Some(String::from("caller-argv0")), spawn_search_path: Some(search_path.to_owned()), ..Default::default() @@ -12841,10 +13294,10 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); sidecar: &mut NativeSidecar, vm_id: &str, nested: bool, - request: crate::protocol::JavascriptChildProcessSpawnRequest, + request: agentos_execution::host::ProcessLaunchRequest, ) -> Result { if nested { - spawn_descendant_javascript_child_process_for_test( + spawn_descendant_process_for_test( sidecar, vm_id, "posix-spawnp-parent", @@ -12852,12 +13305,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); request, ) } else { - spawn_javascript_child_process_for_test( - sidecar, - vm_id, - "posix-spawnp-parent", - request, - ) + spawn_child_process_for_test(sidecar, vm_id, "posix-spawnp-parent", request) } } @@ -12872,7 +13320,12 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); PermissionsPolicy::allow_all(), ) .expect("create vm"); - let host_cwd = sidecar.vms.get(&vm_id).expect("created vm").cwd.clone(); + let host_cwd = sidecar + .vms + .get(&vm_id) + .expect("created vm") + .runtime_scratch_root + .clone(); insert_fake_javascript_parent_process( &mut sidecar, &vm_id, @@ -12969,17 +13422,12 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .vms .get_mut(&vm_id) .expect("created vm") - .command_guest_paths - .insert( - String::from("registered-only"), - String::from("/registered-only"), - ); - sidecar - .vms - .get_mut(&vm_id) - .expect("created vm") - .command_guest_paths - .insert(String::from("node"), String::from("/interpreter.wasm")); + .kernel + .register_driver(CommandDriver::new( + "registered-only-test", + ["registered-only"], + )) + .expect("register command outside the explicit search path"); for nested in [false, true] { let scope = if nested { "nested" } else { "top-level" }; @@ -13181,8 +13629,11 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("compile successful WASM fixture"); { let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); - write_fixture(&vm.cwd.join("malformed.wasm"), malformed_wasm); - let successful_host_path = vm.cwd.join("success.wasm"); + write_fixture( + &vm.runtime_scratch_root.join("malformed.wasm"), + malformed_wasm, + ); + let successful_host_path = vm.runtime_scratch_root.join("success.wasm"); write_fixture(&successful_host_path, &successful_wasm); let mut successful_permissions = fs::metadata(&successful_host_path) .expect("stat successful WASM fixture") @@ -13203,10 +13654,10 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .chmod("/success.wasm", 0o755) .expect("mark successful WASM executable"); } - let malformed_request = || crate::protocol::JavascriptChildProcessSpawnRequest { + let malformed_request = || agentos_execution::host::ProcessLaunchRequest { command: String::from("/malformed.wasm"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { spawn_exact_path: true, ..Default::default() }, @@ -13226,7 +13677,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); sidecar.python_engine.javascript_context_count_for_test(), ); for iteration in 0..8 { - if spawn_javascript_child_process_for_test( + if spawn_child_process_for_test( &mut sidecar, &vm_id, "malformed-wasm-parent", @@ -13283,7 +13734,11 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); }, ) .expect("spawn nested kernel parent"); - (handle, vm.guest_env.clone(), vm.cwd.clone()) + ( + handle, + vm.guest_env.clone(), + vm.runtime_scratch_root.clone(), + ) }; let nested_pid = nested_handle.pid(); sidecar @@ -13314,7 +13769,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .kernel .resource_snapshot(); for iteration in 0..8 { - if spawn_descendant_javascript_child_process_for_test( + if spawn_descendant_process_for_test( &mut sidecar, &vm_id, "malformed-wasm-parent", @@ -13355,16 +13810,16 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ); } - let successful_request = || crate::protocol::JavascriptChildProcessSpawnRequest { + let successful_request = || agentos_execution::host::ProcessLaunchRequest { command: String::from("/success.wasm"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { spawn_exact_path: true, ..Default::default() }, }; for iteration in 0..4 { - let spawned = spawn_javascript_child_process_for_test( + let spawned = spawn_child_process_for_test( &mut sidecar, &vm_id, "malformed-wasm-parent", @@ -13391,7 +13846,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let mut reaped = false; for _ in 0..64 { - let event = poll_javascript_child_process_for_test( + let event = poll_child_process_for_test( &mut sidecar, &vm_id, "malformed-wasm-parent", @@ -13437,27 +13892,32 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ) .expect("create vm"); - let vm = sidecar.vms.get(&vm_id).expect("created vm"); + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); assert!( - !vm.command_guest_paths.contains_key("sh"), + !vm.kernel.commands().contains_key("sh"), "test VM must not provide a guest sh command" ); + let parent_env = vm.guest_env.clone(); + let parent_guest_cwd = vm.guest_cwd.clone(); + let parent_host_cwd = vm.host_cwd.clone(); - let request = crate::protocol::JavascriptChildProcessSpawnRequest { + let request = agentos_execution::host::ProcessLaunchRequest { command: String::from("printf hi > out.txt"), args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { shell: true, ..Default::default() }, }; - let error = sidecar - .resolve_javascript_child_process_execution( + let error = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, &request, + false, + None, ) .expect_err("shell-mode command without guest sh must fail instead of tokenizing"); assert!( @@ -13493,11 +13953,11 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); write_fixture(&cwd.join("entry.mjs"), "setInterval(() => {}, 1000);"); start_fake_javascript_process(&mut sidecar, &vm_id, &cwd, "proc-js-binding-child"); - let spawned = spawn_javascript_child_process_for_test( + let spawned = spawn_child_process_for_test( &mut sidecar, &vm_id, "proc-js-binding-child", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![ String::from("add"), @@ -13506,7 +13966,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); String::from("--b"), String::from("3"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, ) .expect("spawn binding collection child process"); @@ -13544,14 +14004,17 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); )) .expect("register math binding collection"); - let vm = sidecar.vms.get(&vm_id).expect("configured vm"); - let resolved = sidecar - .resolve_javascript_child_process_execution( + let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let parent_env = vm.guest_env.clone(); + let parent_guest_cwd = vm.guest_cwd.clone(); + let parent_host_cwd = vm.host_cwd.clone(); + let resolved = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, - &crate::protocol::JavascriptChildProcessSpawnRequest { + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![ String::from("add"), @@ -13560,8 +14023,10 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); String::from("--b"), String::from("3"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, + false, + None, ) .expect("resolve binding collection child process"); @@ -13610,11 +14075,11 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); write_fixture(&cwd.join("entry.mjs"), "setInterval(() => {}, 1000);"); start_fake_javascript_process(&mut sidecar, &vm_id, &cwd, "proc-js-binding-rpc"); - let spawned = spawn_javascript_child_process_for_test( + let spawned = spawn_child_process_for_test( &mut sidecar, &vm_id, "proc-js-binding-rpc", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/__secure_exec/commands/0/agentos-math"), args: vec![ String::from("add"), @@ -13623,7 +14088,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); String::from("--b"), String::from("3"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, ) .expect("spawn binding collection child process over internal command path"); @@ -13661,14 +14126,17 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); )) .expect("register math binding collection"); - let vm = sidecar.vms.get(&vm_id).expect("configured vm"); - let resolved = sidecar - .resolve_javascript_child_process_execution( + let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let parent_env = vm.guest_env.clone(); + let parent_guest_cwd = vm.guest_cwd.clone(); + let parent_host_cwd = vm.host_cwd.clone(); + let resolved = NativeSidecar:::: + resolve_javascript_child_process_execution_with_mode( vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, - &crate::protocol::JavascriptChildProcessSpawnRequest { + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &agentos_execution::host::ProcessLaunchRequest { command: String::from("/__secure_exec/commands/0/agentos-math"), args: vec![ String::from("add"), @@ -13677,8 +14145,10 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); String::from("--b"), String::from("3"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, + false, + None, ) .expect("resolve binding collection child process"); @@ -13775,13 +14245,13 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("register binding collection"); } - let (bindings_before, command_paths_before) = { + let (bindings_before, commands_before) = { let vm = sidecar.vms.get(&vm_id).expect("configured vm"); assert_eq!( vm.bindings.len(), crate::bindings::MAX_REGISTERED_BINDING_COLLECTIONS ); - (vm.bindings.clone(), vm.command_guest_paths.clone()) + (vm.bindings.clone(), vm.kernel.commands()) }; let overflow_response = sidecar @@ -13798,10 +14268,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); match overflow_response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "invalid_state"); - assert!( - rejected.message.contains("registered binding collections"), - "unexpected rejection: {rejected:?}" + assert_eq!(rejected.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + rejected.limit_name.as_deref(), + Some("limits.bindings.maxRegisteredCollections") + ); + assert_eq!( + rejected.configured_limit, + Some(crate::bindings::MAX_REGISTERED_BINDING_COLLECTIONS as u64) ); } other => panic!("expected rejected response, got {other:?}"), @@ -13809,9 +14283,9 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let vm = sidecar.vms.get(&vm_id).expect("configured vm"); assert_eq!(vm.bindings, bindings_before); - assert_eq!(vm.command_guest_paths, command_paths_before); + assert_eq!(vm.kernel.commands(), commands_before); assert!( - !vm.command_guest_paths.contains_key("agentos-overflow"), + !vm.kernel.commands().contains_key("agentos-overflow"), "overflow command path should not be registered" ); } @@ -13862,7 +14336,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("register binding collection"); } - let (bindings_before, command_paths_before) = { + let (bindings_before, commands_before) = { let vm = sidecar.vms.get(&vm_id).expect("configured vm"); assert_eq!(vm.bindings.len(), 4); assert_eq!( @@ -13872,7 +14346,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .sum::(), crate::bindings::MAX_REGISTERED_BINDINGS_PER_VM ); - (vm.bindings.clone(), vm.command_guest_paths.clone()) + (vm.bindings.clone(), vm.kernel.commands()) }; let overflow_response = sidecar @@ -13889,10 +14363,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); match overflow_response.response.payload { ResponsePayload::Rejected(rejected) => { - assert_eq!(rejected.code, "invalid_state"); - assert!( - rejected.message.contains("registered host callbacks"), - "unexpected rejection: {rejected:?}" + assert_eq!(rejected.code, "ERR_AGENTOS_RESOURCE_LIMIT"); + assert_eq!( + rejected.limit_name.as_deref(), + Some("limits.bindings.maxRegisteredBindingsPerVm") + ); + assert_eq!( + rejected.configured_limit, + Some(crate::bindings::MAX_REGISTERED_BINDINGS_PER_VM as u64) ); } other => panic!("expected rejected response, got {other:?}"), @@ -13900,9 +14378,9 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let vm = sidecar.vms.get(&vm_id).expect("configured vm"); assert_eq!(vm.bindings, bindings_before); - assert_eq!(vm.command_guest_paths, command_paths_before); + assert_eq!(vm.kernel.commands(), commands_before); assert!( - !vm.command_guest_paths.contains_key("agentos-overflow"), + !vm.kernel.commands().contains_key("agentos-overflow"), "overflow command path should not be registered" ); } @@ -13947,14 +14425,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "proc-js-binding-denied", ); - let result = spawn_javascript_child_process_sync_for_test( + let result = spawn_child_process_sync_for_test( &mut sidecar, &vm_id, "proc-js-binding-denied", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![String::from("add")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, None, ) @@ -14038,14 +14516,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "proc-js-binding-allowed", ); - let result = spawn_javascript_child_process_sync_for_test( + let result = spawn_child_process_sync_for_test( &mut sidecar, &vm_id, "proc-js-binding-allowed", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![String::from("add")], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, None, ) @@ -14143,18 +14621,18 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "proc-js-binding-invalid-json-file", ); - let result = spawn_javascript_child_process_sync_for_test( + let result = spawn_child_process_sync_for_test( &mut sidecar, &vm_id, "proc-js-binding-invalid-json-file", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![ String::from("add"), String::from("--json-file"), String::from("/workspace/invalid-binding-input.json"), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, None, ) @@ -14251,18 +14729,18 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "proc-js-binding-valid-json", ); - let result = spawn_javascript_child_process_sync_for_test( + let result = spawn_child_process_sync_for_test( &mut sidecar, &vm_id, "proc-js-binding-valid-json", - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("/usr/local/bin/agentos-math"), args: vec![ String::from("add"), String::from("--json"), String::from(r#"{"count":2,"label":"ok"}"#), ], - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + options: agentos_execution::host::ProcessLaunchOptions::default(), }, None, ) @@ -14724,7 +15202,7 @@ if (child.status !== 0) { other => panic!("unexpected execute response: {other:?}"), } } - fn python_vfs_rpc_requests_proxy_into_the_vm_kernel_filesystem() { + fn common_host_filesystem_operations_use_the_vm_kernel_source_of_truth() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -14833,86 +15311,43 @@ export async function loadPyodide() { .expect("handle python bootstrap event"); } - allow_synthetic_python_vfs_reply_drop( - block_on_sidecar!( - sidecar, - sidecar.handle_python_vfs_rpc_request( - &vm_id, - "proc-python-vfs", - PythonVfsRpcRequest { - id: 1, - method: PythonVfsRpcMethod::Mkdir, - path: String::from("/workspace"), - destination: None, - target: None, - mode: None, - uid: None, - gid: None, - atime_ms: None, - mtime_ms: None, - content_base64: None, - recursive: false, - url: None, - http_method: None, - headers: BTreeMap::new(), - body_base64: None, - hostname: None, - family: None, - port: None, - socket_id: None, - command: None, - args: Vec::new(), - argv0: None, - cwd: None, - env: BTreeMap::new(), - shell: false, - max_buffer: None, - timeout_ms: None, - }, - ) + dispatch_test_host_operation( + &mut sidecar, + &vm_id, + "proc-python-vfs", + 1, + agentos_execution::host::HostOperation::Filesystem( + agentos_execution::host::FilesystemOperation::CreateDirectoryAt { + dir_fd: u32::MAX, + path: bounded_test_host_path("/workspace"), + mode: 0o777, + }, ), - "handle python mkdir rpc", - ); - allow_synthetic_python_vfs_reply_drop( - block_on_sidecar!( - sidecar, - sidecar.handle_python_vfs_rpc_request( - &vm_id, - "proc-python-vfs", - PythonVfsRpcRequest { - id: 2, - method: PythonVfsRpcMethod::Write, - path: String::from("/workspace/note.txt"), - destination: None, - target: None, - mode: None, - uid: None, - gid: None, - atime_ms: None, - mtime_ms: None, - content_base64: Some(String::from("aGVsbG8gZnJvbSBzaWRlY2FyIHJwYw==")), - recursive: false, - url: None, - http_method: None, - headers: BTreeMap::new(), - body_base64: None, - hostname: None, - family: None, - port: None, - socket_id: None, - command: None, - args: Vec::new(), - argv0: None, - cwd: None, - env: BTreeMap::new(), - shell: false, - max_buffer: None, - timeout_ms: None, - }, - ) + ) + .expect("dispatch common mkdir operation"); + dispatch_test_host_operation( + &mut sidecar, + &vm_id, + "proc-python-vfs", + 2, + agentos_execution::host::HostOperation::Filesystem( + agentos_execution::host::FilesystemOperation::WriteFileAt { + dir_fd: u32::MAX, + path: bounded_test_host_path("/workspace/note.txt"), + bytes: agentos_execution::host::BoundedBytes::try_new( + b"hello from shared host operation".to_vec(), + &agentos_execution::backend::PayloadLimit::new( + "test.maxWriteBytes", + 1024, + ) + .expect("test write limit"), + ) + .expect("bounded test write"), + mode: None, + }, ), - "handle python write rpc", - ); + ) + .expect("dispatch common write operation"); let content = { let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); @@ -14923,7 +15358,7 @@ export async function loadPyodide() { ) .expect("utf8 file contents") }; - assert_eq!(content, "hello from sidecar rpc"); + assert_eq!(content, "hello from shared host operation"); let process = { let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); @@ -14968,61 +15403,16 @@ await new Promise(() => {}); "#, ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( - String::from("AGENTOS_NODE_SYNC_RPC_ENABLE"), - String::from("1"), - )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-sync"), - active_process_for_tests( - kernel_handle.pid(), - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-sync", + BTreeMap::from([( + String::from("AGENTOS_NODE_SYNC_RPC_ENABLE"), + String::from("1"), + )]), + ); let mut saw_stdout = false; for _ in 0..16 { @@ -15036,7 +15426,6 @@ await new Promise(() => {}); .expect("poll javascript sync rpc event") .expect("javascript sync rpc event") }; - if let ActiveExecutionEvent::Stdout(chunk) = &event { let stdout = String::from_utf8(chunk.clone()).expect("stdout utf8"); if stdout.contains("\"contents\":\"hello from sidecar rpc\"") @@ -15237,30 +15626,6 @@ await new Promise(() => {}); ); } - fn python_vfs_rpc_paths_resolve_textually_and_defer_to_kernel_confinement() { - // Root is `/`: any absolute guest path is addressable and textual - // `.`/`..` segments are resolved here; confinement is enforced at the - // kernel/mount layer (openat2 RESOLVE_BENEATH), not by a prefix check. - assert_eq!( - crate::filesystem::normalize_python_vfs_rpc_path("/workspace/./note.txt") - .expect("normalize workspace path"), - String::from("/workspace/note.txt") - ); - assert_eq!( - crate::filesystem::normalize_python_vfs_rpc_path("/workspace/../etc/passwd") - .expect("normalize resolves .. textually"), - String::from("/etc/passwd") - ); - assert_eq!( - crate::filesystem::normalize_python_vfs_rpc_path("/etc/passwd") - .expect("absolute guest paths are addressable"), - String::from("/etc/passwd") - ); - assert!( - crate::filesystem::normalize_python_vfs_rpc_path("workspace/note.txt").is_err(), - "relative paths must be rejected", - ); - } fn javascript_fs_sync_rpc_resolves_proc_self_against_the_kernel_process() { let mut config = KernelVmConfig::new("vm-js-procfs-rpc"); config.permissions = Permissions::allow_all(); @@ -15294,7 +15659,7 @@ await new Promise(() => {}); &mut kernel, &mut process, kernel_pid, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("fs.readlinkSync"), @@ -15308,7 +15673,7 @@ await new Promise(() => {}); &mut kernel, &mut process, kernel_pid, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("fs.readdirSync"), @@ -15473,63 +15838,18 @@ console.log( "#, ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-fd", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"child_process\",\"console\",\"crypto\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-fd"), - active_process_for_tests( - kernel_handle.pid(), - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } + ); let mut stdout = Vec::new(); let mut stderr = Vec::new(); @@ -15561,10 +15881,11 @@ console.log( ActiveExecutionEvent::Exited(code) => { exit_code = Some(*code); } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) + ActiveExecutionEvent::Common(_) + | ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::HostCallCompletion(_) + | ActiveExecutionEvent::ManagedStreamReadRecheck(_) + | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } => {} } @@ -15636,8 +15957,7 @@ console.log( } } - #[test] - fn javascript_mapped_tmp_open_wx_uses_exclusive_create_once() { + fn javascript_kernel_tmp_open_wx_uses_exclusive_create_once() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -15651,7 +15971,6 @@ console.log( ) .expect("create vm"); let cwd = temp_dir("agentos-native-sidecar-js-open-wx-cwd"); - let mapped_tmp = temp_dir("agentos-native-sidecar-js-open-wx-mapped-tmp"); write_fixture( &cwd.join("entry.mjs"), r#" @@ -15690,35 +16009,15 @@ console.log( "#, ); - let mapped_tmp_json = serde_json::to_string(&vec![mapped_tmp.display().to_string()]) - .expect("serialize mapped tmp access roots"); let (stdout, stderr, exit_code) = run_javascript_entry_with_env( &mut sidecar, &vm_id, &cwd, "proc-js-open-wx", - BTreeMap::from([ - ( - String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), - String::from("[\"buffer\",\"console\",\"fs\",\"os\",\"path\"]"), - ), - ( - String::from("AGENTOS_GUEST_PATH_MAPPINGS"), - serde_json::to_string(&vec![json!({ - "guestPath": "/tmp", - "hostPath": mapped_tmp.display().to_string(), - })]) - .expect("serialize mapped tmp path"), - ), - ( - String::from("AGENTOS_EXTRA_FS_READ_PATHS"), - mapped_tmp_json.clone(), - ), - ( - String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"), - mapped_tmp_json, - ), - ]), + BTreeMap::from([( + String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), + String::from("[\"buffer\",\"console\",\"fs\",\"os\",\"path\"]"), + )]), ); assert_eq!(exit_code, Some(0), "stdout: {stdout}\nstderr: {stderr}"); @@ -15732,11 +16031,14 @@ console.log( stdout.contains("\"mtimeMs\":1704164645000"), "stdout: {stdout}" ); - assert_eq!( - fs::read_to_string(mapped_tmp.join("exclusive-mapped.lock")) - .expect("read mapped host lock file"), - "lock" - ); + let kernel_contents = sidecar + .vms + .get_mut(&vm_id) + .expect("javascript vm") + .kernel + .read_file("/tmp/exclusive-mapped.lock") + .expect("read kernel tmp lock file"); + assert_eq!(kernel_contents, b"lock"); } fn with_wasm_shell_redirect_vm( @@ -15959,7 +16261,7 @@ process.stdout.write(`${JSON.stringify({ ); } - fn javascript_mapped_shadow_readdir_sees_wasm_created_directory() { + fn javascript_kernel_readdir_sees_wasm_created_directory() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -16027,7 +16329,7 @@ process.stdout.write(`${JSON.stringify({ entries, isDirectory })}\n`); assert_eq!(payload["isDirectory"], json!(true), "stdout: {stdout}"); } - fn javascript_mapped_shadow_readdir_merges_wasm_created_children() { + fn javascript_kernel_readdir_sees_wasm_created_children() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -16083,7 +16385,7 @@ process.stdout.write(`${JSON.stringify({ entries, text })}\n`); assert_eq!(payload["text"], json!("hi\n"), "stdout: {stdout}"); } - fn javascript_mapped_shadow_readdir_unions_shadow_and_kernel_children() { + fn javascript_readdir_observes_authoritative_kernel_children() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -16149,7 +16451,7 @@ process.stdout.write(`${JSON.stringify({ entries })}\n`); // stay deleted in the SAME process's merged readdir view and for later // processes — the mapped unlink now mirrors the removal into the kernel, // otherwise the readdir kernel-merge would resurrect it. - fn javascript_mapped_unlink_of_kernel_backed_file_does_not_resurrect() { + fn javascript_unlink_of_kernel_file_is_immediately_authoritative() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -16228,7 +16530,7 @@ process.stdout.write(`${JSON.stringify({ entries })}\n`); assert_eq!(payload["entries"], json!([]), "stdout: {stdout}"); } - fn javascript_mapped_shadow_readdir_sees_same_process_shadow_directory() { + fn javascript_kernel_readdir_sees_same_process_directory() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -16249,7 +16551,7 @@ process.stdout.write(`${JSON.stringify({ entries })}\n`); &connection_id, &session_id, &mut next_request_id, - "proc-js-readdir-own-shadow-dir", + "proc-js-readdir-own-kernel-dir", r#" const fs = require("node:fs"); const dir = "/tmp/fuzz-perf-readdir-32"; @@ -16682,73 +16984,34 @@ await new Promise(() => {}); "#, ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-promises", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"child_process\",\"crypto\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - // ActiveProcess::new defaults host_cwd to "/", which would - // identity-map the whole host filesystem for this process; - // real execute paths always set it, so mirror that here. - let mut process = active_process_for_tests( - kernel_handle.pid(), - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ); - process.host_cwd = cwd.clone(); - vm.active_processes - .insert(String::from("proc-js-promises"), process); - } + ); let mut saw_write_batch = false; let mut saw_read_batch = false; let mut saw_stdout = false; let mut held_exit = None; let mut pending_requests = Vec::new(); - - for _ in 0..40 { + let mut observed_stdout = Vec::new(); + let mut observed_rpc_methods = BTreeMap::::new(); + let mut observed_other_events = BTreeMap::::new(); + + // Common host-service events (preopens, limits, clocks, and + // signals) now share this queue with the compatibility RPCs under + // test. Keep a bounded but sufficiently large budget so those + // runtime-neutral setup calls cannot consume the old 40-event + // allowance before both ten-request batches arrive. + for _ in 0..512 { let event = { let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let process = vm @@ -16765,14 +17028,17 @@ await new Promise(() => {}); }; match event { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::HostRpcRequest(request) => { + *observed_rpc_methods + .entry(request.method.clone()) + .or_default() += 1; if !request.method.starts_with("fs.promises.") { block_on_sidecar!( sidecar, sidecar.handle_execution_event( &vm_id, "proc-js-promises", - ActiveExecutionEvent::JavascriptSyncRpcRequest(request), + ActiveExecutionEvent::HostRpcRequest(request), ) ) .expect("handle javascript promises setup rpc event"); @@ -16807,7 +17073,7 @@ await new Promise(() => {}); sidecar.handle_execution_event( &vm_id, "proc-js-promises", - ActiveExecutionEvent::JavascriptSyncRpcRequest(request), + ActiveExecutionEvent::HostRpcRequest(request), ) ) .expect("handle batched javascript promises rpc event"); @@ -16822,6 +17088,7 @@ await new Promise(() => {}); } ActiveExecutionEvent::Stdout(chunk) => { let stdout = String::from_utf8(chunk).expect("stdout utf8"); + observed_stdout.push(stdout.clone()); if stdout.contains(r#"["value-0","value-1","value-2","value-3","value-4","value-5","value-6","value-7","value-8","value-9"]"#) { saw_stdout = true; break; @@ -16834,6 +17101,34 @@ await new Promise(() => {}); held_exit = Some(code); } other => { + let label = match &other { + ActiveExecutionEvent::Common( + agentos_execution::backend::ExecutionEvent::HostCall { + operation, + .. + }, + ) => format!("host_call:{operation:?}"), + ActiveExecutionEvent::Common(event) => format!("common:{event:?}"), + ActiveExecutionEvent::Stderr(_) => String::from("stderr"), + ActiveExecutionEvent::HostCallCompletion(_) => { + String::from("host_call_completion") + } + ActiveExecutionEvent::ManagedStreamReadRecheck(_) => { + String::from("managed_stream_recheck") + } + ActiveExecutionEvent::ManagedUdpPollRecheck(_) => { + String::from("managed_udp_recheck") + } + ActiveExecutionEvent::SignalState { .. } => { + String::from("signal_state") + } + ActiveExecutionEvent::HostRpcRequest(_) + | ActiveExecutionEvent::Stdout(_) + | ActiveExecutionEvent::Exited(_) => { + unreachable!("handled by an earlier match arm") + } + }; + *observed_other_events.entry(label).or_default() += 1; let _ = block_on_sidecar!( sidecar, sidecar.handle_execution_event(&vm_id, "proc-js-promises", other) @@ -16868,7 +17163,11 @@ await new Promise(() => {}); ); assert!( saw_read_batch, - "expected Promise.all(readFile) to issue a full batch before the first response" + "expected Promise.all(readFile) to issue a full batch before the first response; pending methods={:?}, rpc_methods={observed_rpc_methods:?}, exit={held_exit:?}, stdout={observed_stdout:?}, other_events={observed_other_events:?}", + pending_requests + .iter() + .map(|request| request.method.as_str()) + .collect::>() ); assert!( saw_stdout || held_exit == Some(0), @@ -17038,7 +17337,7 @@ await new Promise(() => {}); ] { let response = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id, method: String::from("crypto.hashDigest"), args: vec![json!(algorithm), base64_arg(&fixture.message)], @@ -17056,7 +17355,7 @@ await new Promise(() => {}); ] { let response = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id, method: String::from("crypto.hmacDigest"), args: vec![ @@ -17078,7 +17377,7 @@ await new Promise(() => {}); ] { let response = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id, method: String::from("crypto.pbkdf2"), args: vec![ @@ -17104,7 +17403,7 @@ await new Promise(() => {}); .to_string(); let scrypt = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id, method: String::from("crypto.scrypt"), args: vec![ @@ -17121,7 +17420,7 @@ await new Promise(() => {}); let cipher = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 1, method: String::from("crypto.cipheriv"), args: vec![ @@ -17146,7 +17445,7 @@ await new Promise(() => {}); let decipher = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 2, method: String::from("crypto.decipheriv"), args: vec![ @@ -17172,7 +17471,7 @@ await new Promise(() => {}); .to_string(); let gcm_cipher = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 3, method: String::from("crypto.cipheriv"), args: vec![ @@ -17208,7 +17507,7 @@ await new Promise(() => {}); .to_string(); let gcm_decipher = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 4, method: String::from("crypto.decipheriv"), args: vec![ @@ -17232,7 +17531,7 @@ await new Promise(() => {}); let subtle_imported_key = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 5, method: String::from("crypto.subtle"), args: vec![json!(serde_json::to_string(&json!({ @@ -17259,7 +17558,7 @@ await new Promise(() => {}); let subtle_encrypted = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 6, method: String::from("crypto.subtle"), args: vec![json!(serde_json::to_string(&json!({ @@ -17284,7 +17583,7 @@ await new Promise(() => {}); let subtle_decrypted = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 7, method: String::from("crypto.subtle"), args: vec![json!(serde_json::to_string(&json!({ @@ -17311,7 +17610,7 @@ await new Promise(() => {}); let generated_prime = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 8, method: String::from("crypto.generatePrimeSync"), args: vec![ @@ -17332,7 +17631,7 @@ await new Promise(() => {}); let generated_safe_prime = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 9, method: String::from("crypto.generatePrimeSync"), args: vec![ @@ -17353,7 +17652,7 @@ await new Promise(() => {}); let generated_prime_buffer = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: next_id + 10, method: String::from("crypto.generatePrimeSync"), args: vec![ @@ -17402,7 +17701,7 @@ await new Promise(() => {}); let sha256 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("crypto.hashDigest"), @@ -17417,7 +17716,7 @@ await new Promise(() => {}); let sha512 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("crypto.hashDigest"), @@ -17434,7 +17733,7 @@ await new Promise(() => {}); let sha1 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("crypto.hashDigest"), @@ -17449,7 +17748,7 @@ await new Promise(() => {}); let sha224 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: 8, method: String::from("crypto.hashDigest"), args: vec![json!("sha224"), json!("YWdlbnQtb3M=")], @@ -17464,7 +17763,7 @@ await new Promise(() => {}); let sha384 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: 9, method: String::from("crypto.hashDigest"), args: vec![json!("sha384"), json!("YWdlbnQtb3M=")], @@ -17481,7 +17780,7 @@ await new Promise(() => {}); let md5 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("crypto.hashDigest"), @@ -17496,7 +17795,7 @@ await new Promise(() => {}); let hmac = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("crypto.hmacDigest"), @@ -17515,7 +17814,7 @@ await new Promise(() => {}); let hmac_sha384 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: 10, method: String::from("crypto.hmacDigest"), args: vec![ @@ -17534,7 +17833,7 @@ await new Promise(() => {}); let pbkdf2 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("crypto.pbkdf2"), @@ -17555,7 +17854,7 @@ await new Promise(() => {}); let pbkdf2_sha384 = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { id: 11, method: String::from("crypto.pbkdf2"), args: vec![ @@ -17576,7 +17875,7 @@ await new Promise(() => {}); let scrypt = crate::execution::service_javascript_crypto_sync_rpc( &mut process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("crypto.scrypt"), @@ -17608,7 +17907,7 @@ await new Promise(() => {}); let cipher_response = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10, method: String::from("crypto.cipheriv"), @@ -17628,7 +17927,7 @@ await new Promise(() => {}); let decipher_response = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 11, method: String::from("crypto.decipheriv"), @@ -17652,7 +17951,7 @@ await new Promise(() => {}); let mut streaming_process = create_crypto_test_process(); let session_id = crate::execution::service_javascript_crypto_sync_rpc( &mut streaming_process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 12, method: String::from("crypto.cipherivCreate"), @@ -17671,7 +17970,7 @@ await new Promise(() => {}); let update = crate::execution::service_javascript_crypto_sync_rpc( &mut streaming_process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 13, method: String::from("crypto.cipherivUpdate"), @@ -17686,7 +17985,7 @@ await new Promise(() => {}); let final_payload = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut streaming_process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 14, method: String::from("crypto.cipherivFinal"), @@ -17717,7 +18016,7 @@ await new Promise(() => {}); let signature = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 15, method: String::from("crypto.sign"), @@ -17731,7 +18030,7 @@ await new Promise(() => {}); .expect("crypto.sign"); let verified = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 16, method: String::from("crypto.verify"), @@ -17748,7 +18047,7 @@ await new Promise(() => {}); let encrypted = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 17, method: String::from("crypto.asymmetricOp"), @@ -17762,7 +18061,7 @@ await new Promise(() => {}); .expect("publicEncrypt"); let decrypted = crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 18, method: String::from("crypto.asymmetricOp"), @@ -17778,7 +18077,7 @@ await new Promise(() => {}); let key_object = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 19, method: String::from("crypto.createKeyObject"), @@ -17792,7 +18091,7 @@ await new Promise(() => {}); let generated_pair = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 20, method: String::from("crypto.generateKeyPairSync"), @@ -17810,7 +18109,7 @@ await new Promise(() => {}); let generated_secret = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 21, method: String::from("crypto.generateKeySync"), @@ -17827,7 +18126,7 @@ await new Promise(() => {}); let generated_prime = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 22, method: String::from("crypto.generatePrimeSync"), @@ -17844,7 +18143,7 @@ await new Promise(() => {}); let mut alice = create_crypto_test_process(); let alice_id = crate::execution::service_javascript_crypto_sync_rpc( &mut alice, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 23, method: String::from("crypto.diffieHellmanSessionCreate"), @@ -17857,7 +18156,7 @@ await new Promise(() => {}); let mut bob = create_crypto_test_process(); let bob_id = crate::execution::service_javascript_crypto_sync_rpc( &mut bob, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 24, method: String::from("crypto.diffieHellmanSessionCreate"), @@ -17870,7 +18169,7 @@ await new Promise(() => {}); let alice_public = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut alice, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 25, method: String::from("crypto.diffieHellmanSessionCall"), @@ -17883,7 +18182,7 @@ await new Promise(() => {}); let bob_public = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut bob, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 26, method: String::from("crypto.diffieHellmanSessionCall"), @@ -17896,7 +18195,7 @@ await new Promise(() => {}); let alice_secret = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut alice, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 27, method: String::from("crypto.diffieHellmanSessionCall"), @@ -17915,7 +18214,7 @@ await new Promise(() => {}); let bob_secret = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut bob, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 28, method: String::from("crypto.diffieHellmanSessionCall"), @@ -17937,7 +18236,7 @@ await new Promise(() => {}); let subtle_digest = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 29, method: String::from("crypto.subtle"), @@ -17956,7 +18255,7 @@ await new Promise(() => {}); let subtle_generated_key = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 30, method: String::from("crypto.subtle"), @@ -17979,7 +18278,7 @@ await new Promise(() => {}); let subtle_exported_key = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 31, method: String::from("crypto.subtle"), @@ -18000,7 +18299,7 @@ await new Promise(() => {}); let subtle_imported_key = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 32, method: String::from("crypto.subtle"), @@ -18023,7 +18322,7 @@ await new Promise(() => {}); let subtle_encrypted = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 33, method: String::from("crypto.subtle"), @@ -18049,7 +18348,7 @@ await new Promise(() => {}); let subtle_decrypted = parse_json_string( crate::execution::service_javascript_crypto_sync_rpc( &mut create_crypto_test_process(), - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 34, method: String::from("crypto.subtle"), @@ -18116,7 +18415,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("sqlite.open"), @@ -18131,7 +18430,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("sqlite.exec"), @@ -18148,7 +18447,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("sqlite.prepare"), @@ -18166,7 +18465,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("sqlite.statement.run"), @@ -18192,7 +18491,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("sqlite.statement.finalize"), @@ -18205,7 +18504,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("sqlite.query"), @@ -18229,7 +18528,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("sqlite.close"), @@ -18242,7 +18541,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 8, method: String::from("sqlite.open"), @@ -18257,7 +18556,7 @@ await new Promise(() => {}); &mut sidecar, &vm_id, process_id, - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 9, method: String::from("sqlite.query"), @@ -19167,110 +19466,20 @@ console.log(JSON.stringify({ lookup, resolve4 })); "#, ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-dns", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"crypto\",\"dns\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-dns"), - active_process_for_tests( - kernel_handle.pid(), - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } - - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut exit_code = None; - for _ in 0..64 { - let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes - .get_mut("proc-js-dns") - .and_then(|process| { - poll_test_execution_event(process, Duration::from_secs(5)) - .expect("poll javascript dns rpc event") - }) - }; - let Some(event) = next_event else { - if exit_code.is_some() { - break; - } - panic!("javascript dns process disappeared before exit"); - }; - - match &event { - ActiveExecutionEvent::Stdout(chunk) => { - append_process_stream_chunk(&mut stdout, chunk, "proc-js-dns", "stdout"); - } - ActiveExecutionEvent::Stderr(chunk) => { - append_process_stream_chunk(&mut stderr, chunk, "proc-js-dns", "stderr"); - } - ActiveExecutionEvent::Exited(code) => { - exit_code = Some(*code); - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) - | ActiveExecutionEvent::SignalState { .. } => {} - } - - block_on_sidecar!( - sidecar, - sidecar.handle_execution_event(&vm_id, "proc-js-dns", event) - ) - .expect("handle javascript dns rpc event"); - } - - let stdout = process_stream_to_string(&stdout); - let stderr = process_stream_to_string(&stderr); + ); + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "proc-js-dns"); assert_eq!(exit_code, Some(0), "stderr: {stderr}"); let parsed: Value = serde_json::from_str(stdout.trim()).expect("parse dns JSON"); assert!( @@ -19363,120 +19572,20 @@ process.exit(0); ), ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-ssrf-protection", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"crypto\",\"dns\",\"events\",\"fs\",\"net\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-ssrf-protection"), - active_process_for_tests( - kernel_handle.pid(), - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } - - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut exit_code = None; - for _ in 0..64 { - let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes - .get_mut("proc-js-ssrf-protection") - .and_then(|process| { - poll_test_execution_event(process, Duration::from_secs(5)) - .expect("poll javascript ssrf event") - }) - }; - let Some(event) = next_event else { - if exit_code.is_some() { - break; - } - panic!("javascript ssrf process disappeared before exit"); - }; - - match &event { - ActiveExecutionEvent::Stdout(chunk) => { - append_process_stream_chunk( - &mut stdout, - chunk, - "proc-js-ssrf-protection", - "stdout", - ); - } - ActiveExecutionEvent::Stderr(chunk) => { - append_process_stream_chunk( - &mut stderr, - chunk, - "proc-js-ssrf-protection", - "stderr", - ); - } - ActiveExecutionEvent::Exited(code) => { - exit_code = Some(*code); - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) - | ActiveExecutionEvent::SignalState { .. } => {} - } - - block_on_sidecar!( - sidecar, - sidecar.handle_execution_event(&vm_id, "proc-js-ssrf-protection", event) - ) - .expect("handle javascript ssrf event"); - } - - let stdout = process_stream_to_string(&stdout); - let stderr = process_stream_to_string(&stderr); + ); + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "proc-js-ssrf-protection"); assert_eq!(exit_code, Some(0), "stderr: {stderr}"); let parsed: Value = serde_json::from_str(stdout.trim()).expect("parse ssrf JSON"); assert_eq!( @@ -20068,122 +20177,20 @@ process.exit(0); ); write_fixture(&cwd.join("entry.mjs"), &entry); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-tls", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"crypto\",\"events\",\"fs\",\"net\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"tls\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-tls"), - active_process_for_vm_tests( - kernel_handle.pid(), - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } - - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut exit_code = None; - for _ in 0..192 { - block_on_sidecar!(sidecar, sidecar.pump_child_process_events(&vm_id)) - .expect("pump javascript TLS completion events"); - if let Some(envelope) = sidecar - .take_matching_process_event_envelope(&vm_id, "proc-js-tls") - .expect("drain javascript TLS completion event") - { - block_on_sidecar!(sidecar, sidecar.handle_process_event_envelope(envelope)) - .expect("handle javascript TLS completion event"); - continue; - } - let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes - .get_mut("proc-js-tls") - .and_then(|process| { - poll_test_execution_event(process, Duration::from_millis(50)) - .expect("poll javascript tls rpc event") - }) - }; - let Some(event) = next_event else { - if exit_code.is_some() { - break; - } - continue; - }; - - match &event { - ActiveExecutionEvent::Stdout(chunk) => { - append_process_stream_chunk(&mut stdout, chunk, "proc-js-tls", "stdout"); - } - ActiveExecutionEvent::Stderr(chunk) => { - append_process_stream_chunk(&mut stderr, chunk, "proc-js-tls", "stderr"); - } - ActiveExecutionEvent::Exited(code) => { - exit_code = Some(*code); - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::JavascriptSyncRpcCompletion(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::PythonSocketConnectCompletion(_) - | ActiveExecutionEvent::SignalState { .. } => {} - } - - block_on_sidecar!( - sidecar, - sidecar.handle_execution_event(&vm_id, "proc-js-tls", event) - ) - .expect("handle javascript tls rpc event"); - } - - let stdout = process_stream_to_string(&stdout); - let stderr = process_stream_to_string(&stderr); + ); + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "proc-js-tls"); assert_eq!(exit_code, Some(0), "stderr: {stderr}"); let parsed: Value = serde_json::from_str(stdout.trim()).expect("parse tls JSON"); assert_eq!(parsed["response"], Value::String(String::from("pong:ping"))); @@ -20219,7 +20226,7 @@ process.exit(0); &mut sidecar, &vm_id, "proc-js-http-listen", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.http_listen"), @@ -20256,7 +20263,7 @@ process.exit(0); &mut sidecar, &vm_id, "proc-js-http-listen", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.http_close"), @@ -20307,7 +20314,7 @@ process.exit(0); &mut sidecar, &vm_id, "proc-js-http-respond", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.http_respond"), @@ -20368,7 +20375,7 @@ process.exit(0); &mut sidecar, &vm_id, "proc-js-http-respond-oversized", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.http_respond"), @@ -20531,7 +20538,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.http2_server_listen"), @@ -20552,7 +20559,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.http2_session_connect"), @@ -20587,7 +20594,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.http2_session_request"), @@ -20629,7 +20636,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.http2_stream_respond"), @@ -20648,7 +20655,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.http2_stream_write"), @@ -20665,7 +20672,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.http2_stream_end"), @@ -20715,7 +20722,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.http2_session_close"), @@ -20729,7 +20736,7 @@ console.log(JSON.stringify(result || { data: "", error: "missing-result", reques &mut sidecar, &vm_id, "proc-js-http2", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 8, method: String::from("net.http2_server_close"), @@ -20943,7 +20950,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10, method: String::from("net.http2_server_listen"), @@ -20962,7 +20969,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 11, method: String::from("net.http2_session_connect"), @@ -20990,7 +20997,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 12, method: String::from("net.http2_session_settings"), @@ -21021,7 +21028,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 13, method: String::from("net.http2_session_set_local_window_size"), @@ -21041,7 +21048,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 14, method: String::from("net.http2_session_request"), @@ -21073,7 +21080,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 15, method: String::from("net.http2_stream_pause"), @@ -21086,7 +21093,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 16, method: String::from("net.http2_stream_resume"), @@ -21100,7 +21107,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 17, method: String::from("net.http2_stream_push_stream"), @@ -21121,7 +21128,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 18, method: String::from("net.http2_stream_close"), @@ -21135,7 +21142,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 19, method: String::from("net.http2_stream_respond_with_file"), @@ -21151,9 +21158,7 @@ setTimeout(() => { ) .expect_err("host-only file path should not be readable by HTTP/2 file response"); match host_file_response { - SidecarError::Kernel(message) => { - assert!(message.contains("ENOENT"), "{message}"); - } + SidecarError::Host(error) => assert_eq!(error.code, "ENOENT"), other => panic!("unexpected host file response error: {other:?}"), } @@ -21161,7 +21166,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 20, method: String::from("net.http2_stream_respond_with_file"), @@ -21204,7 +21209,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 21, method: String::from("net.http2_session_close"), @@ -21218,7 +21223,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-surfaces", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 22, method: String::from("net.http2_server_close"), @@ -21247,7 +21252,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-secure", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 20, method: String::from("net.http2_server_listen"), @@ -21282,7 +21287,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-secure", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 21, method: String::from("net.http2_session_connect"), @@ -21329,7 +21334,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-secure", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 22, method: String::from("net.http2_session_request"), @@ -21362,7 +21367,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-secure", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 23, method: String::from("net.http2_stream_respond"), @@ -21381,7 +21386,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-secure", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 24, method: String::from("net.http2_stream_end"), @@ -21462,7 +21467,7 @@ setTimeout(() => { &mut sidecar, &vm_id, "proc-js-http2-respond", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 25, method: String::from("net.http2_server_respond"), @@ -22067,80 +22072,6 @@ await new Promise(() => {}); } } - fn vm_fetch_kernel_tcp_decodes_chunked_response_body() { - assert_node_available(); - - let mut sidecar = create_test_sidecar(); - let (connection_id, session_id) = - authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); - let vm_id = create_vm( - &mut sidecar, - &connection_id, - &session_id, - PermissionsPolicy::allow_all(), - ) - .expect("create vm"); - let server_cwd = temp_dir("agentos-native-sidecar-host-fetch-js-chunked-cwd"); - write_fixture( - &server_cwd.join("entry.mjs"), - r#" -import http from "node:http"; - -const server = http.createServer((_req, res) => { - res.writeHead(200, { "content-type": "text/plain" }); - res.write("hello "); - res.write("chunked"); - res.end(); -}); - -server.listen(3000, "127.0.0.1", () => { - console.log("READY"); -}); - -await new Promise(() => {}); -"#, - ); - start_fake_javascript_process(&mut sidecar, &vm_id, &server_cwd, "proc-js-server"); - wait_for_process_stdout_contains(&mut sidecar, &vm_id, "proc-js-server", "READY"); - - let response = dispatch_host_vm_fetch( - &mut sidecar, - 907, - &connection_id, - &session_id, - &vm_id, - 3000, - "/chunked", - None, - ) - .expect("host fetch reaches chunked guest HTTP server"); - - sidecar - .kill_process_internal(&vm_id, "proc-js-server", "SIGKILL") - .expect("kill javascript server process"); - - match response.response.payload { - ResponsePayload::VmFetchResult(result) => { - let parsed: Value = - serde_json::from_str(&result.response_json).expect("parse fetch response"); - assert_eq!(parsed["status"], Value::from(200)); - assert_eq!( - parsed["bodyEncoding"], - Value::String(String::from("base64")) - ); - let body = base64::engine::general_purpose::STANDARD - .decode(parsed["body"].as_str().expect("base64 response body")) - .expect("decode response body"); - assert_eq!(body, b"hello chunked"); - assert!( - !body.windows(3).any(|window| window == b"\r\n6"), - "chunk framing leaked into decoded body: {body:?}" - ); - } - other => panic!("unexpected vm_fetch response payload: {other:?}"), - } - } - fn vm_fetch_stream_flushes_chunks_and_cancel_releases_socket() { assert_node_available(); @@ -22274,7 +22205,7 @@ await new Promise(() => {}); .expect("kill javascript server process"); } - fn vm_fetch_kernel_tcp_rejects_chunked_with_content_length() { + fn vm_fetch_kernel_tcp_decodes_chunked_response_body() { assert_node_available(); let mut sidecar = create_test_sidecar(); @@ -22287,20 +22218,17 @@ await new Promise(() => {}); PermissionsPolicy::allow_all(), ) .expect("create vm"); - let server_cwd = temp_dir("agentos-native-sidecar-host-fetch-js-chunked-cl-cwd"); + let server_cwd = temp_dir("agentos-native-sidecar-host-fetch-js-chunked-cwd"); write_fixture( &server_cwd.join("entry.mjs"), r#" -import net from "node:net"; +import http from "node:http"; -const server = net.createServer((socket) => { - socket.end( - "HTTP/1.1 200 OK\r\n" + - "Transfer-Encoding: chunked\r\n" + - "Content-Length: 5\r\n" + - "\r\n" + - "5\r\nhello\r\n0\r\n\r\n" - ); +const server = http.createServer((_req, res) => { + res.writeHead(200, { "content-type": "text/plain" }); + res.write("hello "); + res.write("chunked"); + res.end(); }); server.listen(3000, "127.0.0.1", () => { @@ -22313,28 +22241,105 @@ await new Promise(() => {}); start_fake_javascript_process(&mut sidecar, &vm_id, &server_cwd, "proc-js-server"); wait_for_process_stdout_contains(&mut sidecar, &vm_id, "proc-js-server", "READY"); - let rejected = dispatch_host_vm_fetch( + let response = dispatch_host_vm_fetch( &mut sidecar, - 908, + 907, &connection_id, &session_id, &vm_id, 3000, - "/invalid", + "/chunked", None, ) - .map(rejected_response_message) - .expect("invalid chunked response should reject vm.fetch"); + .expect("host fetch reaches chunked guest HTTP server"); sidecar .kill_process_internal(&vm_id, "proc-js-server", "SIGKILL") .expect("kill javascript server process"); - assert!( - rejected.contains("Transfer-Encoding: chunked") - && rejected.contains("Content-Length"), - "unexpected error: {rejected}" - ); + match response.response.payload { + ResponsePayload::VmFetchResult(result) => { + let parsed: Value = + serde_json::from_str(&result.response_json).expect("parse fetch response"); + assert_eq!(parsed["status"], Value::from(200)); + assert_eq!( + parsed["bodyEncoding"], + Value::String(String::from("base64")) + ); + let body = base64::engine::general_purpose::STANDARD + .decode(parsed["body"].as_str().expect("base64 response body")) + .expect("decode response body"); + assert_eq!(body, b"hello chunked"); + assert!( + !body.windows(3).any(|window| window == b"\r\n6"), + "chunk framing leaked into decoded body: {body:?}" + ); + } + other => panic!("unexpected vm_fetch response payload: {other:?}"), + } + } + + fn vm_fetch_kernel_tcp_rejects_chunked_with_content_length() { + assert_node_available(); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let server_cwd = temp_dir("agentos-native-sidecar-host-fetch-js-chunked-cl-cwd"); + write_fixture( + &server_cwd.join("entry.mjs"), + r#" +import net from "node:net"; + +const server = net.createServer((socket) => { + socket.end( + "HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Length: 5\r\n" + + "\r\n" + + "5\r\nhello\r\n0\r\n\r\n" + ); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log("READY"); +}); + +await new Promise(() => {}); +"#, + ); + start_fake_javascript_process(&mut sidecar, &vm_id, &server_cwd, "proc-js-server"); + wait_for_process_stdout_contains(&mut sidecar, &vm_id, "proc-js-server", "READY"); + + let rejected = dispatch_host_vm_fetch( + &mut sidecar, + 908, + &connection_id, + &session_id, + &vm_id, + 3000, + "/invalid", + None, + ) + .map(rejected_response_message) + .expect("invalid chunked response should reject vm.fetch"); + + sidecar + .kill_process_internal(&vm_id, "proc-js-server", "SIGKILL") + .expect("kill javascript server process"); + + assert!( + rejected.contains("Transfer-Encoding: chunked") + && rejected.contains("Content-Length"), + "unexpected error: {rejected}" + ); } fn vm_fetch_kernel_tcp_socket_cap_failure_closes_no_extra_resources() { @@ -22922,7 +22927,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -22964,7 +22969,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -22984,7 +22989,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.server_poll"), @@ -23004,7 +23009,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.write"), @@ -23024,7 +23029,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.shutdown"), @@ -23037,7 +23042,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.poll"), @@ -23056,7 +23061,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.write"), @@ -23076,7 +23081,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 8, method: String::from("net.shutdown"), @@ -23089,7 +23094,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 9, method: String::from("net.poll"), @@ -23111,7 +23116,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-server", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10, method: String::from("net.poll"), @@ -23143,7 +23148,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23165,7 +23170,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -23185,7 +23190,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.connect"), @@ -23205,7 +23210,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.server_poll"), @@ -23223,7 +23228,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.server_connections"), @@ -23237,7 +23242,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.server_poll"), @@ -23251,7 +23256,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.server_connections"), @@ -23265,7 +23270,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 8, method: String::from("net.destroy"), @@ -23277,7 +23282,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 9, method: String::from("net.destroy"), @@ -23289,7 +23294,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-backlog", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10, method: String::from("net.server_close"), @@ -23351,7 +23356,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23372,7 +23377,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -23392,7 +23397,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.server_poll"), @@ -23456,7 +23461,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &poll_vm_id_for_task, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.poll"), @@ -23466,11 +23471,11 @@ console.log(`BODY:${{body}}`); .await .expect("poll response"); let response = match response { - JavascriptSyncRpcServiceResponse::Json(value) - | JavascriptSyncRpcServiceResponse::SourceBackedJson { value, .. } => value, - JavascriptSyncRpcServiceResponse::Raw(_) - | JavascriptSyncRpcServiceResponse::SourceBackedRaw { .. } - | JavascriptSyncRpcServiceResponse::Deferred { .. } => { + HostServiceResponse::Json(value) + | HostServiceResponse::SourceBackedJson { value, .. } => value, + HostServiceResponse::Raw(_) + | HostServiceResponse::SourceBackedRaw { .. } + | HostServiceResponse::Deferred { .. } => { panic!("net.poll returned a non-JSON response") } }; @@ -23504,7 +23509,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &cleanup_poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.destroy"), @@ -23516,7 +23521,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &cleanup_poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.destroy"), @@ -23528,7 +23533,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &cleanup_poll_vm_id, "proc-js-poll", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.server_close"), @@ -23576,7 +23581,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23594,7 +23599,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.listen"), @@ -23616,7 +23621,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.listen"), @@ -23638,7 +23643,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.listen"), @@ -23660,7 +23665,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("dgram.createSocket"), @@ -23677,7 +23682,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("dgram.bind"), @@ -23698,7 +23703,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-bind-policy", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.listen"), @@ -23747,7 +23752,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-privileged", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23792,7 +23797,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_a, "proc-a", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23807,7 +23812,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_b, "proc-b", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -23825,7 +23830,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_a, "proc-a", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -23840,7 +23845,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_b, "proc-b", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 2, method: String::from("net.connect"), @@ -23866,7 +23871,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_a, "proc-a", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.server_poll"), @@ -23878,7 +23883,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_b, "proc-b", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.server_poll"), @@ -23948,65 +23953,18 @@ console.log(`BODY:${{body}}`); let cwd = temp_dir("agentos-native-sidecar-js-net-unix-cwd"); write_fixture(&cwd.join("entry.mjs"), "setInterval(() => {}, 1000);"); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-unix", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"crypto\",\"events\",\"fs\",\"net\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-unix"), - active_process_for_vm_tests( - kernel_handle.pid(), - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } + ); let bridge = sidecar.bridge.clone(); let dns = sidecar.vms.get(&vm_id).expect("javascript vm").dns.clone(); @@ -24016,10 +23974,9 @@ console.log(`BODY:${{body}}`); .expect("javascript vm") .capabilities .clone(); - let socket_paths = build_javascript_socket_path_context( - sidecar.vms.get(&vm_id).expect("javascript vm"), - ) - .expect("build Unix socket path context"); + let socket_paths = + build_socket_path_context(sidecar.vms.get(&vm_id).expect("javascript vm")) + .expect("build Unix socket path context"); let socket_path = "/tmp/secure-exec.sock"; let listen = { @@ -24035,7 +23992,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 1, method: String::from("net.listen"), @@ -24104,7 +24061,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-unix", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 3, method: String::from("net.connect"), @@ -24138,7 +24095,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 4, method: String::from("net.server_poll"), @@ -24179,7 +24136,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 5, method: String::from("net.server_connections"), @@ -24204,7 +24161,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 6, method: String::from("net.write"), @@ -24234,7 +24191,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 7, method: String::from("net.shutdown"), @@ -24283,7 +24240,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 10, method: String::from("net.write"), @@ -24313,7 +24270,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 11, method: String::from("net.shutdown"), @@ -24362,7 +24319,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: request_id, method: String::from("net.destroy"), @@ -24386,7 +24343,7 @@ console.log(`BODY:${{body}}`); &socket_paths, &mut vm.kernel, process, - &JavascriptSyncRpcRequest { + &HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: 16, method: String::from("net.server_close"), @@ -24442,7 +24399,7 @@ console.log(`BODY:${{body}}`); &mut sidecar, &vm_id, "proc-js-unix", - JavascriptSyncRpcRequest { + HostRpcRequest { raw_bytes_args: std::collections::HashMap::new(), id: request_id, method: String::from(method), @@ -24549,72 +24506,27 @@ console.log(JSON.stringify({ .write_file("/rpc/note.txt", b"hello from nested child".to_vec()) .expect("seed rpc note"); vm.kernel - .write_file( - "/root/child.mjs", + .admit_trusted_initial_runtime_image( + "/workspace/child.mjs", fs::read(cwd.join("child.mjs")).expect("read child fixture"), + 0o644, + vm.limits.wasm.max_module_file_bytes, ) .expect("seed nested child fixture"); } - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-child", + BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( "[\"assert\",\"buffer\",\"console\",\"child_process\",\"crypto\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", ), )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); - - let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-child"), - active_process_for_vm_tests( - kernel_handle.pid(), - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); - } + ); let (stdout, stderr, exit_code) = drain_process_output(&mut sidecar, &vm_id, "proc-js-child"); @@ -24734,78 +24646,39 @@ console.log(JSON.stringify({ .join("\n"), ); - let context = - sidecar - .javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let execution = sidecar - .javascript_engine - .start_execution(StartJavascriptExecutionRequest { - limits: Default::default(), - guest_runtime: Default::default(), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: vec![String::from("./entry.mjs")], - argv0: None, - env: BTreeMap::from([( - String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), - String::from( - "[\"assert\",\"buffer\",\"console\",\"child_process\",\"crypto\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", - ), - )]), - cwd: cwd.clone(), - inline_code: None, - wasm_module_bytes: None, - }) - .expect("start nested SIGCHLD javascript execution"); - - let kernel_handle = { + { let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel - .write_file( - "/root/child.mjs", + .admit_trusted_initial_runtime_image( + "/workspace/child.mjs", fs::read(cwd.join("child.mjs")).expect("read child fixture"), + 0o644, + vm.limits.wasm.max_module_file_bytes, ) .expect("seed nested child fixture"); vm.kernel - .write_file( - "/root/leaf.mjs", + .admit_trusted_initial_runtime_image( + "/workspace/leaf.mjs", fs::read(cwd.join("leaf.mjs")).expect("read leaf fixture"), + 0o644, + vm.limits.wasm.max_module_file_bytes, ) .expect("seed nested leaf fixture"); - vm.kernel - .spawn_process( - JAVASCRIPT_COMMAND, - vec![String::from("./entry.mjs")], - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(String::from("/")), - ..SpawnOptions::default() - }, - ) - .expect("spawn kernel javascript process") - }; - - { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - vm.active_processes.insert( - String::from("proc-js-nested-sigchld"), - active_process_for_vm_tests( - kernel_handle.pid(), - kernel_handle, - vm.runtime_context.clone(), - vm.limits.clone(), - GuestRuntimeKind::JavaScript, - ActiveExecution::Javascript(execution), - ) - .with_host_cwd(cwd.clone()), - ); } + start_javascript_entry_with_env( + &mut sidecar, + &vm_id, + &cwd, + "proc-js-nested-sigchld", + BTreeMap::from([( + String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), + String::from( + "[\"assert\",\"buffer\",\"console\",\"child_process\",\"crypto\",\"events\",\"fs\",\"path\",\"querystring\",\"stream\",\"string_decoder\",\"timers\",\"url\",\"util\",\"zlib\"]", + ), + )]), + ); + let (stdout, stderr, exit_code) = drain_process_output(&mut sidecar, &vm_id, "proc-js-nested-sigchld"); assert_eq!(exit_code, Some(0), "stderr: {stderr}"); @@ -24871,26 +24744,19 @@ console.log(JSON.stringify({ let error = block_on_sidecar!( sidecar, - sidecar.poll_javascript_child_process( - &vm_id, - "proc-js-child-gone", - "ghost-child", - 0, - ) + sidecar.poll_child_process(&vm_id, "proc-js-child-gone", "ghost-child", 0,) ) .expect_err("missing child should surface ECHILD"); match error { - SidecarError::Execution(message) => { - assert!( - message.starts_with("ECHILD:"), - "expected ECHILD code, got {message}" - ); + SidecarError::Host(error) => { + assert_eq!(error.code, "ECHILD"); assert!( - message.contains("proc-js-child-gone/ghost-child"), - "expected child label in error, got {message}" + error.message.contains("proc-js-child-gone/ghost-child"), + "expected child label in error, got {}", + error.message ); } - other => panic!("expected execution error, got {other}"), + other => panic!("expected typed host error, got {other}"), } let queued = sidecar @@ -24902,7 +24768,7 @@ console.log(JSON.stringify({ } #[test] - fn wasm_parent_keeps_descendant_events_for_its_pull_driven_poll_rpc() { + fn typed_child_poll_is_an_immediate_bounded_output_probe() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); @@ -24913,21 +24779,131 @@ console.log(JSON.stringify({ PermissionsPolicy::allow_all(), ) .expect("create vm"); - - let root_kernel_handle = create_kernel_process_handle_for_tests(); - let mut root = active_process_for_tests( - root_kernel_handle.pid(), - root_kernel_handle, + let mut root = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + let mut child = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + child.child_process_bridge_owns_output = true; + child + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"ready".to_vec())) + .expect("queue child output"); + root.child_processes.insert(String::from("child-1"), child); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .insert(String::from("root"), root); + + sidecar + .validate_child_poll_target(&vm_id, "root", &[], "child-1") + .expect("live child validates"); + let result = block_on_sidecar!( + sidecar, + sidecar.poll_child_process(&vm_id, "root", "child-1", 5_000) + ) + .expect("immediate poll"); + assert_eq!(result["type"], "stdout"); + assert_eq!(result["data"]["base64"], "cmVhZHk="); + } + + #[test] + fn typed_child_poll_finite_wait_remains_an_immediate_null_probe() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let mut root = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + root.child_processes.insert( + String::from("child-1"), + spawn_vm_wasm_binding_process(&mut sidecar, &vm_id), + ); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .insert(String::from("root"), root); + let result = block_on_sidecar!( + sidecar, + sidecar.poll_child_process(&vm_id, "root", "child-1", 5_000) + ) + .expect("finite compatibility poll"); + assert_eq!(result, Value::Null); + } + + #[test] + fn typed_child_poll_validation_rejects_a_torn_down_caller() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let root_kernel_handle = create_kernel_process_handle_for_tests(); + let root_pid = root_kernel_handle.pid(); + let mut root = active_process_for_tests( + root_pid, + root_kernel_handle, GuestRuntimeKind::WebAssembly, ActiveExecution::Binding(BindingExecution::default()), ); let child_kernel_handle = create_kernel_process_handle_for_tests(); - let mut child = active_process_for_tests( - child_kernel_handle.pid(), - child_kernel_handle, - GuestRuntimeKind::WebAssembly, - ActiveExecution::Binding(BindingExecution::default()), + root.child_processes.insert( + String::from("child-1"), + active_process_for_tests( + child_kernel_handle.pid(), + child_kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Binding(BindingExecution::default()), + ), ); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .insert(String::from("root"), root); + drop( + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .remove("root"), + ); + let error = sidecar + .validate_child_poll_target(&vm_id, "root", &[], "child-1") + .expect_err("torn-down caller must not poll"); + assert!(error + .to_string() + .contains("no longer has active process root")); + } + + #[test] + fn wasm_parent_keeps_descendant_events_for_its_pull_driven_poll_rpc() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + + let mut root = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + let mut child = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + child.child_process_bridge_owns_output = true; child .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"pull-owned".to_vec())) .expect("queue child output"); @@ -25054,17 +25030,17 @@ try { (parent_pid, read_fd, write_fd, vm.kernel.resource_snapshot()) }; - let spawned = spawn_descendant_javascript_child_process_for_test( + let spawned = spawn_descendant_process_for_test( &mut sidecar, &vm_id, "wasm-write-parent", &[], - crate::protocol::JavascriptChildProcessSpawnRequest { + agentos_execution::host::ProcessLaunchRequest { command: String::from("node"), args: vec![String::from("./writer.mjs")], - options: crate::protocol::JavascriptChildProcessSpawnOptions { + options: agentos_execution::host::ProcessLaunchOptions { spawn_fd_mappings: vec![[3, read_fd]], - spawn_file_actions: vec![crate::protocol::JavascriptPosixSpawnFileAction { + spawn_file_actions: vec![agentos_execution::host::ProcessSpawnFileAction { command: 2, guest_fd: Some(3), fd: 3, @@ -25097,7 +25073,7 @@ try { let park_deadline = Instant::now() + Duration::from_secs(10); let mut prepark_events = Vec::new(); loop { - match runtime_handle.block_on(sidecar.poll_javascript_child_process( + match runtime_handle.block_on(sidecar.poll_child_process( &vm_id, "wasm-write-parent", &child_id, @@ -25121,7 +25097,7 @@ try { if Instant::now() >= park_deadline { let mut events = Vec::new(); for _ in 0..16 { - match poll_javascript_child_process_for_test( + match poll_child_process_for_test( &mut sidecar, &vm_id, "wasm-write-parent", @@ -25178,7 +25154,7 @@ try { let mut exit_code = None; for _ in 0..40 { - let event = poll_javascript_child_process_for_test( + let event = poll_child_process_for_test( &mut sidecar, &vm_id, "wasm-write-parent", @@ -25268,166 +25244,611 @@ try { assert!(!filtered.contains_key("AGENTOS_PARENT_NODE_ALLOW_CHILD_PROCESS")); assert!(!filtered.contains_key("VISIBLE_MARKER")); } + + fn child_sync_post_spawn_failures_are_transactional_at_root_and_nested_depths() { + struct TimerFailureHookReset; + impl Drop for TimerFailureHookReset { + fn drop(&mut self) { + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + } + } + + fn process_at_path_mut<'a>( + process: &'a mut ActiveProcess, + path: &[&str], + ) -> &'a mut ActiveProcess { + let mut current = process; + for child_id in path { + current = current + .child_processes + .get_mut(*child_id) + .unwrap_or_else(|| panic!("missing test child path component {child_id}")); + } + current + } + + fn insert_fixture_child( + sidecar: &mut NativeSidecar, + vm_id: &str, + root_process_id: &str, + parent_path: &[&str], + child_id: &str, + ) -> u32 { + let (parent_pid, runtime_context, limits, env, host_cwd) = { + let vm = sidecar.vms.get(vm_id).expect("test VM"); + let root = vm + .active_processes + .get(root_process_id) + .expect("test root process"); + let mut parent = root; + for path_component in parent_path { + parent = parent + .child_processes + .get(*path_component) + .expect("test parent path"); + } + ( + parent.kernel_pid, + parent.runtime_context.clone(), + parent.limits.clone(), + parent.env.clone(), + parent.host_cwd.clone(), + ) + }; + let kernel_handle = sidecar + .vms + .get_mut(vm_id) + .expect("test VM") + .kernel + .create_virtual_process( + EXECUTION_DRIVER_NAME, + EXECUTION_DRIVER_NAME, + JAVASCRIPT_COMMAND, + vec![String::from(JAVASCRIPT_COMMAND)], + VirtualProcessOptions { + parent_pid: Some(parent_pid), + cwd: Some(String::from("/")), + ..VirtualProcessOptions::default() + }, + ) + .expect("create fixture child process"); + let kernel_pid = kernel_handle.pid(); + let child = active_process_for_vm_tests( + kernel_pid, + kernel_handle, + runtime_context, + limits, + GuestRuntimeKind::JavaScript, + ActiveExecution::Binding(BindingExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(env) + .with_host_cwd(host_cwd); + let vm = sidecar.vms.get_mut(vm_id).expect("test VM"); + let root = vm + .active_processes + .get_mut(root_process_id) + .expect("test root process"); + assert!( + process_at_path_mut(root, parent_path) + .child_processes + .insert(child_id.to_owned(), child) + .is_none(), + "fixture child IDs must be unique" + ); + kernel_pid + } + + fn binding_request(timeout: u64) -> agentos_execution::host::ProcessLaunchRequest { + agentos_execution::host::ProcessLaunchRequest { + command: String::from("/usr/local/bin/agentos-math"), + args: vec![String::from("add")], + options: agentos_execution::host::ProcessLaunchOptions { + timeout: Some(timeout), + ..Default::default() + }, + } + } + + fn assert_failed_pid_reaped_and_budgets_released( + sidecar: &mut NativeSidecar, + vm_id: &str, + failed_pid: u32, + ) { + let vm = sidecar.vms.get_mut(vm_id).expect("test VM"); + assert!( + !vm.kernel.list_processes().contains_key(&failed_pid), + "rolled-back PID {failed_pid} must leave the kernel process table" + ); + let wait_error = vm + .kernel + .waitpid(failed_pid) + .expect_err("rolled-back PID must already be reaped"); + assert_eq!(wait_error.code(), "ESRCH", "unexpected wait error"); + assert_eq!(vm.pending_child_sync_count_budget.used(), 0); + assert_eq!(vm.pending_child_sync_bytes_budget.used(), 0); + } + + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + let _timer_failure_hook_reset = TimerFailureHookReset; + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create VM"); + sidecar + .dispatch_blocking(request( + 800, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::RegisterHostCallbacks(test_bindings_payload( + "math", + "Math utilities", + "add", + )), + )) + .expect("register binding command"); + let cwd = temp_dir("agentos-child-sync-transactional-rollback"); + let root_process_id = "child-sync-parent"; + insert_fake_javascript_parent_process(&mut sidecar, &vm_id, &cwd, root_process_id); + + let root_sibling_pid = + insert_fixture_child(&mut sidecar, &vm_id, root_process_id, &[], "root-sibling"); + let root_baseline = sidecar + .vms + .get(&vm_id) + .expect("test VM") + .kernel + .resource_snapshot(); + NativeSidecar::::arm_child_sync_timer_admission_failure_for_test( + &vm_id, + root_process_id, + &[], + ); + let handle = sidecar + .vms + .get(&vm_id) + .expect("test VM") + .runtime_context + .handle() + .clone(); + let root_error = handle + .block_on(sidecar.defer_javascript_child_process_sync( + &vm_id, + root_process_id, + binding_request(60_000), + None, + )) + .err() + .expect("forced root timer admission must fail"); + assert!( + root_error + .to_string() + .contains("ERR_AGENTOS_TASK_ADMISSION_CLOSED"), + "unexpected root failure: {root_error}" + ); + let (failed_root_child_id, failed_root_pid) = + NativeSidecar::::take_child_sync_rollback_for_test() + .expect("root rollback identity"); + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + { + let vm = sidecar.vms.get(&vm_id).expect("test VM"); + let root = vm + .active_processes + .get(root_process_id) + .expect("root process survives"); + assert!(root.child_processes.contains_key("root-sibling")); + assert!(!root.child_processes.contains_key(&failed_root_child_id)); + assert!(root.pending_child_process_sync.is_empty()); + assert!(vm.kernel.list_processes().contains_key(&root_sibling_pid)); + assert_eq!(vm.kernel.resource_snapshot(), root_baseline); + } + assert_failed_pid_reaped_and_budgets_released(&mut sidecar, &vm_id, failed_root_pid); + + let root_ids_before = sidecar + .vms + .get(&vm_id) + .expect("test VM") + .active_processes + .get(root_process_id) + .expect("root process") + .child_processes + .keys() + .cloned() + .collect::>(); + NativeSidecar::::arm_child_sync_timer_admission_failure_for_test( + &vm_id, + root_process_id, + &[], + ); + let timeout_error = handle + .block_on(sidecar.defer_javascript_child_process_sync( + &vm_id, + root_process_id, + binding_request(u64::MAX), + None, + )) + .err() + .expect("u64::MAX root timeout must reach forced timer admission safely"); + assert!( + timeout_error + .to_string() + .contains("ERR_AGENTOS_TASK_ADMISSION_CLOSED"), + "unexpected u64::MAX root failure: {timeout_error}" + ); + let (_, max_timeout_root_pid) = + NativeSidecar::::take_child_sync_rollback_for_test() + .expect("u64::MAX root rollback identity"); + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + assert_eq!( + sidecar + .vms + .get(&vm_id) + .expect("test VM") + .active_processes + .get(root_process_id) + .expect("root process") + .child_processes + .keys() + .cloned() + .collect::>(), + root_ids_before + ); + assert_failed_pid_reaped_and_budgets_released( + &mut sidecar, + &vm_id, + max_timeout_root_pid, + ); + + let nested_parent_pid = + insert_fixture_child(&mut sidecar, &vm_id, root_process_id, &[], "nested-parent"); + let nested_sibling_pid = insert_fixture_child( + &mut sidecar, + &vm_id, + root_process_id, + &["nested-parent"], + "nested-sibling", + ); + let nested_baseline = sidecar + .vms + .get(&vm_id) + .expect("test VM") + .kernel + .resource_snapshot(); + NativeSidecar::::arm_child_sync_timer_admission_failure_for_test( + &vm_id, + root_process_id, + &["nested-parent"], + ); + let nested_error = handle + .block_on( + sidecar.defer_descendant_javascript_child_process_sync_for_test( + &vm_id, + root_process_id, + &["nested-parent"], + binding_request(60_000), + None, + ), + ) + .err() + .expect("forced nested timer admission must fail"); + assert!( + nested_error + .to_string() + .contains("ERR_AGENTOS_TASK_ADMISSION_CLOSED"), + "unexpected nested failure: {nested_error}" + ); + let (failed_nested_child_id, failed_nested_pid) = + NativeSidecar::::take_child_sync_rollback_for_test() + .expect("nested rollback identity"); + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + { + let vm = sidecar.vms.get(&vm_id).expect("test VM"); + let root = vm + .active_processes + .get(root_process_id) + .expect("root process"); + let nested_parent = root + .child_processes + .get("nested-parent") + .expect("nested parent survives"); + assert_eq!(nested_parent.kernel_pid, nested_parent_pid); + assert!(nested_parent.child_processes.contains_key("nested-sibling")); + assert!(!nested_parent + .child_processes + .contains_key(&failed_nested_child_id)); + assert!(nested_parent.pending_child_process_sync.is_empty()); + assert!(vm.kernel.list_processes().contains_key(&nested_sibling_pid)); + assert_eq!(vm.kernel.resource_snapshot(), nested_baseline); + } + assert_failed_pid_reaped_and_budgets_released(&mut sidecar, &vm_id, failed_nested_pid); + + let nested_ids_before = sidecar + .vms + .get(&vm_id) + .expect("test VM") + .active_processes + .get(root_process_id) + .expect("root process") + .child_processes + .get("nested-parent") + .expect("nested parent") + .child_processes + .keys() + .cloned() + .collect::>(); + NativeSidecar::::arm_child_sync_timer_admission_failure_for_test( + &vm_id, + root_process_id, + &["nested-parent"], + ); + let nested_timeout_error = handle + .block_on( + sidecar.defer_descendant_javascript_child_process_sync_for_test( + &vm_id, + root_process_id, + &["nested-parent"], + binding_request(u64::MAX), + None, + ), + ) + .err() + .expect("u64::MAX nested timeout must reach forced timer admission safely"); + assert!( + nested_timeout_error + .to_string() + .contains("ERR_AGENTOS_TASK_ADMISSION_CLOSED"), + "unexpected u64::MAX nested failure: {nested_timeout_error}" + ); + let (_, max_timeout_nested_pid) = + NativeSidecar::::take_child_sync_rollback_for_test() + .expect("u64::MAX nested rollback identity"); + NativeSidecar::::clear_child_sync_timer_admission_failure_for_test(); + assert_eq!( + sidecar + .vms + .get(&vm_id) + .expect("test VM") + .active_processes + .get(root_process_id) + .expect("root process") + .child_processes + .get("nested-parent") + .expect("nested parent") + .child_processes + .keys() + .cloned() + .collect::>(), + nested_ids_before + ); + assert_failed_pid_reaped_and_budgets_released( + &mut sidecar, + &vm_id, + max_timeout_nested_pid, + ); + } + fn run_service_suite() { // Multiple libtest cases in this sidecar integration binary still // trip teardown/init crashes around V8-backed execution paths, so // keep the broad coverage in one top-level suite. - kernel_socket_queries_ignore_stale_sidecar_guest_addresses(); - find_listener_rejects_without_network_inspect_permission(); - find_listener_returns_listener_with_network_inspect_permission(); - find_bound_udp_rejects_without_network_inspect_permission(); - find_bound_udp_returns_socket_with_network_inspect_permission(); - get_process_snapshot_rejects_without_process_inspect_permission(); - get_process_snapshot_returns_processes_with_process_inspect_permission(); - get_resource_snapshot_rejects_without_process_inspect_permission(); - get_resource_snapshot_returns_kernel_and_queue_counts_with_process_inspect_permission(); - capability_registry_ignores_duplicate_sidecar_kernel_entries(); - loopback_tls_transport_survives_concurrent_handshakes_without_panicking(); - loopback_tls_endpoint_read_survives_competing_drain_and_peer_drop(); - javascript_net_socket_wait_connect_reports_tcp_socket_info(); - javascript_net_socket_read_and_socket_options_work_for_tcp_sockets(); - javascript_net_cross_exec_loopback_routes_through_kernel_socket_table(); - javascript_net_upgrade_socket_aliases_use_tcp_socket_state(); - javascript_dgram_address_and_buffer_size_sync_rpcs_work(); - javascript_tls_client_upgrade_query_and_cipher_list_work(); - javascript_tls_server_client_hello_and_server_upgrade_work(); - javascript_net_server_accept_returns_timeout_then_pending_connection(); - javascript_kernel_stdin_reads_buffered_input_and_reports_timeout_and_eof(); - javascript_sync_rpc_pty_set_raw_mode_toggles_kernel_tty_discipline(); - dispose_vm_removes_per_vm_javascript_import_cache_directory(); - execution_dispose_vm_race_skips_stale_process_events_without_panicking(); - execution_javascript_sync_rpc_handler_ignores_stale_vm_and_process_races(); - execution_poll_event_smoke_skips_queued_stale_process_envelopes_after_dispose(); - execution_poll_event_concurrent_dispose_logs_stale_process_event(); - filesystem_requests_ignore_stale_vm_and_process_races(); - get_zombie_timer_count_reports_kernel_state_before_and_after_waitpid(); - parse_signal_accepts_full_guest_signal_table(); - runtime_child_liveness_only_tracks_owned_children(); - authenticated_connection_id_returns_error_for_unexpected_response(); - opened_session_id_returns_error_for_unexpected_response(); - created_vm_id_returns_error_for_unexpected_response(); - configure_vm_instantiates_memory_mounts_through_the_plugin_registry(); - configure_vm_applies_read_only_mount_wrappers(); - configure_vm_instantiates_host_dir_mounts_through_the_plugin_registry(); - configure_vm_passes_resource_read_limits_to_host_dir_mounts(); - configure_vm_passes_resource_read_limits_to_module_access_mounts(); - configure_vm_rejects_module_access_root_symlink_to_non_node_modules(); - configure_vm_js_bridge_mount_dispatches_filesystem_calls_via_sidecar_requests(); - configure_vm_js_bridge_mount_rejects_oversized_read_payloads(); - configure_vm_js_bridge_mount_rejects_pread_payloads_above_requested_length(); - configure_vm_js_bridge_mount_maps_callback_errors_to_errno_codes(); - configure_vm_js_bridge_mount_readdir_of_mount_root_survives_broken_driver_realpath(); - configure_vm_instantiates_sandbox_agent_mounts_through_the_plugin_registry(); - configure_vm_instantiates_s3_mounts_through_the_plugin_registry(); - object_s3_mount_plugin_is_not_registered(); - configure_vm_instantiates_chunked_local_mounts_through_the_plugin_registry(); - bridge_permissions_map_symlink_operations_to_symlink_access(); - vm_limits_config_reads_filesystem_limits(); - create_vm_applies_filesystem_permission_descriptors_to_kernel_access(); - create_vm_without_permissions_defaults_to_static_deny_all(); - configure_vm_rollback_restore_failure_falls_back_to_static_deny_all(); - binding_registration_rollback_restore_failure_keeps_registry_consistent(); - create_vm_rejects_permission_rules_with_empty_operations(); - configure_vm_rejects_permission_rules_with_empty_paths_or_patterns(); - configure_vm_mounts_bypass_guest_fs_write_policy(); - guest_filesystem_link_and_truncate_preserve_hard_link_semantics(); - configure_vm_sensitive_mounts_bypass_guest_fs_mount_sensitive_policy(); - guest_mount_request_default_deny_rejects_without_changing_operator_mounts(); - scoped_host_filesystem_unscoped_target_requires_exact_guest_root_prefix(); - scoped_host_filesystem_realpath_preserves_paths_outside_guest_root(); - host_filesystem_realpath_fails_closed_on_circular_symlinks(); - configure_vm_host_dir_plugin_fails_closed_for_escape_symlinks(); - execute_starts_python_runtime_instead_of_rejecting_it(); - command_resolution_executes_wasm_command_from_sidecar_path(); - wasm_command_timeout_is_enforced_by_sidecar_poll_path(); - wasm_fd_write_sync_rpc_keeps_stdout_isolated_per_vm(); - wasm_path_open_read_goes_through_kernel_filesystem_permissions(); - wasm_path_open_write_goes_through_kernel_filesystem_permissions(); - wasm_fd_write_sync_rpc_routes_stdout_into_kernel_pty(); - javascript_child_process_searches_path_for_mounted_wasm_commands(); - javascript_child_process_shell_mode_without_guest_sh_fails_loudly(); - javascript_child_process_spawns_path_resolved_binding_commands(); - javascript_child_process_resolves_path_resolved_binding_commands_as_bindings(); - javascript_child_process_spawns_internal_binding_command_paths(); - javascript_child_process_resolves_internal_binding_command_paths_as_bindings(); - bindings_register_host_callbacks_rejects_duplicate_names_without_replacing_existing_bindingkit(); - bindings_register_host_callbacks_rejects_registry_overflow_without_mutating_vm(); - bindings_register_host_callbacks_rejects_total_binding_overflow_without_mutating_vm(); - bindings_javascript_child_process_denies_host_callback_without_permission(); - bindings_javascript_child_process_invokes_binding_with_matching_permission(); - bindings_javascript_child_process_rejects_invalid_json_file_input_before_dispatch(); - bindings_javascript_child_process_accepts_valid_json_input(); - command_resolution_executes_javascript_path_command_with_sidecar_mappings(); - command_resolution_executes_node_eval_command(); - command_resolution_rejects_unknown_command(); - python_vfs_rpc_requests_proxy_into_the_vm_kernel_filesystem(); - javascript_sync_rpc_requests_proxy_into_the_vm_kernel_filesystem(); - javascript_fs_promises_hot_metadata_ops_use_sync_semantics(); - python_vfs_rpc_paths_resolve_textually_and_defer_to_kernel_confinement(); - javascript_fs_sync_rpc_resolves_proc_self_against_the_kernel_process(); - javascript_fd_and_stream_rpc_requests_proxy_into_the_vm_kernel_filesystem(); - javascript_mapped_tmp_open_wx_uses_exclusive_create_once(); - wasm_shell_external_stdout_redirect_writes_file(); - wasm_shell_external_append_redirect_creates_and_concatenates(); - wasm_shell_external_stderr_redirect_writes_file(); - wasm_shell_builtin_and_external_redirects_match(); - javascript_imports_guest_written_modules_after_miss_work(); - javascript_fs_promises_batch_requests_before_waiting_on_sidecar_responses(); - javascript_crypto_basic_sync_rpcs_round_trip_through_sidecar(); - javascript_crypto_advanced_sync_rpcs_round_trip_through_sidecar(); - javascript_sqlite_sync_rpcs_round_trip_and_persist_vm_files(); - javascript_sqlite_builtin_round_trips_through_sidecar_sync_rpc(); - javascript_net_rpc_connects_over_vm_loopback(); - javascript_dgram_rpc_sends_and_receives_vm_loopback_packets(); - javascript_dns_rpc_resolves_localhost(); - javascript_network_ssrf_protection_blocks_private_dns_and_unowned_loopback_targets(); - javascript_dns_rpc_honors_vm_dns_overrides_and_net_connect_uses_sidecar_dns(); - javascript_network_dns_resolve_supports_standard_rrtypes(); - javascript_network_permission_callbacks_fire_for_dns_lookup_connect_and_listen(); - javascript_network_permission_denials_surface_eacces_to_guest_code(); - javascript_tls_rpc_connects_and_serves_over_guest_net(); - javascript_http_listen_and_close_registers_server(); - javascript_http_respond_records_pending_response(); - javascript_http_respond_rejects_oversized_pending_response(); - vm_fetch_response_frame_limit_counts_protocol_overhead(); - request_frame_limit_counts_generated_wire_overhead(); - javascript_http2_listen_connect_request_and_respond_round_trip(); - javascript_http2_guest_h2c_round_trip_does_not_deadlock(); - javascript_http2_request_handler_round_trip_runs_twice_in_one_vm(); - javascript_http2_settings_pause_push_and_file_response_surfaces_work(); - javascript_http2_secure_listen_connect_request_and_respond_round_trip(); - javascript_http2_server_respond_records_pending_response(); - javascript_http_rpc_requests_gets_and_serves_over_guest_net(); - javascript_http_external_get_reaches_host_listener(); - javascript_fetch_posts_to_guest_loopback_http_server(); - javascript_fetch_reaches_http_server_in_parallel_guest_process(); - javascript_net_rpc_listens_accepts_connections_and_reports_listener_state(); - javascript_net_rpc_reports_connection_counts_and_enforces_backlog(); - javascript_network_bind_policy_restricts_hosts_and_ports(); - javascript_network_bind_policy_can_allow_privileged_guest_ports(); - javascript_network_listeners_are_isolated_per_vm_even_with_same_guest_port(); - javascript_net_rpc_listens_and_connects_over_unix_domain_sockets(); - javascript_child_process_rpc_spawns_nested_node_processes_inside_vm_kernel(); - javascript_child_process_rpc_preserves_nested_sigchld_registrations(); - process_event_sender_is_bounded(); - configured_protocol_queue_limits_drive_admission_and_gauges(); - pending_process_events_are_bounded(); - process_event_receiver_overflow_preserves_queued_event(); - binding_execution_event_overflow_is_reported(); - wasm_signal_queue_is_bounded(); - poll_event_rechecks_durable_queue_after_pump(); - descendant_transfer_overflow_preserves_global_queue(); - descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes(); - exit_trailing_requeue_preserves_exit_when_queue_is_full(); - javascript_child_process_poll_reports_echild_when_child_disappears_after_drain(); - javascript_child_process_internal_bootstrap_env_is_allowlisted(); - javascript_net_poll_clamps_guest_wait_to_sidecar_ceiling(); - javascript_net_poll_timeout_does_not_block_concurrent_vm_dispose(); + macro_rules! service_case { + ($case:ident) => {{ + eprintln!("service suite: start {}", stringify!($case)); + $case(); + eprintln!("service suite: pass {}", stringify!($case)); + }}; + } + + service_case!(kernel_socket_queries_ignore_stale_sidecar_guest_addresses); + service_case!(find_listener_rejects_without_network_inspect_permission); + service_case!(find_listener_returns_listener_with_network_inspect_permission); + service_case!(find_bound_udp_rejects_without_network_inspect_permission); + service_case!(find_bound_udp_returns_socket_with_network_inspect_permission); + service_case!(get_process_snapshot_rejects_without_process_inspect_permission); + service_case!(get_process_snapshot_returns_processes_with_process_inspect_permission); + service_case!(get_resource_snapshot_rejects_without_process_inspect_permission); + service_case!(get_resource_snapshot_returns_kernel_and_queue_counts_with_process_inspect_permission); + service_case!(capability_registry_ignores_duplicate_sidecar_kernel_entries); + service_case!(loopback_tls_transport_survives_concurrent_handshakes_without_panicking); + service_case!(loopback_tls_endpoint_read_survives_competing_drain_and_peer_drop); + service_case!(javascript_net_socket_wait_connect_reports_tcp_socket_info); + service_case!(javascript_net_socket_read_and_socket_options_work_for_tcp_sockets); + service_case!(javascript_net_cross_exec_loopback_routes_through_kernel_socket_table); + service_case!(javascript_net_upgrade_socket_aliases_use_tcp_socket_state); + service_case!(javascript_dgram_address_and_buffer_size_sync_rpcs_work); + service_case!(javascript_tls_client_upgrade_query_and_cipher_list_work); + service_case!(javascript_tls_server_client_hello_and_server_upgrade_work); + service_case!(javascript_net_server_accept_returns_timeout_then_pending_connection); + service_case!(javascript_kernel_stdin_reads_buffered_input_and_reports_timeout_and_eof); + service_case!(javascript_sync_rpc_pty_set_raw_mode_toggles_kernel_tty_discipline); + service_case!(dispose_vm_removes_per_vm_javascript_import_cache_directory); + service_case!(execution_dispose_vm_race_skips_stale_process_events_without_panicking); + service_case!(execution_javascript_sync_rpc_handler_ignores_stale_vm_and_process_races); + service_case!( + execution_poll_event_smoke_skips_queued_stale_process_envelopes_after_dispose + ); + service_case!(execution_poll_event_concurrent_dispose_logs_stale_process_event); + service_case!(filesystem_requests_ignore_stale_vm_and_process_races); + service_case!(get_zombie_timer_count_reports_kernel_state_before_and_after_waitpid); + service_case!(parse_signal_accepts_full_guest_signal_table); + service_case!(runtime_child_liveness_only_tracks_owned_children); + service_case!(authenticated_connection_id_returns_error_for_unexpected_response); + service_case!(opened_session_id_returns_error_for_unexpected_response); + service_case!(created_vm_id_returns_error_for_unexpected_response); + service_case!(configure_vm_instantiates_memory_mounts_through_the_plugin_registry); + service_case!(configure_vm_applies_read_only_mount_wrappers); + service_case!(configure_vm_instantiates_host_dir_mounts_through_the_plugin_registry); + service_case!(configure_vm_passes_resource_read_limits_to_host_dir_mounts); + service_case!(configure_vm_passes_resource_read_limits_to_module_access_mounts); + service_case!(configure_vm_rejects_module_access_root_symlink_to_non_node_modules); + service_case!( + configure_vm_js_bridge_mount_dispatches_filesystem_calls_via_sidecar_requests + ); + service_case!(configure_vm_js_bridge_mount_rejects_oversized_read_payloads); + service_case!( + configure_vm_js_bridge_mount_rejects_pread_payloads_above_requested_length + ); + service_case!(configure_vm_js_bridge_mount_maps_callback_errors_to_errno_codes); + service_case!( + configure_vm_js_bridge_mount_readdir_of_mount_root_survives_broken_driver_realpath + ); + service_case!( + configure_vm_instantiates_sandbox_agent_mounts_through_the_plugin_registry + ); + service_case!(configure_vm_instantiates_s3_mounts_through_the_plugin_registry); + service_case!(object_s3_mount_plugin_is_not_registered); + service_case!( + configure_vm_instantiates_chunked_local_mounts_through_the_plugin_registry + ); + service_case!(bridge_permissions_map_symlink_operations_to_symlink_access); + service_case!(vm_limits_config_reads_filesystem_limits); + service_case!(create_vm_applies_filesystem_permission_descriptors_to_kernel_access); + service_case!(create_vm_without_permissions_defaults_to_static_deny_all); + service_case!(configure_vm_rollback_restore_failure_falls_back_to_static_deny_all); + service_case!(binding_registration_rollback_restore_failure_keeps_registry_consistent); + service_case!(create_vm_rejects_permission_rules_with_empty_operations); + service_case!(configure_vm_rejects_permission_rules_with_empty_paths_or_patterns); + service_case!(configure_vm_mounts_bypass_guest_fs_write_policy); + service_case!(guest_filesystem_link_and_truncate_preserve_hard_link_semantics); + service_case!(configure_vm_sensitive_mounts_bypass_guest_fs_mount_sensitive_policy); + service_case!( + guest_mount_request_default_deny_rejects_without_changing_operator_mounts + ); + service_case!(scoped_host_filesystem_unscoped_target_requires_exact_guest_root_prefix); + service_case!(scoped_host_filesystem_realpath_preserves_paths_outside_guest_root); + service_case!(host_filesystem_realpath_fails_closed_on_circular_symlinks); + service_case!(configure_vm_host_dir_plugin_fails_closed_for_escape_symlinks); + service_case!(execute_starts_python_runtime_instead_of_rejecting_it); + service_case!(command_resolution_executes_wasm_command_from_sidecar_path); + service_case!(wasm_command_timeout_is_enforced_by_sidecar_poll_path); + service_case!(wasm_fd_write_sync_rpc_keeps_stdout_isolated_per_vm); + service_case!(wasm_path_open_read_goes_through_kernel_filesystem_permissions); + service_case!(wasm_path_open_write_goes_through_kernel_filesystem_permissions); + service_case!(wasm_fd_write_sync_rpc_routes_stdout_into_kernel_pty); + service_case!(javascript_child_process_searches_path_for_mounted_wasm_commands); + service_case!(javascript_child_process_shell_mode_without_guest_sh_fails_loudly); + service_case!(javascript_child_process_spawns_path_resolved_binding_commands); + service_case!( + javascript_child_process_resolves_path_resolved_binding_commands_as_bindings + ); + service_case!(javascript_child_process_spawns_internal_binding_command_paths); + service_case!( + javascript_child_process_resolves_internal_binding_command_paths_as_bindings + ); + service_case!(bindings_register_host_callbacks_rejects_duplicate_names_without_replacing_existing_bindingkit); + service_case!( + bindings_register_host_callbacks_rejects_registry_overflow_without_mutating_vm + ); + service_case!( + bindings_register_host_callbacks_rejects_total_binding_overflow_without_mutating_vm + ); + service_case!( + bindings_javascript_child_process_denies_host_callback_without_permission + ); + service_case!( + bindings_javascript_child_process_invokes_binding_with_matching_permission + ); + service_case!( + bindings_javascript_child_process_rejects_invalid_json_file_input_before_dispatch + ); + service_case!(bindings_javascript_child_process_accepts_valid_json_input); + service_case!( + command_resolution_executes_javascript_path_command_with_sidecar_mappings + ); + service_case!(command_resolution_executes_node_eval_command); + service_case!(command_resolution_rejects_unknown_command); + service_case!(common_host_filesystem_operations_use_the_vm_kernel_source_of_truth); + service_case!(javascript_sync_rpc_requests_proxy_into_the_vm_kernel_filesystem); + service_case!(javascript_fs_promises_hot_metadata_ops_use_sync_semantics); + service_case!(javascript_fs_sync_rpc_resolves_proc_self_against_the_kernel_process); + service_case!( + javascript_fd_and_stream_rpc_requests_proxy_into_the_vm_kernel_filesystem + ); + service_case!(javascript_kernel_tmp_open_wx_uses_exclusive_create_once); + service_case!(wasm_shell_external_stdout_redirect_writes_file); + service_case!(wasm_shell_external_append_redirect_creates_and_concatenates); + service_case!(wasm_shell_external_stderr_redirect_writes_file); + service_case!(wasm_shell_builtin_and_external_redirects_match); + service_case!(javascript_imports_guest_written_modules_after_miss_work); + service_case!( + javascript_fs_promises_batch_requests_before_waiting_on_sidecar_responses + ); + service_case!(javascript_crypto_basic_sync_rpcs_round_trip_through_sidecar); + service_case!(javascript_crypto_advanced_sync_rpcs_round_trip_through_sidecar); + service_case!(javascript_sqlite_sync_rpcs_round_trip_and_persist_vm_files); + service_case!(javascript_sqlite_builtin_round_trips_through_sidecar_sync_rpc); + service_case!(javascript_net_rpc_connects_over_vm_loopback); + service_case!(javascript_dgram_rpc_sends_and_receives_vm_loopback_packets); + service_case!(javascript_dns_rpc_resolves_localhost); + service_case!( + javascript_network_ssrf_protection_blocks_private_dns_and_unowned_loopback_targets + ); + service_case!( + javascript_dns_rpc_honors_vm_dns_overrides_and_net_connect_uses_sidecar_dns + ); + service_case!(javascript_network_dns_resolve_supports_standard_rrtypes); + service_case!( + javascript_network_permission_callbacks_fire_for_dns_lookup_connect_and_listen + ); + service_case!(javascript_network_permission_denials_surface_eacces_to_guest_code); + service_case!(javascript_tls_rpc_connects_and_serves_over_guest_net); + service_case!(javascript_http_listen_and_close_registers_server); + service_case!(javascript_http_respond_records_pending_response); + service_case!(javascript_http_respond_rejects_oversized_pending_response); + service_case!(vm_fetch_response_frame_limit_counts_protocol_overhead); + service_case!(request_frame_limit_counts_generated_wire_overhead); + service_case!(javascript_http2_listen_connect_request_and_respond_round_trip); + service_case!(javascript_http2_guest_h2c_round_trip_does_not_deadlock); + service_case!(javascript_http2_request_handler_round_trip_runs_twice_in_one_vm); + service_case!(javascript_http2_settings_pause_push_and_file_response_surfaces_work); + service_case!(javascript_http2_secure_listen_connect_request_and_respond_round_trip); + service_case!(javascript_http2_server_respond_records_pending_response); + service_case!(javascript_http_rpc_requests_gets_and_serves_over_guest_net); + service_case!(javascript_http_external_get_reaches_host_listener); + service_case!(javascript_fetch_posts_to_guest_loopback_http_server); + service_case!(javascript_fetch_reaches_http_server_in_parallel_guest_process); + service_case!( + javascript_net_rpc_listens_accepts_connections_and_reports_listener_state + ); + service_case!(javascript_net_rpc_reports_connection_counts_and_enforces_backlog); + service_case!(javascript_network_bind_policy_restricts_hosts_and_ports); + service_case!(javascript_network_bind_policy_can_allow_privileged_guest_ports); + service_case!( + javascript_network_listeners_are_isolated_per_vm_even_with_same_guest_port + ); + service_case!(javascript_net_rpc_listens_and_connects_over_unix_domain_sockets); + service_case!( + javascript_child_process_rpc_spawns_nested_node_processes_inside_vm_kernel + ); + service_case!(javascript_child_process_rpc_preserves_nested_sigchld_registrations); + service_case!(process_event_sender_is_bounded); + service_case!(configured_protocol_queue_limits_drive_admission_and_gauges); + service_case!(pending_process_events_are_bounded); + service_case!(process_event_receiver_overflow_preserves_queued_event); + service_case!(binding_execution_event_overflow_is_reported); + service_case!(wasm_signal_queue_is_bounded); + service_case!(poll_event_rechecks_durable_queue_after_pump); + service_case!(descendant_transfer_overflow_preserves_global_queue); + service_case!( + descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes + ); + service_case!(exit_trailing_requeue_preserves_exit_when_queue_is_full); + service_case!( + javascript_child_process_poll_reports_echild_when_child_disappears_after_drain + ); + service_case!(javascript_child_process_internal_bootstrap_env_is_allowlisted); + service_case!(javascript_net_poll_clamps_guest_wait_to_sidecar_ceiling); + service_case!(javascript_net_poll_timeout_does_not_block_concurrent_vm_dispose); } #[test] fn service_sidecar_response_completion_is_bounded() { - completed_sidecar_responses_evict_oldest_beyond_cap(); + completed_sidecar_responses_reject_beyond_cap_without_eviction(); taking_sidecar_responses_releases_completion_gauge(); } @@ -25490,13 +25911,13 @@ try { } #[test] - fn service_dirty_host_shadow_sync_precedes_spawn_actions() { - dirty_host_shadow_sync_precedes_top_level_and_nested_spawn_actions(); + fn service_posix_spawnp_path_and_recursive_shebang_match_linux() { + posix_spawnp_path_and_recursive_shebang_match_linux(); } #[test] - fn service_posix_spawnp_path_and_recursive_shebang_match_linux() { - posix_spawnp_path_and_recursive_shebang_match_linux(); + fn service_guest_exact_wasm_exec_enforces_kernel_execute_dac() { + guest_exact_wasm_exec_rejects_non_executable_kernel_image_with_eacces(); } #[test] @@ -25504,6 +25925,11 @@ try { repeated_malformed_wasm_spawns_restore_top_level_and_nested_baselines(); } + #[test] + fn service_child_sync_post_spawn_failures_are_transactional() { + child_sync_post_spawn_failures_are_transactional_at_root_and_nested_depths(); + } + #[test] fn service_state_handle_tables_are_bounded() { sqlite_database_handles_are_bounded(); @@ -25622,6 +26048,16 @@ try { run_isolated_service_test("javascript-fs-promises-hot-metadata"); } + #[test] + fn javascript_fs_promises_batch_requests_before_waiting_regression() { + run_isolated_service_test("javascript-fs-promises-batch"); + } + + #[test] + fn javascript_child_process_nested_sigchld_regression() { + run_isolated_service_test("javascript-child-process-nested-sigchld"); + } + #[test] fn wasm_shell_external_stdout_redirect_writes_file_regression() { run_isolated_service_test("wasm-shell-external-stdout-redirect"); @@ -25643,23 +26079,23 @@ try { } #[test] - fn javascript_mapped_shadow_readdir_sees_wasm_created_directory_regression() { - run_isolated_service_test("mapped-shadow-readdir-wasm-directory"); + fn javascript_kernel_readdir_sees_wasm_created_directory_regression() { + run_isolated_service_test("kernel-readdir-wasm-directory"); } #[test] - fn javascript_mapped_shadow_readdir_merges_wasm_created_children_regression() { - run_isolated_service_test("mapped-shadow-readdir-wasm-children"); + fn javascript_kernel_readdir_sees_wasm_created_children_regression() { + run_isolated_service_test("kernel-readdir-wasm-children"); } #[test] - fn javascript_mapped_shadow_readdir_unions_shadow_and_kernel_children_regression() { - run_isolated_service_test("mapped-shadow-readdir-shadow-kernel-union"); + fn javascript_readdir_observes_authoritative_kernel_children_regression() { + run_isolated_service_test("kernel-readdir-authoritative-state"); } #[test] - fn javascript_mapped_shadow_readdir_sees_same_process_shadow_directory_regression() { - run_isolated_service_test("mapped-shadow-readdir-same-process-shadow"); + fn javascript_kernel_readdir_sees_same_process_directory_regression() { + run_isolated_service_test("kernel-readdir-same-process"); } #[test] @@ -25682,6 +26118,16 @@ try { run_isolated_service_test("wasm-command-timeout"); } + #[test] + fn aab_managed_wasm_pipe_is_kernel_owned_despite_guest_env_spoofing() { + run_isolated_service_test("managed-wasm-kernel-pipe"); + } + + #[test] + fn aab_managed_wasm_descendant_pipe_completes_eof_hup_exit_and_cleanup() { + run_isolated_service_test("managed-wasm-descendant-pipe-eof"); + } + #[test] fn aab_wasm_path_open_read_uses_kernel_filesystem_permissions() { run_isolated_service_test("wasm-fs-permissions"); @@ -25798,6 +26244,15 @@ try { "javascript-fs-promises-hot-metadata" => { javascript_fs_promises_hot_metadata_ops_use_sync_semantics(); } + "javascript-fs-promises-batch" => { + javascript_fs_promises_batch_requests_before_waiting_on_sidecar_responses(); + } + "javascript-child-process-nested-sigchld" => { + javascript_child_process_rpc_preserves_nested_sigchld_registrations(); + } + "javascript-fd-stream-rpc" => { + javascript_fd_and_stream_rpc_requests_proxy_into_the_vm_kernel_filesystem(); + } "javascript-pty-raw-mode" => { javascript_sync_rpc_pty_set_raw_mode_toggles_kernel_tty_discipline(); } @@ -25813,20 +26268,20 @@ try { "wasm-shell-builtin-external-redirect-parity" => { wasm_shell_builtin_and_external_redirects_match(); } - "mapped-shadow-readdir-wasm-directory" => { - javascript_mapped_shadow_readdir_sees_wasm_created_directory(); + "kernel-readdir-wasm-directory" => { + javascript_kernel_readdir_sees_wasm_created_directory(); } - "mapped-shadow-readdir-wasm-children" => { - javascript_mapped_shadow_readdir_merges_wasm_created_children(); + "kernel-readdir-wasm-children" => { + javascript_kernel_readdir_sees_wasm_created_children(); } - "mapped-shadow-readdir-shadow-kernel-union" => { - javascript_mapped_shadow_readdir_unions_shadow_and_kernel_children(); + "kernel-readdir-authoritative-state" => { + javascript_readdir_observes_authoritative_kernel_children(); } - "mapped-shadow-readdir-same-process-shadow" => { - javascript_mapped_shadow_readdir_sees_same_process_shadow_directory(); + "kernel-readdir-same-process" => { + javascript_kernel_readdir_sees_same_process_directory(); } "mapped-unlink-kernel-backed-no-resurrect" => { - javascript_mapped_unlink_of_kernel_backed_file_does_not_resurrect(); + javascript_unlink_of_kernel_file_is_immediately_authoritative(); } "javascript-readdir-raw-dirent-semantics" => { javascript_readdir_raw_payload_preserves_dirent_semantics(); @@ -25837,6 +26292,12 @@ try { "wasm-command-timeout" => { wasm_command_timeout_is_enforced_by_sidecar_poll_path(); } + "managed-wasm-kernel-pipe" => { + managed_wasm_pipe_is_kernel_owned_despite_guest_env_spoofing(); + } + "managed-wasm-descendant-pipe-eof" => { + managed_wasm_descendant_pipe_publishes_data_eof_hup_exit_and_cleanup(); + } "wasm-fs-permissions" => { wasm_path_open_read_goes_through_kernel_filesystem_permissions(); } diff --git a/crates/native-sidecar/tests/signal.rs b/crates/native-sidecar/tests/signal.rs index 6f2d890880..ba429e8b0a 100644 --- a/crates/native-sidecar/tests/signal.rs +++ b/crates/native-sidecar/tests/signal.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant}; use support::{ assert_node_available, authenticate_wire, collect_process_output_wire_with_timeout, create_vm_wire_with_metadata, execute_wire, new_sidecar, open_session_wire, temp_dir, - wire_request, wire_vm, write_fixture, + wire_request, wire_vm, write_fixture, write_guest_file_wire, }; fn wait_for_process_output( @@ -128,7 +128,6 @@ fn embedded_runtime_signal_routes_sigterm_and_process_kill() { &cwd, HashMap::new(), ); - execute_wire( &mut sidecar, 4, @@ -266,7 +265,6 @@ fn embedded_runtime_signal_stop_continue_updates_kernel_state_and_guest_handler( &cwd, HashMap::new(), ); - execute_wire( &mut sidecar, 4, @@ -630,6 +628,15 @@ fn embedded_runtime_process_group_kill_terminates_detached_tree() { &cwd, HashMap::new(), ); + write_guest_file_wire( + &mut sidecar, + 3_001, + &connection_id, + &session_id, + &vm_id, + "/group-child.mjs", + std::fs::read_to_string(&child_entry).expect("read process-group child fixture"), + ); execute_wire( &mut sidecar, @@ -791,6 +798,15 @@ fn pty_resize_delivers_sigwinch_to_nested_foreground_runtime() { allowed_builtins, )]), ); + write_guest_file_wire( + &mut sidecar, + 3_001, + &connection_id, + &session_id, + &vm_id, + "/child.mjs", + std::fs::read_to_string(&child_entry).expect("read PTY child fixture"), + ); let ownership = wire_vm(&connection_id, &session_id, &vm_id); let started = sidecar .dispatch_wire_blocking(wire_request( @@ -937,6 +953,15 @@ fn embedded_runtime_signal_delivers_sigchld_on_child_exit() { allowed_builtins, )]), ); + write_guest_file_wire( + &mut sidecar, + 3_001, + &connection_id, + &session_id, + &vm_id, + "/child.mjs", + std::fs::read_to_string(&child_entry).expect("read SIGCHLD child fixture"), + ); execute_wire( &mut sidecar, diff --git a/crates/native-sidecar/tests/socket_state_queries.rs b/crates/native-sidecar/tests/socket_state_queries.rs index 0a24825a34..5b69c274f7 100644 --- a/crates/native-sidecar/tests/socket_state_queries.rs +++ b/crates/native-sidecar/tests/socket_state_queries.rs @@ -12,7 +12,7 @@ use std::time::{Duration, Instant}; use support::{ assert_node_available, authenticate_wire, create_vm_wire_with_metadata, execute_wire, new_sidecar, open_session_wire, temp_dir, wasm_signal_state_module, wire_request, wire_vm, - write_fixture, + write_fixture, write_guest_file_wire, }; fn wait_for_process_output( @@ -693,6 +693,15 @@ fn sidecar_tracks_javascript_sigchld_and_delivers_it_on_child_exit() { allowed_builtins, )]), ); + write_guest_file_wire( + &mut sidecar, + 3_001, + &connection_id, + &session_id, + &vm_id, + "/child.mjs", + fs::read_to_string(&child_entry).expect("read SIGCHLD child fixture"), + ); execute_wire( &mut sidecar, diff --git a/crates/native-sidecar/tests/stdio_binary.rs b/crates/native-sidecar/tests/stdio_binary.rs index ec9ee68b72..a93e18d093 100644 --- a/crates/native-sidecar/tests/stdio_binary.rs +++ b/crates/native-sidecar/tests/stdio_binary.rs @@ -744,11 +744,9 @@ fn native_sidecar_binary_runs_the_framed_protocol_over_stdio() { wire_request( 13, wire_vm(&connection_id, &session_id, &vm_id), - RequestPayload::SnapshotRootFilesystemRequest( - agentos_native_sidecar::wire::SnapshotRootFilesystemRequest { - max_bytes: 64 * 1024 * 1024, - }, - ), + RequestPayload::SnapshotRootFilesystemRequest(SnapshotRootFilesystemRequest { + max_bytes: 64 * 1024 * 1024, + }), ), ); let snapshot = recv_response(&mut control, &codec, 13, &mut buffered_events); @@ -990,8 +988,8 @@ fn native_sidecar_binary_supports_js_bridge_host_filesystem_access() { js_bridge_result(call, Some(json!("/existing.txt")), None) } ("stat" | "lstat", "/existing.txt") => { - let metadata = - fs::metadata(host_root.join("existing.txt")).expect("stat host file"); + let metadata = fs::metadata(host_root.join("existing.txt")) + .expect("stat existing host file"); js_bridge_result( call, Some(json!({ diff --git a/crates/native-sidecar/tests/support/mod.rs b/crates/native-sidecar/tests/support/mod.rs index 7e9411410f..e389b6f271 100644 --- a/crates/native-sidecar/tests/support/mod.rs +++ b/crates/native-sidecar/tests/support/mod.rs @@ -125,6 +125,50 @@ pub fn wire_vm( ) } +pub fn write_guest_file_wire( + sidecar: &mut NativeSidecar, + request_id: agentos_native_sidecar::wire::RequestId, + connection_id: &str, + session_id: &str, + vm_id: &str, + path: &str, + contents: impl Into, +) { + let result = sidecar + .dispatch_wire_blocking(wire_request( + request_id, + wire_vm(connection_id, session_id, vm_id), + agentos_native_sidecar::wire::RequestPayload::GuestFilesystemCallRequest( + agentos_native_sidecar::wire::GuestFilesystemCallRequest { + operation: agentos_native_sidecar::wire::GuestFilesystemOperation::WriteFile, + path: path.to_owned(), + destination_path: None, + target: None, + content: Some(contents.into()), + encoding: Some(agentos_native_sidecar::wire::RootFilesystemEntryEncoding::Utf8), + recursive: false, + max_depth: None, + mode: None, + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + }, + ), + )) + .expect("write guest fixture through wire filesystem API"); + assert!( + matches!( + &result.response.payload, + agentos_native_sidecar::wire::ResponsePayload::GuestFilesystemResultResponse(_) + ), + "unexpected guest fixture write response: {:?}", + result.response.payload + ); +} + pub fn wire_permissions_allow_all() -> agentos_native_sidecar::wire::PermissionsPolicy { agentos_native_sidecar::wire::PermissionsPolicy { fs: Some( @@ -729,6 +773,7 @@ pub fn wasm_signal_state_module() -> Vec { (import "host_process" "proc_sigaction" (func $proc_sigaction (type $proc_sigaction_t))) (memory (export "memory") 1) (data (i32.const 32) "signal:ready\n") + (func (export "__wasi_signal_trampoline") (param i32)) (func $_start (export "_start") (drop (call $proc_sigaction diff --git a/crates/native-sidecar/tests/wasm_raw_abi.rs b/crates/native-sidecar/tests/wasm_raw_abi.rs new file mode 100644 index 0000000000..95e4096e0e --- /dev/null +++ b/crates/native-sidecar/tests/wasm_raw_abi.rs @@ -0,0 +1,1468 @@ +mod support; + +use agentos_native_sidecar::wire::{ + ExecuteRequest, GuestRuntimeKind, RequestPayload, ResponsePayload, WasmPermissionTier, +}; +use agentos_wasm_abi_generator::{ + imports_module, raw_call_assertion_module, single_import_module, AbiImport, AbiManifest, + CallArguments, RawCallAssertion, +}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::time::Duration; +use support::{ + assert_node_available, authenticate_wire, collect_process_output_wire_with_timeout, + new_sidecar, open_session_wire, temp_dir, wire_request, wire_vm, write_fixture, +}; + +const ABI_MANIFEST: &str = include_str!("../../execution/assets/agentos-wasm-abi.json"); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum MemoryContractObligation { + InvalidInputRange, + WrappedRange, + InvalidOutputRange, + AggregateCollectionBound, + ShortOutput, + AtomicCopyout, + NoSideEffect, +} + +/// A manifest import whose signature and behavior witness one or more raw-memory +/// obligations. More than one witness may share a signature because identical +/// WebAssembly types can have different semantic directions (for example, +/// scalar inputs, an input byte range, or two output pointers). +struct MemoryContractWitness { + module: &'static str, + name: &'static str, + obligations: &'static [MemoryContractObligation], +} + +const MEMORY_CONTRACT_WITNESSES: &[MemoryContractWitness] = &[ + MemoryContractWitness { + module: "host_fs", + name: "remount", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "fd_write", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::AggregateCollectionBound, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_user", + name: "getpwuid", + obligations: &[ + MemoryContractObligation::ShortOutput, + MemoryContractObligation::AtomicCopyout, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "fd_getxattr", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "path_removexattr", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "random_get", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "fd_pipe", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_user", + name: "setgroups", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::AggregateCollectionBound, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_user", + name: "getresuid", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + ], + }, + MemoryContractWitness { + module: "host_tty", + name: "set_attr", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "fd_socketpair", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_net", + name: "net_send", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_net", + name: "net_recv", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_user", + name: "getuid", + obligations: &[MemoryContractObligation::InvalidOutputRange], + }, + // This is the sole memory-bearing signature with an i64 result and is + // exercised by `raw_i64_path_input_contract_module` below. + MemoryContractWitness { + module: "host_fs", + name: "path_size", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "path_getxattr", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "path_setxattr", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_net", + name: "net_getaddrinfo", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "fd_sendmsg_rights", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::AggregateCollectionBound, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "path_mknod", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_fs", + name: "path_statfs", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + ], + }, + MemoryContractWitness { + module: "host_net", + name: "net_dns_query_rr_v1", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "fd_record_lock", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_itimer_real", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_ppoll_v1", + obligations: &[ + MemoryContractObligation::AggregateCollectionBound, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_spawn", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_spawn_v2", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_spawn_v3", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "host_process", + name: "proc_spawn_v4", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "clock_time_get", + obligations: &[MemoryContractObligation::InvalidOutputRange], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "fd_pread", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "fd_seek", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "path_filestat_set_times", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "path_open", + obligations: &[ + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ], + }, + MemoryContractWitness { + module: "wasi_snapshot_preview1", + name: "path_open", + obligations: &[ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::NoSideEffect, + ], + }, +]; + +/// Representatives for the six raw signature shapes which carry no guest +/// memory. They remain covered by the all-import invocation test (and proc_exit +/// has its dedicated terminal-call test), but deliberately are not padded with +/// meaningless pointer assertions. +const NON_MEMORY_SIGNATURE_WITNESSES: &[(&str, &str)] = &[ + ("host_fs", "fd_size"), + ("host_process", "proc_setrlimit"), + ("host_fs", "fd_zero_range"), + ("host_fs", "ftruncate"), + ("wasi_snapshot_preview1", "proc_exit"), + ("wasi_snapshot_preview1", "sched_yield"), +]; + +fn abi_signature(import: &AbiImport) -> String { + format!( + "({})->({})", + import.params.join(","), + import.results.join(",") + ) +} + +fn manifest_import<'a>(manifest: &'a AbiManifest, module: &str, name: &str) -> &'a AbiImport { + manifest + .imports + .iter() + .find(|import| import.module == module && import.name == name) + .unwrap_or_else(|| panic!("missing ABI import {module}.{name}")) +} + +fn run_raw_module(name: &str, module: &[u8], tier: WasmPermissionTier) -> (String, String, i32) { + run_raw_module_with_metadata(name, module, tier, HashMap::new()) +} + +fn run_raw_module_with_metadata( + name: &str, + module: &[u8], + tier: WasmPermissionTier, + metadata: HashMap, +) -> (String, String, i32) { + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("raw-abi.wasm"); + write_fixture(&entrypoint, module); + let connection_id = authenticate_wire(&mut sidecar, "conn-raw-abi"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + metadata, + ); + let process_id = format!("process-{name}"); + let started = sidecar + .dispatch_wire_blocking(wire_request( + 4, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.clone(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: Some(tier), + }), + )) + .expect("start generated raw-ABI fixture through sidecar"); + assert!( + matches!( + started.response.payload, + ResponsePayload::ProcessStartedResponse(_) + ), + "unexpected raw-ABI start response: {:?}", + started.response.payload + ); + collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &process_id, + Duration::from_secs(30), + ) +} + +#[test] +fn every_permitted_raw_abi_import_is_invoked_at_every_permission_tier() { + assert_node_available(); + let manifest = AbiManifest::parse(ABI_MANIFEST); + + for (tier_name, tier) in [ + ("isolated", WasmPermissionTier::Isolated), + ("read-only", WasmPermissionTier::ReadOnly), + ("read-write", WasmPermissionTier::ReadWrite), + ("full", WasmPermissionTier::Full), + ] { + // A fresh VM gives scalar state-mutating calls (credentials, umask, + // fd flags) no opportunity to contaminate another permission tier. + let permitted = manifest.permitted_imports(tier_name); + assert!(!permitted.is_empty(), "{tier_name} ABI must not be empty"); + let (stdout, stderr, exit_code) = run_raw_module( + &format!("wasm-raw-abi-{tier_name}"), + &imports_module(&permitted, true, CallArguments::Hostile), + tier, + ); + assert_eq!( + exit_code, 0, + "{tier_name} raw ABI invocation failed: stdout={stdout} stderr={stderr}" + ); + } +} + +#[test] +fn preview1_proc_exit_and_compatibility_alias_execute_through_sidecar() { + assert_node_available(); + let manifest = AbiManifest::parse(ABI_MANIFEST); + let proc_exit = manifest + .imports + .iter() + .find(|import| import.module == "wasi_snapshot_preview1" && import.name == "proc_exit") + .expect("Preview1 proc_exit manifest entry"); + + for module in ["wasi_snapshot_preview1", "wasi_unstable"] { + let mut import = proc_exit.clone(); + import.module = module.to_string(); + let (stdout, stderr, exit_code) = run_raw_module( + &format!("wasm-raw-abi-proc-exit-{module}"), + &single_import_module(&import, true, CallArguments::Zero), + WasmPermissionTier::Full, + ); + assert_eq!( + exit_code, 0, + "{module}.proc_exit failed through sidecar: stdout={stdout} stderr={stderr}" + ); + } +} + +fn raw_abi_memory_contract_assertions() -> Vec { + let i32c = |value: i64| format!("(i32.const {value})"); + let i64c = |value: i64| format!("(i64.const {value})"); + vec![ + // Fixed-size and multi-output destinations are prevalidated in full. + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "clock_time_get", + [i32c(0), i64c(0), i32c(65_532)], + 21, + ), + RawCallAssertion::i32( + "wasi_unstable", + "clock_time_get", + [i32c(0), i64c(0), i32c(65_532)], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "path_statfs", + [ + i32c(-1), + i32c(0), + i32c(0), + i32c(296), + i32c(304), + i32c(312), + i32c(320), + i32c(65_532), + ], + 21, + ), + RawCallAssertion::i32( + "host_net", + "net_socket", + [i32c(0), i32c(0), i32c(0), i32c(65_534)], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_getrlimit", + [i32c(7), i32c(272), i32c(65_532)], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_getrlimit", + [i32c(-1), i32c(616), i32c(624)], + 28, + ), + RawCallAssertion::i32( + "host_process", + "proc_setrlimit", + [i32c(-1), i64c(-1), i64c(-1)], + 28, + ), + RawCallAssertion::i32("host_process", "proc_kill", [i32c(-1), i32c(-1)], 28), + RawCallAssertion::i32( + "host_process", + "proc_kill", + [i32c(2_147_483_647), i32c(0)], + 71, + ), + RawCallAssertion::i32( + "host_process", + "proc_sigaction", + [i32c(-1), i32c(-1), i32c(-1), i32c(-1), i32c(-1)], + 28, + ), + // The owned raw signal ABI spans the complete two-word mask domain. + // Ignore is deliverable without a guest trampoline; a user handler is + // rejected when this raw fixture does not export one. + RawCallAssertion::i32( + "host_process", + "proc_sigaction", + [i32c(64), i32c(1), i32c(0), i32c(0), i32c(0)], + 0, + ), + RawCallAssertion::i32( + "host_process", + "proc_sigaction", + [i32c(65), i32c(1), i32c(0), i32c(0), i32c(0)], + 28, + ), + RawCallAssertion::i32( + "host_process", + "proc_sigaction", + [i32c(2), i32c(2), i32c(0), i32c(0), i32c(0)], + 58, + ), + RawCallAssertion::i32( + "host_tty", + "get_size", + [i32c(-1), i32c(288), i32c(65_535)], + 21, + ), + RawCallAssertion::i32( + "host_user", + "getresuid", + [i32c(280), i32c(284), i32c(65_534)], + 21, + ), + RawCallAssertion::i32( + "host_system", + "get_identity", + [i32c(0), i32c(65_520), i32c(32)], + 21, + ), + RawCallAssertion::i32("host_process", "fd_pipe", [i32c(65_534), i32c(640)], 21), + RawCallAssertion::i32( + "host_process", + "fd_socketpair", + [i32c(0), i32c(0), i32c(0), i32c(65_534), i32c(640)], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "fd_getxattr", + [ + i32c(-1), + i32c(0), + i32c(0), + i32c(65_520), + i32c(32), + i32c(640), + ], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "path_getxattr", + [ + i32c(-1), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(65_520), + i32c(32), + i32c(0), + i32c(640), + ], + 21, + ), + RawCallAssertion::i32( + "host_net", + "net_getaddrinfo", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_itimer_real", + [i32c(0), i64c(0), i64c(0), i32c(640), i32c(65_532)], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "fd_seek", + [i32c(-1), i64c(0), i32c(0), i32c(65_532)], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "path_open", + [ + i32c(3), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i64c(0), + i64c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + // Pointer+length wrap and OOB input ranges never reach the resource. + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "random_get", + [i32c(65_520), i32c(32)], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "remount", + [i32c(65_520), i32c(32), i32c(0), i32c(0)], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "path_removexattr", + [i32c(-1), i32c(65_520), i32c(32), i32c(0), i32c(0), i32c(0)], + 21, + ), + RawCallAssertion::i32( + "host_net", + "net_send", + [i32c(-1), i32c(65_520), i32c(32), i32c(0), i32c(640)], + 21, + ), + RawCallAssertion::i32( + "host_net", + "net_recv", + [i32c(-1), i32c(65_520), i32c(32), i32c(0), i32c(640)], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "path_setxattr", + [ + i32c(3), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(65_520), + i32c(32), + i32c(0), + i32c(0), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "fd_sendmsg_rights", + [ + i32c(-1), + i32c(65_520), + i32c(32), + i32c(0), + i32c(0), + i32c(0), + i32c(640), + ], + 21, + ), + RawCallAssertion::i32( + "host_net", + "net_dns_query_rr_v1", + [ + i32c(65_520), + i32c(32), + i32c(12), + i32c(1_024), + i32c(64), + i32c(1_088), + i32c(1_092), + i32c(1_096), + ], + 21, + ), + RawCallAssertion::i32( + "host_fs", + "path_mknod", + [i32c(-1), i32c(65_520), i32c(32), i32c(0), i64c(0)], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "fd_pread", + [i32c(-1), i32c(65_528), i32c(2), i64c(0), i32c(640)], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "path_filestat_set_times", + [ + i32c(-1), + i32c(0), + i32c(65_520), + i32c(32), + i64c(0), + i64c(0), + i32c(0), + ], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "path_open", + [ + i32c(3), + i32c(0), + i32c(65_520), + i32c(32), + i32c(0), + i64c(0), + i64c(0), + i32c(0), + i32c(640), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_spawn_v4", + [ + i32c(65_520), + i32c(32), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(512), + ], + 21, + ), + // Every spawn ABI prevalidates its result pointer. The following + // wait-any probe then proves that none of the rejected calls created a + // child before discovering the bad copyout range. + RawCallAssertion::i32( + "host_process", + "proc_spawn", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(1), + i32c(2), + i32c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_spawn_v2", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(1), + i32c(2), + i32c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_spawn_v3", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_spawn_v4", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(65_534), + ], + 21, + ), + RawCallAssertion::i32( + "host_process", + "proc_waitpid", + [i32c(-1), i32c(1), i32c(640), i32c(644)], + 12, + ), + RawCallAssertion::i32( + "host_tty", + "set_attr", + [i32c(-1), i32c(0), i32c(65_532)], + 21, + ), + RawCallAssertion::i32("host_tty", "set_size", [i32c(-1), i32c(-1), i32c(-1)], 28), + RawCallAssertion::i32("host_user", "setgroups", [i32c(1), i32c(65_534)], 21), + // Counts and decoded collections are rejected before reading tables. + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "fd_write", + [i32c(1), i32c(0), i32c(1_025), i32c(512)], + 28, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "poll_oneoff", + [i32c(0), i32c(0), i32c(1_025), i32c(512)], + 28, + ), + RawCallAssertion::i32( + "host_net", + "net_poll", + [i32c(0), i32c(-1), i32c(0), i32c(512)], + 28, + ), + RawCallAssertion::i32( + "host_process", + "proc_ppoll_v1", + [ + i32c(0), + i32c(-1), + i64c(0), + i64c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(512), + ], + 28, + ), + RawCallAssertion::i32("host_user", "setgroups", [i32c(65), i32c(65_535)], 28), + RawCallAssertion::i32( + "host_user", + "getpwnam", + [i32c(65_535), i32c(4_097), i32c(1_024), i32c(0), i32c(512)], + 37, + ), + RawCallAssertion::i32( + "host_process", + "proc_spawn_v4", + [ + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(1_048_577), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(0), + i32c(512), + ], + 1, + ), + // Open the fixture itself so F_GETLK reaches its four-output copyout. + // F_GETLK is read-only; the bad first result pointer must leave every + // later result sentinel untouched. + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "path_open", + [ + i32c(3), + i32c(0), + i32c(700), + i32c(12), + i32c(0), + i64c(0), + i64c(0), + i32c(0), + i32c(680), + ], + 0, + ), + RawCallAssertion::i32( + "host_process", + "fd_record_lock", + [ + String::from("(i32.load (i32.const 680))"), + i32c(12), + i32c(0), + i64c(0), + i64c(0), + i32c(65_534), + i32c(640), + i32c(648), + i32c(656), + ], + 21, + ), + RawCallAssertion::i32( + "wasi_snapshot_preview1", + "fd_close", + [String::from("(i32.load (i32.const 680))")], + 0, + ), + // Short account buffers publish required length without partial data. + RawCallAssertion::i32("host_user", "getuid", [i32c(65_534)], 21), + RawCallAssertion::i32("host_user", "getuid", [i32c(600)], 0), + RawCallAssertion::i32( + "host_user", + "getpwuid", + [ + String::from("(i32.load (i32.const 600))"), + i32c(1_024), + i32c(0), + i32c(512), + ], + 68, + ), + RawCallAssertion::i32( + "host_system", + "get_identity", + [i32c(0), i32c(1_024), i32c(1)], + 37, + ), + ] +} + +fn raw_i64_path_input_contract_module(manifest: &AbiManifest) -> Vec { + let path_size = manifest_import(manifest, "host_fs", "path_size"); + assert_eq!( + abi_signature(path_size), + "(i32,i32,i32,i32)->(i64)", + "path_size raw-memory witness changed signature" + ); + let proc_exit = manifest_import(manifest, "wasi_snapshot_preview1", "proc_exit"); + assert_eq!(abi_signature(proc_exit), "(i32)->()"); + + wat::parse_str( + r#" +(module + (type $path_size_t (func (param i32 i32 i32 i32) (result i64))) + (type $proc_exit_t (func (param i32))) + (import "host_fs" "path_size" (func $path_size (type $path_size_t))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (type $proc_exit_t))) + (memory (export "memory") 1) + (func (export "_start") + ;; The wrapped path range must return the ABI's i64 error sentinel without + ;; consulting the filesystem or trapping the guest. + (if + (i64.ne + (call $path_size + (i32.const 3) + (i32.const 65520) + (i32.const 32) + (i32.const 0) + ) + (i64.const -1) + ) + (then (call $proc_exit (i32.const 1)) unreachable) + ) + ) +) +"#, + ) + .expect("compile i64 raw-memory contract module") +} + +fn raw_fixed_limit_family_module() -> Vec { + wat::parse_str( + r#" +(module + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "poll_oneoff" (func $poll_oneoff (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) + (import "host_net" "net_poll" (func $net_poll (param i32 i32 i32 i32) (result i32))) + (import "host_user" "setgroups" (func $setgroups (param i32 i32) (result i32))) + (import "host_user" "getpwnam" (func $getpwnam (param i32 i32 i32 i32 i32) (result i32))) + (import "host_fs" "path_setxattr" (func $path_setxattr (param i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32))) + (memory (export "memory") 4) + (data (i32.const 120000) "missing") + (func $fail (param $code i32) + (call $proc_exit (local.get $code)) + unreachable + ) + (func (export "_start") (local $index i32) (local $result i32) + ;; A 4096-byte unknown account name and a 255-byte valid xattr name. + (memory.fill (i32.const 110000) (i32.const 97) (i32.const 4096)) + (memory.fill (i32.const 120016) (i32.const 97) (i32.const 256)) + (i32.store (i32.const 120016) (i32.const 1919251317)) ;; "user" + (i32.store8 (i32.const 120020) (i32.const 46)) ;; "." + + ;; Build 1024 inert pollfds and 64 valid supplementary group IDs. + (local.set $index (i32.const 0)) + (block $poll_done + (loop $poll_fill + (br_if $poll_done (i32.ge_u (local.get $index) (i32.const 1024))) + (i32.store + (i32.add (i32.const 8192) (i32.mul (local.get $index) (i32.const 8))) + (i32.const -1) + ) + (local.set $index (i32.add (local.get $index) (i32.const 1))) + (br $poll_fill) + ) + ) + (local.set $index (i32.const 0)) + (block $groups_done + (loop $groups_fill + (br_if $groups_done (i32.ge_u (local.get $index) (i32.const 64))) + (i32.store + (i32.add (i32.const 100000) (i32.mul (local.get $index) (i32.const 4))) + (local.get $index) + ) + (local.set $index (i32.add (local.get $index) (i32.const 1))) + (br $groups_fill) + ) + ) + + ;; Exact fixed-table boundaries are accepted; limit+1 is rejected before + ;; any table walk. Zero-filled subscriptions are immediate clock waits. + (if (i32.ne (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1024) (i32.const 18000)) (i32.const 0)) + (then (call $fail (i32.const 201)))) + (if (i32.ne (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1025) (i32.const 18000)) (i32.const 28)) + (then (call $fail (i32.const 202)))) + (if (i32.ne (call $net_poll (i32.const 8192) (i32.const 1024) (i32.const 0) (i32.const 18004)) (i32.const 0)) + (then (call $fail (i32.const 203)))) + (if (i32.ne (call $net_poll (i32.const 8192) (i32.const 1025) (i32.const 0) (i32.const 18004)) (i32.const 28)) + (then (call $fail (i32.const 204)))) + (if (i32.ne (call $poll_oneoff (i32.const 20000) (i32.const 70000) (i32.const 1024) (i32.const 103000)) (i32.const 0)) + (then (call $fail (i32.const 205)))) + (if (i32.ne (call $poll_oneoff (i32.const 20000) (i32.const 70000) (i32.const 1025) (i32.const 103000)) (i32.const 28)) + (then (call $fail (i32.const 206)))) + + ;; Fixed list/string/value caps use the same boundary and warning contract. + ;; The VM runs as a non-root identity, so the exact setgroups boundary must + ;; reach the kernel and return EPERM (63); the decoder rejects limit+1 as + ;; EINVAL (28) before that permission check. + (if (i32.ne (call $setgroups (i32.const 64) (i32.const 100000)) (i32.const 63)) + (then (call $fail (i32.const 207)))) + (if (i32.ne (call $setgroups (i32.const 65) (i32.const 100000)) (i32.const 28)) + (then (call $fail (i32.const 208)))) + (if (i32.ne (call $getpwnam (i32.const 110000) (i32.const 4096) (i32.const 0) (i32.const 0) (i32.const 115000)) (i32.const 44)) + (then (call $fail (i32.const 209)))) + (if (i32.ne (call $getpwnam (i32.const 110000) (i32.const 4097) (i32.const 0) (i32.const 0) (i32.const 115000)) (i32.const 37)) + (then (call $fail (i32.const 210)))) + (local.set $result + (call $path_setxattr + (i32.const -1) (i32.const 120000) (i32.const 7) + (i32.const 120016) (i32.const 255) + (i32.const 131072) (i32.const 65536) + (i32.const 0) (i32.const 1))) + (if (i32.ne (local.get $result) (i32.const 44)) + (then (call $fail (i32.const 211)))) + (if (i32.ne + (call $path_setxattr + (i32.const -1) (i32.const 120000) (i32.const 7) + (i32.const 120016) (i32.const 255) + (i32.const 131072) (i32.const 65537) + (i32.const 0) (i32.const 1)) + (i32.const 1)) + (then (call $fail (i32.const 212)))) + (if (i32.ne + (call $path_setxattr + (i32.const -1) (i32.const 120000) (i32.const 7) + (i32.const 120016) (i32.const 256) + (i32.const 131072) (i32.const 0) + (i32.const 0) (i32.const 1)) + (i32.const 68)) + (then (call $fail (i32.const 213)))) + ) +) +"#, + ) + .expect("compile fixed-limit family module") +} + +fn raw_blocking_read_deadline_module() -> Vec { + wat::parse_str( + r#" +(module + (import "host_net" "net_poll" (func $net_poll (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) + (memory (export "memory") 1) + (func (export "_start") + (if + (i32.ne + (call $net_poll (i32.const 0) (i32.const 0) (i32.const -1) (i32.const 16)) + (i32.const 73) + ) + (then (call $proc_exit (i32.const 221)) unreachable) + ) + ) +) +"#, + ) + .expect("compile blocking-read deadline module") +} + +#[test] +fn raw_abi_manifest_signature_families_have_auditable_memory_contracts() { + let manifest = AbiManifest::parse(ABI_MANIFEST); + let manifest_signatures = manifest + .imports + .iter() + .map(abi_signature) + .collect::>(); + assert_eq!( + manifest_signatures.len(), + 29, + "review every new raw signature shape and classify its guest-memory contract" + ); + + let assertion_imports = manifest.imports_with_aliases(); + let assertion_names = raw_abi_memory_contract_assertions() + .into_iter() + .map(|assertion| (assertion.module, assertion.name)) + .collect::>(); + let mut memory_signatures = BTreeSet::new(); + let mut obligation_witnesses = BTreeMap::>::new(); + for witness in MEMORY_CONTRACT_WITNESSES { + let import = assertion_imports + .iter() + .find(|import| import.module == witness.module && import.name == witness.name) + .unwrap_or_else(|| { + panic!( + "raw-memory witness references missing import {}.{}", + witness.module, witness.name + ) + }); + memory_signatures.insert(abi_signature(import)); + if import.results.as_slice() == ["i32"] { + assert!( + assertion_names.contains(&(witness.module.to_owned(), witness.name.to_owned())), + "memory witness {}.{} must execute in the hostile assertion module", + witness.module, + witness.name + ); + } else { + assert_eq!( + (witness.module, witness.name), + ("host_fs", "path_size"), + "only path_size uses the separately checked i64 memory fixture" + ); + } + assert!( + !witness.obligations.is_empty(), + "memory witness {}.{} must name its semantic obligation", + witness.module, + witness.name + ); + for obligation in witness.obligations { + obligation_witnesses + .entry(*obligation) + .or_default() + .push(format!("{}.{}", witness.module, witness.name)); + } + } + + let non_memory_signatures = NON_MEMORY_SIGNATURE_WITNESSES + .iter() + .map(|(module, name)| abi_signature(manifest_import(&manifest, module, name))) + .collect::>(); + assert_eq!(memory_signatures.len(), 23); + assert_eq!(non_memory_signatures.len(), 6); + assert!( + memory_signatures.is_disjoint(&non_memory_signatures), + "a signature shape cannot be both memory-bearing and scalar-only" + ); + assert_eq!( + memory_signatures + .union(&non_memory_signatures) + .cloned() + .collect::>(), + manifest_signatures, + "every generated-manifest signature must have an explicit raw-memory or non-memory contract" + ); + + for obligation in [ + MemoryContractObligation::InvalidInputRange, + MemoryContractObligation::WrappedRange, + MemoryContractObligation::InvalidOutputRange, + MemoryContractObligation::AggregateCollectionBound, + MemoryContractObligation::ShortOutput, + MemoryContractObligation::AtomicCopyout, + MemoryContractObligation::NoSideEffect, + ] { + assert!( + obligation_witnesses.contains_key(&obligation), + "missing raw-memory witness for {obligation:?}" + ); + } +} + +#[test] +fn raw_abi_memory_directions_reject_hostile_ranges_before_host_work() { + assert_node_available(); + let manifest = AbiManifest::parse(ABI_MANIFEST); + let assertions = raw_abi_memory_contract_assertions(); + let setup = r#" + (i64.store (i32.const 272) (i64.const 1234605616436508552)) + (i32.store (i32.const 280) (i32.const 287454020)) + (i32.store (i32.const 284) (i32.const 1432778632)) + (i32.store (i32.const 288) (i32.const 16909060)) + (i64.store (i32.const 296) (i64.const 72623859790382856)) + (i32.store (i32.const 512) (i32.const 0)) + (i64.store (i32.const 616) (i64.const 1084818905618843912)) + (i64.store (i32.const 624) (i64.const 506097522914230528)) + (i32.store (i32.const 640) (i32.const 287454020)) + (i32.store (i32.const 644) (i32.const 1432778632)) + (i64.store (i32.const 648) (i64.const 72623859790382856)) + (i64.store (i32.const 656) (i64.const 1230066625199609624)) + (i64.store (i32.const 700) (i64.const 3344312367813452146)) + (i64.store (i32.const 708) (i64.const 1836278135)) + (i64.store (i32.const 1024) (i64.const 1230066625199609624)) +"#; + let postconditions = r#" + (if (i64.ne (i64.load (i32.const 272)) (i64.const 1234605616436508552)) (then (call $assert_fail (i32.const 101)) unreachable)) + (if (i32.ne (i32.load (i32.const 280)) (i32.const 287454020)) (then (call $assert_fail (i32.const 102)) unreachable)) + (if (i32.ne (i32.load (i32.const 284)) (i32.const 1432778632)) (then (call $assert_fail (i32.const 103)) unreachable)) + (if (i32.ne (i32.load (i32.const 288)) (i32.const 16909060)) (then (call $assert_fail (i32.const 104)) unreachable)) + (if (i64.ne (i64.load (i32.const 296)) (i64.const 72623859790382856)) (then (call $assert_fail (i32.const 105)) unreachable)) + (if (i32.eqz (i32.load (i32.const 512))) (then (call $assert_fail (i32.const 106)) unreachable)) + (if (i64.ne (i64.load (i32.const 616)) (i64.const 1084818905618843912)) (then (call $assert_fail (i32.const 107)) unreachable)) + (if (i64.ne (i64.load (i32.const 624)) (i64.const 506097522914230528)) (then (call $assert_fail (i32.const 108)) unreachable)) + (if (i32.ne (i32.load (i32.const 640)) (i32.const 287454020)) (then (call $assert_fail (i32.const 109)) unreachable)) + (if (i32.ne (i32.load (i32.const 644)) (i32.const 1432778632)) (then (call $assert_fail (i32.const 110)) unreachable)) + (if (i64.ne (i64.load (i32.const 648)) (i64.const 72623859790382856)) (then (call $assert_fail (i32.const 111)) unreachable)) + (if (i64.ne (i64.load (i32.const 656)) (i64.const 1230066625199609624)) (then (call $assert_fail (i32.const 112)) unreachable)) + (if (i64.ne (i64.load (i32.const 1024)) (i64.const 1230066625199609624)) (then (call $assert_fail (i32.const 113)) unreachable)) +"#; + let module = raw_call_assertion_module(&manifest, &assertions, setup, postconditions); + let (stdout, stderr, exit_code) = + run_raw_module("wasm-raw-abi-memory", &module, WasmPermissionTier::Full); + assert_eq!( + exit_code, 0, + "raw ABI memory proof failed: stdout={stdout} stderr={stderr}" + ); + + let i64_module = raw_i64_path_input_contract_module(&manifest); + let (stdout, stderr, exit_code) = run_raw_module( + "wasm-raw-abi-memory-i64", + &i64_module, + WasmPermissionTier::Full, + ); + assert_eq!( + exit_code, 0, + "raw ABI i64 memory proof failed: stdout={stdout} stderr={stderr}" + ); +} + +#[test] +fn raw_abi_fixed_tables_lists_and_strings_prove_boundary_plus_one_and_warning() { + assert_node_available(); + let module = raw_fixed_limit_family_module(); + let (stdout, stderr, exit_code) = run_raw_module_with_metadata( + "wasm-raw-abi-fixed-limits", + &module, + WasmPermissionTier::Full, + HashMap::from([(String::from("resource.max_open_fds"), String::from("1024"))]), + ); + assert_eq!( + exit_code, 0, + "raw ABI fixed-limit proof failed: stdout={stdout} stderr={stderr}" + ); + assert!( + stdout.is_empty(), + "fixed-limit probes must not write stdout" + ); + for limit_name in [ + "wasm.abi.maxIovecs", + "wasm.abi.maxPollFds", + "wasm.abi.maxPollSubscriptions", + "wasm.abi.maxSupplementaryGroups", + "wasm.abi.maxAccountNameBytes", + "wasm.abi.maxXattrNameBytes", + "wasm.abi.maxXattrValueBytes", + ] { + assert!( + stderr.contains(limit_name), + "missing 80% boundary warning for {limit_name}: {stderr}" + ); + } +} + +#[test] +fn raw_abi_blocking_read_warns_at_eighty_percent_before_typed_expiry() { + assert_node_available(); + let module = raw_blocking_read_deadline_module(); + let (stdout, stderr, exit_code) = run_raw_module_with_metadata( + "wasm-raw-abi-blocking-deadline", + &module, + WasmPermissionTier::Full, + HashMap::from([( + String::from("resource.max_blocking_read_ms"), + String::from("50"), + )]), + ); + assert_eq!( + exit_code, 0, + "raw ABI blocking-read deadline proof failed: stdout={stdout} stderr={stderr}" + ); + assert!(stdout.is_empty()); + assert!( + stderr.contains("blocking poll is nearing limits.resources.maxBlockingReadMs (50 ms)"), + "80% warning must precede the typed timeout: {stderr}" + ); + assert!( + stderr.contains("blocking poll exceeded limits.resources.maxBlockingReadMs (50 ms)"), + "hard expiry must retain the configured setting: {stderr}" + ); +} + +#[test] +fn known_unsupported_preview1_imports_fail_closed() { + assert_node_available(); + for module in ["wasi_snapshot_preview1", "wasi_unstable"] { + for (name, params) in [ + ("fd_advise", vec!["i32", "i64", "i64", "i32"]), + ("fd_fdstat_set_rights", vec!["i32", "i64", "i64"]), + ] { + let import = AbiImport { + module: module.to_string(), + name: name.to_string(), + params: params.into_iter().map(String::from).collect(), + results: vec![String::from("i32")], + }; + let (stdout, stderr, exit_code) = run_raw_module( + &format!("wasm-raw-unsupported-{module}-{name}"), + &single_import_module(&import, false, CallArguments::Zero), + WasmPermissionTier::Full, + ); + assert_ne!( + exit_code, 0, + "unsupported {}.{} linked: stdout={stdout} stderr={stderr}", + import.module, import.name + ); + assert!( + stderr.contains(&import.name) || stderr.contains(&import.module), + "unexpected unsupported-import error for {}.{}: {stderr}", + import.module, + import.name + ); + } + } +} diff --git a/crates/native-sidecar/tests/xfstests_correctness.rs b/crates/native-sidecar/tests/xfstests_correctness.rs index 175fcdf938..75f18f730e 100644 --- a/crates/native-sidecar/tests/xfstests_correctness.rs +++ b/crates/native-sidecar/tests/xfstests_correctness.rs @@ -224,7 +224,7 @@ fn create_xfstests_vm_wire( ..agentos_vm_config::ResourceLimitsConfig::default() }), wasm: Some(agentos_vm_config::WasmLimitsConfig { - runner_cpu_time_limit_ms: Some( + active_cpu_time_limit_ms: Some( u64::try_from((xfstest_timeout() + Duration::from_secs(30)).as_millis()) .expect("bounded xfstests timeout fits u64 milliseconds"), ), diff --git a/crates/resource/Cargo.toml b/crates/resource/Cargo.toml new file mode 100644 index 0000000000..f89a39e800 --- /dev/null +++ b/crates/resource/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "agentos-resource" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Runtime-neutral bounded resource accounting for AgentOS kernel services" + +[dependencies] +event-listener = "5.4" diff --git a/crates/resource/src/lib.rs b/crates/resource/src/lib.rs new file mode 100644 index 0000000000..4778d642a8 --- /dev/null +++ b/crates/resource/src/lib.rs @@ -0,0 +1,915 @@ +#![forbid(unsafe_code)] + +//! Runtime-neutral resource admission used by the kernel-owned resource layer. + +use std::collections::BTreeMap; +use std::fmt; +use std::future::{poll_fn, Future}; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use event_listener::{Event, EventListener}; + +/// Low-cardinality resource classes used by the sidecar admission policy. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ResourceClass { + Capabilities, + ReadyHandles, + Sockets, + Connections, + BufferedBytes, + Datagrams, + HandleCommands, + HandleCommandBytes, + BridgeCalls, + BridgeRequestBytes, + BridgeResponseBytes, + AsyncCompletions, + AsyncCompletionBytes, + UdpDatagrams, + UdpBytes, + TlsBytes, + Timers, + Tasks, + ExecutorSlots, + ExecutorBytes, + Http2Connections, + Http2Streams, + Http2BufferedBytes, + Http2HeaderBytes, + Http2DataBytes, + Http2Commands, + Http2CommandBytes, + Http2Events, + Http2EventBytes, +} + +impl ResourceClass { + pub const ALL: [Self; 29] = [ + Self::Capabilities, + Self::ReadyHandles, + Self::Sockets, + Self::Connections, + Self::BufferedBytes, + Self::Datagrams, + Self::HandleCommands, + Self::HandleCommandBytes, + Self::BridgeCalls, + Self::BridgeRequestBytes, + Self::BridgeResponseBytes, + Self::AsyncCompletions, + Self::AsyncCompletionBytes, + Self::UdpDatagrams, + Self::UdpBytes, + Self::TlsBytes, + Self::Timers, + Self::Tasks, + Self::ExecutorSlots, + Self::ExecutorBytes, + Self::Http2Connections, + Self::Http2Streams, + Self::Http2BufferedBytes, + Self::Http2HeaderBytes, + Self::Http2DataBytes, + Self::Http2Commands, + Self::Http2CommandBytes, + Self::Http2Events, + Self::Http2EventBytes, + ]; + + pub const fn name(self) -> &'static str { + match self { + Self::Capabilities => "capabilities", + Self::ReadyHandles => "readyHandles", + Self::Sockets => "sockets", + Self::Connections => "connections", + Self::BufferedBytes => "bufferedBytes", + Self::Datagrams => "datagrams", + Self::HandleCommands => "handleCommands", + Self::HandleCommandBytes => "handleCommandBytes", + Self::BridgeCalls => "bridgeCalls", + Self::BridgeRequestBytes => "bridgeRequestBytes", + Self::BridgeResponseBytes => "bridgeResponseBytes", + Self::AsyncCompletions => "asyncCompletions", + Self::AsyncCompletionBytes => "asyncCompletionBytes", + Self::UdpDatagrams => "udpDatagrams", + Self::UdpBytes => "udpBytes", + Self::TlsBytes => "tlsBytes", + Self::Timers => "timers", + Self::Tasks => "tasks", + Self::ExecutorSlots => "executorSlots", + Self::ExecutorBytes => "executorBytes", + Self::Http2Connections => "http2Connections", + Self::Http2Streams => "http2Streams", + Self::Http2BufferedBytes => "http2BufferedBytes", + Self::Http2HeaderBytes => "http2HeaderBytes", + Self::Http2DataBytes => "http2DataBytes", + Self::Http2Commands => "http2Commands", + Self::Http2CommandBytes => "http2CommandBytes", + Self::Http2Events => "http2Events", + Self::Http2EventBytes => "http2EventBytes", + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResourceLimit { + pub maximum: usize, + pub config_path: String, +} + +impl ResourceLimit { + pub fn new(maximum: usize, config_path: impl Into) -> Self { + Self { + maximum, + config_path: config_path.into(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LimitError { + pub scope: String, + pub resource: ResourceClass, + pub used: usize, + pub requested: usize, + pub limit: usize, + pub config_path: String, +} + +impl fmt::Display for LimitError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "ERR_AGENTOS_RESOURCE_LIMIT: scope={} resource={} used={} requested={} limit={}; raise {}", + self.scope, + self.resource.name(), + self.used, + self.requested, + self.limit, + self.config_path + ) + } +} + +impl std::error::Error for LimitError {} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResourceUsage { + pub used: usize, + pub limit: Option, +} + +/// Optional low-cardinality telemetry sink for the kernel-owned ledger. +/// +/// The observer can mirror current usage into sidecar runtime metrics, but it +/// cannot admit, release, or otherwise mutate accounting state. +pub trait ResourceUsageObserver: fmt::Debug + Send + Sync { + fn observe_usage(&self, resource: ResourceClass, used: usize); +} + +#[derive(Debug, Default)] +struct CounterState { + used: usize, + warning_active: bool, +} + +#[derive(Debug)] +struct LedgerState { + counters: BTreeMap, +} + +#[derive(Debug)] +struct LedgerInner { + scope: String, + limits: BTreeMap, + state: Mutex, + capacity_changed: Event, + capacity_generation: AtomicU64, + integrity_failed: AtomicBool, + observer: Option>, +} + +/// One process or VM accounting scope. A child ledger reserves its parent first, +/// so aggregate process policy and tenant policy cover the same allocation. +#[derive(Clone, Debug)] +pub struct ResourceLedger { + inner: Arc, + parent: Option>, +} + +impl ResourceLedger { + pub fn root( + scope: impl Into, + limits: impl IntoIterator, + ) -> Self { + Self::new(scope, limits, None, None) + } + + pub fn root_with_observer( + scope: impl Into, + limits: impl IntoIterator, + observer: Arc, + ) -> Self { + Self::new(scope, limits, None, Some(observer)) + } + + pub fn child( + scope: impl Into, + limits: impl IntoIterator, + parent: Arc, + ) -> Self { + Self::new(scope, limits, Some(parent), None) + } + + fn new( + scope: impl Into, + limits: impl IntoIterator, + parent: Option>, + observer: Option>, + ) -> Self { + Self { + inner: Arc::new(LedgerInner { + scope: scope.into(), + limits: limits.into_iter().collect(), + state: Mutex::new(LedgerState { + counters: BTreeMap::new(), + }), + capacity_changed: Event::new(), + capacity_generation: AtomicU64::new(0), + integrity_failed: AtomicBool::new(false), + observer, + }), + parent, + } + } + + pub fn scope(&self) -> &str { + &self.inner.scope + } + + /// Reserve before allocation. A zero-sized reservation is valid and owns no + /// counters, which lets callers keep one unconditional cleanup path. + pub fn reserve( + &self, + resource: ResourceClass, + amount: usize, + ) -> Result { + let mut allocations = Vec::with_capacity(if self.parent.is_some() { 2 } else { 1 }); + if let Some(parent) = &self.parent { + parent.reserve_into(resource, amount, &mut allocations)?; + } + if let Err(error) = self.reserve_local(resource, amount) { + release_allocations(&mut allocations); + return Err(error); + } + if amount != 0 { + allocations.push(Allocation { + ledger: Arc::clone(&self.inner), + resource, + amount, + }); + } + Ok(Reservation { + resource, + amount, + allocations, + }) + } + + fn reserve_into( + &self, + resource: ResourceClass, + amount: usize, + allocations: &mut Vec, + ) -> Result<(), LimitError> { + if let Some(parent) = &self.parent { + parent.reserve_into(resource, amount, allocations)?; + } + if let Err(error) = self.reserve_local(resource, amount) { + release_allocations(allocations); + return Err(error); + } + if amount != 0 { + allocations.push(Allocation { + ledger: Arc::clone(&self.inner), + resource, + amount, + }); + } + Ok(()) + } + + fn reserve_local(&self, resource: ResourceClass, amount: usize) -> Result<(), LimitError> { + if amount == 0 { + return Ok(()); + } + let mut state = self.inner.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}", + self.inner.scope, + resource.name() + ); + poisoned.into_inner() + }); + let counter = state.counters.entry(resource).or_default(); + let requested_total = counter.used.checked_add(amount); + if let Some(limit) = self.inner.limits.get(&resource) { + if requested_total.is_none_or(|total| total > limit.maximum) { + return Err(LimitError { + scope: self.inner.scope.clone(), + resource, + used: counter.used, + requested: amount, + limit: limit.maximum, + config_path: limit.config_path.clone(), + }); + } + } + counter.used = requested_total.unwrap_or(usize::MAX); + maybe_warn(&self.inner, resource, counter); + observe_usage(&self.inner, resource, counter.used); + Ok(()) + } + + pub fn usage(&self, resource: ResourceClass) -> ResourceUsage { + let state = self.inner.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}", + self.inner.scope, + resource.name() + ); + poisoned.into_inner() + }); + ResourceUsage { + used: state + .counters + .get(&resource) + .map_or(0, |counter| counter.used), + limit: self.inner.limits.get(&resource).map(|limit| limit.maximum), + } + } + + pub fn configured_limit(&self, resource: ResourceClass) -> Option { + self.inner.limits.get(&resource).cloned() + } + + /// Best-effort readiness probe that does not acquire and immediately drop + /// a reservation. Admission must still call [`Self::reserve`]; this method + /// exists for POLLOUT-style hints where a transient reservation would emit + /// a false capacity-change notification and churn blocked producers. + pub fn capacity_available(&self, resource: ResourceClass, amount: usize) -> bool { + if let Some(parent) = &self.parent { + if !parent.capacity_available(resource, amount) { + return false; + } + } + if amount == 0 { + return true; + } + let state = self.inner.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering capacity probe scope={} resource={}", + self.inner.scope, + resource.name() + ); + poisoned.into_inner() + }); + let used = state + .counters + .get(&resource) + .map_or(0, |counter| counter.used); + self.inner.limits.get(&resource).is_none_or(|limit| { + used.checked_add(amount) + .is_some_and(|total| total <= limit.maximum) + }) + } + + pub fn is_zero(&self) -> bool { + let state = self.inner.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={}", + self.inner.scope + ); + poisoned.into_inner() + }); + state.counters.values().all(|counter| counter.used == 0) + } + + pub fn integrity_ok(&self) -> bool { + !self.inner.integrity_failed.load(Ordering::Acquire) + } + + /// Wait without polling until any owner releases capacity. Callers must + /// retry their full multi-resource admission after this notification. + pub fn capacity_changed(&self) -> impl Future + Send + 'static { + let local = CapacityChangeWatch::new(Arc::clone(&self.inner)); + let parent = self + .parent + .as_ref() + .map(|parent| CapacityChangeWatch::new(Arc::clone(&parent.inner))); + wait_for_capacity_change(local, parent) + } + + /// Pause an async source until the requested capacity is available. The + /// notification is armed before each retry so a concurrent release cannot + /// be missed between observing the limit and awaiting capacity. + pub async fn reserve_when_available( + &self, + resource: ResourceClass, + amount: usize, + ) -> Result { + loop { + // Arm both accounting scopes before admission so a concurrent + // release cannot be missed. Ledgers are currently process -> VM; + // flatten this wait set if another hierarchy level is introduced. + let local_changed = CapacityChangeWatch::new(Arc::clone(&self.inner)); + let parent_changed = self + .parent + .as_ref() + .map(|parent| CapacityChangeWatch::new(Arc::clone(&parent.inner))); + match self.reserve(resource, amount) { + Ok(reservation) => return Ok(reservation), + Err(error) if amount > error.limit => return Err(error), + Err(_) => { + wait_for_capacity_change(local_changed, parent_changed).await; + } + } + } + } +} + +#[derive(Debug)] +struct CapacityChangeWatch { + ledger: Arc, + observed_generation: u64, + listener: EventListener, +} + +impl CapacityChangeWatch { + fn new(ledger: Arc) -> Self { + // Listen before sampling the generation. A release between these two + // operations either changes the generation or wakes this listener. + let listener = ledger.capacity_changed.listen(); + let observed_generation = ledger.capacity_generation.load(Ordering::Acquire); + Self { + ledger, + observed_generation, + listener, + } + } + + fn changed(&self) -> bool { + self.ledger.capacity_generation.load(Ordering::Acquire) != self.observed_generation + } +} + +async fn wait_for_capacity_change( + mut local: CapacityChangeWatch, + mut parent: Option, +) { + if local.changed() || parent.as_ref().is_some_and(CapacityChangeWatch::changed) { + return; + } + poll_fn(move |context| { + if local.changed() || Pin::new(&mut local.listener).poll(context).is_ready() { + return Poll::Ready(()); + } + if let Some(parent) = &mut parent { + if parent.changed() || Pin::new(&mut parent.listener).poll(context).is_ready() { + return Poll::Ready(()); + } + } + Poll::Pending + }) + .await; +} + +fn maybe_warn(inner: &LedgerInner, resource: ResourceClass, counter: &mut CounterState) { + let Some(limit) = inner.limits.get(&resource) else { + return; + }; + // Integer comparisons avoid rounding and make zero impossible to treat as + // near-limit. The warning rearms only after usage falls below 70%. + let near = + counter.used != 0 && counter.used.saturating_mul(100) >= limit.maximum.saturating_mul(80); + if near && !counter.warning_active { + counter.warning_active = true; + eprintln!( + "WARN_AGENTOS_RESOURCE_NEAR_LIMIT: scope={} resource={} used={} limit={} config={}", + inner.scope, + resource.name(), + counter.used, + limit.maximum, + limit.config_path + ); + } +} + +#[derive(Debug)] +struct Allocation { + ledger: Arc, + resource: ResourceClass, + amount: usize, +} + +fn release_allocation(allocation: &Allocation) { + let mut state = allocation.ledger.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering release scope={} resource={}", + allocation.ledger.scope, + allocation.resource.name() + ); + poisoned.into_inner() + }); + let counter = state.counters.entry(allocation.resource).or_default(); + if allocation.amount > counter.used { + eprintln!( + "ERR_AGENTOS_RESOURCE_ACCOUNTING_UNDERFLOW: scope={} resource={} used={} release={}", + allocation.ledger.scope, + allocation.resource.name(), + counter.used, + allocation.amount + ); + allocation + .ledger + .integrity_failed + .store(true, Ordering::Release); + counter.used = 0; + } else { + counter.used -= allocation.amount; + } + if let Some(limit) = allocation.ledger.limits.get(&allocation.resource) { + if counter.used.saturating_mul(100) < limit.maximum.saturating_mul(70) { + counter.warning_active = false; + } + } + observe_usage(&allocation.ledger, allocation.resource, counter.used); + allocation + .ledger + .capacity_generation + .fetch_add(1, Ordering::AcqRel); + allocation.ledger.capacity_changed.notify(usize::MAX); +} + +fn observe_usage(inner: &LedgerInner, resource: ResourceClass, used: usize) { + let Some(observer) = &inner.observer else { + return; + }; + observer.observe_usage(resource, used); +} + +fn release_allocations(allocations: &mut Vec) { + for allocation in allocations.drain(..).rev() { + release_allocation(&allocation); + } +} + +/// Exact ownership of one admitted amount. Moving the value transfers ownership; +/// dropping it releases every scope exactly once. +#[derive(Debug)] +pub struct Reservation { + resource: ResourceClass, + amount: usize, + allocations: Vec, +} + +/// Cloneable ownership used when a charged payload crosses an in-process +/// response router. The underlying reservation releases only after the final +/// envelope/consumer drops it. +#[derive(Clone)] +pub struct SharedReservation(Arc); + +impl SharedReservation { + pub fn new(reservation: Reservation) -> Self { + Self(Arc::new(reservation)) + } + + pub fn resource(&self) -> ResourceClass { + self.0.resource() + } + + pub fn amount(&self) -> usize { + self.0.amount() + } +} + +impl fmt::Debug for SharedReservation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("SharedReservation") + .field("resource", &self.resource()) + .field("amount", &self.amount()) + .finish_non_exhaustive() + } +} + +impl PartialEq for SharedReservation { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for SharedReservation {} + +impl Reservation { + pub fn resource(&self) -> ResourceClass { + self.resource + } + + pub fn amount(&self) -> usize { + self.amount + } + + /// Split ownership without changing any ledger counter. + pub fn split(&mut self, amount: usize) -> Option { + if amount > self.amount { + return None; + } + self.amount -= amount; + let mut allocations = Vec::with_capacity(self.allocations.len()); + for allocation in &mut self.allocations { + allocation.amount -= amount; + allocations.push(Allocation { + ledger: Arc::clone(&allocation.ledger), + resource: allocation.resource, + amount, + }); + } + Some(Self { + resource: self.resource, + amount, + allocations, + }) + } + + /// Merge two reservations only when they refer to the same scopes and class. + pub fn merge(&mut self, mut other: Self) -> Result<(), Self> { + if self.resource != other.resource || self.allocations.len() != other.allocations.len() { + return Err(other); + } + if self + .allocations + .iter() + .zip(&other.allocations) + .any(|(left, right)| !Arc::ptr_eq(&left.ledger, &right.ledger)) + { + return Err(other); + } + let Some(total) = self.amount.checked_add(other.amount) else { + return Err(other); + }; + for (left, right) in self.allocations.iter_mut().zip(&other.allocations) { + left.amount += right.amount; + } + self.amount = total; + other.amount = 0; + other.allocations.clear(); + Ok(()) + } +} + +impl Drop for Reservation { + fn drop(&mut self) { + release_allocations(&mut self.allocations); + self.amount = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::mpsc; + use std::task::{Context, Wake, Waker}; + use std::thread; + use std::time::Duration; + + #[derive(Debug)] + struct ThreadWake(thread::Thread); + + impl Wake for ThreadWake { + fn wake(self: Arc) { + self.0.unpark(); + } + + fn wake_by_ref(self: &Arc) { + self.0.unpark(); + } + } + + fn block_on(future: F) -> F::Output { + let waker = Waker::from(Arc::new(ThreadWake(thread::current()))); + let mut context = Context::from_waker(&waker); + let mut future = Box::pin(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => thread::park_timeout(Duration::from_secs(1)), + } + } + } + + fn limit(maximum: usize) -> [(ResourceClass, ResourceLimit); 1] { + [( + ResourceClass::BufferedBytes, + ResourceLimit::new(maximum, "runtime.resources.maxSocketBufferedBytes"), + )] + } + + #[test] + fn child_reservation_charges_and_releases_both_scopes() { + let process = Arc::new(ResourceLedger::root("process", limit(10))); + let vm = ResourceLedger::child("vm-1", limit(6), Arc::clone(&process)); + let reservation = vm.reserve(ResourceClass::BufferedBytes, 6).unwrap(); + assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6); + assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 6); + let error = vm.reserve(ResourceClass::BufferedBytes, 1).unwrap_err(); + assert_eq!(error.scope, "vm-1"); + assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6); + drop(reservation); + assert!(process.is_zero()); + assert!(vm.is_zero()); + } + + #[test] + fn named_limit_proves_boundary_warning_typed_rejection_and_rollback() { + let process = Arc::new(ResourceLedger::root("process", limit(12))); + let vm = ResourceLedger::child("vm-1", limit(10), Arc::clone(&process)); + + let boundary = vm + .reserve(ResourceClass::BufferedBytes, 10) + .expect("the exact configured boundary must be admitted"); + assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 10); + assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 10); + assert!( + vm.inner + .state + .lock() + .expect("VM ledger state") + .counters + .get(&ResourceClass::BufferedBytes) + .expect("buffer counter") + .warning_active, + "80%-threshold warning must be active at the exact boundary" + ); + + let error = vm + .reserve(ResourceClass::BufferedBytes, 1) + .expect_err("limit plus one must fail"); + assert_eq!(error.scope, "vm-1"); + assert_eq!(error.resource, ResourceClass::BufferedBytes); + assert_eq!(error.used, 10); + assert_eq!(error.requested, 1); + assert_eq!(error.limit, 10); + assert_eq!( + error.config_path, + "runtime.resources.maxSocketBufferedBytes" + ); + assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 10); + assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 10); + assert!(process.integrity_ok()); + assert!(vm.integrity_ok()); + + drop(boundary); + assert!(process.is_zero()); + assert!(vm.is_zero()); + assert!( + !vm.inner + .state + .lock() + .expect("VM ledger state") + .counters + .get(&ResourceClass::BufferedBytes) + .expect("buffer counter") + .warning_active, + "release below 70% must rearm the warning" + ); + + let near = vm + .reserve(ResourceClass::BufferedBytes, 8) + .expect("the 80% warning threshold must remain admissible"); + assert!( + vm.inner + .state + .lock() + .expect("VM ledger state") + .counters + .get(&ResourceClass::BufferedBytes) + .expect("buffer counter") + .warning_active + ); + drop(near); + assert!(process.is_zero()); + assert!(vm.is_zero()); + } + + #[test] + fn failed_child_admission_rolls_back_parent() { + let process = Arc::new(ResourceLedger::root("process", limit(10))); + let vm = ResourceLedger::child("vm-1", limit(2), Arc::clone(&process)); + let error = vm.reserve(ResourceClass::BufferedBytes, 3).unwrap_err(); + assert_eq!(error.scope, "vm-1"); + assert!(process.is_zero()); + assert!(vm.is_zero()); + } + + #[test] + fn capacity_probe_checks_parent_and_child_without_changing_usage() { + let process = Arc::new(ResourceLedger::root("process", limit(3))); + let vm = ResourceLedger::child("vm-1", limit(5), Arc::clone(&process)); + let held = process + .reserve(ResourceClass::BufferedBytes, 2) + .expect("reserve process capacity"); + + assert!(vm.capacity_available(ResourceClass::BufferedBytes, 1)); + assert!(!vm.capacity_available(ResourceClass::BufferedBytes, 2)); + assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 2); + assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 0); + + drop(held); + assert!(vm.capacity_available(ResourceClass::BufferedBytes, 2)); + } + + #[test] + fn split_and_merge_transfer_without_counter_drift() { + let ledger = ResourceLedger::root("vm-1", limit(10)); + let mut source = ledger.reserve(ResourceClass::BufferedBytes, 8).unwrap(); + let transferred = source.split(3).unwrap(); + assert_eq!(source.amount(), 5); + assert_eq!(transferred.amount(), 3); + assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8); + source.merge(transferred).unwrap(); + assert_eq!(source.amount(), 8); + assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8); + drop(source); + assert!(ledger.is_zero()); + } + + #[test] + fn impossible_async_reservation_returns_typed_error() { + let ledger = ResourceLedger::root("vm-1", limit(4)); + let error = + block_on(ledger.reserve_when_available(ResourceClass::BufferedBytes, 5)).unwrap_err(); + assert_eq!(error.resource, ResourceClass::BufferedBytes); + assert_eq!(error.requested, 5); + assert_eq!(error.limit, 4); + assert_eq!( + error.config_path, + "runtime.resources.maxSocketBufferedBytes" + ); + } + + #[test] + fn child_waiter_wakes_when_only_parent_capacity_changes() { + let process = Arc::new(ResourceLedger::root("process", limit(1))); + let vm = Arc::new(ResourceLedger::child( + "vm-1", + limit(2), + Arc::clone(&process), + )); + let held = process + .reserve(ResourceClass::BufferedBytes, 1) + .expect("fill parent"); + let waiting_vm = Arc::clone(&vm); + let (started_tx, started_rx) = mpsc::channel(); + let (result_tx, result_rx) = mpsc::channel(); + let waiter = thread::spawn(move || { + started_tx.send(()).expect("signal waiter start"); + let result = + block_on(waiting_vm.reserve_when_available(ResourceClass::BufferedBytes, 1)); + result_tx.send(result).expect("send waiter result"); + }); + started_rx.recv().expect("waiter started"); + thread::sleep(Duration::from_millis(10)); + assert!(!waiter.is_finished()); + drop(held); + let reservation = result_rx + .recv_timeout(Duration::from_secs(1)) + .expect("parent release must wake child waiter") + .expect("reservation"); + waiter.join().expect("waiter thread"); + drop(reservation); + assert!(process.is_zero()); + assert!(vm.is_zero()); + } + + #[test] + fn accounting_underflow_latches_integrity_failure() { + let ledger = ResourceLedger::root("vm-1", limit(1)); + let inner = Arc::clone(&ledger.inner); + let malformed = Reservation { + resource: ResourceClass::BufferedBytes, + amount: 1, + allocations: vec![Allocation { + ledger: inner, + resource: ResourceClass::BufferedBytes, + amount: 1, + }], + }; + drop(malformed); + assert!(!ledger.integrity_ok()); + assert!(ledger.is_zero()); + } +} diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index a4bb5a7024..f0203b477e 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -7,4 +7,5 @@ repository.workspace = true description = "Process-owned AgentOS async runtime and bounded blocking executor" [dependencies] +agentos-resource = { workspace = true } tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "sync", "time", "net"] } diff --git a/crates/runtime/src/accounting.rs b/crates/runtime/src/accounting.rs index 5e4fa7f62f..d09af1dd47 100644 --- a/crates/runtime/src/accounting.rs +++ b/crates/runtime/src/accounting.rs @@ -1,813 +1,67 @@ -//! Hierarchical count/byte admission with exact RAII ownership. +//! Runtime telemetry adapter for the kernel-owned resource ledger. -use std::collections::BTreeMap; -use std::fmt; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +pub use agentos_resource::{ + LimitError, Reservation, ResourceClass, ResourceLedger, ResourceLimit, ResourceUsage, + ResourceUsageObserver, SharedReservation, +}; use crate::metrics::{BufferMetricClass, ResourceMetricClass, RuntimeMetrics}; -/// Low-cardinality resource classes used by the sidecar admission policy. -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum ResourceClass { - Capabilities, - ReadyHandles, - Sockets, - Connections, - BufferedBytes, - Datagrams, - HandleCommands, - HandleCommandBytes, - BridgeCalls, - BridgeRequestBytes, - BridgeResponseBytes, - AsyncCompletions, - AsyncCompletionBytes, - UdpDatagrams, - UdpBytes, - TlsBytes, - Timers, - Tasks, - ExecutorSlots, - ExecutorBytes, - Http2Connections, - Http2Streams, - Http2BufferedBytes, - Http2HeaderBytes, - Http2DataBytes, - Http2Commands, - Http2CommandBytes, - Http2Events, - Http2EventBytes, -} - -impl ResourceClass { - pub const ALL: [Self; 29] = [ - Self::Capabilities, - Self::ReadyHandles, - Self::Sockets, - Self::Connections, - Self::BufferedBytes, - Self::Datagrams, - Self::HandleCommands, - Self::HandleCommandBytes, - Self::BridgeCalls, - Self::BridgeRequestBytes, - Self::BridgeResponseBytes, - Self::AsyncCompletions, - Self::AsyncCompletionBytes, - Self::UdpDatagrams, - Self::UdpBytes, - Self::TlsBytes, - Self::Timers, - Self::Tasks, - Self::ExecutorSlots, - Self::ExecutorBytes, - Self::Http2Connections, - Self::Http2Streams, - Self::Http2BufferedBytes, - Self::Http2HeaderBytes, - Self::Http2DataBytes, - Self::Http2Commands, - Self::Http2CommandBytes, - Self::Http2Events, - Self::Http2EventBytes, - ]; - - pub const fn name(self) -> &'static str { - match self { - Self::Capabilities => "capabilities", - Self::ReadyHandles => "readyHandles", - Self::Sockets => "sockets", - Self::Connections => "connections", - Self::BufferedBytes => "bufferedBytes", - Self::Datagrams => "datagrams", - Self::HandleCommands => "handleCommands", - Self::HandleCommandBytes => "handleCommandBytes", - Self::BridgeCalls => "bridgeCalls", - Self::BridgeRequestBytes => "bridgeRequestBytes", - Self::BridgeResponseBytes => "bridgeResponseBytes", - Self::AsyncCompletions => "asyncCompletions", - Self::AsyncCompletionBytes => "asyncCompletionBytes", - Self::UdpDatagrams => "udpDatagrams", - Self::UdpBytes => "udpBytes", - Self::TlsBytes => "tlsBytes", - Self::Timers => "timers", - Self::Tasks => "tasks", - Self::ExecutorSlots => "executorSlots", - Self::ExecutorBytes => "executorBytes", - Self::Http2Connections => "http2Connections", - Self::Http2Streams => "http2Streams", - Self::Http2BufferedBytes => "http2BufferedBytes", - Self::Http2HeaderBytes => "http2HeaderBytes", - Self::Http2DataBytes => "http2DataBytes", - Self::Http2Commands => "http2Commands", - Self::Http2CommandBytes => "http2CommandBytes", - Self::Http2Events => "http2Events", - Self::Http2EventBytes => "http2EventBytes", - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResourceLimit { - pub maximum: usize, - pub config_path: String, -} - -impl ResourceLimit { - pub fn new(maximum: usize, config_path: impl Into) -> Self { - Self { - maximum, - config_path: config_path.into(), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct LimitError { - pub scope: String, - pub resource: ResourceClass, - pub used: usize, - pub requested: usize, - pub limit: usize, - pub config_path: String, -} - -impl fmt::Display for LimitError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - formatter, - "ERR_AGENTOS_RESOURCE_LIMIT: scope={} resource={} used={} requested={} limit={}; raise {}", - self.scope, - self.resource.name(), - self.used, - self.requested, - self.limit, - self.config_path - ) - } -} - -impl std::error::Error for LimitError {} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResourceUsage { - pub used: usize, - pub limit: Option, -} - -#[derive(Debug, Default)] -struct CounterState { - used: usize, - warning_active: bool, -} - -#[derive(Debug)] -struct LedgerState { - counters: BTreeMap, -} - -#[derive(Debug)] -struct LedgerInner { - scope: String, - limits: BTreeMap, - state: Mutex, - capacity_changed: tokio::sync::Notify, - integrity_failed: AtomicBool, - metrics: Option, -} - -/// One process or VM accounting scope. A child ledger reserves its parent first, -/// so aggregate process policy and tenant policy cover the same allocation. -#[derive(Clone, Debug)] -pub struct ResourceLedger { - inner: Arc, - parent: Option>, -} - -impl ResourceLedger { - pub fn root( - scope: impl Into, - limits: impl IntoIterator, - ) -> Self { - Self::new(scope, limits, None, None) - } - - pub fn root_with_metrics( - scope: impl Into, - limits: impl IntoIterator, - metrics: RuntimeMetrics, - ) -> Self { - Self::new(scope, limits, None, Some(metrics)) - } - - pub fn child( - scope: impl Into, - limits: impl IntoIterator, - parent: Arc, - ) -> Self { - Self::new(scope, limits, Some(parent), None) - } - - fn new( - scope: impl Into, - limits: impl IntoIterator, - parent: Option>, - metrics: Option, - ) -> Self { - Self { - inner: Arc::new(LedgerInner { - scope: scope.into(), - limits: limits.into_iter().collect(), - state: Mutex::new(LedgerState { - counters: BTreeMap::new(), - }), - capacity_changed: tokio::sync::Notify::new(), - integrity_failed: AtomicBool::new(false), - metrics, - }), - parent, - } - } - - pub fn scope(&self) -> &str { - &self.inner.scope - } - - /// Reserve before allocation. A zero-sized reservation is valid and owns no - /// counters, which lets callers keep one unconditional cleanup path. - pub fn reserve( - &self, - resource: ResourceClass, - amount: usize, - ) -> Result { - let mut allocations = Vec::with_capacity(if self.parent.is_some() { 2 } else { 1 }); - if let Some(parent) = &self.parent { - parent.reserve_into(resource, amount, &mut allocations)?; - } - if let Err(error) = self.reserve_local(resource, amount) { - release_allocations(&mut allocations); - return Err(error); - } - if amount != 0 { - allocations.push(Allocation { - ledger: Arc::clone(&self.inner), - resource, - amount, - }); - } - Ok(Reservation { - resource, - amount, - allocations, - }) - } - - fn reserve_into( - &self, - resource: ResourceClass, - amount: usize, - allocations: &mut Vec, - ) -> Result<(), LimitError> { - if let Some(parent) = &self.parent { - parent.reserve_into(resource, amount, allocations)?; - } - if let Err(error) = self.reserve_local(resource, amount) { - release_allocations(allocations); - return Err(error); - } - if amount != 0 { - allocations.push(Allocation { - ledger: Arc::clone(&self.inner), - resource, - amount, - }); - } - Ok(()) - } - - fn reserve_local(&self, resource: ResourceClass, amount: usize) -> Result<(), LimitError> { - if amount == 0 { - return Ok(()); - } - let mut state = self.inner.state.lock().unwrap_or_else(|poisoned| { - eprintln!( - "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}", - self.inner.scope, - resource.name() - ); - poisoned.into_inner() - }); - let counter = state.counters.entry(resource).or_default(); - let requested_total = counter.used.checked_add(amount); - if let Some(limit) = self.inner.limits.get(&resource) { - if requested_total.is_none_or(|total| total > limit.maximum) { - return Err(LimitError { - scope: self.inner.scope.clone(), - resource, - used: counter.used, - requested: amount, - limit: limit.maximum, - config_path: limit.config_path.clone(), - }); +impl ResourceUsageObserver for RuntimeMetrics { + fn observe_usage(&self, resource: ResourceClass, used: usize) { + match resource { + ResourceClass::Capabilities => { + self.observe_resource(ResourceMetricClass::Capabilities, used) } - } - counter.used = requested_total.unwrap_or(usize::MAX); - maybe_warn(&self.inner, resource, counter); - observe_usage(&self.inner, resource, counter.used); - Ok(()) - } - - pub fn usage(&self, resource: ResourceClass) -> ResourceUsage { - let state = self.inner.state.lock().unwrap_or_else(|poisoned| { - eprintln!( - "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}", - self.inner.scope, - resource.name() - ); - poisoned.into_inner() - }); - ResourceUsage { - used: state - .counters - .get(&resource) - .map_or(0, |counter| counter.used), - limit: self.inner.limits.get(&resource).map(|limit| limit.maximum), - } - } - - pub fn configured_limit(&self, resource: ResourceClass) -> Option { - self.inner.limits.get(&resource).cloned() - } - - /// Best-effort readiness probe that does not acquire and immediately drop - /// a reservation. Admission must still call [`Self::reserve`]; this method - /// exists for POLLOUT-style hints where a transient reservation would emit - /// a false capacity-change notification and churn blocked producers. - pub fn capacity_available(&self, resource: ResourceClass, amount: usize) -> bool { - if let Some(parent) = &self.parent { - if !parent.capacity_available(resource, amount) { - return false; + ResourceClass::ReadyHandles => { + self.observe_resource(ResourceMetricClass::ReadyHandles, used) } - } - if amount == 0 { - return true; - } - let state = self.inner.state.lock().unwrap_or_else(|poisoned| { - eprintln!( - "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering capacity probe scope={} resource={}", - self.inner.scope, - resource.name() - ); - poisoned.into_inner() - }); - let used = state - .counters - .get(&resource) - .map_or(0, |counter| counter.used); - self.inner.limits.get(&resource).is_none_or(|limit| { - used.checked_add(amount) - .is_some_and(|total| total <= limit.maximum) - }) - } - - pub fn is_zero(&self) -> bool { - let state = self.inner.state.lock().unwrap_or_else(|poisoned| { - eprintln!( - "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={}", - self.inner.scope - ); - poisoned.into_inner() - }); - state.counters.values().all(|counter| counter.used == 0) - } - - pub fn integrity_ok(&self) -> bool { - !self.inner.integrity_failed.load(Ordering::Acquire) - } - - /// Wait without polling until any owner releases capacity. Callers must - /// retry their full multi-resource admission after this notification. - pub async fn capacity_changed(&self) { - let local_changed = self.inner.capacity_changed.notified(); - if let Some(parent) = &self.parent { - let parent_changed = parent.inner.capacity_changed.notified(); - tokio::select! { - _ = local_changed => {} - _ = parent_changed => {} + ResourceClass::Sockets => self.observe_resource(ResourceMetricClass::Sockets, used), + ResourceClass::Connections => { + self.observe_resource(ResourceMetricClass::Connections, used) } - } else { - local_changed.await; - } - } - - /// Pause an async source until the requested capacity is available. The - /// notification is armed before each retry so a concurrent release cannot - /// be missed between observing the limit and awaiting capacity. - pub async fn reserve_when_available( - &self, - resource: ResourceClass, - amount: usize, - ) -> Result { - loop { - // Arm both accounting scopes before admission so a concurrent - // release cannot be missed. Ledgers are currently process -> VM; - // flatten this wait set if another hierarchy level is introduced. - let local_changed = self.inner.capacity_changed.notified(); - let parent_changed = self - .parent - .as_ref() - .map(|parent| parent.inner.capacity_changed.notified()); - match self.reserve(resource, amount) { - Ok(reservation) => return Ok(reservation), - Err(error) if amount > error.limit => return Err(error), - Err(_) => { - if let Some(parent_changed) = parent_changed { - tokio::select! { - _ = local_changed => {} - _ = parent_changed => {} - } - } else { - local_changed.await; - } - } + ResourceClass::BufferedBytes | ResourceClass::HandleCommandBytes => { + self.observe_buffer(BufferMetricClass::Native, used) } + ResourceClass::Datagrams | ResourceClass::UdpDatagrams => { + self.observe_resource(ResourceMetricClass::Datagrams, used) + } + ResourceClass::HandleCommands => { + self.observe_resource(ResourceMetricClass::HandleCommands, used) + } + ResourceClass::BridgeCalls => { + self.observe_resource(ResourceMetricClass::BridgeCalls, used) + } + ResourceClass::BridgeRequestBytes | ResourceClass::BridgeResponseBytes => { + self.observe_buffer(BufferMetricClass::Bridge, used) + } + ResourceClass::AsyncCompletions => { + self.observe_resource(ResourceMetricClass::AsyncCompletions, used) + } + ResourceClass::AsyncCompletionBytes => { + self.observe_buffer(BufferMetricClass::Bridge, used) + } + ResourceClass::UdpBytes => self.observe_buffer(BufferMetricClass::Datagram, used), + ResourceClass::TlsBytes => self.observe_buffer(BufferMetricClass::Tls, used), + ResourceClass::Timers => self.observe_resource(ResourceMetricClass::Timers, used), + ResourceClass::Tasks => self.observe_resource(ResourceMetricClass::Tasks, used), + ResourceClass::ExecutorSlots => {} + ResourceClass::ExecutorBytes => self.observe_buffer(BufferMetricClass::Executor, used), + ResourceClass::Http2BufferedBytes => { + self.observe_buffer(BufferMetricClass::Http2, used) + } + ResourceClass::Http2Connections => { + self.observe_resource(ResourceMetricClass::Http2Connections, used) + } + ResourceClass::Http2Streams => { + self.observe_resource(ResourceMetricClass::Http2Streams, used) + } + ResourceClass::Http2HeaderBytes + | ResourceClass::Http2DataBytes + | ResourceClass::Http2Commands + | ResourceClass::Http2CommandBytes + | ResourceClass::Http2Events + | ResourceClass::Http2EventBytes => {} } } } - -fn maybe_warn(inner: &LedgerInner, resource: ResourceClass, counter: &mut CounterState) { - let Some(limit) = inner.limits.get(&resource) else { - return; - }; - // Integer comparisons avoid rounding and make zero impossible to treat as - // near-limit. The warning rearms only after usage falls below 70%. - let near = - counter.used != 0 && counter.used.saturating_mul(100) >= limit.maximum.saturating_mul(80); - if near && !counter.warning_active { - counter.warning_active = true; - eprintln!( - "WARN_AGENTOS_RESOURCE_NEAR_LIMIT: scope={} resource={} used={} limit={} config={}", - inner.scope, - resource.name(), - counter.used, - limit.maximum, - limit.config_path - ); - } -} - -#[derive(Debug)] -struct Allocation { - ledger: Arc, - resource: ResourceClass, - amount: usize, -} - -fn release_allocation(allocation: &Allocation) { - let mut state = allocation.ledger.state.lock().unwrap_or_else(|poisoned| { - eprintln!( - "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering release scope={} resource={}", - allocation.ledger.scope, - allocation.resource.name() - ); - poisoned.into_inner() - }); - let counter = state.counters.entry(allocation.resource).or_default(); - if allocation.amount > counter.used { - eprintln!( - "ERR_AGENTOS_RESOURCE_ACCOUNTING_UNDERFLOW: scope={} resource={} used={} release={}", - allocation.ledger.scope, - allocation.resource.name(), - counter.used, - allocation.amount - ); - allocation - .ledger - .integrity_failed - .store(true, Ordering::Release); - counter.used = 0; - } else { - counter.used -= allocation.amount; - } - if let Some(limit) = allocation.ledger.limits.get(&allocation.resource) { - if counter.used.saturating_mul(100) < limit.maximum.saturating_mul(70) { - counter.warning_active = false; - } - } - observe_usage(&allocation.ledger, allocation.resource, counter.used); - // `notify_one` retains a permit when no waiter is currently polled, which - // closes the release-between-retry-and-await race. - allocation.ledger.capacity_changed.notify_one(); -} - -fn observe_usage(inner: &LedgerInner, resource: ResourceClass, used: usize) { - let Some(metrics) = &inner.metrics else { - return; - }; - match resource { - ResourceClass::Capabilities => { - metrics.observe_resource(ResourceMetricClass::Capabilities, used) - } - ResourceClass::ReadyHandles => { - metrics.observe_resource(ResourceMetricClass::ReadyHandles, used) - } - ResourceClass::Sockets => metrics.observe_resource(ResourceMetricClass::Sockets, used), - ResourceClass::Connections => { - metrics.observe_resource(ResourceMetricClass::Connections, used) - } - ResourceClass::BufferedBytes => metrics.observe_buffer(BufferMetricClass::Native, used), - ResourceClass::Datagrams => metrics.observe_resource(ResourceMetricClass::Datagrams, used), - ResourceClass::HandleCommands => { - metrics.observe_resource(ResourceMetricClass::HandleCommands, used) - } - ResourceClass::HandleCommandBytes => { - metrics.observe_buffer(BufferMetricClass::Native, used) - } - ResourceClass::BridgeCalls => { - metrics.observe_resource(ResourceMetricClass::BridgeCalls, used) - } - ResourceClass::BridgeRequestBytes | ResourceClass::BridgeResponseBytes => { - metrics.observe_buffer(BufferMetricClass::Bridge, used) - } - ResourceClass::AsyncCompletions => { - metrics.observe_resource(ResourceMetricClass::AsyncCompletions, used) - } - ResourceClass::AsyncCompletionBytes => { - metrics.observe_buffer(BufferMetricClass::Bridge, used) - } - ResourceClass::UdpDatagrams => { - metrics.observe_resource(ResourceMetricClass::Datagrams, used) - } - ResourceClass::UdpBytes => metrics.observe_buffer(BufferMetricClass::Datagram, used), - ResourceClass::TlsBytes => metrics.observe_buffer(BufferMetricClass::Tls, used), - ResourceClass::Timers => metrics.observe_resource(ResourceMetricClass::Timers, used), - ResourceClass::Tasks => metrics.observe_resource(ResourceMetricClass::Tasks, used), - ResourceClass::ExecutorSlots => {} - ResourceClass::ExecutorBytes => metrics.observe_buffer(BufferMetricClass::Executor, used), - ResourceClass::Http2BufferedBytes => metrics.observe_buffer(BufferMetricClass::Http2, used), - ResourceClass::Http2Connections => { - metrics.observe_resource(ResourceMetricClass::Http2Connections, used) - } - ResourceClass::Http2Streams => { - metrics.observe_resource(ResourceMetricClass::Http2Streams, used) - } - ResourceClass::Http2HeaderBytes - | ResourceClass::Http2DataBytes - | ResourceClass::Http2Commands - | ResourceClass::Http2CommandBytes - | ResourceClass::Http2Events - | ResourceClass::Http2EventBytes => {} - } -} - -fn release_allocations(allocations: &mut Vec) { - for allocation in allocations.drain(..).rev() { - release_allocation(&allocation); - } -} - -/// Exact ownership of one admitted amount. Moving the value transfers ownership; -/// dropping it releases every scope exactly once. -#[derive(Debug)] -pub struct Reservation { - resource: ResourceClass, - amount: usize, - allocations: Vec, -} - -/// Cloneable ownership used when a charged payload crosses an in-process -/// response router. The underlying reservation releases only after the final -/// envelope/consumer drops it. -#[derive(Clone)] -pub struct SharedReservation(Arc); - -impl SharedReservation { - pub fn new(reservation: Reservation) -> Self { - Self(Arc::new(reservation)) - } - - pub fn resource(&self) -> ResourceClass { - self.0.resource() - } - - pub fn amount(&self) -> usize { - self.0.amount() - } -} - -impl fmt::Debug for SharedReservation { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("SharedReservation") - .field("resource", &self.resource()) - .field("amount", &self.amount()) - .finish_non_exhaustive() - } -} - -impl PartialEq for SharedReservation { - fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } -} - -impl Eq for SharedReservation {} - -impl Reservation { - pub fn resource(&self) -> ResourceClass { - self.resource - } - - pub fn amount(&self) -> usize { - self.amount - } - - /// Split ownership without changing any ledger counter. - pub fn split(&mut self, amount: usize) -> Option { - if amount > self.amount { - return None; - } - self.amount -= amount; - let mut allocations = Vec::with_capacity(self.allocations.len()); - for allocation in &mut self.allocations { - allocation.amount -= amount; - allocations.push(Allocation { - ledger: Arc::clone(&allocation.ledger), - resource: allocation.resource, - amount, - }); - } - Some(Self { - resource: self.resource, - amount, - allocations, - }) - } - - /// Merge two reservations only when they refer to the same scopes and class. - pub fn merge(&mut self, mut other: Self) -> Result<(), Self> { - if self.resource != other.resource || self.allocations.len() != other.allocations.len() { - return Err(other); - } - if self - .allocations - .iter() - .zip(&other.allocations) - .any(|(left, right)| !Arc::ptr_eq(&left.ledger, &right.ledger)) - { - return Err(other); - } - let Some(total) = self.amount.checked_add(other.amount) else { - return Err(other); - }; - for (left, right) in self.allocations.iter_mut().zip(&other.allocations) { - left.amount += right.amount; - } - self.amount = total; - other.amount = 0; - other.allocations.clear(); - Ok(()) - } -} - -impl Drop for Reservation { - fn drop(&mut self) { - release_allocations(&mut self.allocations); - self.amount = 0; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn limit(maximum: usize) -> [(ResourceClass, ResourceLimit); 1] { - [( - ResourceClass::BufferedBytes, - ResourceLimit::new(maximum, "runtime.resources.maxSocketBufferedBytes"), - )] - } - - #[test] - fn child_reservation_charges_and_releases_both_scopes() { - let process = Arc::new(ResourceLedger::root("process", limit(10))); - let vm = ResourceLedger::child("vm-1", limit(6), Arc::clone(&process)); - let reservation = vm.reserve(ResourceClass::BufferedBytes, 6).unwrap(); - assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6); - assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 6); - let error = vm.reserve(ResourceClass::BufferedBytes, 1).unwrap_err(); - assert_eq!(error.scope, "vm-1"); - assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6); - drop(reservation); - assert!(process.is_zero()); - assert!(vm.is_zero()); - } - - #[test] - fn failed_child_admission_rolls_back_parent() { - let process = Arc::new(ResourceLedger::root("process", limit(10))); - let vm = ResourceLedger::child("vm-1", limit(2), Arc::clone(&process)); - let error = vm.reserve(ResourceClass::BufferedBytes, 3).unwrap_err(); - assert_eq!(error.scope, "vm-1"); - assert!(process.is_zero()); - assert!(vm.is_zero()); - } - - #[test] - fn capacity_probe_checks_parent_and_child_without_changing_usage() { - let process = Arc::new(ResourceLedger::root("process", limit(3))); - let vm = ResourceLedger::child("vm-1", limit(5), Arc::clone(&process)); - let held = process - .reserve(ResourceClass::BufferedBytes, 2) - .expect("reserve process capacity"); - - assert!(vm.capacity_available(ResourceClass::BufferedBytes, 1)); - assert!(!vm.capacity_available(ResourceClass::BufferedBytes, 2)); - assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 2); - assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 0); - - drop(held); - assert!(vm.capacity_available(ResourceClass::BufferedBytes, 2)); - } - - #[test] - fn split_and_merge_transfer_without_counter_drift() { - let ledger = ResourceLedger::root("vm-1", limit(10)); - let mut source = ledger.reserve(ResourceClass::BufferedBytes, 8).unwrap(); - let transferred = source.split(3).unwrap(); - assert_eq!(source.amount(), 5); - assert_eq!(transferred.amount(), 3); - assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8); - source.merge(transferred).unwrap(); - assert_eq!(source.amount(), 8); - assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8); - drop(source); - assert!(ledger.is_zero()); - } - - #[tokio::test] - async fn impossible_async_reservation_returns_typed_error() { - let ledger = ResourceLedger::root("vm-1", limit(4)); - let error = ledger - .reserve_when_available(ResourceClass::BufferedBytes, 5) - .await - .unwrap_err(); - assert_eq!(error.resource, ResourceClass::BufferedBytes); - assert_eq!(error.requested, 5); - assert_eq!(error.limit, 4); - assert_eq!( - error.config_path, - "runtime.resources.maxSocketBufferedBytes" - ); - } - - #[tokio::test] - async fn child_waiter_wakes_when_only_parent_capacity_changes() { - let process = Arc::new(ResourceLedger::root("process", limit(1))); - let vm = Arc::new(ResourceLedger::child( - "vm-1", - limit(2), - Arc::clone(&process), - )); - let held = process - .reserve(ResourceClass::BufferedBytes, 1) - .expect("fill parent"); - let waiting_vm = Arc::clone(&vm); - let waiter = tokio::spawn(async move { - waiting_vm - .reserve_when_available(ResourceClass::BufferedBytes, 1) - .await - }); - tokio::task::yield_now().await; - assert!(!waiter.is_finished()); - drop(held); - let reservation = tokio::time::timeout(std::time::Duration::from_secs(1), waiter) - .await - .expect("parent release must wake child waiter") - .expect("waiter task") - .expect("reservation"); - drop(reservation); - assert!(process.is_zero()); - assert!(vm.is_zero()); - } - - #[test] - fn accounting_underflow_latches_integrity_failure() { - let ledger = ResourceLedger::root("vm-1", limit(1)); - let inner = Arc::clone(&ledger.inner); - let malformed = Reservation { - resource: ResourceClass::BufferedBytes, - amount: 1, - allocations: vec![Allocation { - ledger: inner, - resource: ResourceClass::BufferedBytes, - amount: 1, - }], - }; - drop(malformed); - assert!(!ledger.integrity_ok()); - assert!(ledger.is_zero()); - } -} diff --git a/crates/runtime/src/executor.rs b/crates/runtime/src/executor.rs new file mode 100644 index 0000000000..17bded2eab --- /dev/null +++ b/crates/runtime/src/executor.rs @@ -0,0 +1,293 @@ +use std::fmt; +use std::sync::{Arc, Mutex}; + +use crate::metrics::{ExecutorMetricClass, RuntimeMetrics}; + +pub const VM_EXECUTOR_LIMIT_CONFIG_PATH: &str = "runtime.executor.maxActiveVms"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VmExecutorAdmissionSnapshot { + pub active: usize, + pub maximum: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VmExecutorAdmissionError { + Limit { active: usize, maximum: usize }, + Poisoned, +} + +impl VmExecutorAdmissionError { + pub fn code(&self) -> &'static str { + match self { + Self::Limit { .. } => "ERR_AGENTOS_VM_EXECUTOR_LIMIT", + Self::Poisoned => "ERR_AGENTOS_VM_EXECUTOR_POISONED", + } + } + + pub fn config_path(&self) -> &'static str { + VM_EXECUTOR_LIMIT_CONFIG_PATH + } +} + +impl fmt::Display for VmExecutorAdmissionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Limit { active, maximum } => write!( + formatter, + "{}: active guest executors reached limit of {maximum} (active={active}); raise {}", + self.code(), + self.config_path() + ), + Self::Poisoned => write!( + formatter, + "{}: process VM executor admission lock poisoned", + self.code() + ), + } + } +} + +impl std::error::Error for VmExecutorAdmissionError {} + +#[derive(Debug)] +struct VmExecutorAdmissionInner { + state: Mutex, + maximum: usize, + metrics: RuntimeMetrics, +} + +#[derive(Debug, Default)] +struct VmExecutorAdmissionState { + active: usize, + near_limit_warning_emitted: bool, +} + +fn near_limit_threshold(maximum: usize) -> usize { + maximum.saturating_sub(maximum / 5).max(1) +} + +/// Process-wide admission for dedicated guest executor threads. +/// +/// Clones share one active count across every engine. The permit must remain +/// owned by the executor generation until its OS thread exits; a detached or +/// stale generation therefore stays charged while it is quarantined. +#[derive(Clone, Debug)] +pub struct VmExecutorAdmission { + inner: Arc, +} + +impl VmExecutorAdmission { + pub(crate) fn new(maximum: usize, metrics: RuntimeMetrics) -> Self { + Self { + inner: Arc::new(VmExecutorAdmissionInner { + state: Mutex::new(VmExecutorAdmissionState::default()), + maximum, + metrics, + }), + } + } + + pub fn maximum(&self) -> usize { + self.inner.maximum + } + + pub fn snapshot(&self) -> VmExecutorAdmissionSnapshot { + let active = self + .inner + .state + .lock() + .map(|state| state.active) + .unwrap_or_else(|_| { + eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_POISONED: process VM executor admission snapshot failed" + ); + self.inner.maximum + }); + VmExecutorAdmissionSnapshot { + active, + maximum: self.inner.maximum, + } + } + + pub fn try_acquire(&self) -> Result { + self.try_acquire_at_most(self.inner.maximum) + } + + /// Acquire against a caller-requested ceiling without creating a second + /// counter. The process configuration remains the hard upper bound. + pub fn try_acquire_at_most( + &self, + requested_maximum: usize, + ) -> Result { + let maximum = requested_maximum.min(self.inner.maximum); + let mut state = self + .inner + .state + .lock() + .map_err(|_| VmExecutorAdmissionError::Poisoned)?; + if state.active >= maximum { + return Err(VmExecutorAdmissionError::Limit { + active: state.active, + maximum, + }); + } + state.active += 1; + let process_warning_threshold = near_limit_threshold(self.inner.maximum); + if state.active >= process_warning_threshold && !state.near_limit_warning_emitted { + state.near_limit_warning_emitted = true; + eprintln!( + "WARN_AGENTOS_VM_EXECUTOR_NEAR_LIMIT: active={} limit={} threshold={}; raise {} before sustained saturation", + state.active, + self.inner.maximum, + process_warning_threshold, + VM_EXECUTOR_LIMIT_CONFIG_PATH + ); + } + self.inner + .metrics + .observe_executor(ExecutorMetricClass::Vm, state.active, 0); + Ok(VmExecutorPermit { + admission: self.clone(), + }) + } +} + +/// RAII ownership of one process-wide guest executor slot. +#[derive(Debug)] +pub struct VmExecutorPermit { + admission: VmExecutorAdmission, +} + +impl Drop for VmExecutorPermit { + fn drop(&mut self) { + match self.admission.inner.state.lock() { + Ok(mut state) if state.active > 0 => { + state.active -= 1; + if state.active < near_limit_threshold(self.admission.inner.maximum) { + state.near_limit_warning_emitted = false; + } + self.admission.inner.metrics.observe_executor( + ExecutorMetricClass::Vm, + state.active, + 0, + ); + } + Ok(_) => eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_ACCOUNTING_UNDERFLOW: executor permit released at zero" + ), + Err(_) => { + eprintln!("ERR_AGENTOS_VM_EXECUTOR_POISONED: executor permit could not be released") + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clones_share_saturation_and_permit_drop_releases_capacity() { + let metrics = RuntimeMetrics::new(); + let admission = VmExecutorAdmission::new(2, metrics.clone()); + let clone = admission.clone(); + + let first = admission.try_acquire().expect("first executor permit"); + let second = clone.try_acquire().expect("second executor permit"); + let error = admission + .try_acquire() + .expect_err("third executor must saturate the process quota"); + assert_eq!( + error, + VmExecutorAdmissionError::Limit { + active: 2, + maximum: 2, + } + ); + assert_eq!(error.code(), "ERR_AGENTOS_VM_EXECUTOR_LIMIT"); + assert_eq!(error.config_path(), "runtime.executor.maxActiveVms"); + + let active = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active; + assert_eq!(active.current, 2); + assert_eq!(active.high_water, 2); + + drop(first); + assert_eq!(admission.snapshot().active, 1); + let replacement = clone + .try_acquire() + .expect("dropped permit must release shared capacity"); + drop(second); + drop(replacement); + assert_eq!(admission.snapshot().active, 0); + let active = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active; + assert_eq!(active.current, 0); + assert_eq!(active.high_water, 2); + } + + #[test] + fn requested_ceiling_uses_the_process_counter_and_never_raises_process_limit() { + let admission = VmExecutorAdmission::new(3, RuntimeMetrics::new()); + let first = admission + .try_acquire_at_most(1) + .expect("requested sub-ceiling admits first executor"); + assert!(matches!( + admission.try_acquire_at_most(1), + Err(VmExecutorAdmissionError::Limit { + active: 1, + maximum: 1 + }) + )); + let second = admission + .try_acquire_at_most(usize::MAX) + .expect("larger request remains bounded by process quota"); + let third = admission + .try_acquire_at_most(usize::MAX) + .expect("fill process quota"); + assert!(matches!( + admission.try_acquire_at_most(usize::MAX), + Err(VmExecutorAdmissionError::Limit { + active: 3, + maximum: 3 + }) + )); + drop((first, second, third)); + assert_eq!(admission.snapshot().active, 0); + } + + #[test] + fn near_limit_warning_is_coalesced_and_rearmed_below_eighty_percent() { + let admission = VmExecutorAdmission::new(5, RuntimeMetrics::new()); + let mut permits = (0..4) + .map(|_| admission.try_acquire().expect("fill to warning threshold")) + .collect::>(); + assert!( + admission + .inner + .state + .lock() + .expect("admission state") + .near_limit_warning_emitted + ); + + permits.pop(); + assert!( + !admission + .inner + .state + .lock() + .expect("admission state") + .near_limit_warning_emitted, + "falling below 80% must rearm the warning" + ); + permits.push(admission.try_acquire().expect("re-enter warning threshold")); + assert!( + admission + .inner + .state + .lock() + .expect("admission state") + .near_limit_warning_emitted + ); + } +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 71392d0879..84752e7ba8 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -25,11 +25,16 @@ use metrics::{ pub mod accounting; pub mod capability; +pub mod executor; pub mod fairness; pub mod metrics; pub mod readiness; pub mod supervision; +pub use executor::{ + VmExecutorAdmission, VmExecutorAdmissionError, VmExecutorAdmissionSnapshot, VmExecutorPermit, + VM_EXECUTOR_LIMIT_CONFIG_PATH, +}; pub use supervision::{ TaskClass, TaskClassSnapshot, TaskOwner, TaskSpawnError, TaskSupervisor, TaskTerminalReason, TaskTerminalReport, @@ -1114,7 +1119,7 @@ pub struct RuntimeContext { fairness: FairWorkBroker, terminal_failure: Arc>>, task_poll_watchdog: Duration, - max_active_vm_executors: usize, + vm_executors: VmExecutorAdmission, vm_executor_teardown_timeout: Duration, blocking_job_timeout: Duration, admission_open: Arc, @@ -1146,7 +1151,11 @@ impl RuntimeContext { } pub fn max_active_vm_executors(&self) -> usize { - self.max_active_vm_executors + self.vm_executors.maximum() + } + + pub fn vm_executor_admission(&self) -> &VmExecutorAdmission { + &self.vm_executors } pub fn vm_executor_teardown_timeout(&self) -> Duration { @@ -1257,7 +1266,7 @@ impl RuntimeContext { fairness: self.fairness.clone(), terminal_failure: Arc::new(Mutex::new(None)), task_poll_watchdog: self.task_poll_watchdog, - max_active_vm_executors: self.max_active_vm_executors, + vm_executors: self.vm_executors.clone(), vm_executor_teardown_timeout: self.vm_executor_teardown_timeout, blocking_job_timeout: self.blocking_job_timeout, admission_open, @@ -1482,16 +1491,18 @@ impl SidecarRuntime { ), )); let metrics = RuntimeMetrics::new(); + let vm_executors = + VmExecutorAdmission::new(config.max_active_vm_executors, metrics.clone()); let fairness = FairWorkBroker::new(config.fairness.scheduler_config(), metrics.clone()) .map_err(|error| { RuntimeBuildError(format!( "ERR_AGENTOS_RUNTIME_FAIRNESS_START: failed to build process fairness broker: {error}" )) })?; - let resources = Arc::new(ResourceLedger::root_with_metrics( + let resources = Arc::new(ResourceLedger::root_with_observer( "sidecar-process", resource_limits, - metrics.clone(), + Arc::new(metrics.clone()), )); let admission_open = Arc::new(AtomicBool::new(true)); let admission_gate = Arc::new(Mutex::new(())); @@ -1535,7 +1546,7 @@ impl SidecarRuntime { fairness, terminal_failure: Arc::new(Mutex::new(None)), task_poll_watchdog: Duration::from_millis(config.task_poll_watchdog_ms), - max_active_vm_executors: config.max_active_vm_executors, + vm_executors, vm_executor_teardown_timeout: Duration::from_millis( config.vm_executor_teardown_timeout_ms, ), diff --git a/crates/runtime/src/readiness.rs b/crates/runtime/src/readiness.rs index 2719884e65..74ad6939a8 100644 --- a/crates/runtime/src/readiness.rs +++ b/crates/runtime/src/readiness.rs @@ -80,6 +80,12 @@ pub struct ReadyObservation { pub revision: u64, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SignalObservation { + pub signal: i32, + pub delivery_token: u64, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct ReadyBatch { pub generation: u64, @@ -236,7 +242,7 @@ impl WakeFailure { struct ReadyState { handles: BTreeMap, last_delivered_capability: Option, - signals: BTreeSet, + signals: BTreeMap, timers: BTreeSet, wake: WakeState, next_epoch: u64, @@ -323,7 +329,7 @@ impl SessionReadyBroker { state: Arc::new(Mutex::new(ReadyState { handles: BTreeMap::new(), last_delivered_capability: None, - signals: BTreeSet::new(), + signals: BTreeMap::new(), timers: BTreeSet::new(), wake: WakeState::Idle, next_epoch: 1, @@ -343,13 +349,18 @@ impl SessionReadyBroker { self.max_batch_handles } - pub fn mark_signal_ready(&self, generation: u64, signal: i32) -> Result<(), ReadyError> { + pub fn mark_signal_ready( + &self, + generation: u64, + signal: i32, + delivery_token: u64, + ) -> Result<(), ReadyError> { self.validate_generation(generation)?; if !(1..=64).contains(&signal) { return Err(ReadyError::InvalidSignal { signal }); } let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?; - state.signals.insert(signal); + state.signals.insert(signal, delivery_token); self.schedule_wake_locked(&mut state, false) } @@ -372,18 +383,21 @@ impl SessionReadyBroker { generation: u64, epoch: u64, max: usize, - ) -> Result, ReadyError> { + ) -> Result, ReadyError> { self.validate_generation(generation)?; let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?; self.validate_epoch(&state, epoch)?; let values = state .signals .iter() - .copied() + .map(|(signal, delivery_token)| SignalObservation { + signal: *signal, + delivery_token: *delivery_token, + }) .take(max.max(1)) .collect::>(); for value in &values { - state.signals.remove(value); + state.signals.remove(&value.signal); } Ok(values) } @@ -826,7 +840,9 @@ mod tests { async fn signals_and_timers_share_one_wake_but_keep_durable_control_state() { let (broker, mut wakes) = SessionReadyBroker::new(8, 8).expect("broker"); for _ in 0..10_000 { - broker.mark_signal_ready(8, 15).expect("coalesce SIGTERM"); + broker + .mark_signal_ready(8, 15, 41) + .expect("coalesce SIGTERM"); broker.mark_timer_ready(8, 91).expect("coalesce timer"); } let wake = wakes.recv().await.expect("one control wake"); @@ -837,7 +853,13 @@ mod tests { let batch = broker.ready_batch(8, wake.epoch, 8).expect("control batch"); assert!(batch.signals_ready); assert!(batch.timers_ready); - assert_eq!(broker.drain_signals(8, wake.epoch, 8).unwrap(), vec![15]); + assert_eq!( + broker.drain_signals(8, wake.epoch, 8).unwrap(), + vec![SignalObservation { + signal: 15, + delivery_token: 41, + }] + ); assert_eq!(broker.drain_timers(8, wake.epoch, 8).unwrap(), vec![91]); broker .complete_wake(8, wake.epoch, &[]) diff --git a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare index a74b8bbba6..5b15f04036 100644 --- a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare +++ b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare @@ -670,6 +670,7 @@ type QueueSnapshotEntry struct { type ResourceSnapshotResponse struct { runningProcesses: u64 + stoppedProcesses: u64 exitedProcesses: u64 fdTables: u64 openFds: u64 diff --git a/crates/sidecar-protocol/src/protocol.rs b/crates/sidecar-protocol/src/protocol.rs index 91518f4f3b..07594a59bd 100644 --- a/crates/sidecar-protocol/src/protocol.rs +++ b/crates/sidecar-protocol/src/protocol.rs @@ -2730,6 +2730,10 @@ pub struct JavascriptPosixSpawnFileAction { #[serde(rename_all = "camelCase")] pub struct JavascriptSpawnHostNetFd { pub guest_fd: u32, + /// Decimal kernel open-file-description identity. Present for managed + /// execution; absent only for the standalone Node compatibility path. + #[serde(default)] + pub description_id: Option, #[serde(default)] pub close_on_exec: bool, #[serde(default)] @@ -2901,13 +2905,13 @@ pub struct JavascriptNetListenRequest { } #[derive(Debug, Deserialize)] -pub struct JavascriptDgramCreateSocketRequest { +pub struct DgramCreateSocketOptions { #[serde(rename = "type")] pub socket_type: String, } #[derive(Debug, Deserialize)] -pub struct JavascriptDgramBindRequest { +pub struct DgramBindOptions { #[serde(default)] pub address: Option, #[serde(default)] @@ -2915,7 +2919,7 @@ pub struct JavascriptDgramBindRequest { } #[derive(Debug, Deserialize)] -pub struct JavascriptDgramSendRequest { +pub struct DgramSendOptions { #[serde(default)] pub address: Option, #[serde(default)] @@ -2923,7 +2927,7 @@ pub struct JavascriptDgramSendRequest { } #[derive(Debug, Deserialize)] -pub struct JavascriptDgramConnectRequest { +pub struct DgramConnectOptions { #[serde(default)] pub address: Option, pub port: u16, diff --git a/crates/sidecar-protocol/src/wire.rs b/crates/sidecar-protocol/src/wire.rs index 61e2620560..b93ef6e3b5 100644 --- a/crates/sidecar-protocol/src/wire.rs +++ b/crates/sidecar-protocol/src/wire.rs @@ -440,7 +440,6 @@ fn legacy_limits_config( max_readdir_entries: legacy_u64(metadata, "resource.max_readdir_entries"), max_recursive_fs_depth: legacy_u64(metadata, "resource.max_recursive_fs_depth"), max_recursive_fs_entries: legacy_u64(metadata, "resource.max_recursive_fs_entries"), - max_wasm_fuel: legacy_u64(metadata, "resource.max_wasm_fuel"), max_wasm_memory_bytes: legacy_u64(metadata, "resource.max_wasm_memory_bytes"), max_wasm_stack_bytes: legacy_u64(metadata, "resource.max_wasm_stack_bytes"), }; @@ -562,7 +561,9 @@ fn legacy_limits_config( sync_read_limit_bytes: legacy_u64(metadata, "limits.wasm.sync_read_limit_bytes"), prewarm_timeout_ms: legacy_u64(metadata, "limits.wasm.prewarm_timeout_ms"), runner_heap_limit_mb: legacy_u64(metadata, "limits.wasm.runner_heap_limit_mb"), - runner_cpu_time_limit_ms: legacy_u64(metadata, "limits.wasm.runner_cpu_time_limit_ms"), + active_cpu_time_limit_ms: legacy_u64(metadata, "limits.wasm.active_cpu_time_limit_ms"), + wall_clock_limit_ms: legacy_u64(metadata, "limits.wasm.wall_clock_limit_ms"), + deterministic_fuel: legacy_u64(metadata, "limits.wasm.deterministic_fuel"), }; let process = agentos_vm_config::ProcessLimitsConfig { max_spawn_file_actions: legacy_u64(metadata, "limits.process.max_spawn_file_actions") @@ -575,6 +576,14 @@ fn legacy_limits_config( pending_stdin_bytes: legacy_u64(metadata, "limits.process.pending_stdin_bytes"), pending_event_count: legacy_u64(metadata, "limits.process.pending_event_count"), pending_event_bytes: legacy_u64(metadata, "limits.process.pending_event_bytes"), + max_pending_child_sync_count: legacy_u64( + metadata, + "limits.process.max_pending_child_sync_count", + ), + max_pending_child_sync_bytes: legacy_u64( + metadata, + "limits.process.max_pending_child_sync_bytes", + ), }; let config = agentos_vm_config::VmLimitsConfig { @@ -637,7 +646,6 @@ fn legacy_has_resource_limits(config: &agentos_vm_config::ResourceLimitsConfig) || config.max_process_argv_bytes.is_some() || config.max_process_env_bytes.is_some() || config.max_readdir_entries.is_some() - || config.max_wasm_fuel.is_some() || config.max_wasm_memory_bytes.is_some() || config.max_wasm_stack_bytes.is_some() } @@ -705,7 +713,9 @@ fn legacy_has_wasm_limits(config: &agentos_vm_config::WasmLimitsConfig) -> bool || config.sync_read_limit_bytes.is_some() || config.prewarm_timeout_ms.is_some() || config.runner_heap_limit_mb.is_some() - || config.runner_cpu_time_limit_ms.is_some() + || config.active_cpu_time_limit_ms.is_some() + || config.wall_clock_limit_ms.is_some() + || config.deterministic_fuel.is_some() } fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> bool { @@ -714,6 +724,8 @@ fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> || config.pending_stdin_bytes.is_some() || config.pending_event_count.is_some() || config.pending_event_bytes.is_some() + || config.max_pending_child_sync_count.is_some() + || config.max_pending_child_sync_bytes.is_some() } // Ownership-scope constructor ergonomics. The generated BARE union exposes only the @@ -1322,16 +1334,28 @@ mod tests { } #[test] - fn legacy_metadata_preserves_wasm_runner_cpu_limit_as_only_new_field() { - let metadata = BTreeMap::from([( - String::from("limits.wasm.runner_cpu_time_limit_ms"), - String::from("987"), - )]); + fn legacy_metadata_preserves_wasm_cpu_fields() { + let metadata = BTreeMap::from([ + ( + String::from("limits.wasm.active_cpu_time_limit_ms"), + String::from("987"), + ), + ( + String::from("limits.wasm.wall_clock_limit_ms"), + String::from("654"), + ), + ( + String::from("limits.wasm.deterministic_fuel"), + String::from("321"), + ), + ]); let config = legacy_limits_config(&metadata).expect("limits config"); let wasm = config.wasm.expect("wasm limits"); - assert_eq!(wasm.runner_cpu_time_limit_ms, Some(987)); + assert_eq!(wasm.active_cpu_time_limit_ms, Some(987)); + assert_eq!(wasm.wall_clock_limit_ms, Some(654)); + assert_eq!(wasm.deterministic_fuel, Some(321)); } #[test] diff --git a/crates/v8-runtime/src/bridge.rs b/crates/v8-runtime/src/bridge.rs index 9ec869c662..7003864136 100644 --- a/crates/v8-runtime/src/bridge.rs +++ b/crates/v8-runtime/src/bridge.rs @@ -613,6 +613,110 @@ fn bridge_response_payload_to_v8<'s>( raw_bytes_to_uint8array(scope, payload) } +struct DecodedBridgeError { + code: String, + message: String, + details: Option, +} + +fn decode_bridge_error(payload: &[u8]) -> Option { + let LimitedCborValue(ciborium::Value::Map(entries)) = + ciborium::de::from_reader_with_recursion_limit(payload, MAX_CBOR_BRIDGE_DEPTH).ok()? + else { + return None; + }; + let mut code = None; + let mut message = None; + let mut details = None; + for (key, value) in entries { + let ciborium::Value::Text(key) = key else { + continue; + }; + match key.as_str() { + "code" => { + if let ciborium::Value::Text(value) = value { + code = Some(value); + } + } + "message" => { + if let ciborium::Value::Text(value) = value { + message = Some(value); + } + } + "details" if value != ciborium::Value::Null => details = Some(value), + _ => {} + } + } + Some(DecodedBridgeError { + code: code?, + message: message?, + details, + }) +} + +fn bridge_error_exception<'s>( + scope: &mut v8::HandleScope<'s>, + payload: &[u8], +) -> v8::Local<'s, v8::Value> { + let decoded = decode_bridge_error(payload); + let message = decoded + .as_ref() + .map(|error| error.message.as_str()) + .unwrap_or_else(|| std::str::from_utf8(payload).unwrap_or("bridge host call failed")); + let message = v8::String::new(scope, message).expect("bridge error message allocation"); + let exception = v8::Exception::error(scope, message); + let Some(decoded) = decoded else { + return exception; + }; + let object = exception + .to_object(scope) + .expect("Error exceptions are objects"); + let code_key = v8::String::new(scope, "code").expect("code property key"); + let code = v8::String::new(scope, &decoded.code).expect("bridge error code allocation"); + if object.set(scope, code_key.into(), code.into()) != Some(true) { + eprintln!( + "ERR_AGENTOS_BRIDGE_ERROR_PROPERTY: failed to attach the typed bridge error code" + ); + } + if let Some(details) = decoded.details { + match cbor_to_v8_inner(scope, &details, 0) { + Ok(details) => { + let details_key = + v8::String::new(scope, "details").expect("details property key"); + if object.set(scope, details_key.into(), details) != Some(true) { + eprintln!( + "ERR_AGENTOS_BRIDGE_ERROR_PROPERTY: failed to attach typed bridge error details" + ); + } + } + Err(error) => eprintln!( + "ERR_AGENTOS_BRIDGE_ERROR_DETAILS: failed to decode typed bridge error details: {error}" + ), + } + } + exception +} + +fn runtime_error_exception<'s>( + scope: &mut v8::HandleScope<'s>, + message: &str, +) -> v8::Local<'s, v8::Value> { + let message = v8::String::new(scope, message).expect("runtime error message allocation"); + let exception = v8::Exception::error(scope, message); + let object = exception + .to_object(scope) + .expect("Error exceptions are objects"); + let code_key = v8::String::new(scope, "code").expect("code property key"); + let code = v8::String::new(scope, "ERR_AGENTOS_BRIDGE_RUNTIME") + .expect("runtime error code allocation"); + if object.set(scope, code_key.into(), code.into()) != Some(true) { + eprintln!( + "ERR_AGENTOS_BRIDGE_ERROR_PROPERTY: failed to attach the bridge runtime error code" + ); + } + exception +} + /// Serialize a V8 value to CBOR bytes. pub fn serialize_cbor_value( scope: &mut v8::HandleScope, @@ -1883,12 +1987,17 @@ fn sync_bridge_callback<'s>( // Perform sync-blocking bridge call let max_response_bytes = bridge_response_declaration(scope, &data.method, &args); - match ctx.sync_call_response_with_max_response_bytes( + match ctx.sync_call_frame_with_max_response_bytes( &data.method, encoded_args, max_response_bytes, ) { Ok(Some(response)) => { + if response.status == 1 { + let exception = bridge_error_exception(scope, &response.payload); + scope.throw_exception(exception); + return; + } let v8_val = bridge_response_payload_to_v8(scope, response.status, &response.payload); if let Some(val) = v8_val { rv.set(val); @@ -1902,14 +2011,7 @@ fn sync_bridge_callback<'s>( rv.set_undefined(); } Err(err_msg) => { - let msg = v8::String::new(scope, &err_msg).unwrap(); - let exc = v8::Exception::error(scope, msg); - if let Some(code) = bridge_error_code(&err_msg) { - let exc_object = exc.to_object(scope).unwrap(); - let code_key = v8::String::new(scope, "code").unwrap(); - let code_value = v8::String::new(scope, code).unwrap(); - let _ = exc_object.set(scope, code_key.into(), code_value.into()); - } + let exc = runtime_error_exception(scope, &err_msg); scope.throw_exception(exc); } } @@ -2181,26 +2283,19 @@ pub fn resolve_pending_promise( pending: &PendingPromises, call_id: u64, status: u8, - result: Option>, - error: Option, + payload: Option>, ) -> Result<(), String> { let resolver_global = pending .remove(call_id) .ok_or_else(|| format!("no pending promise for call_id {}", call_id))?; let resolver = v8::Local::new(scope, &resolver_global); - if let Some(err_msg) = error { - let msg = v8::String::new(scope, &err_msg).unwrap(); - let exc = v8::Exception::error(scope, msg); - if let Some(code) = bridge_error_code(&err_msg) { - let exc_object = exc.to_object(scope).unwrap(); - let code_key = v8::String::new(scope, "code").unwrap(); - let code_value = v8::String::new(scope, code).unwrap(); - let _ = exc_object.set(scope, code_key.into(), code_value.into()); - } + if status == 1 { + let payload = payload.as_deref().unwrap_or_default(); + let exc = bridge_error_exception(scope, payload); resolver.reject(scope, exc); - } else if let Some(result_bytes) = result { - let v8_val = bridge_response_payload_to_v8(scope, status, &result_bytes); + } else if let Some(payload) = payload { + let v8_val = bridge_response_payload_to_v8(scope, status, &payload); if let Some(val) = v8_val { resolver.resolve(scope, val); } else { @@ -2219,42 +2314,10 @@ pub fn resolve_pending_promise( Ok(()) } -fn bridge_error_code(message: &str) -> Option<&str> { - const TRUSTED_PREFIXES: &[&str] = &[ - "ERR_AGENTOS_NODE_SYNC_RPC", - "ERR_AGENTOS_PYTHON_VFS_RPC", - "ERR_AGENTOS_BRIDGE", - ]; - - let mut segments = message.split(':').map(str::trim); - let first = segments.next()?; - if is_errno_segment(first) { - return Some(first); - } - - if TRUSTED_PREFIXES.contains(&first) { - let second = segments.next()?; - if is_errno_segment(second) { - return Some(second); - } - } - - None -} - -fn is_errno_segment(segment: &str) -> bool { - segment.len() >= 2 - && segment.starts_with('E') - && !segment.starts_with("ERR_") - && segment[1..] - .bytes() - .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') -} - #[cfg(test)] mod tests { use super::{ - bridge_error_code, clear_vm_context_registry_for_test, declared_bridge_response_bytes, + clear_vm_context_registry_for_test, declared_bridge_response_bytes, decode_bridge_error, deserialize_cbor_value, fill_vm_context_registry_for_test, register_async_bridge_fns, register_sync_bridge_fns, reserve_vm_context_slot, reset_vm_context_registry, serialize_cbor_value, vm_context_capacity_error, vm_context_registry_len_for_test, @@ -2309,26 +2372,39 @@ mod tests { } #[test] - fn bridge_error_code_rejects_guest_controlled_errno_segments() { - assert_eq!(bridge_error_code("user said 'EACCES: denied'"), None); - assert_eq!( - bridge_error_code("prefix: user said 'EPERM': more text"), - None - ); - assert_eq!(bridge_error_code("ERR_AGENTOS_FAKE: EACCES: denied"), None); + fn bridge_error_decoder_rejects_unstructured_diagnostics() { + assert!(decode_bridge_error(b"user said 'EACCES: denied'").is_none()); + assert!(decode_bridge_error(b"ERR_AGENTOS_FAKE: EACCES: denied").is_none()); } #[test] - fn bridge_error_code_accepts_trusted_secure_exec_prefixes() { - assert_eq!( - bridge_error_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"), - Some("EACCES") - ); - assert_eq!( - bridge_error_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"), - Some("ENOENT") - ); - assert_eq!(bridge_error_code("EEXIST: already exists"), Some("EEXIST")); + fn bridge_error_decoder_preserves_structured_fields() { + let mut payload = Vec::new(); + ciborium::into_writer( + &ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(String::from("EACCES")), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(String::from("permission denied")), + ), + ( + ciborium::Value::Text(String::from("details")), + ciborium::Value::Map(vec![( + ciborium::Value::Text(String::from("limitName")), + ciborium::Value::Text(String::from("maxBytes")), + )]), + ), + ]), + &mut payload, + ) + .expect("encode structured error"); + let decoded = decode_bridge_error(&payload).expect("decode structured error"); + assert_eq!(decoded.code, "EACCES"); + assert_eq!(decoded.message, "permission denied"); + assert!(decoded.details.is_some()); } #[test] diff --git a/crates/v8-runtime/src/embedded_runtime.rs b/crates/v8-runtime/src/embedded_runtime.rs index 96e0178151..cc4917636c 100644 --- a/crates/v8-runtime/src/embedded_runtime.rs +++ b/crates/v8-runtime/src/embedded_runtime.rs @@ -197,25 +197,27 @@ impl EmbeddedV8Runtime { return Ok(false); } - let detached = { + let shutdown = { let mut mgr = self .session_mgr .lock() .expect("session manager lock poisoned"); - mgr.detach_session_if_output_generation( + mgr.begin_destroy_session_if_output_generation( ®istration.session_id, registration.generation, ) .map_err(other_io_error)? }; - if detached { + let destroyed = shutdown.is_some(); + if let Some(shutdown) = shutdown { + shutdown.finish(); remove_session_output_if_current( &self.session_outputs, ®istration.session_id, registration.generation, ); } - Ok(detached) + Ok(destroyed) } pub fn session_handle(self: &Arc, session_id: String) -> EmbeddedV8SessionHandle { @@ -286,7 +288,7 @@ impl EmbeddedV8Runtime { let phase_start = Instant::now(); let result = registry .settle(session_id, output_generation, response) - .map_err(other_io_error); + .map_err(io::Error::other); record_sync_bridge_host_phase( "sync_rpc_dispatch", "direct_response_settlement", @@ -519,12 +521,12 @@ impl EmbeddedV8SessionHandle { .map_err(other_io_error) } - pub fn publish_signal(&self, signal: i32) -> io::Result<()> { + pub fn publish_signal(&self, signal: i32, delivery_token: u64) -> io::Result<()> { self.runtime .session_mgr .lock() .expect("session manager lock poisoned") - .publish_signal(&self.session_id, signal) + .publish_signal(&self.session_id, signal, delivery_token) .map_err(other_io_error) } @@ -644,11 +646,25 @@ impl EmbeddedRuntimeHandle { } pub fn shutdown(&self) { - let _ = self.shutdown_stream.shutdown(Shutdown::Both); - if let Ok(mut guard) = self.join_handle.lock() { - if let Some(handle) = guard.take() { - let _ = handle.join(); + let handle = match self.join_handle.lock() { + Ok(mut guard) => guard.take(), + Err(poisoned) => { + let mut guard = poisoned.into_inner(); + if guard.is_some() { + eprintln!( + "FATAL_AGENTOS_EMBEDDED_RUNTIME_JOIN_STATE_POISONED: context=explicit; recovering join handle to fail closed" + ); + } + guard.take() + } + }; + if let Some(handle) = handle { + if let Err(error) = self.shutdown_stream.shutdown(Shutdown::Both) { + eprintln!( + "ERR_AGENTOS_EMBEDDED_RUNTIME_SHUTDOWN_STREAM: context=explicit error={error}" + ); } + join_embedded_runtime_thread(handle, "explicit"); } self.release_codec(); } @@ -662,14 +678,38 @@ impl EmbeddedRuntimeHandle { impl Drop for EmbeddedRuntimeHandle { fn drop(&mut self) { - let _ = self.shutdown_stream.shutdown(Shutdown::Both); - if let Some(handle) = self.join_handle.get_mut().ok().and_then(Option::take) { - let _ = handle.join(); + let handle = match self.join_handle.get_mut() { + Ok(slot) => slot.take(), + Err(poisoned) => { + let slot = poisoned.into_inner(); + if slot.is_some() { + eprintln!( + "FATAL_AGENTOS_EMBEDDED_RUNTIME_JOIN_STATE_POISONED: context=drop; recovering join handle to fail closed" + ); + } + slot.take() + } + }; + if let Some(handle) = handle { + if let Err(error) = self.shutdown_stream.shutdown(Shutdown::Both) { + eprintln!( + "ERR_AGENTOS_EMBEDDED_RUNTIME_SHUTDOWN_STREAM: context=drop error={error}" + ); + } + join_embedded_runtime_thread(handle, "drop"); } self.release_codec(); } } +fn join_embedded_runtime_thread(handle: thread::JoinHandle<()>, context: &str) { + if handle.join().is_err() { + eprintln!( + "FATAL_AGENTOS_EMBEDDED_RUNTIME_THREAD_PANIC: context={context} embedded runtime thread panicked" + ); + } +} + pub fn spawn_embedded_runtime_ipc( max_concurrency: Option, runtime: agentos_runtime::RuntimeContext, @@ -756,7 +796,11 @@ fn run_embedded_runtime( ))); handle_connection(stream, connection_id, session_mgr, snapshot_cache); - let _ = writer_handle.join(); + if writer_handle.join().is_err() { + eprintln!( + "FATAL_AGENTOS_EMBEDDED_RUNTIME_WRITER_PANIC: connection={connection_id} writer thread panicked" + ); + } } fn ipc_writer_thread( @@ -818,15 +862,23 @@ fn handle_connection( } } - { + let shutdowns = { let mut mgr = session_mgr.lock().expect("session manager lock poisoned"); - for session_id in session_ids { - if let Err(error) = mgr.detach_session(&session_id) { - eprintln!( - "ERR_AGENTOS_VM_EXECUTOR_QUARANTINE: failed to detach session {session_id}: {error}" - ); - } - } + session_ids + .into_iter() + .filter_map(|session_id| match mgr.begin_destroy_session(&session_id) { + Ok(shutdown) => Some(shutdown), + Err(error) => { + eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_SHUTDOWN: failed to destroy session {session_id}: {error}" + ); + None + } + }) + .collect::>() + }; + for shutdown in shutdowns { + shutdown.finish(); } } @@ -855,17 +907,15 @@ fn dispatch_runtime_command( .map_err(other_io_error) } RuntimeCommand::DestroySession { session_id } => { - // Explicit destruction is a quiescence boundary. Remove the entry - // while holding the manager lock, then join after releasing it so - // the executor cannot leak into a successor session and the event - // dispatcher remains free to drain terminal output. - let shutdown = session_mgr + // Session executors are untrusted guest threads and cannot be + // joined on the runtime dispatch path. Detach them into the + // bounded quarantine; the manager retains the resource permit and + // reaps completed threads on subsequent runtime activity. + session_mgr .lock() .expect("session manager lock poisoned") - .begin_destroy_session(&session_id) - .map_err(other_io_error)?; - shutdown.finish(); - Ok(()) + .detach_session_for_destroy(&session_id) + .map_err(other_io_error) } RuntimeCommand::PauseSession { session_id } => { let mgr = session_mgr.lock().expect("session manager lock poisoned"); @@ -892,7 +942,7 @@ fn dispatch_runtime_command( let result = registry .settle(&session_id, output_generation, response) .map(|_| ()) - .map_err(other_io_error); + .map_err(io::Error::other); record_sync_bridge_host_phase( "sync_rpc_dispatch", "direct_response_settlement", @@ -936,10 +986,14 @@ fn dispatch_runtime_command( .expect("session manager lock poisoned") .remove_readiness(&session_id, capability_id, capability_generation) .map_err(other_io_error), - RuntimeCommand::PublishSignal { session_id, signal } => session_mgr + RuntimeCommand::PublishSignal { + session_id, + signal, + delivery_token, + } => session_mgr .lock() .expect("session manager lock poisoned") - .publish_signal(&session_id, signal) + .publish_signal(&session_id, signal, delivery_token) .map_err(other_io_error), RuntimeCommand::PublishTimer { session_id, @@ -1091,9 +1145,7 @@ mod tests { } fn test_runtime_context() -> agentos_runtime::RuntimeContext { - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("test process runtime") - .context() + crate::test_runtime_context() } fn test_output_channel( @@ -1220,6 +1272,37 @@ mod tests { ); } + #[test] + fn embedded_runtime_shutdown_recovers_poisoned_join_state() { + let _codec_guard = EMBEDDED_RUNTIME_CODEC_TEST_LOCK + .lock() + .expect("embedded runtime codec test lock poisoned"); + let (_stream, handle) = spawn_embedded_runtime_ipc(Some(1), test_runtime_context()) + .expect("spawn embedded runtime"); + + std::thread::scope(|scope| { + assert!( + scope + .spawn(|| { + let _guard = handle + .join_handle + .lock() + .expect("acquire embedded runtime join lock"); + panic!("poison embedded runtime join lock"); + }) + .join() + .is_err(), + "test poisoner must panic" + ); + }); + + handle.shutdown(); + assert!( + !handle.is_alive(), + "shutdown must recover the handle and join the runtime thread" + ); + } + #[test] fn embedded_runtime_session_shared_runtime_is_lazy() { let _codec_guard = EMBEDDED_RUNTIME_CODEC_TEST_LOCK @@ -1713,25 +1796,34 @@ mod tests { .session_handle(session_id.into()) .destroy() .expect("destroy first session"); - - let deadline = Instant::now() + Duration::from_secs(5); - loop { - let reconciled = { - let mut manager = runtime - .session_mgr - .lock() - .expect("session manager lock poisoned"); - manager.quarantined_session_count() == 0 && manager.active_slot_count() == 0 - }; - if reconciled { - break; - } - assert!( - Instant::now() < deadline, - "destroyed generation did not release its quarantined executor permit" + let shutdown_handles = { + let mut manager = runtime + .session_mgr + .lock() + .expect("session manager lock poisoned"); + assert_eq!( + manager.session_count(), + 0, + "nonblocking destroy must remove the session immediately" ); - thread::yield_now(); + // DestroySession cannot join an untrusted guest thread on the + // runtime dispatch path. Drain the bounded quarantine and join it + // outside the manager lock before reusing the single executor + // slot in this test. + manager.take_session_shutdown_handles() + }; + for handle in shutdown_handles { + handle.join().expect("join detached session executor"); } + assert_eq!( + runtime + .session_mgr + .lock() + .expect("session manager lock poisoned") + .active_slot_count(), + 0, + "the detached executor permit must release after its thread joins" + ); let (_second_receiver, _second_registration) = runtime .register_session_with_capacity(session_id, 1, Arc::clone(runtime.runtime.resources())) diff --git a/crates/v8-runtime/src/execution.rs b/crates/v8-runtime/src/execution.rs index 24c1c03f8c..b3b21657e5 100644 --- a/crates/v8-runtime/src/execution.rs +++ b/crates/v8-runtime/src/execution.rs @@ -3207,10 +3207,7 @@ fn add_esm_runtime_prelude(source: &str) -> String { if source.contains("require(") && !source.contains("createRequire(import.meta.url)") && !source.contains("createRequire(") - && !source.contains("const require =") - && !source.contains("let require =") - && !source.contains("var require =") - && !source.contains("function require(") + && !source_declares_identifier(source, "require") { prelude .push_str("const require = globalThis._moduleModule.createRequire(import.meta.url);\n"); @@ -3223,6 +3220,33 @@ fn add_esm_runtime_prelude(source: &str) -> String { } } +fn source_declares_identifier(source: &str, name: &str) -> bool { + for keyword in ["const", "let", "var", "function", "class"] { + let mut cursor = 0usize; + while let Some(start) = find_code_pattern(source, keyword, cursor) { + let mut index = start + keyword.len(); + while source + .as_bytes() + .get(index) + .is_some_and(u8::is_ascii_whitespace) + { + index += 1; + } + let end = index.saturating_add(name.len()); + if source.get(index..end) == Some(name) + && source + .as_bytes() + .get(end) + .is_none_or(|byte| !is_js_ident_continue(*byte)) + { + return true; + } + cursor = start + keyword.len(); + } + } + false +} + #[cfg(test)] fn needs_esm_global_alias(source: &str, name: &str, triggers: &[&str]) -> bool { if !triggers.iter().any(|trigger| source.contains(trigger)) { @@ -3495,6 +3519,20 @@ mod tests { assert_eq!(strip_leading_shebang("#!/usr/bin/env node"), ""); } + #[test] + fn esm_runtime_prelude_does_not_redeclare_minified_require_binding() { + let source = "const require=H(import.meta.url); require(\"node:fs\");"; + assert_eq!(add_esm_runtime_prelude(source), source); + } + + #[test] + fn esm_runtime_prelude_injects_require_when_it_is_only_called() { + let source = "const fs = require(\"node:fs\");"; + assert!(add_esm_runtime_prelude(source).starts_with( + "const require = globalThis._moduleModule.createRequire(import.meta.url);\n" + )); + } + /// Shared writer that captures output for test inspection struct SharedWriter(Arc>>); @@ -3672,10 +3710,7 @@ export const file = new File([], "empty.txt"); #[test] fn v8_consolidated_tests() { isolate::init_v8_platform(); - let runtime = - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("test process runtime") - .context(); + let runtime = crate::test_runtime_context(); // --- Isolate lifecycle (moved from isolate::tests to consolidate V8 tests) --- // Create and destroy 3 isolates sequentially without crash @@ -4396,13 +4431,35 @@ export const file = new File([], "empty.txt"); let ctx = isolate::create_context(&mut iso); let mut response_buf = Vec::new(); + let mut error_payload = Vec::new(); + ciborium::into_writer( + &ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(String::from("ENOENT")), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(String::from("file not found")), + ), + ( + ciborium::Value::Text(String::from("details")), + ciborium::Value::Map(vec![( + ciborium::Value::Text(String::from("path")), + ciborium::Value::Text(String::from("/missing")), + )]), + ), + ]), + &mut error_payload, + ) + .unwrap(); crate::ipc_binary::write_frame( &mut response_buf, &crate::ipc_binary::BinaryFrame::BridgeResponse { session_id: String::new(), call_id: 1, status: 1, - payload: "ENOENT: file not found".as_bytes().to_vec(), + payload: error_payload, }, ) .unwrap(); @@ -4425,7 +4482,14 @@ export const file = new File([], "empty.txt"); ); } - assert!(eval_throws(&mut iso, &ctx, "_testBridge('arg')")); + assert_eq!( + eval( + &mut iso, + &ctx, + "try { _testBridge('arg'); 'no-error' } catch (error) { `${error.code}:${error.message}:${error.details.path}` }", + ), + "ENOENT:file not found:/missing" + ); } // --- Part 10: Multiple bridge functions with argument passing --- @@ -4574,8 +4638,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(result_v8), None) - .unwrap(); + bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(result_v8)).unwrap(); } assert_eq!(pending.len(), 0); @@ -4634,9 +4697,32 @@ export const file = new File([], "empty.txt"); scope, &pending, 1, - 0, - None, - Some("ENOENT: file not found".into()), + 1, + Some({ + let mut payload = Vec::new(); + ciborium::into_writer( + &ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(String::from("ENOENT")), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(String::from("file not found")), + ), + ( + ciborium::Value::Text(String::from("details")), + ciborium::Value::Map(vec![( + ciborium::Value::Text(String::from("path")), + ciborium::Value::Text(String::from("/missing")), + )]), + ), + ]), + &mut payload, + ) + .expect("encode async bridge error"); + payload + }), ) .unwrap(); } @@ -4655,11 +4741,65 @@ export const file = new File([], "empty.txt"); assert_eq!(promise.state(), v8::PromiseState::Rejected); let rejection = promise.result(scope); let obj = v8::Local::::try_from(rejection).unwrap(); + let code_key = v8::String::new(scope, "code").unwrap(); + let code_val = obj.get(scope, code_key.into()).unwrap(); + assert_eq!(code_val.to_rust_string_lossy(scope), "ENOENT"); let msg_key = v8::String::new(scope, "message").unwrap(); let msg_val = obj.get(scope, msg_key.into()).unwrap(); + assert_eq!(msg_val.to_rust_string_lossy(scope), "file not found"); + let details_key = v8::String::new(scope, "details").unwrap(); + let details = obj.get(scope, details_key.into()).unwrap(); + let details = v8::Local::::try_from(details).unwrap(); + let path_key = v8::String::new(scope, "path").unwrap(); + assert_eq!( + details + .get(scope, path_key.into()) + .unwrap() + .to_rust_string_lossy(scope), + "/missing" + ); + } + + // A diagnostic string that looks like an errno must remain only a + // message. Stable codes come exclusively from the structured field. + eval( + &mut iso, + &ctx, + "var _spoofedErrorPromise = _asyncFn('spoof')", + ); + assert_eq!(pending.len(), 1); + { + let scope = &mut v8::HandleScope::new(&mut iso); + let local = v8::Local::new(scope, &ctx); + let scope = &mut v8::ContextScope::new(scope, local); + bridge::resolve_pending_promise( + scope, + &pending, + 2, + 1, + Some(b"EACCES: guest-controlled diagnostic".to_vec()), + ) + .unwrap(); + } + { + let scope = &mut v8::HandleScope::new(&mut iso); + let local = v8::Local::new(scope, &ctx); + let scope = &mut v8::ContextScope::new(scope, local); + let source = v8::String::new(scope, "_spoofedErrorPromise").unwrap(); + let script = v8::Script::compile(scope, source, None).unwrap(); + let promise = + v8::Local::::try_from(script.run(scope).unwrap()).unwrap(); + assert_eq!(promise.state(), v8::PromiseState::Rejected); + let error = v8::Local::::try_from(promise.result(scope)).unwrap(); + let code_key = v8::String::new(scope, "code").unwrap(); + assert!(error.get(scope, code_key.into()).unwrap().is_undefined()); + let message_key = v8::String::new(scope, "message").unwrap(); assert_eq!( - msg_val.to_rust_string_lossy(scope), - "ENOENT: file not found" + error + .get(scope, message_key.into()) + .unwrap() + .to_rust_string_lossy(scope), + "EACCES: guest-controlled diagnostic" ); } } @@ -4702,7 +4842,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - bridge::resolve_pending_promise(scope, &pending, 2, 0, Some(r2), None).unwrap(); + bridge::resolve_pending_promise(scope, &pending, 2, 0, Some(r2)).unwrap(); } assert_eq!(pending.len(), 1); @@ -4711,7 +4851,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(r1), None).unwrap(); + bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(r1)).unwrap(); } assert_eq!(pending.len(), 0); @@ -4775,7 +4915,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap(); + bridge::resolve_pending_promise(scope, &pending, 1, 0, None).unwrap(); } // Promise should be fulfilled with undefined @@ -4832,7 +4972,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap(); + bridge::resolve_pending_promise(scope, &pending, 1, 0, None).unwrap(); } // After resolution + microtask flush, _thenRan should be true @@ -5451,6 +5591,63 @@ export const file = new File([], "empty.txt"); eval(&mut iso, &ctx, "_eventLoopResult"), "event-loop-resolved" ); + + // The asynchronous session lane must preserve the same structured + // error object as the synchronous bridge callback. + eval( + &mut iso, + &ctx, + "var _eventLoopError = 'pending'; _asyncFn('error').catch(function(error) { _eventLoopError = `${error.code}:${error.message}:${error.details.path}`; })", + ); + assert_eq!(pending.len(), 1); + let mut error_payload = Vec::new(); + ciborium::into_writer( + &ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(String::from("EACCES")), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(String::from("permission denied")), + ), + ( + ciborium::Value::Text(String::from("details")), + ciborium::Value::Map(vec![( + ciborium::Value::Text(String::from("path")), + ciborium::Value::Text(String::from("/private")), + )]), + ), + ]), + &mut error_payload, + ) + .expect("encode asynchronous session error"); + tx.send(crate::session::SessionCommand::Message( + crate::runtime_protocol::SessionMessage::BridgeResponse( + crate::runtime_protocol::BridgeResponse { + call_id: 2, + status: 1, + payload: error_payload, + reservation: None, + }, + ), + )) + .unwrap(); + let completed = { + let scope = &mut v8::HandleScope::new(&mut iso); + let local = v8::Local::new(scope, &ctx); + let scope = &mut v8::ContextScope::new(scope, local); + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) + }; + assert!(matches!( + completed, + crate::session::EventLoopStatus::Completed + )); + assert_eq!(pending.len(), 0); + assert_eq!( + eval(&mut iso, &ctx, "_eventLoopError"), + "EACCES:permission denied:/private" + ); } // --- Part 32: Event loop — multiple BridgeResponses resolved in sequence --- @@ -7321,15 +7518,30 @@ export const file = new File([], "empty.txt"); let mut response_buf = Vec::new(); // Batch response (call_id=1): error (simulating unsupported batch method) + let mut batch_error_payload = Vec::new(); + ciborium::into_writer( + &ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(String::from("ENOSYS")), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(String::from( + "No handler for bridge method: _batchResolveModules", + )), + ), + ]), + &mut batch_error_payload, + ) + .unwrap(); crate::ipc_binary::write_frame( &mut response_buf, &crate::ipc_binary::BinaryFrame::BridgeResponse { session_id: String::new(), call_id: 1, status: 1, - payload: "No handler for bridge method: _batchResolveModules" - .as_bytes() - .to_vec(), + payload: batch_error_payload, }, ) .unwrap(); diff --git a/crates/v8-runtime/src/host_call.rs b/crates/v8-runtime/src/host_call.rs index 55126dded8..c18d6801e9 100644 --- a/crates/v8-runtime/src/host_call.rs +++ b/crates/v8-runtime/src/host_call.rs @@ -28,14 +28,17 @@ fn syncrpc_lat_enabled() -> bool { fn record_syncrpc_lat(ns: u64) { let m = SYNCRPC_LAT.get_or_init(|| std::sync::Mutex::new((0, 0, 0))); let Ok(mut a) = m.lock() else { + eprintln!( + "ERR_AGENTOS_SYNCRPC_LAT_METRICS_POISONED: sync bridge latency metrics lock poisoned" + ); return; }; - a.0 += 1; - a.1 = a.1.wrapping_add(ns / 1000); + a.0 = a.0.saturating_add(1); + a.1 = a.1.saturating_add(ns / 1000); a.2 = a.2.max(ns / 1000); if a.0 % 25 == 0 { if let Ok(path) = std::env::var("AGENTOS_SYNCRPC_LAT_FILE") { - let _ = std::fs::write( + if let Err(error) = std::fs::write( &path, format!( "calls={} total_us={} avg_us={} max_us={}\n", @@ -44,7 +47,12 @@ fn record_syncrpc_lat(ns: u64) { a.1 / a.0, a.2 ), - ); + ) { + eprintln!( + "WARN_AGENTOS_SYNCRPC_LAT_METRICS_WRITE: path={} error={error}", + path + ); + } } } } @@ -72,13 +80,16 @@ pub(crate) fn record_sync_bridge_host_phase(method: &str, stage: &str, elapsed: } let stats = SYNC_BRIDGE_HOST_PHASES.get_or_init(|| std::sync::Mutex::new(BTreeMap::new())); let Ok(mut stats) = stats.lock() else { + eprintln!( + "ERR_AGENTOS_SYNC_BRIDGE_PHASE_METRICS_POISONED: sync bridge phase metrics lock poisoned" + ); return; }; let elapsed_us = elapsed.as_micros() as u64; let key = format!("{method}:{stage}"); let entry = stats.entry(key).or_default(); - entry.calls += 1; - entry.total_us = entry.total_us.wrapping_add(elapsed_us); + entry.calls = entry.calls.saturating_add(1); + entry.total_us = entry.total_us.saturating_add(elapsed_us); entry.max_us = entry.max_us.max(elapsed_us); if let Ok(path) = std::env::var("AGENTOS_SYNC_BRIDGE_HOST_PHASES_FILE") { @@ -93,7 +104,12 @@ pub(crate) fn record_sync_bridge_host_phase(method: &str, stage: &str, elapsed: value.calls, value.total_us, avg_us, value.max_us )); } - let _ = std::fs::write(path, lines); + if let Err(error) = std::fs::write(&path, lines) { + eprintln!( + "WARN_AGENTOS_SYNC_BRIDGE_PHASE_METRICS_WRITE: path={} error={error}", + path + ); + } } } @@ -103,6 +119,9 @@ fn track_sync_bridge_call_method(call_id: u64, method: &str) { } let methods = SYNC_BRIDGE_CALL_METHODS.get_or_init(|| std::sync::Mutex::new(HashMap::new())); let Ok(mut methods) = methods.lock() else { + eprintln!( + "ERR_AGENTOS_SYNC_BRIDGE_METHOD_TRACKING_POISONED: sync bridge method tracking lock poisoned" + ); return; }; if methods.len() > 4096 { @@ -113,8 +132,15 @@ fn track_sync_bridge_call_method(call_id: u64, method: &str) { fn cleanup_sync_bridge_call_tracking(call_id: u64) { if let Some(methods) = SYNC_BRIDGE_CALL_METHODS.get() { - if let Ok(mut methods) = methods.lock() { - methods.remove(&call_id); + match methods.lock() { + Ok(mut methods) => { + methods.remove(&call_id); + } + Err(_) => { + eprintln!( + "ERR_AGENTOS_SYNC_BRIDGE_METHOD_TRACKING_POISONED: could not remove call_id={call_id} from diagnostic tracking" + ); + } } } } @@ -298,32 +324,91 @@ fn transfer_response_reservation( agentos_runtime::accounting::SharedReservation::new(reservation) } -fn bounded_terminal_error_payload(error: &str, maximum_bytes: usize) -> Vec { - if error.len() <= maximum_bytes { - return error.as_bytes().to_vec(); +fn encode_terminal_error_payload(code: &str, message: &str, maximum_bytes: usize) -> Vec { + let encode = |message: &str| { + let value = ciborium::Value::Map(vec![ + ( + ciborium::Value::Text(String::from("code")), + ciborium::Value::Text(code.to_owned()), + ), + ( + ciborium::Value::Text(String::from("message")), + ciborium::Value::Text(message.to_owned()), + ), + ]); + let mut payload = Vec::new(); + ciborium::into_writer(&value, &mut payload) + .expect("a bridge error map containing only strings must encode"); + payload + }; + + let payload = encode(message); + if payload.len() <= maximum_bytes { + return payload; } - // Preserve the stable typed code whenever the declared response capacity - // can hold it. Production bridge declarations reserve at least 4 KiB; the - // shorter fallback only applies to synthetic or misconfigured callers. - let code = error.split_once(':').map_or(error, |(code, _)| code); - if code.len() <= maximum_bytes { - return code.as_bytes().to_vec(); + // Production bridge declarations reserve at least 4 KiB. For smaller + // synthetic limits, shorten the diagnostic without ever deriving the + // stable code from that diagnostic. + let mut boundary = message.len().min(maximum_bytes); + while boundary > 0 && !message.is_char_boundary(boundary) { + boundary -= 1; + } + while boundary > 0 { + let payload = encode(&message[..boundary]); + if payload.len() <= maximum_bytes { + return payload; + } + boundary -= 1; + while boundary > 0 && !message.is_char_boundary(boundary) { + boundary -= 1; + } + } + let payload = encode(""); + if payload.len() <= maximum_bytes { + payload + } else { + Vec::new() } - code.as_bytes()[..maximum_bytes.min(code.len())].to_vec() } -fn bridge_call_timeout_error(call_id: u64, timeout: Duration) -> String { +fn bridge_error_payload_message(payload: &[u8]) -> String { + let Ok(ciborium::Value::Map(entries)) = ciborium::from_reader::(payload) + else { + return String::from_utf8_lossy(payload).to_string(); + }; + let mut code = None; + let mut message = None; + for (key, value) in entries { + let (ciborium::Value::Text(key), ciborium::Value::Text(value)) = (key, value) else { + continue; + }; + match key.as_str() { + "code" => code = Some(value), + "message" => message = Some(value), + _ => {} + } + } + match (code, message) { + (Some(code), Some(message)) => format!("{code}: {message}"), + (_, Some(message)) => message, + _ => String::from_utf8_lossy(payload).to_string(), + } +} + +fn bridge_call_timeout_message(call_id: u64, timeout: Duration) -> String { format!( - "ERR_AGENTOS_BRIDGE_CALL_TIMEOUT: bridge call_id {call_id} exceeded its {} ms deadline; raise limits.reactor.operationDeadlineMs", + "bridge call_id {call_id} exceeded its {} ms deadline; raise limits.reactor.operationDeadlineMs", timeout.as_millis() ) } fn deliver_bridge_call_timeout(call_id: u64, target: BridgeCallTarget) -> Result { - let error = bridge_call_timeout_error(call_id, target.timeout); - let payload = bounded_terminal_error_payload( - &error, + let message = bridge_call_timeout_message(call_id, target.timeout); + let error = format!("ERR_AGENTOS_BRIDGE_CALL_TIMEOUT: {message}"); + let payload = encode_terminal_error_payload( + "ERR_AGENTOS_BRIDGE_CALL_TIMEOUT", + &message, target .max_response_bytes .min(target.response_reservation.amount()), @@ -405,6 +490,57 @@ pub struct BridgeCallRegistry { max_pending: usize, } +/// A bridge-response settlement failure whose classification survives the +/// in-process `io::Error` transport. Callers must branch on `kind`, never on +/// the diagnostic text: the latter may contain guest-controlled values. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BridgeSettlementErrorKind { + StaleCompletion, + Other, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BridgeSettlementError { + kind: BridgeSettlementErrorKind, + message: String, +} + +impl BridgeSettlementError { + pub fn stale_completion(message: impl Into) -> Self { + Self { + kind: BridgeSettlementErrorKind::StaleCompletion, + message: message.into(), + } + } + + pub fn kind(&self) -> BridgeSettlementErrorKind { + self.kind + } + + /// Retained for diagnostic assertions; production classification uses + /// `kind()`. + pub fn contains(&self, needle: &str) -> bool { + self.message.contains(needle) + } +} + +impl From for BridgeSettlementError { + fn from(message: String) -> Self { + Self { + kind: BridgeSettlementErrorKind::Other, + message, + } + } +} + +impl std::fmt::Display for BridgeSettlementError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for BridgeSettlementError {} + impl BridgeCallRegistry { pub fn new(max_pending: usize) -> Self { Self { @@ -680,7 +816,7 @@ impl BridgeCallRegistry { supplied_session_id: &str, supplied_generation: Option, mut response: BridgeResponse, - ) -> Result<(), String> { + ) -> Result<(), BridgeSettlementError> { let call_id = response.call_id; let mut pending = self.pending.lock().map_err(|_| { String::from("ERR_AGENTOS_BRIDGE_REGISTRY_POISONED: bridge registry lock poisoned") @@ -697,10 +833,10 @@ impl BridgeCallRegistry { if retired.session_id == supplied_session_id && retired.session_generation == supplied_generation { - return Err(format!( + return Err(BridgeSettlementError::stale_completion(format!( "ERR_AGENTOS_BRIDGE_STALE_COMPLETION: response for canceled host-visible bridge call_id {} in session {} generation {:?}", response.call_id, supplied_session_id, supplied_generation - )); + ))); } return Err(format!( "ERR_AGENTOS_BRIDGE_STALE_GENERATION: response call_id {} named session {} generation {:?}, expected {} generation {:?}", @@ -709,25 +845,29 @@ impl BridgeCallRegistry { supplied_generation, retired.session_id, retired.session_generation - )); + ) + .into()); } return Err(format!( "ERR_AGENTOS_BRIDGE_UNKNOWN_CALL_ID: response for unknown bridge call_id {}", response.call_id - )); + ) + .into()); } }; if target.session_id != supplied_session_id { return Err(format!( "ERR_AGENTOS_BRIDGE_STALE_GENERATION: response call_id {} named session {}, expected {}", response.call_id, supplied_session_id, target.session_id - )); + ) + .into()); } if target.session_generation != supplied_generation { return Err(format!( "ERR_AGENTOS_BRIDGE_STALE_GENERATION: response call_id {} generation {:?}, expected {:?}", response.call_id, supplied_generation, target.session_generation - )); + ) + .into()); } // Identity validation must not consume a legitimate route when a stale // response arrives. Once identity is validated, however, settlement is @@ -753,7 +893,7 @@ impl BridgeCallRegistry { .insert(call_id, &target, self.max_pending); } let error = deliver_bridge_call_timeout(call_id, target)?; - return Err(error); + return Err(error.into()); } if response.payload.len() > target.max_response_bytes { @@ -763,8 +903,14 @@ impl BridgeCallRegistry { response.payload.len(), target.max_response_bytes ); - let payload = bounded_terminal_error_payload( - &error, + let payload = encode_terminal_error_payload( + "ERR_AGENTOS_BRIDGE_RESPONSE_LIMIT", + &format!( + "response call_id {} contains {} bytes, exceeding its declared maximum of {}; raise limits.reactor.maxBridgeResponseBytes", + response.call_id, + response.payload.len(), + target.max_response_bytes + ), target .max_response_bytes .min(target.response_reservation.amount()), @@ -785,7 +931,7 @@ impl BridgeCallRegistry { response.call_id ) })?; - return Err(error); + return Err(error.into()); } if let Some(reservation) = response.reservation.take() { @@ -795,8 +941,12 @@ impl BridgeCallRegistry { reservation.resource(), reservation.amount() ); - let payload = bounded_terminal_error_payload( - &error, + let payload = encode_terminal_error_payload( + "ERR_AGENTOS_BRIDGE_RESPONSE_ACCOUNTING", + &format!( + "response call_id {} carries a producer-side reservation; bridge response ownership must come from the call's admission reservation", + response.call_id + ), target .max_response_bytes .min(target.response_reservation.amount()), @@ -817,7 +967,7 @@ impl BridgeCallRegistry { response.call_id ) })?; - return Err(error); + return Err(error.into()); } if let Err(limit_error) = grow_response_reservation(&mut target, response.payload.len()) { @@ -827,8 +977,13 @@ impl BridgeCallRegistry { response.payload.len(), limit_error ); - let payload = bounded_terminal_error_payload( - &error, + let payload = encode_terminal_error_payload( + "ERR_AGENTOS_BRIDGE_RESPONSE_LIMIT", + &format!( + "response call_id {} could not reserve {} concrete bytes; raise limits.reactor.maxBridgeResponseBytes", + response.call_id, + response.payload.len() + ), target .max_response_bytes .min(target.response_reservation.amount()), @@ -849,7 +1004,7 @@ impl BridgeCallRegistry { response.call_id ) })?; - return Err(error); + return Err(error.into()); } response.reservation = Some(transfer_response_reservation( @@ -1240,6 +1395,25 @@ impl BridgeCallContext { method: &str, args: Vec, max_response_bytes: usize, + ) -> Result, String> { + let response = + self.sync_call_frame_with_max_response_bytes(method, args, max_response_bytes)?; + match response { + Some(response) if response.status == 1 => { + Err(bridge_error_payload_message(&response.payload)) + } + response => Ok(response), + } + } + + /// Perform a sync bridge call while preserving the structured status=1 + /// payload. The V8 adapter uses this to attach typed error fields without + /// reconstructing an errno from a diagnostic string. + pub fn sync_call_frame_with_max_response_bytes( + &self, + method: &str, + args: Vec, + max_response_bytes: usize, ) -> Result, String> { let call_id = self.next_call_id.fetch_add(1, Ordering::Relaxed); track_sync_bridge_call_method(call_id, method); @@ -1247,8 +1421,18 @@ impl BridgeCallContext { // Optional diagnostic tracking. Correctness comes from the atomic // counter and recv_response(call_id) identity validation. if self.track_pending_calls { - let mut pending = self.pending_calls.lock().unwrap(); + let mut pending = match self.pending_calls.lock() { + Ok(pending) => pending, + Err(_) => { + cleanup_sync_bridge_call_tracking(call_id); + return Err(String::from( + "ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED: pending bridge-call lock poisoned during admission", + )); + } + }; if !pending.insert(call_id) { + drop(pending); + cleanup_sync_bridge_call_tracking(call_id); return Err(format!("duplicate call_id: {}", call_id)); } } @@ -1257,11 +1441,17 @@ impl BridgeCallContext { self.call_id_router { let phase_start = Instant::now(); - let runtime = self.runtime.as_ref().ok_or_else(|| { - String::from( + let runtime = match self.runtime.as_ref() { + Some(runtime) => runtime, + None => { + return Err(self.cleanup_pending_call_after_error( + call_id, + String::from( "ERR_AGENTOS_RUNTIME_NOT_INJECTED: direct bridge calls require a session RuntimeContext", - ) - })?; + ), + )); + } + }; let receiver = match registry.register_sync_with_timeout( runtime, args.len(), @@ -1273,8 +1463,7 @@ impl BridgeCallContext { ) { Ok(receiver) => receiver, Err(error) => { - self.remove_pending_call(call_id); - return Err(error); + return Err(self.cleanup_pending_call_after_error(call_id, error)); } }; record_sync_bridge_host_phase(method, "host_register_route", phase_start.elapsed()); @@ -1298,16 +1487,17 @@ impl BridgeCallContext { let phase_start = Instant::now(); if let Some(route) = pending_route.as_ref() { if let Err(error) = route.mark_host_visible() { - self.remove_pending_call(call_id); - return Err(error); + return Err(self.cleanup_pending_call_after_error(call_id, error)); } } if let Err(e) = self.sender.send_event(bridge_call) { if let Some(route) = pending_route.take() { route.cancel_unpublished(); } - self.remove_pending_call(call_id); - return Err(format!("failed to write BridgeCall: {}", e)); + return Err(self.cleanup_pending_call_after_error( + call_id, + format!("failed to write BridgeCall: {}", e), + )); } record_sync_bridge_host_phase(method, "host_send_event", phase_start.elapsed()); @@ -1322,10 +1512,12 @@ impl BridgeCallContext { recv(abort_rx) -> _ => Err(String::from("execution aborted")), default(self.bridge_call_timeout) => { let registry = self.call_id_router.as_ref().expect("direct response route"); - registry.timeout(call_id)?; - receiver.recv().map_err(|_| { - String::from("bridge response target closed during deadline settlement") - }) + match registry.timeout(call_id) { + Ok(_) => receiver.recv().map_err(|_| { + String::from("bridge response target closed during deadline settlement") + }), + Err(error) => Err(error), + } }, } } else { @@ -1336,10 +1528,14 @@ impl BridgeCallContext { )), Err(crossbeam_channel::RecvTimeoutError::Timeout) => { let registry = self.call_id_router.as_ref().expect("direct response route"); - registry.timeout(call_id)?; - receiver.recv().map_err(|_| { - String::from("bridge response target closed during deadline settlement") - }) + match registry.timeout(call_id) { + Ok(_) => receiver.recv().map_err(|_| { + String::from( + "bridge response target closed during deadline settlement", + ) + }), + Err(error) => Err(error), + } } } }; @@ -1356,8 +1552,7 @@ impl BridgeCallContext { frame } Err(e) => { - self.remove_pending_call(call_id); - return Err(e); + return Err(self.cleanup_pending_call_after_error(call_id, e)); } } } else { @@ -1378,8 +1573,7 @@ impl BridgeCallContext { frame } Err(e) => { - self.remove_pending_call(call_id); - return Err(e); + return Err(self.cleanup_pending_call_after_error(call_id, e)); } } }; @@ -1388,16 +1582,12 @@ impl BridgeCallContext { } let phase_start = Instant::now(); - self.remove_pending_call(call_id); + self.remove_pending_call(call_id)?; record_sync_bridge_host_phase(method, "host_cleanup", phase_start.elapsed()); // Validate and extract BridgeResponse let phase_start = Instant::now(); - if response.status == 1 { - let result = Err(String::from_utf8_lossy(&response.payload).to_string()); - record_sync_bridge_host_phase(method, "host_extract_response", phase_start.elapsed()); - result - } else if response.payload.is_empty() && response.status != 2 { + if response.payload.is_empty() && response.status == 0 { record_sync_bridge_host_phase(method, "host_extract_response", phase_start.elapsed()); Ok(None) } else { @@ -1539,27 +1729,56 @@ impl BridgeCallContext { self.dispatch_async_call(prepared) } - fn remove_pending_call(&self, call_id: u64) { + fn remove_pending_call(&self, call_id: u64) -> Result<(), String> { cleanup_sync_bridge_call_tracking(call_id); if self.track_pending_calls { - self.pending_calls.lock().unwrap().remove(&call_id); + self.pending_calls + .lock() + .map_err(|_| { + String::from( + "ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED: pending bridge-call lock poisoned during cleanup", + ) + })? + .remove(&call_id); + } + Ok(()) + } + + fn cleanup_pending_call_after_error(&self, call_id: u64, error: String) -> String { + match self.remove_pending_call(call_id) { + Ok(()) => error, + Err(cleanup_error) => format!("{cleanup_error}; original_error={error}"), } } /// Check if a call_id is currently pending. - pub fn is_call_pending(&self, call_id: u64) -> bool { + pub fn is_call_pending(&self, call_id: u64) -> Result { if !self.track_pending_calls { - return false; + return Ok(false); } - self.pending_calls.lock().unwrap().contains(&call_id) + self.pending_calls + .lock() + .map(|pending| pending.contains(&call_id)) + .map_err(|_| { + String::from( + "ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED: pending bridge-call lock poisoned during inspection", + ) + }) } /// Number of pending calls. - pub fn pending_count(&self) -> usize { + pub fn pending_count(&self) -> Result { if !self.track_pending_calls { - return 0; + return Ok(0); } - self.pending_calls.lock().unwrap().len() + self.pending_calls + .lock() + .map(|pending| pending.len()) + .map_err(|_| { + String::from( + "ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED: pending bridge-call lock poisoned during inspection", + ) + }) } } @@ -1582,9 +1801,7 @@ mod tests { use std::sync::Arc; fn test_runtime_context() -> RuntimeContext { - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("test process runtime") - .context() + crate::test_runtime_context() } fn limited_bridge_runtime( @@ -1734,7 +1951,18 @@ mod tests { #[test] fn sync_call_error_response() { - let response_bytes = make_response_bytes(1, None, Some("ENOENT: no such file".into())); + let payload = encode_terminal_error_payload("ENOENT", "no such file", 4096); + let mut response_bytes = Vec::new(); + ipc_binary::write_frame( + &mut response_bytes, + &BinaryFrame::BridgeResponse { + session_id: String::new(), + call_id: 1, + status: 1, + payload, + }, + ) + .expect("encode structured error response"); let ctx = BridgeCallContext::new( Box::new(Vec::new()), Box::new(Cursor::new(response_bytes)), @@ -1746,6 +1974,59 @@ mod tests { assert_eq!(result.unwrap_err(), "ENOENT: no such file"); } + #[test] + fn terminal_error_payload_never_derives_code_from_message() { + let payload = + encode_terminal_error_payload("EIO", "EACCES: guest-controlled diagnostic", 4096); + let ciborium::Value::Map(entries) = + ciborium::from_reader::(payload.as_slice()) + .expect("decode structured bridge error") + else { + panic!("structured bridge error must be a CBOR map"); + }; + let text_field = |name: &str| { + entries.iter().find_map(|(key, value)| match (key, value) { + (ciborium::Value::Text(key), ciborium::Value::Text(value)) if key == name => { + Some(value.as_str()) + } + _ => None, + }) + }; + assert_eq!(text_field("code"), Some("EIO")); + assert_eq!( + text_field("message"), + Some("EACCES: guest-controlled diagnostic") + ); + } + + #[test] + fn sync_call_frame_preserves_structured_error_payload() { + let payload = encode_terminal_error_payload("EACCES", "permission denied", 4096); + let mut response_bytes = Vec::new(); + ipc_binary::write_frame( + &mut response_bytes, + &BinaryFrame::BridgeResponse { + session_id: String::new(), + call_id: 1, + status: 1, + payload: payload.clone(), + }, + ) + .expect("encode structured error response"); + let ctx = BridgeCallContext::new( + Box::new(Vec::new()), + Box::new(Cursor::new(response_bytes)), + "session-1".into(), + ); + + let response = ctx + .sync_call_frame_with_max_response_bytes("_fsReadFile", vec![0xc0], 4096) + .expect("receive structured response") + .expect("error response frame"); + assert_eq!(response.status, 1); + assert_eq!(response.payload, payload); + } + #[test] fn sync_call_call_id_increments() { // Prepare two sequential responses @@ -1773,9 +2054,46 @@ mod tests { "session-1".into(), ); - assert_eq!(ctx.pending_count(), 0); + assert_eq!(ctx.pending_count().expect("inspect pending calls"), 0); let _ = ctx.sync_call("_fn", vec![]); - assert_eq!(ctx.pending_count(), 0); + assert_eq!(ctx.pending_count().expect("inspect pending calls"), 0); + } + + #[test] + fn poisoned_pending_call_state_returns_a_typed_error() { + let mut ctx = BridgeCallContext::new( + Box::new(Vec::new()), + Box::new(Cursor::new(Vec::new())), + "session-1".into(), + ); + ctx.track_pending_calls = true; + + std::thread::scope(|scope| { + let pending_calls = &ctx.pending_calls; + assert!( + scope + .spawn(|| { + let _guard = pending_calls.lock().expect("acquire pending-call lock"); + panic!("poison pending-call lock"); + }) + .join() + .is_err(), + "test poisoner must panic" + ); + }); + + let error = ctx + .sync_call("_fn", vec![]) + .expect_err("poisoned correctness state must reject admission"); + assert!(error.starts_with("ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED:")); + assert!(ctx + .is_call_pending(1) + .expect_err("poisoned inspection must be typed") + .starts_with("ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED:")); + assert!(ctx + .pending_count() + .expect_err("poisoned inspection must be typed") + .starts_with("ERR_AGENTOS_BRIDGE_PENDING_CALLS_POISONED:")); } #[test] @@ -2061,6 +2379,7 @@ mod tests { }, ) .expect_err("canceled host-visible route must reject a stale completion"); + assert_eq!(error.kind(), BridgeSettlementErrorKind::StaleCompletion); assert!(error.contains("ERR_AGENTOS_BRIDGE_STALE_COMPLETION")); let mismatched = registry @@ -2075,6 +2394,7 @@ mod tests { }, ) .expect_err("a different generation must not inherit stale-completion status"); + assert_eq!(mismatched.kind(), BridgeSettlementErrorKind::Other); assert!(mismatched.contains("ERR_AGENTOS_BRIDGE_STALE_GENERATION")); let _unpublished_waiter = registry @@ -2623,9 +2943,19 @@ mod tests { let terminal = waiter.recv().expect("receive bounded terminal error"); assert_eq!(terminal.status, 1); - assert_eq!(terminal.payload.len(), 4); - assert_eq!(terminal.reservation.as_ref().unwrap().amount(), 4); - assert_eq!(resources.usage(ResourceClass::BridgeResponseBytes).used, 4); + assert!(terminal.payload.len() <= 4); + assert_eq!( + terminal.reservation.as_ref().unwrap().amount(), + terminal.payload.len() + ); + assert_eq!( + resources.usage(ResourceClass::BridgeResponseBytes).used, + terminal.payload.len() + ); + assert!( + terminal.payload.is_empty(), + "a budget too small for the typed envelope must not emit a truncated string sentinel" + ); drop(terminal); assert!(resources.is_zero()); } diff --git a/crates/v8-runtime/src/lib.rs b/crates/v8-runtime/src/lib.rs index f788f0dfd1..cb95179cd0 100644 --- a/crates/v8-runtime/src/lib.rs +++ b/crates/v8-runtime/src/lib.rs @@ -10,3 +10,27 @@ pub mod session; pub mod snapshot; pub mod stream; pub mod timeout; + +#[cfg(test)] +pub(crate) fn test_runtime_context() -> agentos_runtime::RuntimeContext { + // Rust runs this crate's unit tests in parallel, while `SidecarRuntime::process` + // deliberately shares one process-wide executor admission counter. Give the + // ordinary unit-test process enough aggregate capacity that unrelated test + // SessionManagers do not contend with each other. Tests for configured and + // cross-manager saturation use isolated subprocesses with explicit small + // limits and must not call this helper. + const TEST_PROCESS_VM_EXECUTOR_LIMIT: usize = 64; + let config = agentos_runtime::RuntimeConfig { + max_active_vm_executors: TEST_PROCESS_VM_EXECUTOR_LIMIT, + ..agentos_runtime::RuntimeConfig::default() + }; + let runtime = agentos_runtime::SidecarRuntime::process(&config) + .expect("test process runtime") + .context(); + assert_eq!( + runtime.max_active_vm_executors(), + TEST_PROCESS_VM_EXECUTOR_LIMIT, + "ordinary V8 unit tests must share the explicit test process quota" + ); + runtime +} diff --git a/crates/v8-runtime/src/runtime_protocol.rs b/crates/v8-runtime/src/runtime_protocol.rs index 3d71736716..89722686ff 100644 --- a/crates/v8-runtime/src/runtime_protocol.rs +++ b/crates/v8-runtime/src/runtime_protocol.rs @@ -52,6 +52,7 @@ pub enum RuntimeCommand { PublishSignal { session_id: String, signal: i32, + delivery_token: u64, }, PublishTimer { session_id: String, diff --git a/crates/v8-runtime/src/session.rs b/crates/v8-runtime/src/session.rs index 91ad64dd65..cc30d51f3f 100644 --- a/crates/v8-runtime/src/session.rs +++ b/crates/v8-runtime/src/session.rs @@ -13,12 +13,11 @@ use agentos_bridge::queue_tracker::warn_limit_exhausted; use agentos_bridge::queue_tracker::{register_queue, QueueGauge, TrackedLimit}; use agentos_bridge::{bridge_contract, BridgeCallConvention}; use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger}; -use agentos_runtime::metrics::{ExecutorMetricClass, RuntimeMetrics}; use agentos_runtime::readiness::{ ReadyAcknowledgement, ReadyBatch as RuntimeReadyBatch, ReadyFlags, ReadyObservation, ReadyWake, SessionReadyBroker as RuntimeSessionReadyBroker, }; -use agentos_runtime::RuntimeContext; +use agentos_runtime::{RuntimeContext, VmExecutorPermit}; use crossbeam_channel::{Receiver, Select, Sender}; use crate::execution; @@ -133,9 +132,9 @@ impl SessionReadiness { self.forward_runtime_wake_locked(&mut state) } - fn publish_signal(&self, signal: i32) -> Result<(), String> { + fn publish_signal(&self, signal: i32, delivery_token: u64) -> Result<(), String> { self.broker - .mark_signal_ready(self.generation, signal) + .mark_signal_ready(self.generation, signal, delivery_token) .map_err(|error| error.to_string())?; let mut state = self.wakes.lock().map_err(|_| { String::from("ERR_AGENTOS_READY_STATE_POISONED: session readiness lock poisoned") @@ -202,7 +201,10 @@ impl SessionReadiness { .map_err(|error| error.to_string()) } - fn drain_signals(&self, batch: &RuntimeReadyBatch) -> Result, String> { + fn drain_signals( + &self, + batch: &RuntimeReadyBatch, + ) -> Result, String> { self.broker .drain_signals(batch.generation, batch.epoch, self.max_batch_handles) .map_err(|error| error.to_string()) @@ -502,6 +504,119 @@ struct WarmWorkerPool { state: Mutex, } +#[derive(Default)] +struct SessionManagerAdmissionState { + active: usize, + near_limit_warning_emitted: bool, +} + +struct SessionManagerAdmissionInner { + state: Mutex, + maximum: usize, +} + +/// Manager-local executor ceiling. This is intentionally distinct from the +/// process-wide `VmExecutorAdmission`: a local cap constrains only generations +/// owned by this manager, while the runtime admission constrains the aggregate +/// across every manager and engine in the process. +#[derive(Clone)] +struct SessionManagerAdmission { + inner: Arc, +} + +impl SessionManagerAdmission { + fn new(maximum: usize) -> Self { + Self { + inner: Arc::new(SessionManagerAdmissionInner { + state: Mutex::new(SessionManagerAdmissionState::default()), + maximum, + }), + } + } + + fn try_acquire(&self) -> Result { + let mut state = self.inner.state.lock().map_err(|_| { + String::from( + "ERR_AGENTOS_VM_EXECUTOR_POISONED: V8 session-manager admission lock poisoned", + ) + })?; + if state.active >= self.inner.maximum { + return Err(format!( + "ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of {} (active={}); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + self.inner.maximum, state.active + )); + } + state.active += 1; + let warning_threshold = self + .inner + .maximum + .saturating_sub(self.inner.maximum / 5) + .max(1); + if state.active >= warning_threshold && !state.near_limit_warning_emitted { + state.near_limit_warning_emitted = true; + eprintln!( + "WARN_AGENTOS_V8_SESSION_EXECUTOR_NEAR_LIMIT: active={} limit={} threshold={}; raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + state.active, self.inner.maximum, warning_threshold + ); + } + Ok(SessionManagerPermit { + admission: self.clone(), + }) + } + + fn active(&self) -> usize { + self.inner + .state + .lock() + .map(|state| state.active) + .unwrap_or_else(|_| { + eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_POISONED: V8 session-manager admission snapshot failed" + ); + self.inner.maximum + }) + } + + #[cfg(test)] + fn maximum(&self) -> usize { + self.inner.maximum + } +} + +struct SessionManagerPermit { + admission: SessionManagerAdmission, +} + +impl Drop for SessionManagerPermit { + fn drop(&mut self) { + match self.admission.inner.state.lock() { + Ok(mut state) if state.active > 0 => { + state.active -= 1; + let warning_threshold = self + .admission + .inner + .maximum + .saturating_sub(self.admission.inner.maximum / 5) + .max(1); + if state.active < warning_threshold { + state.near_limit_warning_emitted = false; + } + } + Ok(_) => eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_ACCOUNTING_UNDERFLOW: V8 session-manager permit released at zero" + ), + Err(_) => eprintln!( + "ERR_AGENTOS_VM_EXECUTOR_POISONED: V8 session-manager permit could not be released" + ), + } + } +} + +struct SessionExecutorPermit { + _process: VmExecutorPermit, + _manager: SessionManagerPermit, +} + struct SessionAssignment { heap_limit_mb: Option, cpu_time_limit_ms: Option, @@ -510,7 +625,7 @@ struct SessionAssignment { shutdown_rx: Receiver<()>, ready_rx: Receiver, ready_broker: Arc, - slot_permit: SessionSlotPermit, + slot_permit: SessionExecutorPermit, event_tx: RuntimeEventSender, call_id_router: CallIdRouter, shared_call_id: SharedCallIdCounter, @@ -569,12 +684,15 @@ fn record_v8_session_phase(stage: &str, elapsed: Duration) { } let phases = V8_SESSION_PHASES.get_or_init(|| Mutex::new(BTreeMap::new())); let Ok(mut phases) = phases.lock() else { + eprintln!( + "ERR_AGENTOS_V8_SESSION_PHASE_METRICS_POISONED: V8 session phase metrics lock poisoned" + ); return; }; let stats = phases.entry(stage.to_string()).or_default(); - stats.calls += 1; + stats.calls = stats.calls.saturating_add(1); let elapsed_ns = elapsed.as_nanos(); - stats.total_ns += elapsed_ns; + stats.total_ns = stats.total_ns.saturating_add(elapsed_ns); stats.max_ns = stats.max_ns.max(elapsed_ns); let Some(path) = std::env::var_os("AGENTOS_V8_SESSION_PHASES_FILE") else { @@ -594,7 +712,12 @@ fn record_v8_session_phase(stage: &str, elapsed: Duration) { stats.calls )); } - let _ = std::fs::write(path, output); + if let Err(error) = std::fs::write(&path, output) { + eprintln!( + "WARN_AGENTOS_V8_SESSION_PHASE_METRICS_WRITE: path={} error={error}", + std::path::Path::new(&path).display() + ); + } } #[cfg(not(test))] @@ -648,6 +771,16 @@ fn warm_key_prefix(key: &WarmPoolKey) -> String { .collect() } +fn join_warm_worker(join_handle: thread::JoinHandle<()>, action: &'static str, key: &WarmPoolKey) { + if join_handle.join().is_err() { + eprintln!( + "FATAL_AGENTOS_V8_WARM_WORKER_PANIC: action={action} key={} heap={} warm worker panicked", + warm_key_prefix(key), + key.heap_limit_mb + ); + } +} + impl WarmWorkerPool { fn claim(&self, key: &WarmPoolKey) -> Option { let mut state = self.state.lock().expect("warm worker pool lock poisoned"); @@ -674,7 +807,6 @@ impl WarmWorkerPool { self: &Arc, runtime: RuntimeContext, snapshot_cache: Arc, - slot_control: SlotControl, bridge_code: String, userland_code: String, heap_limit_mb: Option, @@ -702,7 +834,6 @@ impl WarmWorkerPool { if let Err(error) = runtime.blocking().submit(requested_bytes, move || { pool.refill_until( snapshot_cache, - slot_control, spawn_key, bridge_code, userland_code, @@ -724,7 +855,6 @@ impl WarmWorkerPool { fn refill_until( &self, snapshot_cache: Arc, - slot_control: SlotControl, key: WarmPoolKey, bridge_code: String, userland_code: String, @@ -771,7 +901,7 @@ impl WarmWorkerPool { }; if let Some((evicted_key, worker)) = evicted { drop(worker.assignment_tx); - let _ = worker.join_handle.join(); + join_warm_worker(worker.join_handle, "evict", &evicted_key); eprintln!( "agentos-v8-runtime: warm worker evicted key={} heap={}", warm_key_prefix(&evicted_key), @@ -782,7 +912,6 @@ impl WarmWorkerPool { let worker = spawn_warm_worker( Arc::clone(&snapshot_cache), - Arc::clone(&slot_control), key.clone(), bridge_code.clone(), userland_code.clone(), @@ -797,7 +926,7 @@ impl WarmWorkerPool { let workers = state.workers.entry(key.clone()).or_default(); if workers.len() >= desired { drop(worker.assignment_tx); - let _ = worker.join_handle.join(); + join_warm_worker(worker.join_handle, "discard_excess", &key); break; } workers.push(worker); @@ -820,7 +949,6 @@ impl WarmWorkerPool { #[cfg(not(test))] fn spawn_warm_worker( snapshot_cache: Arc, - slot_control: SlotControl, key: WarmPoolKey, bridge_code: String, userland_code: String, @@ -836,7 +964,6 @@ fn spawn_warm_worker( .spawn(move || { let precreated = precreate_warm_isolate( snapshot_cache, - slot_control, worker_bridge_code, worker_userland_code, heap_limit_mb, @@ -875,7 +1002,7 @@ fn spawn_warm_worker( warm_key_prefix(&key), key.heap_limit_mb ); - let _ = join_handle.join(); + join_warm_worker(join_handle, "startup_failure", &key); None } Err(error) => { @@ -884,7 +1011,7 @@ fn spawn_warm_worker( warm_key_prefix(&key), key.heap_limit_mb ); - let _ = join_handle.join(); + join_warm_worker(join_handle, "startup_disconnect", &key); None } } @@ -893,7 +1020,6 @@ fn spawn_warm_worker( #[cfg(test)] fn spawn_warm_worker( _snapshot_cache: Arc, - _slot_control: SlotControl, _key: WarmPoolKey, _bridge_code: String, _userland_code: String, @@ -905,7 +1031,6 @@ fn spawn_warm_worker( #[cfg(not(test))] fn precreate_warm_isolate( snapshot_cache: Arc, - _slot_control: SlotControl, bridge_code: String, userland_code: String, heap_limit_mb: Option, @@ -1039,62 +1164,6 @@ impl SessionShutdown { } } -/// Concurrency slot tracker shared across session threads -type SlotControl = Arc<(Mutex, Condvar)>; - -/// An admitted V8 executor slot. It is acquired before spawning or assigning -/// an OS thread and remains owned by that generation until the thread exits. -/// Detached/stuck generations therefore stay quarantined instead of lending -/// their capacity to a successor VM. -struct SessionSlotPermit { - control: SlotControl, - metrics: RuntimeMetrics, -} - -impl SessionSlotPermit { - fn try_acquire( - control: &SlotControl, - maximum: usize, - metrics: RuntimeMetrics, - ) -> Result { - let (lock, _) = &**control; - let mut active = lock - .lock() - .map_err(|_| String::from("ERR_AGENTOS_VM_EXECUTOR_POISONED: slot lock poisoned"))?; - if *active >= maximum { - return Err(format!( - "ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 executors reached limit of {maximum}; raise runtime.executor.maxActiveVms" - )); - } - *active += 1; - metrics.observe_executor(ExecutorMetricClass::Vm, *active, 0); - Ok(Self { - control: Arc::clone(control), - metrics, - }) - } -} - -impl Drop for SessionSlotPermit { - fn drop(&mut self) { - let (lock, cvar) = &*self.control; - match lock.lock() { - Ok(mut active) if *active > 0 => { - *active -= 1; - self.metrics - .observe_executor(ExecutorMetricClass::Vm, *active, 0); - cvar.notify_all(); - } - Ok(_) => eprintln!( - "ERR_AGENTOS_VM_EXECUTOR_ACCOUNTING_UNDERFLOW: executor permit released at zero" - ), - Err(_) => { - eprintln!("ERR_AGENTOS_VM_EXECUTOR_POISONED: executor permit could not be released") - } - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ExecutionAbortReason { /// Caller explicitly terminated the execution (e.g. session destroy). @@ -1210,8 +1279,9 @@ pub struct SessionManager { /// thread itself retains the concurrency permit, so a successor cannot /// consume capacity that is still running untrusted code. quarantined: Vec, - max_concurrency: usize, - slot_control: SlotControl, + /// Every admitted generation also holds a process-owned permit, so this + /// manager-local ceiling can narrow but never raise the process limit. + manager_executor_admission: SessionManagerAdmission, /// Typed runtime event sender shared across session threads. event_tx: RuntimeEventSender, /// Call_id → session_id routing table for BridgeResponse dispatch @@ -1248,8 +1318,7 @@ impl SessionManager { SessionManager { sessions: HashMap::new(), quarantined: Vec::new(), - max_concurrency, - slot_control: Arc::new((Mutex::new(0), Condvar::new())), + manager_executor_admission: SessionManagerAdmission::new(max_concurrency), event_tx: event_tx.into(), call_id_router, shared_call_id: Arc::new(AtomicU64::new(1)), @@ -1262,7 +1331,7 @@ impl SessionManager { #[cfg(test)] pub(crate) fn max_concurrency(&self) -> usize { - self.max_concurrency + self.manager_executor_admission.maximum() } /// Get the snapshot cache for pre-warming from WarmSnapshot messages. @@ -1281,7 +1350,6 @@ impl SessionManager { self.warm_pool.ensure_count( self.runtime.clone(), Arc::clone(&self.snapshot_cache), - Arc::clone(&self.slot_control), bridge_code, userland_code, heap_limit_mb, @@ -1385,11 +1453,16 @@ impl SessionManager { return Err(format!("session {} already exists", session_id)); } - let slot_permit = SessionSlotPermit::try_acquire( - &self.slot_control, - self.max_concurrency, - self.runtime.metrics().clone(), - )?; + let manager_permit = self.manager_executor_admission.try_acquire()?; + let process_permit = self + .runtime + .vm_executor_admission() + .try_acquire() + .map_err(|error| error.to_string())?; + let slot_permit = SessionExecutorPermit { + _process: process_permit, + _manager: manager_permit, + }; let cpu_time_limit_ms = normalize_cpu_time_limit_ms(cpu_time_limit_ms); let wall_clock_limit_ms = normalize_wall_clock_limit_ms(wall_clock_limit_ms); @@ -1437,7 +1510,6 @@ impl SessionManager { self.warm_pool.ensure_count( self.runtime.clone(), Arc::clone(&self.snapshot_cache), - Arc::clone(&self.slot_control), hint.bridge_code, hint.userland_code, hint.heap_limit_mb, @@ -1452,7 +1524,6 @@ impl SessionManager { self.warm_pool.ensure_count( self.runtime.clone(), Arc::clone(&self.snapshot_cache), - Arc::clone(&self.slot_control), hint.bridge_code, hint.userland_code, hint.heap_limit_mb, @@ -1523,7 +1594,7 @@ impl SessionManager { } Err(error) => { record_warm_worker_miss(); - let _ = worker.join_handle.join(); + join_warm_worker(worker.join_handle, "assignment_disconnect", &key); Err(error.0) } } @@ -1577,6 +1648,18 @@ impl SessionManager { } pub(crate) fn detach_session(&mut self, session_id: &str) -> Result<(), String> { + self.detach_session_inner(session_id, true) + } + + pub(crate) fn detach_session_for_destroy(&mut self, session_id: &str) -> Result<(), String> { + self.detach_session_inner(session_id, false) + } + + fn detach_session_inner( + &mut self, + session_id: &str, + report_quarantine_warning: bool, + ) -> Result<(), String> { let entry = self .sessions .get(session_id) @@ -1598,10 +1681,12 @@ impl SessionManager { signal_session_shutdown(&entry.shutdown_tx, session_id); drop(entry.tx); if let Some(join_handle) = entry.join_handle.take() { - eprintln!( - "WARN_AGENTOS_VM_EXECUTOR_QUARANTINED: session={} generation={:?}", - session_id, entry.output_generation - ); + if report_quarantine_warning { + eprintln!( + "WARN_AGENTOS_VM_EXECUTOR_QUARANTINED: session={} generation={:?}", + session_id, entry.output_generation + ); + } self.quarantined.push(QuarantinedSession { session_id: session_id.to_owned(), output_generation: entry.output_generation, @@ -1827,12 +1912,17 @@ impl SessionManager { .publish(capability_id, capability_generation, flags) } - pub fn publish_signal(&self, session_id: &str, signal: i32) -> Result<(), String> { + pub fn publish_signal( + &self, + session_id: &str, + signal: i32, + delivery_token: u64, + ) -> Result<(), String> { let entry = self .sessions .get(session_id) .ok_or_else(|| format!("session {session_id} does not exist"))?; - entry.ready_broker.publish_signal(signal) + entry.ready_broker.publish_signal(signal, delivery_token) } pub fn remove_readiness( @@ -1973,8 +2063,7 @@ impl SessionManager { /// Number of sessions that have acquired a concurrency slot. #[allow(dead_code)] pub fn active_slot_count(&self) -> usize { - let (lock, _) = &*self.slot_control; - *lock.lock().unwrap() + self.manager_executor_admission.active() } pub fn session_output_generation(&self, session_id: &str) -> Option { @@ -3434,7 +3523,11 @@ fn run_event_loop_with_readiness( } } else if let Some((abort_index, abort)) = abort_selection { debug_assert_eq!(index, abort_index); - let _ = operation.recv(abort); + if let Err(error) = operation.recv(abort) { + eprintln!( + "WARN_AGENTOS_SESSION_ABORT_LANE_DISCONNECTED: error={error}" + ); + } scope.terminate_execution(); return EventLoopStatus::Terminated; } else { @@ -3442,7 +3535,11 @@ fn run_event_loop_with_readiness( } } else if let Some((abort_index, abort)) = abort_selection { debug_assert_eq!(index, abort_index); - let _ = operation.recv(abort); + if let Err(error) = operation.recv(abort) { + eprintln!( + "WARN_AGENTOS_SESSION_ABORT_LANE_DISCONNECTED: error={error}" + ); + } scope.terminate_execution(); return EventLoopStatus::Terminated; } else { @@ -3631,11 +3728,16 @@ fn dispatch_ready_batch_callbacks( Err(error) => return readiness_dispatch_failure(error), }; for signal in signals { - let Some(signal_name) = signal_name_for_stream_event(signal) else { + let Some(signal_name) = signal_name_for_stream_event(signal.signal) else { continue; }; let tc = &mut v8::TryCatch::new(scope); - crate::stream::dispatch_signal_event(tc, signal_name, signal); + crate::stream::dispatch_signal_event( + tc, + signal_name, + signal.signal, + signal.delivery_token, + ); tc.perform_microtask_checkpoint(); if let Some(exception) = tc.exception() { let (code, error) = execution::exception_to_result(tc, exception); @@ -3813,19 +3915,28 @@ fn dispatch_event_loop_frame( payload, reservation: _reservation, }) => { - let (result, error) = if status == 1 { - (None, Some(String::from_utf8_lossy(&payload).to_string())) - } else if status == 2 || !payload.is_empty() { + let result = if status == 1 || status == 2 || !payload.is_empty() { // status=0: V8-serialized, status=2: raw binary (Uint8Array) - (Some(payload), None) + // status=1: structured CBOR host error, rejected by the bridge. + Some(payload) } else { - (None, None) + None }; - let _ = crate::bridge::resolve_pending_promise( - scope, pending, call_id, status, result, error, - ); - // Microtasks already flushed in resolve_pending_promise - EventLoopStatus::Completed + match crate::bridge::resolve_pending_promise(scope, pending, call_id, status, result) { + Ok(()) => { + // Microtasks already flushed in resolve_pending_promise. + EventLoopStatus::Completed + } + Err(message) => EventLoopStatus::Failed( + 1, + ExecutionError { + error_type: String::from("Error"), + message: format!("ERR_AGENTOS_BRIDGE_RESPONSE_SETTLEMENT: {message}"), + stack: String::new(), + code: Some(String::from("ERR_AGENTOS_BRIDGE_RESPONSE_SETTLEMENT")), + }, + ), + } } SessionMessage::StreamEvent(StreamEvent { event_type, @@ -3870,14 +3981,22 @@ mod tests { let (tx, _rx) = crossbeam_channel::unbounded(); let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit()); let snap_cache = Arc::new(SnapshotCache::new(4)); - let runtime = - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("create test process runtime") - .context(); + let runtime = crate::test_runtime_context(); let manager = SessionManager::new(max, tx, router, snap_cache, runtime); (manager, _rx) } + #[test] + fn warm_worker_join_contains_worker_panic() { + let key = WarmPoolKey { + snapshot_key_digest: [0; 32], + heap_limit_mb: 64, + }; + let handle = std::thread::spawn(|| panic!("injected warm worker panic")); + + join_warm_worker(handle, "test", &key); + } + #[test] fn zero_cpu_time_limit_is_normalized_to_no_timeout() { assert_eq!(normalize_cpu_time_limit_ms(None), None); @@ -3885,33 +4004,6 @@ mod tests { assert_eq!(normalize_cpu_time_limit_ms(Some(1)), Some(1)); } - #[test] - fn vm_executor_permits_report_active_and_high_water_metrics() { - let control: SlotControl = Arc::new((Mutex::new(0), Condvar::new())); - let metrics = RuntimeMetrics::new(); - - let first = SessionSlotPermit::try_acquire(&control, 2, metrics.clone()) - .expect("acquire first VM executor"); - let second = SessionSlotPermit::try_acquire(&control, 2, metrics.clone()) - .expect("acquire second VM executor"); - let active = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active; - assert_eq!(active.current, 2); - assert_eq!(active.high_water, 2); - - drop(first); - assert_eq!( - metrics.snapshot().executors[ExecutorMetricClass::Vm.index()] - .active - .current, - 1 - ); - - drop(second); - let released = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active; - assert_eq!(released.current, 0); - assert_eq!(released.high_water, 2); - } - #[test] fn configured_executor_and_command_bounds_drive_session_manager() { const SUBPROCESS_ENV: &str = "AGENTOS_V8_CONFIGURED_SESSION_MANAGER_SUBPROCESS"; @@ -3947,6 +4039,13 @@ mod tests { let (event_tx, _event_rx) = crossbeam_channel::unbounded(); let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit()); let mut manager = SessionManager::new( + runtime.max_active_vm_executors(), + event_tx.clone(), + Arc::clone(&router), + Arc::new(SnapshotCache::new(1)), + runtime.clone(), + ); + let mut second_manager = SessionManager::new( runtime.max_active_vm_executors(), event_tx, router, @@ -3954,15 +4053,78 @@ mod tests { runtime, ); - assert_eq!(manager.max_concurrency, 3); + assert_eq!(manager.max_concurrency(), 3); assert_eq!(manager.executor_teardown_timeout, Duration::from_millis(23)); manager .create_session("configured-bounds".into(), None, None, None) .expect("create bounded session"); assert_eq!(manager.sessions["configured-bounds"].command_capacity, 7); + manager + .create_session("shared-process-a".into(), None, None, None) + .expect("second permit through first manager"); + second_manager + .create_session("shared-process-b".into(), None, None, None) + .expect("third permit through second manager"); + assert_eq!(manager.active_slot_count(), 2); + assert_eq!(second_manager.active_slot_count(), 1); + assert_eq!( + manager.runtime.vm_executor_admission().snapshot().active, + 3, + "the process runtime must aggregate permits from both managers" + ); + let saturation = second_manager + .create_session("shared-process-overflow".into(), None, None, None) + .expect_err("all V8 managers must share the process executor quota"); + assert!(saturation.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT")); + manager .destroy_session("configured-bounds") .expect("destroy bounded session"); + second_manager + .create_session("shared-process-successor".into(), None, None, None) + .expect("joined executor must release capacity across managers"); + manager + .destroy_session("shared-process-a") + .expect("destroy first-manager session"); + second_manager + .destroy_session("shared-process-b") + .expect("destroy second-manager session"); + second_manager + .destroy_session("shared-process-successor") + .expect("destroy cross-manager successor"); + assert_eq!(manager.active_slot_count(), 0); + assert_eq!(second_manager.active_slot_count(), 0); + assert_eq!(manager.runtime.vm_executor_admission().snapshot().active, 0); + } + + #[test] + fn manager_local_executor_caps_do_not_block_other_managers() { + let mut first = test_manager(1); + let mut second = test_manager(1); + + first + .create_session("manager-local-first".into(), None, None, None) + .expect("first manager admits its one local executor"); + second + .create_session("manager-local-second".into(), None, None, None) + .expect("second manager has an independent local executor ceiling"); + assert_eq!(first.active_slot_count(), 1); + assert_eq!(second.active_slot_count(), 1); + + let error = first + .create_session("manager-local-overflow".into(), None, None, None) + .expect_err("first manager must enforce its own local ceiling"); + assert!(error.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT")); + assert!(error.contains("manager-local limit of 1")); + + first + .destroy_session("manager-local-first") + .expect("destroy first manager session"); + second + .destroy_session("manager-local-second") + .expect("destroy second manager session"); + assert_eq!(first.active_slot_count(), 0); + assert_eq!(second.active_slot_count(), 0); } fn expect_late_message_warning( @@ -4398,7 +4560,9 @@ mod tests { }, ))) .expect("queue ordinary session command"); - broker.publish_signal(15).expect("publish later SIGTERM"); + broker + .publish_signal(15, 29) + .expect("publish later SIGTERM"); assert!(matches!( recv_session_command(&rx, &shutdown_rx, &ready_rx, &broker), @@ -4412,7 +4576,10 @@ mod tests { assert!(batch.signals_ready); assert_eq!( broker.drain_signals(&batch).expect("drain signal"), - vec![15] + vec![agentos_runtime::readiness::SignalObservation { + signal: 15, + delivery_token: 29, + }] ); broker .complete_batch(&batch, &[]) diff --git a/crates/v8-runtime/src/stream.rs b/crates/v8-runtime/src/stream.rs index 596e034f7c..a9a7b5ec63 100644 --- a/crates/v8-runtime/src/stream.rs +++ b/crates/v8-runtime/src/stream.rs @@ -67,7 +67,12 @@ pub fn dispatch_stream_event(scope: &mut v8::HandleScope, event_type: &str, payl } } -pub fn dispatch_signal_event(scope: &mut v8::HandleScope, signal_name: &str, signal: i32) { +pub fn dispatch_signal_event( + scope: &mut v8::HandleScope, + signal_name: &str, + signal: i32, + delivery_token: u64, +) { let payload = v8::Object::new(scope); let signal_key = v8::String::new(scope, "signal").expect("static V8 string"); let signal_value = v8::String::new(scope, signal_name).expect("signal V8 string"); @@ -78,6 +83,13 @@ pub fn dispatch_signal_event(scope: &mut v8::HandleScope, signal_name: &str, sig let action_key = v8::String::new(scope, "action").expect("static V8 string"); let action_value = v8::String::new(scope, "default").expect("static V8 string"); payload.set(scope, action_key.into(), action_value.into()); + let delivery_token_key = v8::String::new(scope, "deliveryToken").expect("static V8 string"); + let delivery_token_value = v8::Number::new(scope, delivery_token as f64); + payload.set( + scope, + delivery_token_key.into(), + delivery_token_value.into(), + ); dispatch_stream_value(scope, "signal", payload.into()); } diff --git a/crates/v8-runtime/tests/embedded_runtime_session.rs b/crates/v8-runtime/tests/embedded_runtime_session.rs index 47552d90ee..86686c9460 100644 --- a/crates/v8-runtime/tests/embedded_runtime_session.rs +++ b/crates/v8-runtime/tests/embedded_runtime_session.rs @@ -17,6 +17,7 @@ fn run_timing_sensitive_tests() -> bool { static NEXT_TEST_SESSION_ID: AtomicU64 = AtomicU64::new(1); static NEXT_TEST_VM_GENERATION: AtomicU64 = AtomicU64::new(1); const TEST_REACTOR_WORK_QUANTUM: usize = 64; +const TEST_PROCESS_VM_EXECUTOR_LIMIT: usize = 64; fn next_session_id() -> String { format!( @@ -26,7 +27,11 @@ fn next_session_id() -> String { } fn process_runtime_context() -> io::Result { - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + let config = agentos_runtime::RuntimeConfig { + max_active_vm_executors: TEST_PROCESS_VM_EXECUTOR_LIMIT, + ..agentos_runtime::RuntimeConfig::default() + }; + agentos_runtime::SidecarRuntime::process(&config) .map(agentos_runtime::SidecarRuntime::context) .map_err(|error| io::Error::other(error.to_string())) } @@ -879,7 +884,7 @@ fn assert_sync_bridge_response_bypasses_full_ordinary_command_lane() -> io::Resu "unexpected ordinary-lane overload: {overload}" ); for _ in 0..1_024 { - session.publish_signal(10)?; + session.publish_signal(10, 41)?; } session.send_bridge_response(call_id, 0, Vec::new())?; assert_execution_ok(&receiver, &session_id); @@ -1014,8 +1019,7 @@ fn assert_destroy_joins_active_handle_executor() -> io::Result<()> { fn embedded_runtime_session_consolidated_behaviors() -> io::Result<()> { // This integration test is its own process entrypoint. Production // subsystems may retrieve, but never lazily construct, the process runtime. - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .map_err(|error| io::Error::other(error.to_string()))?; + process_runtime_context()?; // Keep the embedded-runtime coverage in one test process. V8 teardown across // multiple integration tests still trips intermittent SIGSEGVs in this crate. assert_create_destroy_reuses_session_ids()?; diff --git a/crates/v8-runtime/tests/runtime_context_architecture.rs b/crates/v8-runtime/tests/runtime_context_architecture.rs index 346add3232..e198d010d3 100644 --- a/crates/v8-runtime/tests/runtime_context_architecture.rs +++ b/crates/v8-runtime/tests/runtime_context_architecture.rs @@ -28,6 +28,65 @@ fn production_v8_runtime_never_discovers_the_process_runtime() { ); } +#[test] +fn process_runtime_is_the_only_vm_executor_admission_owner() { + let manifest_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let session = + fs::read_to_string(manifest_root.join("src/session.rs")).expect("read V8 session source"); + let session_compact: String = session + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + for local_admission in [ + "SlotControl", + "SessionSlotPermit", + "activeV8executors", + "observe_executor(ExecutorMetricClass::Vm", + ] { + assert!( + !session_compact.contains(local_admission), + "V8 must not own process executor accounting primitive {local_admission}" + ); + } + assert!( + session_compact.contains("slot_permit:SessionExecutorPermit") + && session_compact.contains(".vm_executor_admission().try_acquire()") + && session_compact.contains("self.manager_executor_admission.try_acquire()") + && session_compact.contains("self.manager_executor_admission.active()"), + "V8 assignment must combine process-global admission with an independent manager-local ceiling" + ); + + let runtime = fs::read_to_string(manifest_root.join("../runtime/src/lib.rs")) + .expect("read process runtime source"); + let runtime_compact: String = runtime + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + assert!( + runtime_compact.contains("vm_executors:VmExecutorAdmission") + && runtime_compact.contains("vm_executors:self.vm_executors.clone()") + && runtime_compact.contains( + "VmExecutorAdmission::new(config.max_active_vm_executors,metrics.clone())" + ), + "one process-owned admission must be cloned unchanged into every RuntimeContext scope" + ); + + let admission = fs::read_to_string(manifest_root.join("../runtime/src/executor.rs")) + .expect("read runtime-neutral executor admission"); + for engine_name in ["V8", "Wasmtime", "JavascriptExecution", "WasmExecution"] { + assert!( + !admission.contains(engine_name), + "runtime executor admission must not name engine {engine_name}" + ); + } + assert!( + admission.contains("pub struct VmExecutorAdmission") + && admission.contains("pub struct VmExecutorPermit") + && admission.contains("WARN_AGENTOS_VM_EXECUTOR_NEAR_LIMIT"), + "runtime must expose engine-neutral RAII admission with near-limit warning" + ); +} + #[test] fn node_server_close_waits_for_accepted_connections_to_drain() { let source = fs::read_to_string( @@ -46,10 +105,13 @@ fn node_server_close_waits_for_accepted_connections_to_drain() { "accepted-socket teardown must re-check the Node server close drain gate" ); assert!( - compact.contains("Promise.resolve(_netServerCloseRaw(serverId)).then(") - && compact.contains("this._pendingTransportCloses-=1") - && compact.contains("this._emitCloseIfDrained();"), - "listener teardown must complete asynchronously before entering the Node server close drain gate" + compact.contains("constfinishTransportClose=(error)=>{") + && compact.contains("this._pendingTransportCloses-=1;") + && compact.contains("this._emitCloseIfDrained();") + && compact.contains( + "Promise.resolve(_netServerCloseRaw(serverId,unlinkNodePath)).then(()=>finishTransportClose(),finishTransportClose,);" + ), + "listener teardown must complete through the asynchronous accounting helper before entering the Node server close drain gate" ); assert!( compact.contains("this._pendingTransportCloses!==0") diff --git a/crates/vfs-store/tests/local.rs b/crates/vfs-store/tests/local.rs index 4cbe726a7e..15bb9f9858 100644 --- a/crates/vfs-store/tests/local.rs +++ b/crates/vfs-store/tests/local.rs @@ -1,3 +1,4 @@ +use agentos_runtime::{RuntimeConfig, SidecarRuntime}; use agentos_vfs::{FileBlockStore, SqliteMetadataStore}; use rusqlite::Connection; use vfs::adapter::MountedEngineFileSystem; @@ -7,6 +8,12 @@ use vfs::engine::{BlockKey, BlockStore, VirtualFileSystem}; use vfs::posix::MountedFileSystem; use vfs::posix::{MemoryFileSystem, MountOptions, MountTable}; +fn test_runtime_context() -> agentos_runtime::RuntimeContext { + SidecarRuntime::process(&RuntimeConfig::default()) + .expect("create test runtime") + .context() +} + #[tokio::test] async fn file_block_store_persists_blocks() { let temp = tempfile::tempdir().unwrap(); @@ -436,14 +443,11 @@ async fn chunked_local_reopens_and_cleans_stale_blocks() { #[test] fn chunked_local_mounted_adapter_creates_exclusive_files_with_modes() { let temp = tempfile::tempdir().unwrap(); - let runtime = - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); + let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, test_runtime_context()); mounted.mkdir("/nested", false).unwrap(); mounted @@ -460,14 +464,11 @@ fn chunked_local_mounted_adapter_creates_exclusive_files_with_modes() { #[test] fn chunked_local_mounted_adapter_preserves_sparse_pwrite_allocation() { let temp = tempfile::tempdir().unwrap(); - let runtime = - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); + let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, test_runtime_context()); mounted.write_file("/sparse", Vec::new()).unwrap(); mounted @@ -482,14 +483,11 @@ fn chunked_local_mounted_adapter_preserves_sparse_pwrite_allocation() { #[test] fn chunked_local_mount_table_preserves_sparse_pwrite_allocation() { let temp = tempfile::tempdir().unwrap(); - let runtime = - agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) - .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); + let mounted = MountedEngineFileSystem::with_runtime_context(fs, test_runtime_context()); let mut table = MountTable::new(MemoryFileSystem::new()); vfs::posix::VirtualFileSystem::mkdir(&mut table, "/mnt", false).unwrap(); table diff --git a/crates/vfs/assets/base-filesystem.json b/crates/vfs/assets/base-filesystem.json index e641efddd3..3404d53032 100644 --- a/crates/vfs/assets/base-filesystem.json +++ b/crates/vfs/assets/base-filesystem.json @@ -6,6 +6,7 @@ "builtAt": "2026-06-23T03:47:12.644Z", "transforms": [ "Normalize HOSTNAME to secure-exec", + "Allow traversal into the AgentOS /root/node_modules compatibility projection", "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", "Restore Alpine 3.22's /etc/services database and add the VM's Docker-style /etc/hosts entries for libc lookup parity" @@ -336,7 +337,7 @@ { "path": "/root", "type": "directory", - "mode": "700", + "mode": "711", "uid": 0, "gid": 0 }, diff --git a/crates/vfs/src/adapter/mounted_fs.rs b/crates/vfs/src/adapter/mounted_fs.rs index eaaa312dca..0b14f83466 100644 --- a/crates/vfs/src/adapter/mounted_fs.rs +++ b/crates/vfs/src/adapter/mounted_fs.rs @@ -1,6 +1,6 @@ use crate::posix::{ - MountedFileSystem, VfsError as PosixVfsError, VfsResult as PosixVfsResult, VirtualDirEntry, - VirtualStat, + FileExtent, MountedFileSystem, VfsError as PosixVfsError, VfsResult as PosixVfsResult, + VirtualDirEntry, VirtualStat, }; use agentos_runtime::{BlockingJobError, RuntimeContext}; use std::any::Any; @@ -514,6 +514,22 @@ where }) } + fn extent_at(&mut self, path: &str, index: usize) -> PosixVfsResult> { + let inner = Arc::clone(&self.inner); + let path = path.to_owned(); + let reserved_bytes = path.len(); + self.run(reserved_bytes, async move { + inner.extent_at(&path, index).await + }) + .map(|extent| { + extent.map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + }) + }) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> PosixVfsResult> { let inner = Arc::clone(&self.inner); let path = path.to_owned(); @@ -623,6 +639,22 @@ mod tests { assert_eq!(stat.mode & 0o777, 0o600); assert_eq!(stat.mode & S_IFREG, S_IFREG); assert_eq!(stat.size, 5); + assert_eq!( + mounted + .extent_at("/work/nested/file.txt", 0) + .expect("query first extent"), + Some(FileExtent { + start: 0, + end: 5, + unwritten: false, + }) + ); + assert_eq!( + mounted + .extent_at("/work/nested/file.txt", 1) + .expect("query past final extent"), + None + ); } #[test] diff --git a/crates/vfs/src/engine/engines/chunked.rs b/crates/vfs/src/engine/engines/chunked.rs index e8b6362c3c..c0e8bae563 100644 --- a/crates/vfs/src/engine/engines/chunked.rs +++ b/crates/vfs/src/engine/engines/chunked.rs @@ -5,9 +5,9 @@ use crate::engine::types::{ decode_unwritten_extents, encode_unwritten_extents, normalize_path, set_xattr_value, unwritten_after_allocate, unwritten_after_collapse, unwritten_after_insert, unwritten_after_truncate, unwritten_after_write, unwritten_after_zero, unwritten_byte_ranges, - validate_xattr_name, BlockKey, ChunkEdit, ChunkRange, CreateInodeAttrs, Dentry, InodeMeta, - InodePatch, InodeType, SnapshotId, Storage, Timespec, VirtualStat, DEFAULT_CHUNK_SIZE, - DEFAULT_INLINE_THRESHOLD, INTERNAL_XATTR_PREFIX, + unwritten_sector_ranges, validate_xattr_name, BlockKey, ChunkEdit, ChunkRange, + CreateInodeAttrs, Dentry, FileExtent, InodeMeta, InodePatch, InodeType, SnapshotId, Storage, + Timespec, VirtualStat, DEFAULT_CHUNK_SIZE, DEFAULT_INLINE_THRESHOLD, INTERNAL_XATTR_PREFIX, }; use crate::engine::vfs::{Snapshottable, VirtualFileSystem}; use async_trait::async_trait; @@ -1154,6 +1154,22 @@ impl VirtualFileSystem for ChunkedFs { unwritten_byte_ranges(&meta.xattrs, meta.size) } + async fn extent_at(&self, path: &str, index: usize) -> VfsResult> { + let meta = self.metadata.resolve(path).await?; + self.ensure_file(path, &meta)?; + let unwritten = unwritten_sector_ranges(&meta.xattrs)?; + let extent = crate::extent::classified_file_extent_at( + crate::extent::sector_byte_ranges(meta.allocated_extents.iter().copied(), meta.size), + crate::extent::sector_byte_ranges(unwritten, meta.size), + index, + ); + Ok(extent.map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })) + } + async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult> { let meta = self.metadata.resolve(path).await?; self.ensure_file(path, &meta)?; diff --git a/crates/vfs/src/engine/types.rs b/crates/vfs/src/engine/types.rs index 6292c36390..62f9fdc86f 100644 --- a/crates/vfs/src/engine/types.rs +++ b/crates/vfs/src/engine/types.rs @@ -26,32 +26,40 @@ pub const S_IFIFO: u32 = 0o010000; pub(crate) fn decode_unwritten_extents( xattrs: &BTreeMap>, ) -> VfsResult> { - let Some(encoded) = xattrs.get(INODE_UNWRITTEN_EXTENTS_XATTR) else { - return Ok(Vec::new()); - }; - if encoded.len() % 16 != 0 { + Ok(unwritten_sector_ranges(xattrs)?.collect()) +} + +pub(crate) fn unwritten_sector_ranges( + xattrs: &BTreeMap>, +) -> VfsResult + Clone + '_> { + let encoded = xattrs + .get(INODE_UNWRITTEN_EXTENTS_XATTR) + .map(Vec::as_slice) + .unwrap_or_default(); + if !encoded.len().is_multiple_of(16) { return Err(VfsError::new( "EIO", "corrupt internal unwritten-extent metadata", )); } - let mut ranges = Vec::with_capacity(encoded.len() / 16); + let mut prior_end = None; for chunk in encoded.chunks_exact(16) { - let start = u64::from_le_bytes(chunk[..8].try_into().expect("eight-byte extent start")); - let end = u64::from_le_bytes(chunk[8..].try_into().expect("eight-byte extent end")); - if start >= end - || ranges - .last() - .is_some_and(|(_, prior_end)| *prior_end >= start) - { + let (start, end) = decode_unwritten_extent(chunk); + if start >= end || prior_end.is_some_and(|prior_end| prior_end >= start) { return Err(VfsError::new( "EIO", "corrupt internal unwritten-extent ordering", )); } - ranges.push((start, end)); + prior_end = Some(end); } - Ok(ranges) + Ok(encoded.chunks_exact(16).map(decode_unwritten_extent)) +} + +fn decode_unwritten_extent(chunk: &[u8]) -> (u64, u64) { + let start = u64::from_le_bytes(chunk[..8].try_into().expect("eight-byte extent start")); + let end = u64::from_le_bytes(chunk[8..].try_into().expect("eight-byte extent end")); + (start, end) } pub(crate) fn encode_unwritten_extents( @@ -74,14 +82,7 @@ pub(crate) fn unwritten_byte_ranges( xattrs: &BTreeMap>, size: u64, ) -> VfsResult> { - Ok(decode_unwritten_extents(xattrs)? - .into_iter() - .filter_map(|(start, end)| { - let start = start.saturating_mul(512).min(size); - let end = end.saturating_mul(512).min(size); - (start < end).then_some((start, end)) - }) - .collect()) + Ok(crate::extent::sector_byte_ranges(unwritten_sector_ranges(xattrs)?, size).collect()) } fn normalize_sector_extents(mut extents: Vec<(u64, u64)>) -> Vec<(u64, u64)> { @@ -545,19 +546,13 @@ pub fn set_xattr_value( format!("extended attribute does not exist: {name}"), )); } - let old_len = xattrs.get(name).map_or(0, Vec::len); let list_bytes = xattrs.keys().map(|key| key.len() + 1).sum::(); - let value_bytes = xattrs.values().map(Vec::len).sum::(); - let new_total = list_bytes - .saturating_add(value_bytes) - .saturating_sub(old_len) - .saturating_add(if exists { 0 } else { name.len() + 1 }) - .saturating_add(value.len()); + let new_total = list_bytes.saturating_add(if exists { 0 } else { name.len() + 1 }); if new_total > XATTR_LIST_MAX { return Err(VfsError::new( "ENOSPC", format!( - "inode extended attributes require {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes" + "inode extended attribute name list requires {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes" ), )); } @@ -585,6 +580,13 @@ pub struct ObjectMeta { pub xattrs: BTreeMap>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FileExtent { + pub start: u64, + pub end: u64, + pub unwritten: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ObjectEntry { pub name: String, diff --git a/crates/vfs/src/engine/vfs.rs b/crates/vfs/src/engine/vfs.rs index 2f00b213c2..56a19c0cae 100644 --- a/crates/vfs/src/engine/vfs.rs +++ b/crates/vfs/src/engine/vfs.rs @@ -1,5 +1,5 @@ use crate::engine::error::{VfsError, VfsResult}; -use crate::engine::types::{Dentry, SnapshotId, VirtualStat}; +use crate::engine::types::{Dentry, FileExtent, SnapshotId, VirtualStat}; use async_trait::async_trait; #[async_trait] @@ -201,6 +201,21 @@ pub trait VirtualFileSystem: Send + Sync { async fn unwritten_ranges(&self, _path: &str) -> VfsResult> { Ok(Vec::new()) } + /// Returns one allocated extent, split at written/unwritten boundaries. + async fn extent_at(&self, path: &str, index: usize) -> VfsResult> { + let allocated = self.allocated_ranges(path).await?; + let unwritten = self.unwritten_ranges(path).await?; + Ok(crate::extent::classified_file_extent_at( + allocated.iter().copied(), + unwritten.iter().copied(), + index, + ) + .map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })) + } async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult>; async fn pwrite(&self, path: &str, content: &[u8], offset: u64) -> VfsResult<()>; async fn append(&self, path: &str, content: &[u8]) -> VfsResult; diff --git a/crates/vfs/src/extent.rs b/crates/vfs/src/extent.rs new file mode 100644 index 0000000000..30f85dc8d1 --- /dev/null +++ b/crates/vfs/src/extent.rs @@ -0,0 +1,121 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ClassifiedFileExtent { + pub start: u64, + pub end: u64, + pub unwritten: bool, +} + +pub(crate) fn classified_file_extent_at( + allocated: A, + unwritten: U, + wanted: usize, +) -> Option +where + A: IntoIterator, + U: Iterator + Clone, +{ + let mut observed = 0usize; + for (start, end) in allocated { + let mut cursor = start; + for (unwritten_start, unwritten_end) in unwritten.clone() { + if unwritten_end <= cursor || unwritten_start >= end { + continue; + } + if cursor < unwritten_start { + if observed == wanted { + return Some(ClassifiedFileExtent { + start: cursor, + end: unwritten_start.min(end), + unwritten: false, + }); + } + observed = observed.saturating_add(1); + } + let overlap_start = cursor.max(unwritten_start); + let overlap_end = end.min(unwritten_end); + if overlap_start < overlap_end { + if observed == wanted { + return Some(ClassifiedFileExtent { + start: overlap_start, + end: overlap_end, + unwritten: true, + }); + } + observed = observed.saturating_add(1); + cursor = overlap_end; + } + if cursor == end { + break; + } + } + if cursor < end { + if observed == wanted { + return Some(ClassifiedFileExtent { + start: cursor, + end, + unwritten: false, + }); + } + observed = observed.saturating_add(1); + } + } + None +} + +pub(crate) fn sector_byte_ranges( + extents: I, + size: u64, +) -> impl Iterator + Clone +where + I: Iterator + Clone, +{ + extents.filter_map(move |(start, end)| { + let start = start.saturating_mul(512).min(size); + let end = end.saturating_mul(512).min(size); + (start < end).then_some((start, end)) + }) +} + +#[cfg(test)] +mod tests { + use super::{classified_file_extent_at, ClassifiedFileExtent}; + + #[test] + fn classifies_index_without_collecting_output_extents() { + let allocated = [(0, 2048), (3072, 4096)]; + let unwritten = [(512, 1024), (3072, 4096)]; + let expected = [ + ClassifiedFileExtent { + start: 0, + end: 512, + unwritten: false, + }, + ClassifiedFileExtent { + start: 512, + end: 1024, + unwritten: true, + }, + ClassifiedFileExtent { + start: 1024, + end: 2048, + unwritten: false, + }, + ClassifiedFileExtent { + start: 3072, + end: 4096, + unwritten: true, + }, + ]; + + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!( + classified_file_extent_at(allocated.into_iter(), unwritten.into_iter(), index), + Some(expected) + ); + } + assert_eq!( + classified_file_extent_at(allocated, unwritten.into_iter(), expected.len()), + None + ); + } +} diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index 9d49c1cefe..7b45ce8bcd 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -3,5 +3,6 @@ #[cfg(not(target_arch = "wasm32"))] pub mod adapter; pub mod engine; +mod extent; pub mod package_format; pub mod posix; diff --git a/crates/vfs/src/posix/mount_table.rs b/crates/vfs/src/posix/mount_table.rs index 01522c6e04..f00ae4db47 100644 --- a/crates/vfs/src/posix/mount_table.rs +++ b/crates/vfs/src/posix/mount_table.rs @@ -1,7 +1,8 @@ use super::root_fs::RootFileSystem; use super::usage::{FileSystemStats, FileSystemUsage}; use super::vfs::{ - VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualUtimeSpec, + FileExtent, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, + VirtualUtimeSpec, }; use std::any::Any; use std::collections::VecDeque; @@ -278,6 +279,20 @@ pub trait MountedFileSystem: Any { fn unwritten_ranges(&mut self, _path: &str) -> VfsResult> { Ok(Vec::new()) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + let allocated = self.allocated_ranges(path)?; + let unwritten = self.unwritten_ranges(path)?; + Ok(crate::extent::classified_file_extent_at( + allocated.iter().copied(), + unwritten.iter().copied(), + index, + ) + .map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })) + } fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult>; fn pwrite(&mut self, path: &str, content: Vec, offset: u64) -> VfsResult<()> { let mut existing = self.read_file(path)?; @@ -544,6 +559,10 @@ where VirtualFileSystem::unwritten_ranges(&mut self.inner, path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + VirtualFileSystem::extent_at(&mut self.inner, path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { VirtualFileSystem::pread(&mut self.inner, path, offset, length) } @@ -742,6 +761,10 @@ where (**self).unwritten_ranges(path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + (**self).extent_at(path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { (**self).pread(path, offset, length) } @@ -1016,6 +1039,10 @@ where self.inner.unwritten_ranges(path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + self.inner.extent_at(path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { self.inner.pread(path, offset, length) } @@ -2480,6 +2507,13 @@ impl VirtualFileSystem for MountTable { .unwritten_ranges(&relative_path) } + fn extent_at(&mut self, path: &str, extent_index: usize) -> VfsResult> { + let (mount_index, relative_path) = self.resolve_content_index(path)?; + self.mounts[mount_index] + .filesystem + .extent_at(&relative_path, extent_index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { let (index, relative_path) = self.resolve_content_index(path)?; let before = self.atime_snapshot(index, &relative_path, false)?; diff --git a/crates/vfs/src/posix/overlay_fs.rs b/crates/vfs/src/posix/overlay_fs.rs index 362975cfbb..745f7b31ac 100644 --- a/crates/vfs/src/posix/overlay_fs.rs +++ b/crates/vfs/src/posix/overlay_fs.rs @@ -1,6 +1,6 @@ use super::vfs::{ - normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, - VirtualStat, VirtualUtimeSpec, + normalize_path, FileExtent, MemoryFileSystem, VfsError, VfsResult, VirtualDirEntry, + VirtualFileSystem, VirtualStat, VirtualUtimeSpec, }; use base64::Engine; use std::collections::BTreeSet; @@ -2105,6 +2105,20 @@ impl VirtualFileSystem for OverlayFileSystem { } } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + if self.touches_internal_metadata(path) || self.is_whited_out(path) { + return Err(Self::entry_not_found(path)); + } + if self.exists_in_upper(path) { + self.writable_upper(path)?.extent_at(path, index) + } else { + let Some(lower_index) = self.find_lower_by_exists(path) else { + return Err(Self::entry_not_found(path)); + }; + self.lowers[lower_index].extent_at(path, index) + } + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { if self.touches_internal_metadata(path) { return Err(Self::entry_not_found(path)); diff --git a/crates/vfs/src/posix/root_fs.rs b/crates/vfs/src/posix/root_fs.rs index 78c93523f4..a119d21112 100644 --- a/crates/vfs/src/posix/root_fs.rs +++ b/crates/vfs/src/posix/root_fs.rs @@ -3,8 +3,8 @@ use super::usage::{ RootFilesystemResourceLimits, DEFAULT_MAX_FILESYSTEM_BYTES, DEFAULT_MAX_INODE_COUNT, }; use super::vfs::{ - normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualFileSystem, VirtualStat, - VirtualUtimeSpec, MAX_PATH_LENGTH, + normalize_path, FileExtent, MemoryFileSystem, VfsError, VfsResult, VirtualFileSystem, + VirtualStat, VirtualUtimeSpec, MAX_PATH_LENGTH, }; use crate::posix::vfs::VirtualDirEntry; use base64::Engine; @@ -274,6 +274,10 @@ impl RootFileSystem { Ok(()) } + pub fn is_read_only_mode(&self) -> bool { + self.mode == RootFilesystemMode::ReadOnly + } + pub fn finish_bootstrap(&mut self) { if self.bootstrap_finished { return; @@ -482,6 +486,10 @@ impl VirtualFileSystem for RootFileSystem { self.overlay.unwritten_ranges(path) } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + self.overlay.extent_at(path, index) + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { self.overlay.pread(path, offset, length) } diff --git a/crates/vfs/src/posix/vfs.rs b/crates/vfs/src/posix/vfs.rs index cb38cd02f6..9efd60f1e8 100644 --- a/crates/vfs/src/posix/vfs.rs +++ b/crates/vfs/src/posix/vfs.rs @@ -183,19 +183,13 @@ pub fn set_xattr_value( format!("extended attribute does not exist: {name}"), )); } - let old_len = xattrs.get(name).map_or(0, Vec::len); let list_bytes = xattrs.keys().map(|key| key.len() + 1).sum::(); - let value_bytes = xattrs.values().map(Vec::len).sum::(); - let new_total = list_bytes - .saturating_add(value_bytes) - .saturating_sub(old_len) - .saturating_add(if exists { 0 } else { name.len() + 1 }) - .saturating_add(value.len()); + let new_total = list_bytes.saturating_add(if exists { 0 } else { name.len() + 1 }); if new_total > XATTR_LIST_MAX { return Err(VfsError::new( "ENOSPC", format!( - "inode extended attributes require {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes" + "inode extended attribute name list requires {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes" ), )); } @@ -287,6 +281,13 @@ pub enum VirtualUtimeSpec { Omit, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FileExtent { + pub start: u64, + pub end: u64, + pub unwritten: bool, +} + pub trait VirtualFileSystem { fn read_file(&mut self, path: &str) -> VfsResult>; fn read_text_file(&mut self, path: &str) -> VfsResult { @@ -670,6 +671,24 @@ pub trait VirtualFileSystem { fn unwritten_ranges(&mut self, _path: &str) -> VfsResult> { Ok(Vec::new()) } + /// Returns one allocated extent, split at written/unwritten boundaries. + /// + /// Implementations with native extent metadata should override this so an + /// indexed FIEMAP query does not allocate complete range lists. + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + let allocated = self.allocated_ranges(path)?; + let unwritten = self.unwritten_ranges(path)?; + Ok(crate::extent::classified_file_extent_at( + allocated.iter().copied(), + unwritten.iter().copied(), + index, + ) + .map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })) + } fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult>; /// Writes caller-owned bytes at an offset after checking that the in-memory /// file can grow without overflowing addressable memory. @@ -2117,6 +2136,33 @@ impl VirtualFileSystem for MemoryFileSystem { } } + fn extent_at(&mut self, path: &str, index: usize) -> VfsResult> { + let inode = self.inode_mut_for_existing_path(path, "fiemap", true)?; + match &inode.kind { + InodeKind::File { data } => Ok(crate::extent::classified_file_extent_at( + crate::extent::sector_byte_ranges( + inode.metadata.allocated_extents.iter().copied(), + data.len() as u64, + ), + crate::extent::sector_byte_ranges( + inode.metadata.unwritten_extents.iter().copied(), + data.len() as u64, + ), + index, + ) + .map(|extent| FileExtent { + start: extent.start, + end: extent.end, + unwritten: extent.unwritten, + })), + InodeKind::Directory => Err(VfsError::is_directory("fiemap", path)), + InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fiemap", path)), + InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => { + Ok(None) + } + } + } + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { let inode = self.inode_mut_for_existing_path(path, "open", true)?; match &mut inode.kind { @@ -2453,14 +2499,7 @@ fn allocated_block_count(extents: &[(u64, u64)]) -> u64 { } fn allocation_byte_ranges(extents: &[(u64, u64)], size: u64) -> Vec<(u64, u64)> { - extents - .iter() - .filter_map(|&(start, end)| { - let start = start.saturating_mul(512).min(size); - let end = end.saturating_mul(512).min(size); - (start < end).then_some((start, end)) - }) - .collect() + crate::extent::sector_byte_ranges(extents.iter().copied(), size).collect() } fn checked_file_len(value: u64, description: &'static str) -> VfsResult { diff --git a/crates/vfs/tests/conformance.rs b/crates/vfs/tests/conformance.rs index 4cb8296ecf..35d5f8c86a 100644 --- a/crates/vfs/tests/conformance.rs +++ b/crates/vfs/tests/conformance.rs @@ -5,9 +5,9 @@ use std::time::Duration; use vfs::engine::engines::{ChunkedFs, ChunkedFsOptions, ObjectFs}; use vfs::engine::mem::{InMemoryMetadataStore, MemoryBlockStore, MemoryObjectBackend}; use vfs::engine::{ - BlockKey, CachedMetadataStore, ChunkEdit, ChunkRange, CreateInodeAttrs, InodePatch, InodeType, - MetadataStore, ObjectBackend, SnapshotId, Storage, VfsResult, VirtualFileSystem, S_IFBLK, - S_IFIFO, + BlockKey, CachedMetadataStore, ChunkEdit, ChunkRange, CreateInodeAttrs, FileExtent, InodePatch, + InodeType, MetadataStore, ObjectBackend, SnapshotId, Storage, VfsResult, VirtualFileSystem, + S_IFBLK, S_IFIFO, }; #[tokio::test] @@ -409,6 +409,36 @@ async fn chunked_fs_zero_range_reallocates_exact_bytes_and_honors_keep_size() { fs.unwritten_ranges("/zeroed").await.unwrap(), vec![(512, 1024), (3072, 3584)] ); + let mut indexed_extents = Vec::new(); + for index in 0..5 { + indexed_extents.push(fs.extent_at("/zeroed", index).await.unwrap()); + } + assert_eq!( + indexed_extents, + vec![ + Some(FileExtent { + start: 0, + end: 512, + unwritten: false, + }), + Some(FileExtent { + start: 512, + end: 1024, + unwritten: true, + }), + Some(FileExtent { + start: 1024, + end: 2048, + unwritten: false, + }), + Some(FileExtent { + start: 3072, + end: 3584, + unwritten: true, + }), + None, + ] + ); fs.pwrite("/zeroed", &vec![b'y'; 512], 512).await.unwrap(); assert_eq!( fs.unwritten_ranges("/zeroed").await.unwrap(), diff --git a/crates/vfs/tests/posix_root_fs.rs b/crates/vfs/tests/posix_root_fs.rs index 26af4db923..f70d09a7f1 100644 --- a/crates/vfs/tests/posix_root_fs.rs +++ b/crates/vfs/tests/posix_root_fs.rs @@ -85,6 +85,16 @@ fn assert_error_code(result: Result assert_eq!(error.code(), expected); } +#[test] +fn bundled_root_allows_compatibility_module_traversal_without_directory_listing() { + let mut root = RootFileSystem::from_descriptor(RootFilesystemDescriptor::default()) + .expect("create bundled root filesystem"); + + let stat = root.stat("/root").expect("stat /root"); + assert_eq!(stat.mode & 0o7777, 0o711); + assert_eq!((stat.uid, stat.gid), (0, 0)); +} + #[test] fn overlay_filesystem_prefers_higher_lowers_and_hides_whiteouts() { let mut higher = MemoryFileSystem::new(); diff --git a/crates/vfs/tests/posix_vfs.rs b/crates/vfs/tests/posix_vfs.rs index 351ac98f51..cdb7103c53 100644 --- a/crates/vfs/tests/posix_vfs.rs +++ b/crates/vfs/tests/posix_vfs.rs @@ -1,7 +1,7 @@ use std::{fmt::Debug, thread::sleep, time::Duration}; use vfs::posix::{ - normalize_path, validate_path, MemoryFileSystem, VfsResult, VirtualFileSystem, RENAME_EXCHANGE, - RENAME_NOREPLACE, S_IFLNK, S_IFREG, XATTR_CREATE, XATTR_REPLACE, + normalize_path, validate_path, FileExtent, MemoryFileSystem, VfsResult, VirtualFileSystem, + RENAME_EXCHANGE, RENAME_NOREPLACE, S_IFLNK, S_IFREG, XATTR_CREATE, XATTR_REPLACE, }; fn assert_error_code(result: vfs::posix::VfsResult, expected: &str) { @@ -689,6 +689,34 @@ fn zero_range_zeroes_exact_bytes_reallocates_holes_and_honors_keep_size() { filesystem.unwritten_ranges("/zeroed").unwrap(), vec![(512, 1024), (3072, 4096)] ); + assert_eq!( + (0..5) + .map(|index| filesystem.extent_at("/zeroed", index).unwrap()) + .collect::>(), + vec![ + Some(FileExtent { + start: 0, + end: 512, + unwritten: false, + }), + Some(FileExtent { + start: 512, + end: 1024, + unwritten: true, + }), + Some(FileExtent { + start: 1024, + end: 2048, + unwritten: false, + }), + Some(FileExtent { + start: 3072, + end: 4096, + unwritten: true, + }), + None, + ] + ); filesystem .pwrite("/zeroed", vec![b'b'; 512], 512) .expect("convert one unwritten sector to data"); @@ -937,3 +965,61 @@ fn xattrs_follow_inode_identity_and_survive_snapshots() { "ENODATA", ); } + +#[test] +fn xattr_value_and_name_list_limits_accept_boundary_and_rollback_plus_one() { + let mut filesystem = MemoryFileSystem::new(); + filesystem.write_file("/value", b"data").unwrap(); + let exact_value = vec![b'x'; 64 * 1024]; + filesystem + .set_xattr( + "/value", + "user.limit", + exact_value.clone(), + XATTR_CREATE, + true, + ) + .expect("64 KiB xattr value is Linux-valid"); + assert_error_code( + filesystem.set_xattr( + "/value", + "user.limit", + vec![b'y'; 64 * 1024 + 1], + XATTR_REPLACE, + true, + ), + "E2BIG", + ); + assert_eq!( + filesystem + .get_xattr("/value", "user.limit", true) + .expect("rejected replacement preserves old value"), + exact_value + ); + + filesystem.write_file("/list", b"data").unwrap(); + for index in 0..256 { + let name = format!("user.{index:04}.{}", "n".repeat(245)); + assert_eq!(name.len(), 255); + filesystem + .set_xattr("/list", &name, Vec::new(), XATTR_CREATE, true) + .expect("name list entry within 64 KiB boundary"); + } + let names = filesystem + .list_xattrs("/list", true) + .expect("exact 64 KiB xattr name list"); + assert_eq!( + names.iter().map(|name| name.len() + 1).sum::(), + 64 * 1024 + ); + + assert_error_code( + filesystem.set_xattr("/list", "user.overflow", Vec::new(), XATTR_CREATE, true), + "ENOSPC", + ); + assert_eq!( + filesystem.list_xattrs("/list", true).unwrap(), + names, + "name-list overflow must not partially insert the new attribute" + ); +} diff --git a/crates/vm-config/src/lib.rs b/crates/vm-config/src/lib.rs index 1599d58628..4052e23893 100644 --- a/crates/vm-config/src/lib.rs +++ b/crates/vm-config/src/lib.rs @@ -169,6 +169,8 @@ pub struct VmUserConfig { pub group_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + /// Initial supplementary process credentials. An explicit group record is + /// authoritative and is not given extra members from this list. pub supplementary_gids: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -191,6 +193,8 @@ pub struct VmUserAccountConfig { #[ts(optional)] pub gecos: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] + /// Initial process credentials only. These gids do not add the account to + /// an explicit `/etc/group` record's member list. pub supplementary_gids: Vec, } @@ -201,9 +205,17 @@ pub struct VmGroupConfig { pub gid: u32, pub name: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] + /// Authoritative `/etc/group` membership. Process supplementary gids are + /// intentionally not merged into this list. pub members: Vec, } +// The libc account ABI uses a 4096-byte text buffer and reserves one byte for +// the terminating NUL. Keep configuration-derived records representable by +// every executor adapter before they reach the kernel account database. +const MAX_ACCOUNT_RECORD_BYTES: usize = 4095; +const MAX_GROUP_MEMBERS: usize = 256; + impl VmUserConfig { fn validate(&self) -> Result<(), VmConfigError> { const MAX_SUPPLEMENTARY_GIDS: usize = 64; @@ -218,24 +230,14 @@ impl VmUserConfig { "user.supplementaryGids exceeds limit of {MAX_SUPPLEMENTARY_GIDS}" ))); } - for (label, value) in [ - ("user.username", self.username.as_deref()), - ("user.groupName", self.group_name.as_deref()), - ] { - if value.is_some_and(|value| { - value.is_empty() - || value.contains([':', '\n', '\r', '\0']) - || value.chars().any(char::is_whitespace) - }) { - return Err(VmConfigError::new(format!("{label} is invalid"))); - } + if let Some(username) = self.username.as_deref() { + validate_account_name("user.username", username)?; } - if self - .gecos - .as_deref() - .is_some_and(|value| value.contains([':', '\n', '\r', '\0'])) - { - return Err(VmConfigError::new("user.gecos is invalid")); + if let Some(group_name) = self.group_name.as_deref() { + validate_account_name("user.groupName", group_name)?; + } + if let Some(gecos) = self.gecos.as_deref() { + validate_account_record_field("user.gecos", gecos)?; } let accounts = self.accounts.as_deref().unwrap_or_default(); let groups = self.groups.as_deref().unwrap_or_default(); @@ -253,14 +255,10 @@ impl VmUserConfig { let mut account_names = std::collections::BTreeSet::new(); for account in accounts { validate_account_name("user.accounts[].username", &account.username)?; - validate_guest_path("user.accounts[].homedir", &account.homedir)?; - validate_guest_path("user.accounts[].shell", &account.shell)?; - if account - .gecos - .as_deref() - .is_some_and(|value| value.contains([':', '\n', '\r', '\0'])) - { - return Err(VmConfigError::new("user.accounts[].gecos is invalid")); + validate_account_path("user.accounts[].homedir", &account.homedir)?; + validate_account_path("user.accounts[].shell", &account.shell)?; + if let Some(gecos) = account.gecos.as_deref() { + validate_account_record_field("user.accounts[].gecos", gecos)?; } if account.supplementary_gids.len() > MAX_SUPPLEMENTARY_GIDS { return Err(VmConfigError::new(format!( @@ -279,11 +277,25 @@ impl VmUserConfig { account.username ))); } + validate_passwd_record( + "user.accounts[]", + &account.username, + account.uid, + account.gid, + account.gecos.as_deref().unwrap_or_default(), + &account.homedir, + &account.shell, + )?; } let mut group_gids = std::collections::BTreeSet::new(); let mut group_names = std::collections::BTreeSet::new(); for group in groups { validate_account_name("user.groups[].name", &group.name)?; + if group.members.len() > MAX_GROUP_MEMBERS { + return Err(VmConfigError::new(format!( + "user.groups[].members exceeds limit of {MAX_GROUP_MEMBERS}" + ))); + } for member in &group.members { validate_account_name("user.groups[].members[]", member)?; } @@ -299,24 +311,184 @@ impl VmUserConfig { group.name ))); } + validate_group_record("user.groups[]", &group.name, group.gid, &group.members)?; } - if let Some(homedir) = self.homedir.as_deref() { - validate_guest_path("user.homedir", homedir)?; - } - if let Some(shell) = self.shell.as_deref() { - validate_guest_path("user.shell", shell)?; - } + + let username = self.username.as_deref().unwrap_or("agentos"); + let homedir = self.homedir.as_deref().unwrap_or("/home/agentos"); + let shell = self.shell.as_deref().unwrap_or("/bin/sh"); + let gecos = self.gecos.as_deref().unwrap_or_default(); + validate_account_name("user.username", username)?; + validate_account_path("user.homedir", homedir)?; + validate_account_path("user.shell", shell)?; + validate_passwd_record( + "user", + username, + self.uid.unwrap_or(1000), + self.gid.unwrap_or(1000), + gecos, + homedir, + shell, + )?; + validate_materialized_groups(self, accounts, groups, username)?; Ok(()) } } fn validate_account_name(label: &str, value: &str) -> Result<(), VmConfigError> { if value.is_empty() - || value.contains([':', '\n', '\r', '\0']) + || value.contains([':', ',', '\n', '\r', '\0']) || value.chars().any(char::is_whitespace) { return Err(VmConfigError::new(format!("{label} is invalid"))); } + validate_account_text_bound(label, value) +} + +fn validate_account_record_field(label: &str, value: &str) -> Result<(), VmConfigError> { + if value.contains([':', '\n', '\r', '\0']) { + return Err(VmConfigError::new(format!("{label} is invalid"))); + } + validate_account_text_bound(label, value) +} + +fn validate_account_path(label: &str, value: &str) -> Result<(), VmConfigError> { + validate_guest_path(label, value)?; + validate_account_record_field(label, value) +} + +fn validate_account_text_bound(label: &str, value: &str) -> Result<(), VmConfigError> { + if value.len() > MAX_ACCOUNT_RECORD_BYTES { + return Err(VmConfigError::new(format!( + "{label} exceeds limit of {MAX_ACCOUNT_RECORD_BYTES} UTF-8 bytes" + ))); + } + Ok(()) +} + +fn validate_passwd_record( + label: &str, + username: &str, + uid: u32, + gid: u32, + gecos: &str, + homedir: &str, + shell: &str, +) -> Result<(), VmConfigError> { + let field_bytes = [ + username.len(), + uid.to_string().len(), + gid.to_string().len(), + gecos.len(), + homedir.len(), + shell.len(), + ]; + validate_account_record_size(label, field_bytes.into_iter(), 7) +} + +fn validate_group_record( + label: &str, + name: &str, + gid: u32, + members: &[String], +) -> Result<(), VmConfigError> { + if members.len() > MAX_GROUP_MEMBERS { + return Err(VmConfigError::new(format!( + "{label}.members exceeds limit of {MAX_GROUP_MEMBERS}" + ))); + } + let member_separators = members.len().saturating_sub(1); + validate_account_record_size( + label, + std::iter::once(name.len()) + .chain(std::iter::once(gid.to_string().len())) + .chain(members.iter().map(|member| member.len())), + 4 + member_separators, + ) +} + +fn validate_account_record_size( + label: &str, + mut field_bytes: impl Iterator, + syntax_bytes: usize, +) -> Result<(), VmConfigError> { + let record_bytes = field_bytes.try_fold(syntax_bytes, usize::checked_add); + if !record_bytes.is_some_and(|bytes| bytes <= MAX_ACCOUNT_RECORD_BYTES) { + return Err(VmConfigError::new(format!( + "{label} rendered account record exceeds {MAX_ACCOUNT_RECORD_BYTES} bytes (the 4096-byte ABI buffer includes its terminating NUL)" + ))); + } + Ok(()) +} + +fn validate_materialized_groups( + config: &VmUserConfig, + accounts: &[VmUserAccountConfig], + groups: &[VmGroupConfig], + primary_username: &str, +) -> Result<(), VmConfigError> { + let primary_gid = config.gid.unwrap_or(1000); + let primary_group_name = config.group_name.as_deref().unwrap_or(primary_username); + let mut materialized = groups + .iter() + .map(|group| (group.gid, (group.name.clone(), group.members.clone()))) + .collect::>(); + materialized.entry(primary_gid).or_insert_with(|| { + ( + primary_group_name.to_owned(), + vec![primary_username.to_owned()], + ) + }); + let authoritative_group_gids = materialized + .keys() + .copied() + .collect::>(); + + let mut effective_accounts = accounts + .iter() + .map(|account| { + ( + account.uid, + ( + account.username.as_str(), + account.gid, + account.supplementary_gids.as_slice(), + ), + ) + }) + .collect::>(); + let primary_supplementary_gids = config.supplementary_gids.as_deref().unwrap_or_default(); + effective_accounts.insert( + config.uid.unwrap_or(1000), + (primary_username, primary_gid, primary_supplementary_gids), + ); + + for (_, (username, account_gid, supplementary_gids)) in effective_accounts { + for group_gid in std::iter::once(account_gid).chain(supplementary_gids.iter().copied()) { + // Credentials and the account database are separate Linux state: + // an explicit group record is authoritative and is never mutated + // merely because a process carries its gid. + if authoritative_group_gids.contains(&group_gid) { + continue; + } + let (_, members) = materialized + .entry(group_gid) + .or_insert_with(|| (format!("group{group_gid}"), Vec::new())); + if !members.iter().any(|member| member == username) { + members.push(username.to_owned()); + } + } + } + + let mut gids_by_name = BTreeMap::<&str, u32>::new(); + for (gid, (name, members)) in &materialized { + if let Some(previous_gid) = gids_by_name.insert(name, *gid) { + return Err(VmConfigError::new(format!( + "materialized user group name {name:?} maps to both gid {previous_gid} and gid {gid}; synthesized group names must not collide" + ))); + } + validate_group_record("materialized user group", name, *gid, members)?; + } Ok(()) } @@ -1123,7 +1295,6 @@ limits_struct!(ResourceLimitsConfig { max_readdir_entries, max_recursive_fs_depth, max_recursive_fs_entries, - max_wasm_fuel, max_wasm_memory_bytes, max_wasm_stack_bytes, }); @@ -1241,7 +1412,9 @@ limits_struct!(WasmLimitsConfig { sync_read_limit_bytes, prewarm_timeout_ms, runner_heap_limit_mb, - runner_cpu_time_limit_ms, + active_cpu_time_limit_ms, + wall_clock_limit_ms, + deterministic_fuel, }); limits_struct!(ProcessLimitsConfig { @@ -1250,6 +1423,8 @@ limits_struct!(ProcessLimitsConfig { pending_stdin_bytes, pending_event_count, pending_event_bytes, + max_pending_child_sync_count, + max_pending_child_sync_bytes, }); #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)] @@ -1436,6 +1611,99 @@ mod tests { assert!(invalid_name.validate(usize::MAX).is_err()); } + #[test] + fn user_config_rejects_materialized_group_name_collisions() { + let config = CreateVmConfig { + user: Some(VmUserConfig { + uid: Some(0), + gid: Some(0), + username: Some(String::from("root")), + supplementary_gids: Some(vec![44]), + groups: Some(vec![VmGroupConfig { + gid: 99, + name: String::from("group44"), + members: Vec::new(), + }]), + ..VmUserConfig::default() + }), + ..CreateVmConfig::default() + }; + + let error = config + .validate(usize::MAX) + .expect_err("synthesized group name collision must fail"); + assert!(error + .to_string() + .contains("synthesized group names must not collide")); + } + + #[test] + fn user_config_bounds_rendered_account_records_and_group_members() { + let valid_maximum = CreateVmConfig { + user: Some(VmUserConfig { + uid: Some(0), + gid: Some(0), + username: Some(String::from("u")), + homedir: Some(String::from("/")), + shell: Some(String::from("/")), + // `u:x:0:0::/:/` is exactly 4095 bytes. + gecos: Some("x".repeat(4083)), + groups: Some(vec![VmGroupConfig { + gid: 7, + name: String::from("g"), + members: vec![String::from("m"); MAX_GROUP_MEMBERS], + }]), + ..VmUserConfig::default() + }), + ..CreateVmConfig::default() + }; + valid_maximum + .validate(usize::MAX) + .expect("4095-byte record and 256 group members must fit"); + + let oversized_passwd = CreateVmConfig { + user: Some(VmUserConfig { + uid: Some(0), + gid: Some(0), + username: Some(String::from("u")), + homedir: Some(String::from("/")), + shell: Some(String::from("/")), + gecos: Some("x".repeat(4084)), + ..VmUserConfig::default() + }), + ..CreateVmConfig::default() + }; + assert!(oversized_passwd.validate(usize::MAX).is_err()); + + let oversized_group_record = CreateVmConfig { + user: Some(VmUserConfig { + groups: Some(vec![VmGroupConfig { + gid: 7, + name: String::from("g"), + members: vec!["a".repeat(2045), "b".repeat(2045)], + }]), + ..VmUserConfig::default() + }), + ..CreateVmConfig::default() + }; + assert!(oversized_group_record.validate(usize::MAX).is_err()); + + let too_many_members = CreateVmConfig { + user: Some(VmUserConfig { + groups: Some(vec![VmGroupConfig { + gid: 7, + name: String::from("g"), + members: (0..=MAX_GROUP_MEMBERS) + .map(|index| format!("m{index}")) + .collect(), + }]), + ..VmUserConfig::default() + }), + ..CreateVmConfig::default() + }; + assert!(too_many_members.validate(usize::MAX).is_err()); + } + #[test] fn validate_rejects_fetch_limit_above_frame_cap() { let config = CreateVmConfig { @@ -1580,6 +1848,46 @@ mod tests { } } + #[test] + fn wasm_cpu_fields_round_trip_without_legacy_aliases() { + let config: CreateVmConfig = serde_json::from_value(serde_json::json!({ + "limits": { + "wasm": { + "activeCpuTimeLimitMs": 30_000, + "wallClockLimitMs": 45_000, + "deterministicFuel": 1_000_000 + } + } + })) + .expect("decode WASM CPU fields"); + let wasm = config + .limits + .as_ref() + .and_then(|limits| limits.wasm.as_ref()) + .expect("WASM limits"); + assert_eq!(wasm.active_cpu_time_limit_ms, Some(30_000)); + assert_eq!(wasm.wall_clock_limit_ms, Some(45_000)); + assert_eq!(wasm.deterministic_fuel, Some(1_000_000)); + + let json = serde_json::to_string(&config).expect("serialize WASM CPU fields"); + assert!(json.contains("activeCpuTimeLimitMs")); + assert!(json.contains("wallClockLimitMs")); + assert!(json.contains("deterministicFuel")); + + let removed_fuel_name = ["maxWasm", "Fuel"].concat(); + let removed_runner_cpu_name = ["runnerCpu", "TimeLimitMs"].concat(); + for legacy_limits in [ + serde_json::json!({ "resources": { (removed_fuel_name): 1 } }), + serde_json::json!({ "wasm": { (removed_runner_cpu_name): 1 } }), + ] { + let error = serde_json::from_value::(serde_json::json!({ + "limits": legacy_limits + })) + .expect_err("removed WASM CPU field must be rejected"); + assert!(error.to_string().contains("unknown field")); + } + } + fn js_runtime_config(value: serde_json::Value) -> Result { serde_json::from_value(serde_json::json!({ "jsRuntime": value })) } diff --git a/crates/wasm-abi-generator/Cargo.toml b/crates/wasm-abi-generator/Cargo.toml new file mode 100644 index 0000000000..035cd97cb2 --- /dev/null +++ b/crates/wasm-abi-generator/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "agentos-wasm-abi-generator" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +description = "Pinned WITX to AgentOS WASM ABI manifest generator" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +witx = "=0.9.1" +wat = "1" diff --git a/crates/wasm-abi-generator/src/lib.rs b/crates/wasm-abi-generator/src/lib.rs new file mode 100644 index 0000000000..a116164ab4 --- /dev/null +++ b/crates/wasm-abi-generator/src/lib.rs @@ -0,0 +1,1045 @@ +//! Generated raw-WASM proof fixtures for the checked-in AgentOS host ABI. +//! +//! The fixture builder consumes the same manifest as linker generation and +//! import auditing. Tests therefore cannot silently keep compiling an old, +//! hand-written signature after the owned ABI changes. + +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; + +const PERMISSION_TIERS: [(&str, u8); 4] = [ + ("isolated", 1 << 0), + ("read-only", 1 << 1), + ("read-write", 1 << 2), + ("full", 1 << 3), +]; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoreSignature { + pub id: String, + pub params: Vec, + pub results: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbiBindingSemantics { + pub handler: String, + pub decode: String, + pub encode: String, + pub return_kind: String, + pub execution_class: String, + pub restartability: String, + pub transactional: bool, + pub prevalidate_outputs: bool, + pub permission_tiers: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbiBindingMetadata { + pub id: String, + pub status: String, + pub core_signature: String, + #[serde(flatten)] + pub semantics: AbiBindingSemantics, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct AbiImport { + pub module: String, + pub name: String, + pub params: Vec, + pub results: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbiManifest { + #[serde(default)] + pub schema_version: u32, + #[serde(default)] + pub abi_version: String, + pub module_aliases: BTreeMap, + pub module_policy: BTreeMap>, + pub import_policy_overrides: BTreeMap>, + #[serde(default)] + pub core_signatures: Vec, + #[serde(default)] + pub bindings: BTreeMap, + pub imports: Vec, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ManifestCounts { + pub imports: usize, + pub signatures: usize, + pub alias_bindings: usize, + pub isolated_bindings: usize, + pub read_only_bindings: usize, + pub read_write_bindings: usize, + pub full_bindings: usize, + pub isolated_bindings_with_aliases: usize, + pub read_only_bindings_with_aliases: usize, + pub read_write_bindings_with_aliases: usize, + pub full_bindings_with_aliases: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CallArguments { + /// Valid zero values, useful for terminal imports and narrow smoke cases. + Zero, + /// Invalid fds/scalars and OOB pointers. This reaches every host function + /// without granting it a valid resource on which to perform a side effect. + Hostile, +} + +#[derive(Clone, Debug)] +pub struct RawCallAssertion { + pub module: String, + pub name: String, + /// WAT constant expressions in parameter order. + pub arguments: Vec, + pub expected_i32: i32, +} + +impl RawCallAssertion { + pub fn i32( + module: impl Into, + name: impl Into, + arguments: impl IntoIterator>, + expected_i32: i32, + ) -> Self { + Self { + module: module.into(), + name: name.into(), + arguments: arguments.into_iter().map(Into::into).collect(), + expected_i32, + } + } +} + +impl AbiManifest { + pub fn parse(json: &str) -> Self { + serde_json::from_str(json).expect("parse AgentOS WASM ABI manifest") + } + + pub fn imports_with_aliases(&self) -> Vec { + let mut imports = self.imports.clone(); + for (alias, canonical) in &self.module_aliases { + imports.extend( + self.imports + .iter() + .filter(|import| import.module == *canonical) + .cloned() + .map(|mut import| { + import.module = alias.clone(); + import + }), + ); + } + imports + .sort_by(|left, right| (&left.module, &left.name).cmp(&(&right.module, &right.name))); + imports + } + + pub fn permits(&self, import: &AbiImport, tier: &str) -> bool { + let key = format!("{}.{}", import.module, import.name); + self.import_policy_overrides + .get(&key) + .or_else(|| self.module_policy.get(&import.module)) + .is_some_and(|tiers| tiers.iter().any(|candidate| candidate == tier)) + } + + pub fn permitted_imports(&self, tier: &str) -> Vec { + self.imports_with_aliases() + .into_iter() + .filter(|import| self.permits(import, tier)) + .collect() + } + + pub fn validate_registry(&self) -> Result { + if self.schema_version != 2 { + return Err(format!( + "unsupported AgentOS WASM ABI schema version {}; expected 2", + self.schema_version + )); + } + if self.abi_version.is_empty() { + return Err(String::from("ABI version must not be empty")); + } + + let mut import_keys = BTreeSet::new(); + let mut import_ids = BTreeSet::new(); + let mut signature_ids = BTreeSet::new(); + let mut signature_shapes = BTreeSet::new(); + let mut referenced_signature_ids = BTreeSet::new(); + for signature in &self.core_signatures { + validate_rust_identifier("core signature", &signature.id)?; + if !signature_ids.insert(signature.id.as_str()) { + return Err(format!("duplicate core signature id {}", signature.id)); + } + validate_core_values(&signature.params)?; + validate_core_values(&signature.results)?; + let shape = signature_shape(&signature.params, &signature.results); + if !signature_shapes.insert(shape) { + return Err(format!( + "duplicate core signature shape for {}", + signature.id + )); + } + } + + let signatures_by_id = self + .core_signatures + .iter() + .map(|signature| (signature.id.as_str(), signature)) + .collect::>(); + let mut canonical_tier_counts = BTreeMap::from([ + ("isolated", 0usize), + ("read-only", 0usize), + ("read-write", 0usize), + ("full", 0usize), + ]); + + for import in &self.imports { + let key = format!("{}.{}", import.module, import.name); + if !import_keys.insert(key.clone()) { + return Err(format!("duplicate ABI import {key}")); + } + let metadata = self + .bindings + .get(&key) + .ok_or_else(|| format!("import {key} has no semantic binding"))?; + validate_rust_identifier("import", &metadata.id)?; + if !import_ids.insert(metadata.id.as_str()) { + return Err(format!("duplicate import id {}", metadata.id)); + } + if !matches!(metadata.status.as_str(), "canonical" | "compatibility") { + return Err(format!( + "import {key} has unsupported status {}", + metadata.status + )); + } + validate_core_values(&import.params)?; + validate_core_values(&import.results)?; + let signature = signatures_by_id + .get(metadata.core_signature.as_str()) + .ok_or_else(|| { + format!( + "import {key} references unknown core signature {}", + metadata.core_signature + ) + })?; + referenced_signature_ids.insert(metadata.core_signature.as_str()); + if signature.params != import.params || signature.results != import.results { + return Err(format!( + "import {key} core signature {} does not match its params/results", + metadata.core_signature + )); + } + + let binding = &metadata.semantics; + validate_rust_identifier("handler", &binding.handler)?; + validate_rust_identifier("decoder", &binding.decode)?; + validate_rust_identifier("encoder", &binding.encode)?; + validate_enum_value( + &key, + "return kind", + &binding.return_kind, + &["WasiErrno", "ScalarI32", "ScalarI64", "Void"], + )?; + validate_enum_value( + &key, + "execution class", + &binding.execution_class, + &["Bootstrap", "Host", "Wait", "Local", "Terminal"], + )?; + validate_enum_value( + &key, + "restartability", + &binding.restartability, + &["Never", "SignalRestartable"], + )?; + match (binding.return_kind.as_str(), import.results.as_slice()) { + ("Void", []) => {} + ("WasiErrno" | "ScalarI32", [result]) if result == "i32" => {} + ("ScalarI64", [result]) if result == "i64" => {} + _ => { + return Err(format!( + "import {key} return kind {} does not match core results {:?}", + binding.return_kind, import.results + )); + } + } + + let effective_tiers = self.effective_tiers(import)?; + if binding.permission_tiers != effective_tiers { + return Err(format!( + "import {key} permission tiers {:?} do not match policy {:?}", + binding.permission_tiers, effective_tiers + )); + } + for tier in effective_tiers { + *canonical_tier_counts + .get_mut(tier.as_str()) + .expect("validated permission tier") += 1; + } + } + if self.bindings.len() != self.imports.len() { + let extra = self + .bindings + .keys() + .filter(|key| !import_keys.contains(*key)) + .cloned() + .collect::>(); + return Err(format!( + "semantic binding count {} does not match import count {}; unmapped bindings: {extra:?}", + self.bindings.len(), + self.imports.len() + )); + } + if referenced_signature_ids != signature_ids { + let unused = signature_ids + .difference(&referenced_signature_ids) + .copied() + .collect::>(); + return Err(format!("unreferenced core signatures: {unused:?}")); + } + + for (alias, canonical) in &self.module_aliases { + if alias == canonical { + return Err(format!("module alias {alias} points to itself")); + } + let alias_policy = self + .module_policy + .get(alias) + .ok_or_else(|| format!("alias module {alias} has no permission policy"))?; + validate_permission_tiers(alias_policy)?; + if !self + .imports + .iter() + .any(|import| import.module == *canonical) + { + return Err(format!( + "module alias {alias} references empty or unknown module {canonical}" + )); + } + } + + let imports_with_aliases = self.imports_with_aliases(); + let alias_bindings = imports_with_aliases.len() - self.imports.len(); + let mut aliased_keys = BTreeSet::new(); + let mut all_tier_counts = BTreeMap::from([ + ("isolated", 0usize), + ("read-only", 0usize), + ("read-write", 0usize), + ("full", 0usize), + ]); + for import in &imports_with_aliases { + let key = format!("{}.{}", import.module, import.name); + if !aliased_keys.insert(key.clone()) { + return Err(format!("duplicate effective ABI import {key}")); + } + for (tier, count) in &mut all_tier_counts { + if self.permits(import, tier) { + *count += 1; + } + } + } + + Ok(ManifestCounts { + imports: self.imports.len(), + signatures: self.core_signatures.len(), + alias_bindings, + isolated_bindings: canonical_tier_counts["isolated"], + read_only_bindings: canonical_tier_counts["read-only"], + read_write_bindings: canonical_tier_counts["read-write"], + full_bindings: canonical_tier_counts["full"], + isolated_bindings_with_aliases: all_tier_counts["isolated"], + read_only_bindings_with_aliases: all_tier_counts["read-only"], + read_write_bindings_with_aliases: all_tier_counts["read-write"], + full_bindings_with_aliases: all_tier_counts["full"], + }) + } + + pub fn render_rust_registry(&self) -> Result { + self.validate_registry()?; + + let handler_ids = semantic_ids(&self.bindings, |binding| &binding.handler); + let decode_ids = semantic_ids(&self.bindings, |binding| &binding.decode); + let encode_ids = semantic_ids(&self.bindings, |binding| &binding.encode); + let mut output = String::new(); + writeln!( + output, + "// @generated by scripts/generate-wasm-abi-manifest.mjs; do not edit." + ) + .unwrap(); + writeln!( + output, + "// Source: crates/execution/assets/agentos-wasm-abi.json (schema {}).\n", + self.schema_version + ) + .unwrap(); + output.push_str("#![allow(clippy::too_many_lines)]\n\n"); + output.push_str( + "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum CoreValueType {\n I32,\n I64,\n}\n\n", + ); + render_enum( + &mut output, + "CoreSignatureId", + self.core_signatures + .iter() + .map(|signature| signature.id.as_str()), + true, + ); + render_enum( + &mut output, + "ImportId", + self.imports.iter().map(|import| { + self.binding_metadata(import) + .expect("validated binding") + .id + .as_str() + }), + true, + ); + render_enum( + &mut output, + "HandlerId", + handler_ids.iter().map(String::as_str), + false, + ); + render_enum( + &mut output, + "DecodeId", + decode_ids.iter().map(String::as_str), + false, + ); + render_enum( + &mut output, + "EncodeId", + encode_ids.iter().map(String::as_str), + false, + ); + output.push_str( + "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum ImportStatus {\n Canonical,\n Compatibility,\n}\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum ReturnKind {\n WasiErrno,\n ScalarI32,\n ScalarI64,\n Void,\n}\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum ExecutionClass {\n Bootstrap,\n Host,\n Wait,\n Local,\n Terminal,\n}\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum Restartability {\n Never,\n SignalRestartable,\n}\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub enum PermissionTier {\n Isolated,\n ReadOnly,\n ReadWrite,\n Full,\n}\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n\ + pub struct PermissionTiers(u8);\n\n\ + impl PermissionTiers {\n\ + pub const fn from_bits(bits: u8) -> Self {\n Self(bits)\n }\n\n\ + pub const fn bits(self) -> u8 {\n self.0\n }\n\n\ + pub const fn contains(self, tier: PermissionTier) -> bool {\n\ + let bit = match tier {\n\ + PermissionTier::Isolated => 1 << 0,\n\ + PermissionTier::ReadOnly => 1 << 1,\n\ + PermissionTier::ReadWrite => 1 << 2,\n\ + PermissionTier::Full => 1 << 3,\n\ + };\n\ + self.0 & bit != 0\n\ + }\n\ + }\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n\ + pub struct CoreSignature {\n\ + pub id: CoreSignatureId,\n\ + pub params: &'static [CoreValueType],\n\ + pub results: &'static [CoreValueType],\n\ + }\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n\ + pub struct AbiBinding {\n\ + pub id: ImportId,\n\ + pub module: &'static str,\n\ + pub name: &'static str,\n\ + pub signature: CoreSignatureId,\n\ + pub status: ImportStatus,\n\ + pub handler: HandlerId,\n\ + pub decode: DecodeId,\n\ + pub encode: EncodeId,\n\ + pub return_kind: ReturnKind,\n\ + pub execution_class: ExecutionClass,\n\ + pub restartability: Restartability,\n\ + /// Submit the semantic action as one shared host operation; do not decompose it into adapter-side check/mutate steps.\n\ + pub transactional: bool,\n\ + /// Validate every guest output range before submitting the host operation.\n\ + pub prevalidate_outputs: bool,\n\ + pub permission_tiers: PermissionTiers,\n\ + }\n\n\ + #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n\ + pub struct AliasBinding {\n\ + pub alias_module: &'static str,\n\ + pub canonical_module: &'static str,\n\ + pub import: ImportId,\n\ + pub permission_tiers: PermissionTiers,\n\ + }\n\n", + ); + + writeln!( + output, + "pub const ABI_SCHEMA_VERSION: u32 = {};", + self.schema_version + ) + .unwrap(); + writeln!( + output, + "pub const ABI_VERSION: &str = {:?};\n", + self.abi_version + ) + .unwrap(); + + output.push_str("pub const CORE_SIGNATURES: &[CoreSignature] = &[\n"); + for signature in &self.core_signatures { + writeln!(output, " CoreSignature {{").unwrap(); + writeln!(output, " id: CoreSignatureId::{},", signature.id).unwrap(); + render_value_type_slice(&mut output, "params", &signature.params); + render_value_type_slice(&mut output, "results", &signature.results); + writeln!(output, " }},").unwrap(); + } + output.push_str("];\n\n"); + + output.push_str("pub const ABI_BINDINGS: &[AbiBinding] = &[\n"); + for import in &self.imports { + let metadata = self.binding_metadata(import).expect("validated binding"); + let binding = &metadata.semantics; + let status = if metadata.status == "canonical" { + "Canonical" + } else { + "Compatibility" + }; + writeln!(output, " AbiBinding {{").unwrap(); + writeln!(output, " id: ImportId::{},", metadata.id).unwrap(); + writeln!(output, " module: {:?},", import.module).unwrap(); + writeln!(output, " name: {:?},", import.name).unwrap(); + writeln!( + output, + " signature: CoreSignatureId::{},", + metadata.core_signature + ) + .unwrap(); + writeln!(output, " status: ImportStatus::{status},").unwrap(); + writeln!(output, " handler: HandlerId::{},", binding.handler).unwrap(); + writeln!(output, " decode: DecodeId::{},", binding.decode).unwrap(); + writeln!(output, " encode: EncodeId::{},", binding.encode).unwrap(); + writeln!( + output, + " return_kind: ReturnKind::{},", + binding.return_kind + ) + .unwrap(); + writeln!( + output, + " execution_class: ExecutionClass::{},", + binding.execution_class + ) + .unwrap(); + writeln!( + output, + " restartability: Restartability::{},", + binding.restartability + ) + .unwrap(); + writeln!(output, " transactional: {},", binding.transactional).unwrap(); + writeln!( + output, + " prevalidate_outputs: {},", + binding.prevalidate_outputs + ) + .unwrap(); + writeln!( + output, + " permission_tiers: PermissionTiers::from_bits({}),", + permission_bits(&binding.permission_tiers)? + ) + .unwrap(); + writeln!(output, " }},").unwrap(); + } + output.push_str("];\n\n"); + + output.push_str("pub const ALIAS_BINDINGS: &[AliasBinding] = &[\n"); + for (alias, canonical) in &self.module_aliases { + let alias_tiers = self + .module_policy + .get(alias) + .ok_or_else(|| format!("alias module {alias} has no permission policy"))?; + for import in self + .imports + .iter() + .filter(|import| import.module == *canonical) + { + writeln!(output, " AliasBinding {{").unwrap(); + writeln!(output, " alias_module: {alias:?},").unwrap(); + writeln!(output, " canonical_module: {canonical:?},").unwrap(); + let metadata = self.binding_metadata(import).expect("validated binding"); + writeln!(output, " import: ImportId::{},", metadata.id).unwrap(); + writeln!( + output, + " permission_tiers: PermissionTiers::from_bits({}),", + permission_bits(alias_tiers)? + ) + .unwrap(); + writeln!(output, " }},").unwrap(); + } + } + output.push_str("];\n\n"); + + output.push_str( + "pub fn binding(id: ImportId) -> &'static AbiBinding {\n\ + &ABI_BINDINGS[id as usize]\n\ + }\n\n\ + pub fn core_signature(id: CoreSignatureId) -> &'static CoreSignature {\n\ + &CORE_SIGNATURES[id as usize]\n\ + }\n\n\ + pub fn find_binding(module: &str, name: &str) -> Option<&'static AbiBinding> {\n\ + if let Some(binding) = ABI_BINDINGS\n\ + .iter()\n\ + .find(|binding| binding.module == module && binding.name == name)\n\ + {\n\ + return Some(binding);\n\ + }\n\ + let alias = ALIAS_BINDINGS\n\ + .iter()\n\ + .find(|alias| alias.alias_module == module && binding(alias.import).name == name)?;\n\ + Some(binding(alias.import))\n\ + }\n", + ); + Ok(output) + } + + fn effective_tiers(&self, import: &AbiImport) -> Result, String> { + let key = format!("{}.{}", import.module, import.name); + let tiers = self + .import_policy_overrides + .get(&key) + .or_else(|| self.module_policy.get(&import.module)) + .ok_or_else(|| format!("import {key} has no permission policy"))?; + validate_permission_tiers(tiers)?; + Ok(tiers.clone()) + } + + fn binding_metadata(&self, import: &AbiImport) -> Result<&AbiBindingMetadata, String> { + let key = format!("{}.{}", import.module, import.name); + self.bindings + .get(&key) + .ok_or_else(|| format!("import {key} has no semantic binding")) + } +} + +fn validate_core_values(values: &[String]) -> Result<(), String> { + for value in values { + if !matches!(value.as_str(), "i32" | "i64") { + return Err(format!("unsupported core ABI value type {value}")); + } + } + Ok(()) +} + +fn validate_enum_value( + import: &str, + field: &str, + value: &str, + allowed: &[&str], +) -> Result<(), String> { + if allowed.contains(&value) { + Ok(()) + } else { + Err(format!("import {import} has unsupported {field} {value}")) + } +} + +fn validate_rust_identifier(kind: &str, value: &str) -> Result<(), String> { + let mut chars = value.chars(); + if !chars + .next() + .is_some_and(|character| character.is_ascii_uppercase()) + || !chars.all(|character| character.is_ascii_alphanumeric()) + { + return Err(format!( + "{kind} id {value:?} is not a PascalCase Rust identifier" + )); + } + Ok(()) +} + +fn validate_permission_tiers(tiers: &[String]) -> Result<(), String> { + let mut seen = BTreeSet::new(); + for tier in tiers { + if !PERMISSION_TIERS + .iter() + .any(|(candidate, _)| tier == candidate) + { + return Err(format!("unknown ABI permission tier {tier}")); + } + if !seen.insert(tier) { + return Err(format!("duplicate ABI permission tier {tier}")); + } + } + if tiers.is_empty() { + return Err(String::from("ABI permission tier list must not be empty")); + } + Ok(()) +} + +fn permission_bits(tiers: &[String]) -> Result { + validate_permission_tiers(tiers)?; + Ok(PERMISSION_TIERS + .iter() + .filter(|(tier, _)| tiers.iter().any(|candidate| candidate == tier)) + .fold(0, |bits, (_, bit)| bits | bit)) +} + +fn signature_shape(params: &[String], results: &[String]) -> String { + format!("{}->{}", params.join(","), results.join(",")) +} + +fn semantic_ids( + bindings: &BTreeMap, + id: impl Fn(&AbiBindingSemantics) -> &String, +) -> Vec { + let mut values = BTreeSet::new(); + for binding in bindings.values() { + values.insert(id(&binding.semantics).clone()); + } + values.into_iter().collect() +} + +fn render_enum<'a>( + output: &mut String, + name: &str, + variants: impl IntoIterator, + repr: bool, +) { + output.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n"); + if repr { + output.push_str("#[repr(u16)]\n"); + } + writeln!(output, "pub enum {name} {{").unwrap(); + for variant in variants { + writeln!(output, " {variant},").unwrap(); + } + output.push_str("}\n\n"); +} + +fn render_value_type_slice(output: &mut String, field: &str, values: &[String]) { + if values.is_empty() { + writeln!(output, " {field}: &[],").unwrap(); + return; + } + writeln!(output, " {field}: &[").unwrap(); + for value in values { + let value = match value.as_str() { + "i32" => "I32", + "i64" => "I64", + _ => unreachable!("validated core value type"), + }; + writeln!(output, " CoreValueType::{value},").unwrap(); + } + writeln!(output, " ],").unwrap(); +} + +fn typed_argument(value_type: &str, arguments: CallArguments) -> &'static str { + match (value_type, arguments) { + ("i32", CallArguments::Zero) => "(i32.const 0)", + ("i64", CallArguments::Zero) => "(i64.const 0)", + ("i32", CallArguments::Hostile) => "(i32.const -1)", + ("i64", CallArguments::Hostile) => "(i64.const -1)", + (other, _) => panic!("unsupported ABI value type {other}"), + } +} + +fn import_arguments(import: &AbiImport, arguments: CallArguments) -> String { + // These compatibility setters are the only one-scalar calls where -1 is + // a valid, potentially long-lived request rather than an invalid fd/id. + let arguments = if matches!( + (import.module.as_str(), import.name.as_str()), + ("host_fs", "set_open_mode" | "set_open_direct") + | ("host_process", "sleep_ms") + | ("host_tty", "set_raw_mode") + | ("wasi_snapshot_preview1" | "wasi_unstable", "proc_exit") + ) { + CallArguments::Zero + } else { + arguments + }; + import + .params + .iter() + .map(|param| typed_argument(param, arguments)) + .collect::>() + .join(" ") +} + +fn declare_import(wat: &mut String, import: &AbiImport, local_name: &str) { + let params = import.params.join(" "); + let results = import.results.join(" "); + wat.push_str(&format!( + " (import \"{}\" \"{}\" (func ${local_name}", + import.module, import.name + )); + if !params.is_empty() { + wat.push_str(&format!(" (param {params})")); + } + if !results.is_empty() { + wat.push_str(&format!(" (result {results})")); + } + wat.push_str("))\n"); +} + +/// Build one module which declares every import and optionally invokes all +/// non-terminal imports once. `proc_exit` is omitted from the combined caller +/// so it cannot hide later calls; use [`single_import_module`] for its proof. +pub fn imports_module(imports: &[AbiImport], invoke: bool, arguments: CallArguments) -> Vec { + let mut wat = String::from("(module\n"); + for (index, import) in imports.iter().enumerate() { + declare_import(&mut wat, import, &format!("abi_{index}")); + } + wat.push_str(" (memory (export \"memory\") 1)\n"); + wat.push_str(" (func (export \"_start\")\n"); + if invoke { + for (index, import) in imports.iter().enumerate() { + if matches!( + (import.module.as_str(), import.name.as_str()), + ("wasi_snapshot_preview1" | "wasi_unstable", "proc_exit") + ) { + continue; + } + let args = import_arguments(import, arguments); + if import.results.is_empty() { + wat.push_str(&format!(" (call $abi_{index} {args})\n")); + } else { + wat.push_str(&format!(" (drop (call $abi_{index} {args}))\n")); + } + } + } + wat.push_str(" )\n)\n"); + wat::parse_str(&wat) + .unwrap_or_else(|error| panic!("compile generated ABI caller: {error}\n{wat}")) +} + +pub fn single_import_module(import: &AbiImport, invoke: bool, arguments: CallArguments) -> Vec { + let mut wat = String::from("(module\n"); + declare_import(&mut wat, import, "target"); + wat.push_str(" (memory (export \"memory\") 1)\n (func (export \"_start\")\n"); + if invoke { + let args = import_arguments(import, arguments); + if import.results.is_empty() { + wat.push_str(&format!(" (call $target {args})\n")); + } else { + wat.push_str(&format!(" (drop (call $target {args}))\n")); + } + } + wat.push_str(" )\n)\n"); + wat::parse_str(&wat).expect("compile generated single-import ABI fixture") +} + +/// Build a direct-WAT hostile-memory fixture from named manifest imports. +/// Each assertion is type-checked against the manifest signature and traps if +/// the import does not return the expected stable errno. +pub fn raw_call_assertion_module( + manifest: &AbiManifest, + assertions: &[RawCallAssertion], + setup_wat: &str, + postconditions_wat: &str, +) -> Vec { + let imports = manifest.imports_with_aliases(); + let resolved = assertions + .iter() + .map(|assertion| { + let import = imports + .iter() + .find(|import| import.module == assertion.module && import.name == assertion.name) + .unwrap_or_else(|| { + panic!( + "raw assertion references undeclared import {}.{}", + assertion.module, assertion.name + ) + }); + assert_eq!( + import.params.len(), + assertion.arguments.len(), + "raw assertion argument count for {}.{}", + assertion.module, + assertion.name + ); + assert_eq!( + import.results.as_slice(), + ["i32"], + "raw assertion requires one i32 result for {}.{}", + assertion.module, + assertion.name + ); + import + }) + .collect::>(); + + let mut wat = String::from("(module\n"); + for (index, import) in resolved.iter().enumerate() { + declare_import(&mut wat, import, &format!("assert_{index}")); + } + let failure_exit = imports + .iter() + .find(|import| import.module == "wasi_snapshot_preview1" && import.name == "proc_exit") + .expect("raw assertion fixture requires Preview1 proc_exit"); + declare_import(&mut wat, failure_exit, "assert_fail"); + wat.push_str(" (memory (export \"memory\") 1)\n (func (export \"_start\")\n"); + wat.push_str(setup_wat); + for (index, assertion) in assertions.iter().enumerate() { + let arguments = assertion.arguments.join(" "); + wat.push_str(&format!( + " (if (i32.ne (call $assert_{index} {arguments}) (i32.const {})) (then (call $assert_fail (i32.const {})) unreachable))\n", + assertion.expected_i32, + index + 1, + )); + } + wat.push_str(postconditions_wat); + wat.push_str(" )\n)\n"); + wat::parse_str(&wat).unwrap_or_else(|error| { + panic!("compile generated raw-call assertion module: {error}\n{wat}") + }) +} + +#[cfg(test)] +mod tests { + use super::{ + imports_module, raw_call_assertion_module, AbiManifest, CallArguments, RawCallAssertion, + }; + + const CHECKED_MANIFEST: &str = include_str!("../../execution/assets/agentos-wasm-abi.json"); + + #[test] + fn generated_fixture_calls_each_declared_signature() { + let manifest = AbiManifest::parse( + r#"{ + "moduleAliases": {"legacy": "canonical"}, + "modulePolicy": { + "canonical": ["full"], + "legacy": ["full"], + "wasi_snapshot_preview1": ["full"] + }, + "importPolicyOverrides": {}, + "imports": [ + { + "module": "canonical", + "name": "sample", + "params": ["i32", "i64"], + "results": ["i32"], + "status": "canonical" + }, + { + "module": "wasi_snapshot_preview1", + "name": "proc_exit", + "params": ["i32"], + "results": [], + "status": "canonical" + } + ] + }"#, + ); + let fixture = imports_module( + &manifest.permitted_imports("full"), + true, + CallArguments::Hostile, + ); + assert!(fixture.starts_with(b"\0asm")); + + let assertion = raw_call_assertion_module( + &manifest, + &[RawCallAssertion::i32( + "canonical", + "sample", + ["(i32.const 0)", "(i64.const 0)"], + 0, + )], + "", + "", + ); + assert!(assertion.starts_with(b"\0asm")); + } + + #[test] + fn checked_manifest_has_complete_unique_semantic_bindings() { + let manifest = AbiManifest::parse(CHECKED_MANIFEST); + let counts = manifest.validate_registry().expect("valid ABI registry"); + assert_eq!(counts.imports, 169); + assert_eq!(counts.signatures, 29); + assert_eq!(counts.alias_bindings, 40); + assert_eq!(counts.isolated_bindings, 112); + assert_eq!(counts.read_only_bindings, 121); + assert_eq!(counts.read_write_bindings, 121); + assert_eq!(counts.full_bindings, 169); + assert_eq!(counts.isolated_bindings_with_aliases, 152); + assert_eq!(counts.read_only_bindings_with_aliases, 161); + assert_eq!(counts.read_write_bindings_with_aliases, 161); + assert_eq!(counts.full_bindings_with_aliases, 209); + } + + #[test] + fn checked_manifest_renders_every_binding_and_alias() { + let manifest = AbiManifest::parse(CHECKED_MANIFEST); + let registry = manifest + .render_rust_registry() + .expect("render checked ABI registry"); + assert_eq!(registry.matches(" AbiBinding {").count(), 169); + assert_eq!(registry.matches(" AliasBinding {").count(), 40); + assert!(registry.contains("pub enum ImportId")); + assert!(registry.contains("pub enum HandlerId")); + assert!(registry.contains("pub enum DecodeId")); + assert!(registry.contains("pub enum EncodeId")); + } + + #[test] + fn registry_validation_rejects_duplicate_and_unmapped_imports() { + let mut unmapped = AbiManifest::parse(CHECKED_MANIFEST); + unmapped.bindings.remove("host_fs.chmod"); + assert!(unmapped + .validate_registry() + .expect_err("missing binding must fail") + .contains("has no semantic binding")); + + let mut duplicate = AbiManifest::parse(CHECKED_MANIFEST); + duplicate.imports.push(duplicate.imports[0].clone()); + assert!(duplicate + .validate_registry() + .expect_err("duplicate import must fail") + .contains("duplicate ABI import")); + } + + #[test] + fn aliases_and_versions_reuse_canonical_semantic_ids() { + let manifest = AbiManifest::parse(CHECKED_MANIFEST); + let find = |module: &str, name: &str| { + manifest + .bindings + .get(&format!("{module}.{name}")) + .unwrap_or_else(|| panic!("missing {module}.{name}")) + .semantics + .clone() + }; + assert_eq!( + find("host_process", "proc_spawn").handler, + find("host_process", "proc_spawn_v4").handler + ); + assert_ne!( + find("host_process", "proc_spawn").decode, + find("host_process", "proc_spawn_v4").decode + ); + assert_eq!( + find("host_fs", "fd_chown").decode, + find("host_fs", "fchown").decode + ); + assert_eq!( + find("host_net", "net_close").handler, + find("wasi_snapshot_preview1", "fd_close").handler + ); + } +} diff --git a/crates/wasm-abi-generator/src/main.rs b/crates/wasm-abi-generator/src/main.rs new file mode 100644 index 0000000000..6e226d650d --- /dev/null +++ b/crates/wasm-abi-generator/src/main.rs @@ -0,0 +1,184 @@ +use agentos_wasm_abi_generator::AbiManifest; +use serde::Serialize; +use std::collections::BTreeMap; +use std::env; +use std::io::Read as _; +use std::path::PathBuf; +use witx::{Id, Layout, Type, WasmType}; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct LoweredPreview1 { + module: String, + imports: Vec, + layouts: BTreeMap, +} + +#[derive(Serialize)] +struct LoweredImport { + name: String, + params: Vec<&'static str>, + results: Vec<&'static str>, +} + +#[derive(Serialize)] +struct MemoryLayout { + size: usize, + align: usize, + fields: BTreeMap, +} + +fn wasm_type(value: WasmType) -> &'static str { + match value { + WasmType::I32 => "i32", + WasmType::I64 => "i64", + WasmType::F32 => "f32", + WasmType::F64 => "f64", + } +} + +fn record_layout(document: &witx::Document, name: &str) -> Result { + let named = document + .typename(&Id::new(name)) + .ok_or_else(|| format!("pinned WITX has no type {name}"))?; + let size_align = named.mem_size_align(); + let Type::Record(record) = named.type_().as_ref() else { + return Err(format!("pinned WITX type {name} is not a record")); + }; + let fields = record + .member_layout() + .into_iter() + .map(|member| (member.member.name.as_str().to_owned(), member.offset)) + .collect(); + Ok(MemoryLayout { + size: size_align.size, + align: size_align.align, + fields, + }) +} + +fn prestat_layout(document: &witx::Document) -> Result { + let named = document + .typename(&Id::new("prestat")) + .ok_or_else(|| String::from("pinned WITX has no type prestat"))?; + let size_align = named.mem_size_align(); + let Type::Variant(variant) = named.type_().as_ref() else { + return Err(String::from("pinned WITX prestat is not a variant")); + }; + let payload_offset = variant.payload_offset(); + let dir = variant + .cases + .iter() + .find(|case| case.name.as_str() == "dir") + .and_then(|case| case.tref.as_ref()) + .ok_or_else(|| String::from("pinned WITX prestat has no dir payload"))?; + let Type::Record(dir) = dir.type_().as_ref() else { + return Err(String::from( + "pinned WITX prestat dir payload is not a record", + )); + }; + let name_len = dir + .member_layout() + .into_iter() + .find(|member| member.member.name.as_str() == "pr_name_len") + .ok_or_else(|| String::from("pinned WITX prestat dir has no pr_name_len"))?; + Ok(MemoryLayout { + size: size_align.size, + align: size_align.align, + fields: BTreeMap::from([ + (String::from("tag"), 0), + (String::from("name_len"), payload_offset + name_len.offset), + ]), + }) +} + +fn subscription_layout(document: &witx::Document) -> Result { + let named = document + .typename(&Id::new("subscription")) + .ok_or_else(|| String::from("pinned WITX has no type subscription"))?; + let size_align = named.mem_size_align(); + let Type::Record(record) = named.type_().as_ref() else { + return Err(String::from("pinned WITX subscription is not a record")); + }; + let members = record.member_layout(); + let userdata = members + .iter() + .find(|member| member.member.name.as_str() == "userdata") + .ok_or_else(|| String::from("pinned WITX subscription has no userdata"))?; + let union = members + .iter() + .find(|member| member.member.name.as_str() == "u") + .ok_or_else(|| String::from("pinned WITX subscription has no union"))?; + let Type::Variant(variant) = union.member.tref.type_().as_ref() else { + return Err(String::from( + "pinned WITX subscription union is not a variant", + )); + }; + Ok(MemoryLayout { + size: size_align.size, + align: size_align.align, + fields: BTreeMap::from([ + (String::from("userdata"), userdata.offset), + (String::from("type"), union.offset), + ( + String::from("clock_or_fd"), + union.offset + variant.payload_offset(), + ), + ]), + }) +} + +fn main() -> Result<(), Box> { + let argument = env::args_os().nth(1); + if argument.as_deref() == Some(std::ffi::OsStr::new("--render-registry")) { + let mut json = String::new(); + std::io::stdin().read_to_string(&mut json)?; + let manifest = AbiManifest::parse(&json); + let registry = manifest + .render_rust_registry() + .map_err(|error| format!("invalid AgentOS WASM ABI manifest: {error}"))?; + print!("{registry}"); + return Ok(()); + } + + let path = argument.map(PathBuf::from).ok_or( + "usage: agentos-wasm-abi-generator | --render-registry", + )?; + let document = witx::load(&[path])?; + let module = document + .module(&Id::new("wasi_snapshot_preview1")) + .ok_or("pinned WITX has no wasi_snapshot_preview1 module")?; + let mut imports = module + .funcs() + .map(|function| { + let (params, results) = function.wasm_signature(); + LoweredImport { + name: function.name.as_str().to_owned(), + params: params.into_iter().map(wasm_type).collect(), + results: results.into_iter().map(wasm_type).collect(), + } + }) + .collect::>(); + imports.sort_by(|left, right| left.name.cmp(&right.name)); + + let mut layouts = BTreeMap::new(); + for name in ["ciovec", "iovec", "dirent", "event", "fdstat", "filestat"] { + layouts.insert(name.to_owned(), record_layout(&document, name)?); + } + layouts.insert(String::from("prestat"), prestat_layout(&document)?); + layouts.insert( + String::from("subscription"), + subscription_layout(&document)?, + ); + + serde_json::to_writer_pretty( + std::io::stdout(), + &LoweredPreview1 { + module: module.name.as_str().to_owned(), + imports, + layouts, + }, + )?; + println!(); + Ok(()) +} diff --git a/docs/design/runtime-neutral-executors.md b/docs/design/runtime-neutral-executors.md index 9728d09eff..69d42158a3 100644 --- a/docs/design/runtime-neutral-executors.md +++ b/docs/design/runtime-neutral-executors.md @@ -30,8 +30,9 @@ executors before adding Wasmtime as a second standalone-WASM backend. implementation in the kernel or its kernel-owned resource layer. Executors adapt guest ABIs to that implementation; they do not provide per-engine versions. -- The first implementation stays in the existing crates. A new shared crate is - not required unless dependency pressure proves that one is necessary. +- Host operations stay in the existing kernel, execution, and sidecar crates. + The proven kernel/runtime/VFS accounting cycle is isolated in the small + runtime-neutral `agentos-resource` leaf crate described in Section 11. This is prerequisite architecture, not Wasmtime adapter work. The Wasmtime executor specification depends on the exit gates in this document. All work in @@ -488,6 +489,11 @@ The target has one mutable source of truth: resources, not a mutable shadow copy; - a module loader may cache immutable bytes under ordinary bounded cache rules, but it cannot maintain writable filesystem state outside the kernel; and +- an executor may stage one immutable, kernel-authorized executable image into + VM-private scratch storage when its engine requires a host path. That staging + is a one-way engine input: it is generation-bound, is not exposed as guest + filesystem state, is never consulted for later path or module resolution, + and is never reconciled back into the kernel VFS; - V8 may temporarily retain an ABI fd-alias map while Node-WASI is migrated, but aliases resolve to kernel descriptors and cannot own offsets, rights, contents, or lifecycle. @@ -548,11 +554,19 @@ readiness state rather than a polling timer. loopback-only interface enumeration, or unsupported `mlock` stay in the owned sysroot and behave identically under both engines. -## 11. Proposed code organization +## 11. Code organization -No new crate is required initially: +Implementation exposed one real dependency cycle: the kernel and process +runtime both need the hierarchical reservation ledger, while VFS already +depends on the process runtime. The ledger therefore lives in the deliberately +small `agentos-resource` leaf crate. It contains no Tokio, VFS, executor, or +product dependency; kernel configuration remains the policy authority and the +runtime can only observe usage for telemetry. ```text +crates/resource/src/ + lib.rs runtime-neutral ledger, reservations, change wakes + crates/kernel/src/ process_table.rs authoritative process/signal/wait state process_runtime.rs runtime endpoint, control requests, exit reporter @@ -590,10 +604,10 @@ The exact file split can follow the implementation, but these ownership boundaries are normative. Do not put every operation in one `executor.rs`, one `host.rs`, or one mega-trait. -A later `agentos-executor-host` crate is justified only if the existing crate -graph creates a real dependency cycle or if independent fuzzing/linkage needs -it. Creating a crate merely to hold traits adds packaging and versioning cost -without improving ownership. +A later `agentos-executor-host` crate remains unjustified. The resource leaf +crate exists for the proven kernel/runtime/VFS cycle; creating another crate +merely to hold executor traits would add packaging and versioning cost without +improving ownership. ## 12. Prerequisite-revision workstream sequence diff --git a/docs/design/wasmtime-executor.md b/docs/design/wasmtime-executor.md index 3cd7691f4d..ad4c356d0a 100644 --- a/docs/design/wasmtime-executor.md +++ b/docs/design/wasmtime-executor.md @@ -288,9 +288,9 @@ The first implementation will: - treat the patched sysroot and `toolchain/crates/wasi-ext` imports as the guest ABI source of truth; -- link exactly the Preview1 subset listed in - `crates/execution/assets/wasi-preview1-imports.json` plus the custom imports - required by built software; +- link exactly the Preview1 and custom-import surface generated in + `crates/execution/assets/agentos-wasm-abi.json` and required by built + software; - route every resource-bearing operation to an AgentOS kernel or sidecar service; - avoid constructing a default ambient `WasiCtx` with host preopens, host @@ -372,6 +372,32 @@ This introduces an owned-buffer copy for asynchronous I/O. The current V8 path already performs JavaScript and bridge copies, so the Wasmtime path may still reduce total copying, but the benchmark must measure this rather than assume it. +Signal handlers follow the same memory rule and the existing cooperative +AgentOS ABI. A caught signal may run at an import, `sched_yield`, top-level +call boundary, or another declared safe point; neither V8-WASM nor Wasmtime can +inject `__wasi_signal_trampoline(i32)` into arbitrary pure guest computation +and then resume that computation. Epochs provide bounded STOP scheduling and +terminal interruption, not caught-handler injection. The adapter must: + +- claim exactly one kernel delivery token, invoke the trampoline, and close or + explicitly disarm that token before claiming the next signal; it must never + preclaim a FIFO batch against the kernel's LIFO delivery scopes; +- validate the trampoline's exact type before accepting a user disposition and + initialize the inherited mask through `__agentos_set_initial_sigmask` before + `_start` and after successful exec replacement; +- keep a restartable operation's same durable waiter alive across a handler, + so retry cannot duplicate an accept, read, write, lock, or other side effect; +- let an atomically committed or partial operation result win a simultaneous + signal race; otherwise return `EINTR` unless every delivered handler carried + `SA_RESTART`; and +- reacquire and revalidate memory after the handler, because the handler may + grow or mutate linear memory. Handler trap, exit, exec, nested delivery, and + terminal interruption each have an explicit token-cleanup path. + +The shared host-operation API owns the authoritative restartability enum and +completion-versus-signal arbitration. Engine adapters do not scatter their own +restart booleans or cancel and reissue side-effecting operations. + ## 10. Runtime placement and scheduling The normative async/blocking ownership and waiter sequence are defined in @@ -443,6 +469,17 @@ pthreads. Real pthread support requires a threaded sysroot, a bounded thread-spawn ABI, real mutex/condvar/TLS behavior, group cancellation, and per-VM plus process-wide thread admission. +Wasmtime 46 does not expose a supported host hook that can replace or cancel a +guest blocked in `memory.atomic.wait`. Epoch interruption, fuel, dropping an +async call future, and dropping ordinary Store handles do not provide a hard +reap guarantee for that parked native thread. The threaded profile must +therefore run each WASM thread group in a killable worker process unless a +reviewed later Wasmtime API supplies an equivalent bounded interruption +primitive. The parent sidecar remains the sole kernel and policy authority; +the worker receives typed host operations over a bounded control lane, never +ambient filesystem or network resources. Teardown must first request an orderly +group stop, then terminate and reap the worker at a fixed deadline. + The first executor rejects modules that define or import shared memories and does not expose a thread-spawn import, regardless of Wasmtime's compile-time threads feature default. It must not use the experimental upstream @@ -540,8 +577,10 @@ memory-growth relocation. Wasmtime async execution also allocates a separate native stack used for stack switching when an async host function suspends. Measure this per active Store at -the target concurrency; configure a bounded `async_stack_size` rather than -assuming the default is negligible. +the target concurrency. Configure `async_stack_size` as the selected WASM stack +cap plus 1.5 MiB of host-call headroom (2 MiB for the default 512 KiB profile), +charge the whole reservation before Store admission, and reject overflow rather +than assuming the engine default is negligible. The pooling allocator is deferred. It can improve reuse and high-concurrency instantiation, but requires fixed process-wide slot counts, can reserve roughly @@ -620,7 +659,7 @@ is checked only when every required item under that phase is checked and the revision has been sealed: - [x] Phase 0: specification, inventory, baseline, and locked decisions. -- [ ] Phase 1: complete runtime-neutral kernel/executor prerequisite. +- [x] Phase 1: complete runtime-neutral kernel/executor prerequisite. - [ ] Phase 2: production Wasmtime executor at current V8-WASM parity. - [ ] Phase 3: performance decision, preferred-backend rollout, and initial project completion. @@ -648,80 +687,85 @@ revision has been sealed: ### Phase 1 revision: Complete the runtime-neutral executor prerequisite -- [ ] Resolve the duplicate ownership symbols in the owned wasi-libc and make +- [x] Resolve the duplicate ownership symbols in the owned wasi-libc and make `just tools-rebuild` produce the complete canonical command set. -- [ ] Add the narrow, generation-bound `ProcessRuntimeEndpoint`, durable +- [x] Add the narrow, generation-bound `ProcessRuntimeEndpoint`, durable bounded/coalesced control state, and executor-to-kernel exit reporter. -- [ ] Register real runtime endpoints for every production V8, Python, +- [x] Register real runtime endpoints for every production V8, Python, binding, and compatibility-WASM process; restrict `StubDriverProcess` to tests and explicitly virtual processes. -- [ ] Introduce the runtime-neutral execution lifecycle, control handle, +- [x] Introduce the runtime-neutral execution lifecycle, control handle, bounded event types, and direct host-call reply handles without requiring V8's owned backend object to be `Send`. -- [ ] Split executor-facing operations into typed filesystem, fd, network, +- [x] Split executor-facing operations into typed filesystem, fd, network, process, terminal, signal, identity, clock, and entropy capabilities; reject a single mega-trait or executor switchboard. -- [ ] Preserve typed `{ code, message, details }` errors from the kernel through +- [x] Preserve typed `{ code, message, details }` errors from the kernel through the sidecar and adapters without string-to-errno reconstruction. -- [ ] Make the kernel process table the only owner of signal dispositions, +- [x] Make the kernel process table the only owner of signal dispositions, masks, pending state, stop/continue state, terminating state, and wait events. -- [ ] Route external signals, `SIGPIPE`, `SIGCHLD`, PTY control signals, +- [x] Route external signals, `SIGPIPE`, `SIGCHLD`, PTY control signals, process-group signals, cancellation, and termination through one bounded delivery path. -- [ ] Add handler begin/end checkpoints, exec disposition reset, restart +- [x] Add handler begin/end checkpoints, exec disposition reset, restart behavior, and atomic temporary signal masks for `ppoll`. -- [ ] Replace every `V8SessionHandle` readiness target with a runtime-neutral, +- [x] Replace every `V8SessionHandle` readiness target with a runtime-neutral, bounded, coalesced execution wake handle. -- [ ] Make kernel VFS, fd tables, permission tier, preopens, descriptor rights, +- [x] Make kernel VFS, fd tables, permission tier, preopens, descriptor rights, rlimits, identity, umask, and mount policy the sole mutable filesystem and descriptor authority. -- [ ] Route embedded V8 filesystem calls and module resolution through the +- [x] Route embedded V8 filesystem calls and module resolution through the shared kernel-backed filesystem service. -- [ ] Delete mutable host-shadow inventories, bidirectional reconciliation, +- [x] Delete mutable host-shadow inventories, bidirectional reconciliation, and adapter-owned descriptor/socket state that duplicates kernel state; retain host access only through explicit confined mounts and plugins. -- [ ] Move shared DNS, TCP, UDP, Unix socket, TLS, options, polling, and +- [x] Move shared DNS, TCP, UDP, Unix socket, TLS, options, polling, and readiness behavior behind the same sidecar reactor capabilities used by all executors. -- [ ] Move spawn, exec, wait, fd actions, rlimits, locks, terminal/PTY, +- [x] Move spawn, exec, wait, fd actions, rlimits, locks, terminal/PTY, credentials, account lookup, clocks, timers, entropy, and system identity behind the shared operations. -- [ ] Enforce the async/blocking contract: one process Tokio runtime, guest +- [x] Enforce the async/blocking contract: one process Tokio runtime, guest execution on the bounded non-Tokio executor, direct async waiters, and bounded admission to fixed workers for unavoidable blocking work. -- [ ] Bound and account every request, reply, queue, waiter, decoded array, +- [x] Bound and account every request, reply, queue, waiter, decoded array, retained buffer, blocking job, deadline, fd, socket, process, PTY, and guest-visible output path; warn near limits and return named typed limit errors. -- [ ] Fix process-aware DAC/sticky/read-only checks for link, remove, rename, +- [x] Fix process-aware DAC/sticky/read-only checks for link, remove, rename, symlink, and unlink operations. -- [ ] Reject oversized writes, iovecs, subscriptions, pollfds, groups, argv, +- [x] Reject oversized writes, iovecs, subscriptions, pollfds, groups, argv, env, paths, records, and result encodings before allocation, copying, or side effects. -- [ ] Return `ERANGE` with required lengths for short account buffers and cap +- [x] Return `ERANGE` with required lengths for short account buffers and cap supplementary groups before reading guest memory. -- [ ] Correct the `socketpair(kind, nonblock, cloexec)` ABI and implement the +- [x] Correct the `socketpair(kind, nonblock, cloexec)` ABI and implement the existing bounded kernel `pty_open` path. -- [ ] Make fd xattrs and metadata operate on canonical open descriptions after +- [x] Make fd xattrs and metadata operate on canonical open descriptions after rename/unlink; remove ambient Node filesystem fallbacks and sentinel errors. -- [ ] Replace terminal fd caches and libc shadow state with live kernel +- [x] Replace terminal fd caches and libc shadow state with live kernel terminal identity, termios, foreground-group, resize, and raw-mode state. -- [ ] Generate and check in the pinned Preview1 types/layouts and AgentOS custom - ABI manifest used by both adapters and import-audit tooling. -- [ ] Rebuild and inspect every owned-sysroot command; require zero undeclared +- [x] Generate and check in the pinned Preview1 types/layouts and AgentOS custom + ABI manifest used by both adapters and import-audit tooling. The generated + runtime-neutral registry must cover all 169 manifest imports, 29 core + signatures, 40 `wasi_unstable` aliases, and 110 supported semantic binding + groups, including handler/codec identity, execution class, + restartability, return convention, permissions, and transactional + prevalidation metadata. +- [x] Rebuild and inspect every owned-sysroot command; require zero undeclared imports and explain or remove every compatibility alias/version. -- [ ] Pass the raw ABI, software, filesystem, process/signal, network, terminal, +- [x] Pass the raw ABI, software, filesystem, process/signal, network, terminal, identity/system, hostile-import, and resource-attack suites listed in [`wasmtime-phase-0.md`](./wasmtime-phase-0.md#9-required-differential-proof-suites) through V8-WASM. -- [ ] Pass all exit gates in +- [x] Pass all exit gates in [`runtime-neutral-executors.md`](./runtime-neutral-executors.md#13-exit-gates) for V8, Python, and compatibility WASM. -- [ ] Verify common host services contain no V8, JavaScript, Python, or +- [x] Verify common host services contain no V8, JavaScript, Python, or Wasmtime types and that the Phase 1 tree contains no Wasmtime executor. -- [ ] Seal all Phase 1 work as one independently reviewable JJ revision on top +- [x] Seal all Phase 1 work as one independently reviewable JJ revision on top of Phase 0. ### Phase 2 revision: Add Wasmtime at full current feature parity @@ -748,9 +792,16 @@ revision has been sealed: with typed errors. - [ ] Implement epoch-based termination and active-CPU accounting that pauses while an import is asynchronously waiting. +- [ ] Implement cooperative caught-signal delivery at import/safe-point + boundaries with exact trampoline validation, inherited-mask setup, + one-at-a-time LIFO token settlement, nested delivery, and shared + completion/partial-result versus `SA_RESTART` arbitration; use epochs + only for STOP scheduling and terminal interruption. - [ ] Generate and link the owned Preview1 ABI, `wasi_unstable` alias, and every `host_fs`, `host_net`, `host_process`, `host_tty`, and `host_user` - function/version over the Phase 1 shared operations. + function/version over the Phase 1 shared operations using the generated + registry and one dynamic `func_new_async` trampoline; no handwritten + import-name switchboard is permitted. - [ ] Do not create a `wasmtime-wasi` context or install ambient filesystem, network, process, environment, clock, random, or stdio capabilities. - [ ] Apply the three-phase async guest-memory contract to every waiting import: @@ -837,6 +888,10 @@ revision has been sealed: - [ ] Implement bounded per-VM and process-wide thread admission, one accounted Store/instance/native stack per admitted guest thread where required, and transactional failure when capacity is unavailable. +- [ ] Isolate each threaded WASM thread group in a killable worker process, + keep AgentOS kernel state in the parent, use bounded typed host-operation + IPC, and prove fixed-deadline termination/reaping for a guest parked in + `memory.atomic.wait`. - [ ] Implement pthread mutex, condition variable, TLS, join/detach, exit, cancellation, robust teardown, and required libc behavior. - [ ] Move masks and in-progress signal delivery to per-thread kernel records @@ -869,6 +924,7 @@ revision has been sealed: | Readiness remains tied to `V8SessionHandle` | High | Add runtime-neutral bounded execution readiness sinks. | | Separate Wasmtime and kernel descriptor namespaces | High | Use the kernel fd table directly and delete Node-WASI shadow descriptor machinery. | | Signal masks, dispositions, and pending state remain split across three layers | Critical | Consolidate a bounded runtime-neutral signal broker before Wasmtime admission; test `SIGPIPE`, `SIGCHLD`, `ppoll`, and stop/continue. | +| An adapter preclaims multiple caught signals against the kernel's LIFO delivery scopes | Critical | Claim, invoke, and settle exactly one token at a time; test two-signal ordering, nested `SA_NODEFER`, `SA_RESETHAND`, exec, exit, and trap cleanup. | | Ambient clocks, randomness, hostname, procfs, or devfs leak host state | Critical | Link owned providers only and add hostile raw-import escape tests. | | Runner-local identity and rlimits become Wasmtime Store state | High | Move mutable process state into the kernel and keep only process identity handles in the Store. | | Kernel errno is converted to text and reparsed by adapters | High | Carry typed error code/message/details across the shared boundary. | @@ -880,6 +936,7 @@ revision has been sealed: | Wasmtime compile/binary cost affects all builds | Medium | Measure; use Cargo feature composition or later crate extraction only if justified. | | Permanent dual backends drift | High | Run the same owned-ABI and software parity corpus against both in CI; keep all Linux semantics below the adapters; version one explicit engine feature profile. | | Thread support is mistaken for pthread compatibility | Critical | Keep threads out of initial scope and require a separate sysroot/runtime milestone. | +| A threaded guest parks forever in `memory.atomic.wait` | Critical | Put each threaded thread group in a killable worker process and prove deadline-bounded whole-group reaping. | | Fake libc terminal state diverges from kernel PTYs | High | Replace process-global termios/pgid/winsize stubs with live typed kernel operations. | ## 18. Resolved implementation decisions diff --git a/docs/design/wasmtime-phase-0.md b/docs/design/wasmtime-phase-0.md index 40bbd08cb4..25107f7d3a 100644 --- a/docs/design/wasmtime-phase-0.md +++ b/docs/design/wasmtime-phase-0.md @@ -20,7 +20,7 @@ This document closes Phase 0 of the Wasmtime executor project. It records: The inventory was checked against: -- `crates/execution/assets/wasi-preview1-imports.json`; +- `crates/execution/assets/agentos-wasm-abi.json`; - `crates/execution/assets/runners/wasm-runner.mjs`; - `crates/execution/assets/runners/wasi-module.js`; - `toolchain/crates/wasi-ext/src/lib.rs`; @@ -51,16 +51,18 @@ The tables use these abbreviations: - **in/out**: bytes copied from/to guest memory. All ranges and aggregate sizes are validated before a side effect. No guest-memory borrow crosses an await. -Common default limits are `maxOpenFds=256`, `maxPipes=128`, `maxPtys=128`, +Common default limits are `maxOpenFds=1024`, `maxPipes=128`, `maxPtys=128`, `maxSockets=256`, `maxConnections=256`, `maxSocketBufferedBytes=4 MiB`, `maxSocketDatagramQueueLen=1024`, `maxPreadBytes=64 MiB`, `maxFdWriteBytes=64 MiB`, `maxProcessArgvBytes=1 MiB`, `maxProcessEnvBytes=1 MiB`, `maxReaddirEntries=4096`, a 4096-byte path, 40 symlink traversals, 64 supplementary groups, 255-byte xattr names, and 64 KiB -xattr values. Every adapter additionally caps one decoded iovec/subscription/fd -array at 4096 entries and its encoded descriptor bytes at 1 MiB before copying. -These two adapter caps become named configurable limits if real software reaches -them; they never silently become unbounded. +xattr values. Every adapter additionally caps one decoded iovec, pollfd, or +poll-subscription array at the Linux-aligned 1024 entries and its encoded +descriptor bytes at 1 MiB before copying. Spawn file actions have their own +4096-entry and 1 MiB encoded-byte caps; SCM_RIGHTS carries at most 253 +descriptors. These adapter caps become named configurable limits if real +software reaches them; they never silently become unbounded. Multi-output imports prevalidate every output range before committing a side effect. Operations that allocate guest fds reserve fd capacity before the host @@ -76,7 +78,7 @@ The runner exposes the same object as `wasi_snapshot_preview1` and | --- | --- | --- | --- | | `args_sizes_get`, `args_get` | C; sync; out | `GuestProcessHost::image(pid).argv` from the committed kernel process image | 1 MiB argv cap; prevalidate pointer table and strings; direct-WAT exact bytes/order/OOB plus spawn/exec corpus | | `environ_sizes_get`, `environ_get` | C; sync; out | `GuestProcessHost::image(pid).env`, after the one sidecar-owned guest filtering/default pass | 1 MiB env cap; deterministic ordering; internal-key exclusion and exec replacement tests | -| `clock_time_get`, `clock_res_get` | C; sync; out | `GuestClockHost`; frozen execution realtime plus monotonic/process/thread clocks | One u64 output; exact realtime/resolution, monotonicity, invalid-id and OOB tests | +| `clock_time_get`, `clock_res_get` | C; sync; out | `GuestClockHost`; frozen execution realtime plus monotonic | One u64 output; exact realtime/resolution, monotonicity, invalid-id and OOB tests. Process/thread CPU clock IDs retain the current stable `ENOTSUP` behavior; implementing per-executor CPU clocks is beyond the current V8-WASM parity target. | | `random_get` | C; sync/chunked; out | `GuestEntropyHost`, same provider as kernel `/dev/urandom` | Validate full guest range first; fill in at most 64 KiB host chunks with a 16 MiB per-call cap; zero/OOB/limit/provider-failure tests | | `fd_close` | C; sync; none | `GuestFdHost::close` on the kernel fd table | fd ownership; reuse/refcount/EBADF parity | | `fd_datasync`, `fd_sync` | C; sync; none | `GuestFdHost::sync(DataOnly/All)` | writable/open fd; synchronous-VFS commit and exact errno tests | @@ -85,7 +87,7 @@ The runner exposes the same object as `wasi_snapshot_preview1` and | `fd_filestat_get` | C; sync; out | `GuestMetadataHost::stat_fd` | Exact dev/ino/type/nlink/size/time encoding; open-unlinked fd test | | `fd_filestat_set_size` | C; sync; scalar in | `GuestFdHost::set_len` | write right, read-only mount, filesystem quota; position and rollback tests | | `fd_filestat_set_times` | C compatibility export; sync; scalar in | `GuestMetadataHost::set_times_fd` | NOW/OMIT validation; open-unlinked fd and DAC tests | -| `fd_pread` | C; sync; in iovecs/out bytes+count | `GuestFdHost::read_at` | 4096 iovecs/1 MiB descriptors/64 MiB payload; offset unchanged and malformed-iovec tests | +| `fd_pread` | C; sync; in iovecs/out bytes+count | `GuestFdHost::read_at` | 1024 iovecs/1 MiB descriptors/64 MiB payload; offset unchanged and malformed-iovec tests | | `fd_pwrite` | C; sync; in iovecs/out count | `GuestFdHost::write_at` | Pre-copy 64 MiB cap, quota and read-only checks; vector ordering/rollback tests | | `fd_read` | C; async for pipe/PTY/socket/FIFO; in iovecs/out bytes+count | `GuestFdHost::read` | Same iovec caps, configured blocking deadline, signal/cancel; EOF/EAGAIN/EINTR/backpressure tests | | `fd_write` | C; async for backpressured pipe/socket/PTY; in iovecs/out count | `GuestFdHost::write` or sidecar `write_stdio` for host-visible stdio | Pre-copy 64 MiB cap; partial write, EPIPE/SIGPIPE, ordering and backpressure tests | @@ -105,7 +107,7 @@ The runner exposes the same object as `wasi_snapshot_preview1` and | `path_rename` | C; sync; two paths in | `GuestPathHost::rename_at` | Phase 1 replaces current generic `rename`; atomic/DAC/sticky/cross-mount tests | | `path_symlink` | C; sync; target/path in | `GuestPathHost::symlink_at` | Phase 1 replaces current generic `symlink`; parent DAC/read-only/dangling tests | | `path_unlink_file` | C; sync; path in | `GuestPathHost::unlink_at` | Phase 1 replaces current generic `remove_file`; sticky/open-description tests | -| `poll_oneoff` | C; async; subscriptions in/events+count out | `GuestProcessHost::poll` over kernel readiness, shared clock, signal and cancel broker | 4096 subscriptions/1 MiB descriptors; fd+clock, HUP, timeout, signal race and lost-wakeup tests | +| `poll_oneoff` | C; async; subscriptions in/events+count out | `GuestProcessHost::poll` over kernel readiness, shared clock, signal and cancel broker | 1024 subscriptions/1 MiB descriptors; fd+clock, HUP, timeout, signal race and lost-wakeup tests | | `proc_exit` | C; terminal control flow; none | `GuestProcessHost::exit(status) -> !`; sidecar finalizes kernel process once | Normal exit distinct from trap/signal; 0/42/137, output-drain and child-wait tests | | `sched_yield` | C; async yield/checkpoint; none | VM executor yield plus cancellation/signal checkpoint | Fairness, STOP/terminate observation and no-busy-spin tests | | `fd_advise`, `fd_fdstat_set_rights` | U | No current runner function | Direct import must fail with stable unsupported-import validation error | @@ -122,7 +124,7 @@ Full permission links the whole module. Read-only/read-write link only | `proc_spawn`, `proc_spawn_v2`, `proc_spawn_v3`, `proc_spawn_v4` | A/A/A/C; sync setup with async lifecycle; in/out | Decode all versions to one `GuestProcessHost::spawn(SpawnRequest)` | 256 processes, 1 MiB argv/env, 4096 actions/1 MiB action bytes, fd limits; legacy artifact plus full posix_spawn actions/masks/groups tests | | `proc_exec`, `proc_fexec` | C; terminal control flow; in | `GuestProcessHost::prepare_exec -> ExecPlan`; validate/compile before atomic kernel commit, then replace Store outside import | 256 MiB module, argv/env/fd limits; failed-exec atomicity, CLOEXEC, fd-offset and deleted-fd tests | | `proc_waitpid`, `proc_waitpid_v2`, `proc_waitpid_v3` | A/A/C; async; out | `GuestProcessHost::wait(selector, flags) -> WaitEvent`, with three encoders | Child-count bound; exit-vs-signal/core/stopped/continued/selectors/EINTR tests | -| `proc_kill` | C; sync; none | `GuestSignalHost::send` through kernel signal broker | Signals 0..64, standard pending coalescing; self/child/group/permission/default/caught tests | +| `proc_kill` | C; sync; none | `GuestSignalHost::send` through kernel signal broker | Raw owned ABI and kernel accept signals 0..64 with standard pending coalescing; the current libc deliberately exposes `_NSIG=32` and no realtime-signal API, which remains the parity target until a separate sysroot expansion; self/child/group/permission/default/caught and raw 64/65 boundary tests | | `proc_getpid`, `proc_getppid` | C; sync; out | Kernel process identity accessors | Spawn/exec/reparent stability tests; no environment-derived PID state | | `proc_getrlimit`, `proc_setrlimit` | C; sync; out/none | Kernel-owned `GuestProcessHost::get/set_rlimit` | Initial `RLIMIT_NOFILE`; lowering/inheritance/EMFILE/EPERM tests; no runner shadow | | `proc_umask`, `umask` | C/A; sync; out | Kernel `GuestProcessHost::set/query_umask` | 0777 mask; create/mkdir/spawn/exec/query tests | @@ -139,7 +141,7 @@ Full permission links the whole module. Read-only/read-write link only | `pty_open` | S, but active library surface; sync allocation; out | Phase 1 wires existing kernel/sidecar `open_pty` | 128 PTYs and fd limits; master/slave/isatty/spawn/resize/SIGWINCH tests | | `proc_sigaction` | C; sync; none | Kernel-owned dispositions/masks/flags; guest handler pointer remains adapter state | KILL/STOP rejection, IGN/DFL/user, NODEFER/RESETHAND/RESTART/exec tests | | `proc_signal_mask_v2` | C; sync; out | Kernel signal broker `update_mask` | KILL/STOP filtering, pending-on-unblock and future per-thread tests | -| `proc_ppoll_v1` | C; async; in/out | Atomic temporary-mask registration plus shared poll/signal/timer broker | Same 4096/1 MiB poll caps; signal/readiness ordering, restore and lost-wakeup tests | +| `proc_ppoll_v1` | C; async; in/out | Atomic temporary-mask registration plus shared poll/signal/timer broker | Same 1024-entry/1 MiB poll caps; signal/readiness ordering, restore and lost-wakeup tests | ## 5. `host_net` inventory @@ -166,7 +168,7 @@ continues to own external DNS and socket I/O. | `net_recvfrom` | C; async; capacities in/data+address out | `GuestNetworkHost::recv_from` | UDP 64 KiB, queue/buffer limits; truncation/source/nonblock/cancel tests | | `net_setsockopt` | C; sync command; option bytes in | `GuestNetworkHost::set_option` | Option payload capped at 64 KiB; exact supported level/name/type and timeout tests | | `net_getsockopt` | C; sync snapshot; option bytes out | `GuestNetworkHost::get_option` | Caller/64 KiB cap; error/length/value parity | -| `net_poll` | C; async; pollfds in/out+ready count | Shared readiness broker, not a network-private loop | 4096 fds/1 MiB descriptors, bounded deadline; mixed fd, duplicate, signal/cancel/HUP tests | +| `net_poll` | C; async; pollfds in/out+ready count | Shared readiness broker, not a network-private loop | 1024 fds/1 MiB descriptors, bounded deadline; mixed fd, duplicate, signal/cancel/HUP tests | | `net_close` | A explicit close; async-capable teardown; none | `GuestFdHost::close` | Idempotence is not promised; resource-release and waiter-cancel tests | | `net_tls_connect` | C; async handshake; hostname in | `GuestNetworkHost::upgrade_tls` | TLS buffer/reactor/deadline limits, 4096-byte hostname, permission/cert/SNI/cancel tests | @@ -188,7 +190,14 @@ and live fd/PTY state remain authoritative. | `host_tty.read` | A active crossterm ABI; async; bytes out | `GuestFdHost::read(fd=0, deadline)` | 64 KiB; legacy zero conflates EOF/timeout; byte/EOF/timeout/signal/OOB tests | | `host_tty.isatty` | A; sync; no memory | Same live terminal lookup | Same fd identity tests; no runner cache | | `host_tty.get_size` | A; sync; two u16 out | `GuestTerminalHost::window_size` | Prevalidate both outputs; resize/ENOTTY/OOB tests | +| `host_tty.set_size` | C; sync; scalar in | `GuestTerminalHost::set_window_size` | Validate u16 columns/rows; SIGWINCH, permission, ENOTTY and resize-observation tests | +| `host_tty.get_attr` | C; sync; flags plus seven control bytes out | `GuestTerminalHost::get_attributes` | Prevalidate both output ranges; live termios, dup/replacement, ENOTTY and OOB tests | +| `host_tty.set_attr` | C; sync; flags plus seven control bytes in | `GuestTerminalHost::set_attributes` | Snapshot bounded input before mutation; live termios, inheritance, ENOTTY and OOB tests | +| `host_tty.get_pgrp` | C; sync; pgid out | `GuestTerminalHost::foreground_process_group` | Prevalidate output; session/foreground-group, dup/replacement, ENOTTY and OOB tests | +| `host_tty.set_pgrp` | C; sync; pgid in | `GuestTerminalHost::set_foreground_process_group` | Kernel session/group authority; permission, orphan/session, ENOTTY and signal-routing tests | +| `host_tty.get_sid` | C; sync; sid out | `GuestTerminalHost::session` | Prevalidate output; controlling-terminal/session, dup/replacement, ENOTTY and OOB tests | | `host_tty.set_raw_mode` | A; sync; none | Sidecar `GuestTerminalHost::set_raw_mode`, including generation lease bookkeeping | Enable/disable/nesting/exit restore/background/ENOTTY tests | +| `host_system.get_identity` | C; sync; field selector in/string+required length out | Kernel `SystemIdentity` snapshot (`sysname`, node name, release, version, machine, domain name) | 4096-byte result cap; exact Linux identity, short-buffer `ERANGE`, invalid selector, and OOB tests | Phase 1 also replaces libc's process-global shadow termios, fixed foreground process group, no-op `tcsetpgrp`, and missing resize path with live @@ -261,28 +270,113 @@ Both backend selections execute the same corpus. Tests compare stdout, stderr, bytes, errno, exit status, terminating signal, kernel side effects, and resource accounting—not engine error strings. +### Resource-attack cap evidence map + +This map scopes the Resource attacks row to the named executor-facing limits in +Sections 2–7 and the runtime-neutral request/reply paths. A checked row has +boundary, limit-plus-one, warning, typed-error, and rollback evidence either at +the semantic owner or through a shared admission primitive that the operation +is required to use. An unchecked row is a release-gate test gap; the existence +of a hard-coded rejection alone does not close it. + +- [x] **Owned adapter payloads and counts.** `PayloadLimit` proves exact-limit + admission, limit-plus-one rejection with structured `{limitName, limit, + observed}` details, coalesced 80% warning/rearm, and allocation-free JSON + measurement in + `crates/execution/src/backend/payload.rs`. The bounded byte, string, vector, + and count constructors are required to receive that named limit and reject + before operation construction (`bounded_values_reject_before_admission` and + `common_payload_constructors_require_named_limits`). +- [x] **Runtime-neutral retained resources.** `ResourceLedger` proves exact + child-scope admission, 80% warning/rearm, limit-plus-one typed fields, parent + rollback, and release-to-zero in + `named_limit_proves_boundary_warning_typed_rejection_and_rollback` and + `failed_child_admission_rolls_back_parent`. This is the shared evidence for + reactor sockets/connections/buffers, blocking jobs/bytes, capabilities, + tasks, completions, and other ledger-backed runtime resources. +- [x] **Common execution events and direct replies.** The backend submission + tests prove a full count queue settles only the rejected call's waiter, the + byte boundary remains charged after dequeue until settlement, limit-plus-one + carries the configured name, and settlement releases the charge. Direct + reply and common event tests prove bounded raw/JSON replies, stdout/stderr, + warnings, and runtime faults, including near-limit delivery + (`crates/execution/src/backend/{submission,reply,event}.rs` and + `crates/execution/tests/backend_payload_bounds.rs`). +- [x] **Spawn file actions.** `wasm_spawn_action_decoder_enforces_typed_limits_with_e2big` + covers the 4096-action and 1 MiB encoded-byte families with independent + count/byte rejection and near-limit warnings. The raw ABI spawn-result + prevalidation cases followed by `waitpid(...)=ECHILD` prove rejected requests + do not create a child. +- [x] **Kernel saturation resources.** Kernel resource-accounting and socket + table tests fill and exceed process, open-fd, pipe, PTY, socket, connection, + socket-buffer-byte, and datagram-queue limits, verify stable usage after + rejection, and verify capacity returns after close/drain/reap. The shared + resource-gauge registration and `resource_gauges_track_usage_and_warn_on_approach` + cover the common near-limit warning path. +- [x] **Fixed ABI table caps.** + `raw_abi_fixed_tables_lists_and_strings_prove_boundary_plus_one_and_warning` + proves exact-1024 admission, limit-plus-one rejection before table access, + and the common warning contract for iovecs, pollfds, and Preview1 + subscriptions. `raw_abi_memory_directions_reject_hostile_ranges_before_host_work` + proves malformed tables cannot partially copy out. Spawn actions remain 4096 + and SCM_RIGHTS remains 253. +- [x] **Point-in-time kernel byte/count caps.** + `resource_limits_reject_oversized_spawn_payloads`, + `resource_limits_reject_oversized_pread_and_write_operations`, and + `resource_limits_reject_oversized_readdir_batches` prove exact-boundary + admission, limit-plus-one rejection, and no file/process mutation for argv, + environment, pread, write, and readdir. The shared + `resource_gauges_track_usage_and_warn_on_approach` proof checks the five + stable names plus structured `{limitName, limit, observed}` error details. +- [x] **Fixed semantic string/list caps.** + `raw_abi_fixed_tables_lists_and_strings_prove_boundary_plus_one_and_warning` + covers supplementary groups, account names, xattr names, and xattr values at + the adapter boundary. `xattr_value_and_name_list_limits_accept_boundary_and_rollback_plus_one` + and `xattr_value_limit_accepts_linux_boundary_and_rejects_plus_one_transactionally` + prove the semantic owner accepts 64 KiB, rejects limit plus one with Linux + errno, and preserves the previous path/fd value and encoded name list. +- [x] **Deadlines.** `BlockingReadDeadline` and + `OperationDeadlineTracker` are the common one-shot 80% warning state machines + required by every `maxBlockingReadMs` and `operationDeadlineMs` path, + including synchronous poll and readiness re-park paths. The focused + `operation_deadline_warns_at_eighty_percent_before_success_or_typed_expiry` + proof covers near-limit success, typed expiry, synchronous socket writes, and + reconstruction without clock reset or duplicate warning. Kernel + `blocking_pipe_and_pty_reads_time_out_instead_of_hanging_forever`, raw + `raw_abi_blocking_read_warns_at_eighty_percent_before_typed_expiry`, and + `wasm_parent_child_write_deadline_wakes_after_parent_stops_polling` cover + guest-visible warning/expiry and teardown behavior. + ### Phase 0 artifact evidence -On the rebased `main` tree, `pnpm install --frozen-lockfile` completed and the -Rust half of `just tools-rebuild` produced 139 command modules. The C build -produced 33 top-level modules before stopping in the upstream Git link. Auditing -all 172 produced modules with Binaryen `wasm-dis` found 121 unique -module/function import pairs. Every pair is declared in Sections 3–7; the -artifact set specifically confirms that both `path_chown`/`fd_chown` legacy -aliases and `proc_spawn_v3`/`proc_spawn_v4` plus -`proc_waitpid_v2`/`proc_waitpid_v3` version pairs remain live. - -The complete release-artifact gate is not green on this base revision. The -owned `libc.a` contains duplicate `chown`, `fchown`, `lchown`, and `fchownat` -definitions from patched `posix.o` and `override_ownership.o`, so upstream Git -cannot link and later C tools are not produced. This is a pre-existing -toolchain correctness defect: patch -`0034-posix-ownership-and-access.patch` added the canonical implementation while -`wasi-libc-overrides/ownership.c` retained the old implementation. The -runtime-neutral revision must select one implementation, rerun the complete -rebuild, and commit an automated import audit before satisfying its exit gate. -Phase 0's source inventory remains complete; it does not misreport the partial -artifact build as release validation. +The Phase 1 tree completed `just tools-rebuild` after removing the obsolete +`wasi-libc-overrides/ownership.c` definitions and retaining the canonical +patched-libc ownership implementation. The rebuild produced 119 standalone +Rust commands, compiled 98 C programs, installed the selected C commands, and +confirmed 166 entries in the default command corpus. Because a focused Vim +build was already present in `toolchain/target`, that completed rebuild/copy +invocation staged and audited 167 command entries. The generated ABI remained +current at 169 functions. + +Those 169 manifest rows have 29 distinct core function signatures. The 40 +Preview1 rows also link through the `wasi_unstable` module alias, yielding 209 +effective linker names without duplicating implementations. The inventory +tables contain 111 semantic rows: 110 supported binding groups plus the one +intentional unsupported-import group. The generated registry must preserve +those counts and map every import to explicit handler, decoder, encoder, +execution-class, restartability, return-convention, permission, and +transaction/prevalidation metadata. + +The automated Binaryen audit inspected all 167 staged command entries (136 distinct +modules) and found 145 unique module/function imports. Every observed import +has the exact signature declared in Sections 3–7; no undeclared import or +conflicting signature remains. The artifact set specifically confirms that +both `path_chown`/`fd_chown` legacy aliases and `proc_spawn_v3`/`proc_spawn_v4` +plus `proc_waitpid_v2`/`proc_waitpid_v3` version pairs remain live. Generated +command and package outputs are ignored build evidence and are not committed. +A subsequent focused DuckDB build expanded the optional staged corpus to 168 +commands and 137 distinct modules without changing the 145-import inventory or +the 169-function ABI manifest; it is not part of the 166-command default corpus. ## 10. V8-WASM performance and memory baseline @@ -359,7 +453,12 @@ by `just tools-rebuild` before a release-quality capture. 5. **Engine profiles.** The process caches Engines by exact feature profile and exact stack cap. Unspecified stack uses 512 KiB. Admit at most eight distinct profiles process-wide by default, warn at 80%, and reject the ninth with a - typed limit naming the engine-profile setting. Modules never cross Engines. + typed limit naming the engine-profile setting. Set the async stack to the + WASM stack cap plus 1.5 MiB of host-call headroom (2 MiB for the default + profile), reject arithmetic/platform-size overflow as typed configuration + errors, and charge the complete async-stack reservation per active Store. + This preserves Wasmtime 46's default host-call headroom while making custom + stack profiles explicit and accounted. Modules never cross Engines. 6. **Module cache.** Per Engine, use a 32-entry LRU and a 256 MiB conservative admission budget. Charge `max(module_bytes * 8, 1 MiB)` per compiled Module, warn at 80%, never deserialize native artifacts, and expose hits, misses, diff --git a/examples/resource-limits/server.ts b/examples/resource-limits/server.ts index 8037333fc3..42e47e4b5e 100644 --- a/examples/resource-limits/server.ts +++ b/examples/resource-limits/server.ts @@ -9,7 +9,6 @@ const vm = agentOS({ maxOpenFds: 256, // open file descriptors maxSockets: 128, // open sockets maxFilesystemBytes: 256 * 1024 * 1024, // VFS storage budget - maxWasmFuel: 30_000, // WASM execution budget maxWasmMemoryBytes: 128 * 1024 * 1024, // WASM linear memory maxWasmStackBytes: 4 * 1024 * 1024, // WASM call-stack ceiling }, @@ -32,6 +31,8 @@ const vm = agentOS({ wasm: { prewarmTimeoutMs: 30_000, // WASM compile-cache warmup runnerHeapLimitMb: 2048, // trusted WASI runner V8 heap + activeCpuTimeLimitMs: 30_000, // active standalone-WASM CPU time + wallClockLimitMs: 120_000, // optional elapsed-time backstop }, }, }); diff --git a/justfile b/justfile index d4c518788b..1f61be42a5 100644 --- a/justfile +++ b/justfile @@ -30,9 +30,17 @@ toolchain-preflight: make programs toolchain-copy-commands: - node packages/runtime-core/scripts/copy-wasm-commands.mjs + node packages/runtime-core/scripts/copy-wasm-commands.mjs --require + +toolchain-check-abi: + node scripts/generate-wasm-abi-manifest.mjs + +toolchain-audit-imports: + just toolchain-check-abi + node scripts/audit-wasm-imports.mjs software-build: + pnpm --filter @rivet-dev/agentos-toolchain build pnpm --filter '@agentos-software/*' build # Rebuild and stage the complete default WASM tool set from source. All outputs @@ -41,6 +49,7 @@ tools-rebuild: just toolchain-build just toolchain-copy-commands just software-build + just toolchain-audit-imports install-shell: #!/usr/bin/env bash diff --git a/packages/build-tools/bridge-src/builtins/child-process.ts b/packages/build-tools/bridge-src/builtins/child-process.ts index 282604723c..430e7eb235 100644 --- a/packages/build-tools/bridge-src/builtins/child-process.ts +++ b/packages/build-tools/bridge-src/builtins/child-process.ts @@ -1314,6 +1314,7 @@ function execSync(command, options) { input: opts.input == null ? null : encodeBridgeBytes(opts.input), maxBuffer, shell: true, + stdio: childProcessBridgeStdio(opts.stdio), timeout: Number.isInteger(opts.timeout) && opts.timeout > 0 ? opts.timeout : null, killSignal: normalizeChildProcessSignal(opts.killSignal).signalCode ?? "SIGTERM" }) @@ -1358,6 +1359,12 @@ function execSync(command, options) { } return result.stdout; } +function normalizeChildProcessStdio(stdio) { + return Array.isArray(stdio) ? stdio : typeof stdio === "string" ? [stdio, stdio, stdio] : ["pipe", "pipe", "pipe"]; +} +function childProcessBridgeStdio(stdio) { + return normalizeChildProcessStdio(stdio).map((mode) => typeof mode === "string" ? mode : "pipe"); +} function spawn(command, args, options) { let argsArray = []; let opts = {}; @@ -1375,7 +1382,7 @@ function spawn(command, args, options) { child.spawnfile = command; child.spawnargs = [command, ...argsArray]; child.detached = opts.detached === true; - const stdio = Array.isArray(opts.stdio) ? opts.stdio : opts.stdio === "inherit" ? ["inherit", "inherit", "inherit"] : []; + const stdio = normalizeChildProcessStdio(opts.stdio); // Node fd inheritance: when stdio[1]/stdio[2] is a numeric fd the child's // stdout/stderr is wired to that (host/VFS) descriptor, so the bytes are // written there instead of being delivered on child.stdout/child.stderr @@ -1407,7 +1414,11 @@ function spawn(command, args, options) { pty: opts.agentosPty && typeof opts.agentosPty === "object" ? { cols: Number.isInteger(opts.agentosPty.cols) && opts.agentosPty.cols > 0 ? opts.agentosPty.cols : 80, rows: Number.isInteger(opts.agentosPty.rows) && opts.agentosPty.rows > 0 ? opts.agentosPty.rows : 24 - } : opts.agentosPty === true ? { cols: 80, rows: 24 } : null + } : opts.agentosPty === true ? { cols: 80, rows: 24 } : null, + // A non-empty stdio vector marks this as Node child_process + // ownership. The sidecar must return output to this bridge even + // though the Linux kernel process inherits fd 1/2 internally. + stdio: childProcessBridgeStdio(stdio) }) ])); } catch (error) { @@ -1568,6 +1579,7 @@ function spawnSync(command, args, options) { input: opts.input == null ? null : encodeBridgeBytes(opts.input), maxBuffer, shell: opts.shell === true || typeof opts.shell === "string", + stdio: childProcessBridgeStdio(opts.stdio), timeout, killSignal }) diff --git a/packages/build-tools/bridge-src/builtins/console.ts b/packages/build-tools/bridge-src/builtins/console.ts index 457a9fa163..ea82c7834f 100644 --- a/packages/build-tools/bridge-src/builtins/console.ts +++ b/packages/build-tools/bridge-src/builtins/console.ts @@ -449,7 +449,7 @@ function installBuiltinUtilFormatWithOptions(builtinUtilModule) { }; } if (typeof builtinUtilModule.parseEnv !== "function") { - const envLinePattern = /(?:^|\n)\s*(?:export\s+)?([\w.-]+)\s*=\s*(?:'((?:\\'|[^'])*)'|"((?:\\"|[^\"])*)"|`((?:\\`|[^`])*)`|([^#\r\n]*?))\s*(?:#[^\r\n]*)?(?=\r?\n|$)/g; + const envLinePattern = /(?:^|\n)[\t ]*(?:export[\t ]+)?([\w.-]+)[\t ]*=[\t ]*(?:'((?:\\'|[^'])*)'|"((?:\\"|[^\"])*)"|`((?:\\`|[^`])*)`|([^#\r\n]*?))[\t ]*(?:#[^\r\n]*)?(?=\r?\n|$)/g; builtinUtilModule.parseEnv = function parseEnv(content) { if (typeof content !== "string") { const received = content === null ? "null" : `type ${typeof content} (${String(content)})`; diff --git a/packages/build-tools/bridge-src/builtins/fs.ts b/packages/build-tools/bridge-src/builtins/fs.ts index e92133f795..76f0347e00 100644 --- a/packages/build-tools/bridge-src/builtins/fs.ts +++ b/packages/build-tools/bridge-src/builtins/fs.ts @@ -2407,13 +2407,31 @@ async function fsReadFileAsync(path, options) { } const rawPath = normalizePathLike(path); - const handle = new FileHandle(fs.openSync(rawPath, "r")); + const encoding = typeof options === "string" ? options : options?.encoding; try { - return await handle.readFile(options); - } finally { - if (!handle.closed) { - await handle.close(); + if (encoding) { + return await _fsAsync.readFile.apply(void 0, [rawPath, encoding]); } + const base64Content = await _fsAsync.readFileBinary.apply(void 0, [rawPath]); + return import_buffer.Buffer.from(base64Content, "base64"); + } catch (err) { + if (bridgeErrorCode(err) === "ENOENT") { + throw createFsError( + "ENOENT", + `ENOENT: no such file or directory, open '${rawPath}'`, + "open", + rawPath + ); + } + if (bridgeErrorCode(err) === "EACCES") { + throw createFsError( + "EACCES", + `EACCES: permission denied, open '${rawPath}'`, + "open", + rawPath + ); + } + throw err; } } async function fsWriteFileAsync(file, data, options) { diff --git a/packages/build-tools/bridge-src/builtins/process.ts b/packages/build-tools/bridge-src/builtins/process.ts index 726e7f66ce..9b1f29082e 100644 --- a/packages/build-tools/bridge-src/builtins/process.ts +++ b/packages/build-tools/bridge-src/builtins/process.ts @@ -21,9 +21,10 @@ import { loadWebSocketModule } from "../prelude.js"; function readProcessConfig() { const env = typeof _processConfig !== "undefined" && _processConfig.env || {}; + const internalEnv = globalThis.__agentOSProcessConfigEnv || env; let execArgv = []; try { - const parsed = JSON.parse(env.AGENTOS_NODE_EXEC_ARGV || "[]"); + const parsed = JSON.parse(internalEnv.AGENTOS_NODE_EXEC_ARGV || "[]"); if (Array.isArray(parsed)) execArgv = parsed.map(String); } catch {} return { @@ -288,7 +289,14 @@ function signalDispatch(eventType, payload) { } const signal = payload.signal ?? payload.number; const action = typeof payload.action === "string" ? payload.action : "default"; - _deliverProcessSignal(signal, action); + const deliveryToken = payload.deliveryToken; + try { + _deliverProcessSignal(signal, action); + } finally { + if (deliveryToken !== undefined) { + _processSignalEnd.applySyncPromise(void 0, [deliveryToken]); + } + } } function _addListener(event, listener, once = false) { @@ -960,6 +968,10 @@ function applyProcessConfig(nextConfig) { for (const key of Object.keys(_stdinOnceListeners)) { _stdinOnceListeners[key] = []; } + // Snapshot restore clears the stdin listener tables above. Allow the + // post-restore IPC hook to attach its one framed-input listener again while + // retaining the already registered active-handle identity. + process2._agentOSIpcInstalled = false; setStdinDataValue(nextConfig.stdin ?? ""); setStdinPosition(0); setStdinEnded(false); @@ -979,7 +991,7 @@ function applyProcessConfig(nextConfig) { process2.argv = nextConfig.argv; process2.argv0 = nextConfig.argv0; process2.env = nextConfig.env; - process2.connected = nextConfig.env?.AGENTOS_NODE_IPC === "1"; + process2.connected = globalThis.__agentOSProcessConfigEnv?.AGENTOS_NODE_IPC === "1"; process2.mainModule = void 0; process2._cwd = nextConfig.cwd; process2.stdin.paused = true; diff --git a/packages/build-tools/bridge-src/global-exposure.ts b/packages/build-tools/bridge-src/global-exposure.ts index 2c2d1f3c1b..2678ef63da 100644 --- a/packages/build-tools/bridge-src/global-exposure.ts +++ b/packages/build-tools/bridge-src/global-exposure.ts @@ -297,6 +297,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host process signal-listener state bridge reference.", }, + { + name: "_processSignalEnd", + classification: "hardened", + rationale: "Host process signal-delivery completion bridge reference.", + }, { name: "_processTakeSignal", classification: "hardened", diff --git a/packages/build-tools/scripts/build-base-filesystem.mjs b/packages/build-tools/scripts/build-base-filesystem.mjs index af746199e2..67ca91f35f 100644 --- a/packages/build-tools/scripts/build-base-filesystem.mjs +++ b/packages/build-tools/scripts/build-base-filesystem.mjs @@ -41,6 +41,7 @@ const EXTRA_DIRECTORIES = [ const TRANSFORMS = [ "Normalize HOSTNAME to secure-exec", + "Allow traversal into the AgentOS /root/node_modules compatibility projection", "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", ]; @@ -248,6 +249,9 @@ function normalizeEntry(entry) { if (entry.path === "/etc/hostname" && entry.type === "file") { return { ...entry, content: `${BASE_HOSTNAME}\n` }; } + if (entry.path === "/root" && entry.type === "directory") { + return { ...entry, mode: "711" }; + } return entry; } diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index 77be0da761..cf0a50d886 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -617,7 +617,6 @@ export interface AgentOsLimits { maxProcessArgvBytes?: number; maxProcessEnvBytes?: number; maxReaddirEntries?: number; - maxWasmFuel?: number; maxWasmMemoryBytes?: number; maxWasmStackBytes?: number; }; @@ -690,7 +689,9 @@ export interface AgentOsLimits { syncReadLimitBytes?: number; prewarmTimeoutMs?: number; runnerHeapLimitMb?: number; - runnerCpuTimeLimitMs?: number; + activeCpuTimeLimitMs?: number; + wallClockLimitMs?: number; + deterministicFuel?: number; }; /** Process spawn, I/O, and lifecycle-event backlog limits. */ process?: { @@ -699,6 +700,8 @@ export interface AgentOsLimits { pendingStdinBytes?: number; pendingEventCount?: number; pendingEventBytes?: number; + maxPendingChildSyncCount?: number; + maxPendingChildSyncBytes?: number; }; } @@ -1262,7 +1265,9 @@ const KERNEL_POSIX_BOOTSTRAP_DIR_METADATA: Record< { mode: string; uid: number; gid: number } > = { "/tmp": { mode: "1777", uid: 0, gid: 0 }, - "/root": { mode: "700", uid: 0, gid: 0 }, + // Compatibility module mounts live below /root; allow traversal without + // allowing the default guest to enumerate the root user's home directory. + "/root": { mode: "711", uid: 0, gid: 0 }, "/sys": { mode: "555", uid: 0, gid: 0 }, "/home/agentos": { mode: "2755", uid: 1000, gid: 1000 }, "/workspace": { mode: "755", uid: 1000, gid: 1000 }, diff --git a/packages/core/src/generated/ProcessLimitsConfig.ts b/packages/core/src/generated/ProcessLimitsConfig.ts index 142ef692b8..343197c4f4 100644 --- a/packages/core/src/generated/ProcessLimitsConfig.ts +++ b/packages/core/src/generated/ProcessLimitsConfig.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ProcessLimitsConfig = { maxSpawnFileActions?: number, maxSpawnFileActionBytes?: number, pendingStdinBytes?: number, pendingEventCount?: number, pendingEventBytes?: number, }; +export type ProcessLimitsConfig = { maxSpawnFileActions?: number, maxSpawnFileActionBytes?: number, pendingStdinBytes?: number, pendingEventCount?: number, pendingEventBytes?: number, maxPendingChildSyncCount?: number, maxPendingChildSyncBytes?: number, }; diff --git a/packages/core/src/generated/ResourceLimitsConfig.ts b/packages/core/src/generated/ResourceLimitsConfig.ts index bafed1495c..9904e4b214 100644 --- a/packages/core/src/generated/ResourceLimitsConfig.ts +++ b/packages/core/src/generated/ResourceLimitsConfig.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ResourceLimitsConfig = { cpuCount?: number, maxProcesses?: number, maxOpenFds?: number, maxPipes?: number, maxPtys?: number, maxSockets?: number, maxConnections?: number, maxSocketBufferedBytes?: number, maxSocketDatagramQueueLen?: number, maxFilesystemBytes?: number, maxInodeCount?: number, maxBlockingReadMs?: number, maxPreadBytes?: number, maxFdWriteBytes?: number, maxProcessArgvBytes?: number, maxProcessEnvBytes?: number, maxReaddirEntries?: number, maxRecursiveFsDepth?: number, maxRecursiveFsEntries?: number, maxWasmFuel?: number, maxWasmMemoryBytes?: number, maxWasmStackBytes?: number, }; +export type ResourceLimitsConfig = { cpuCount?: number, maxProcesses?: number, maxOpenFds?: number, maxPipes?: number, maxPtys?: number, maxSockets?: number, maxConnections?: number, maxSocketBufferedBytes?: number, maxSocketDatagramQueueLen?: number, maxFilesystemBytes?: number, maxInodeCount?: number, maxBlockingReadMs?: number, maxPreadBytes?: number, maxFdWriteBytes?: number, maxProcessArgvBytes?: number, maxProcessEnvBytes?: number, maxReaddirEntries?: number, maxRecursiveFsDepth?: number, maxRecursiveFsEntries?: number, maxWasmMemoryBytes?: number, maxWasmStackBytes?: number, }; diff --git a/packages/core/src/generated/WasmLimitsConfig.ts b/packages/core/src/generated/WasmLimitsConfig.ts index 3ba3cc3833..7743ce23bd 100644 --- a/packages/core/src/generated/WasmLimitsConfig.ts +++ b/packages/core/src/generated/WasmLimitsConfig.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, runnerCpuTimeLimitMs?: number, }; +export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, activeCpuTimeLimitMs?: number, wallClockLimitMs?: number, deterministicFuel?: number, }; diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts index 2d376ccbd0..9992feabbc 100644 --- a/packages/core/src/options-schema.ts +++ b/packages/core/src/options-schema.ts @@ -12,11 +12,58 @@ const stringArray = z.array(z.string()); const nonNegativeInteger = z.number().int().nonnegative(); const positiveInteger = z.number().int().positive(); const vmIdSchema = z.number().int().min(0).max(0xffffffff); +const maxAccountRecordBytes = 4095; +const maxGroupMembers = 256; +const utf8Encoder = new TextEncoder(); +const utf8ByteLength = (value: string) => utf8Encoder.encode(value).byteLength; const vmAccountNameSchema = z .string() .min(1) - .refine((value) => !/[:\s\0]/u.test(value), "Invalid account name"); -const vmGuestPathSchema = z.string().startsWith("/"); + .refine((value) => !/[:,\s\0]/u.test(value), "Invalid account name") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const vmGuestPathSchema = z + .string() + .startsWith("/") + .refine((value) => !/[:\r\n\0]/u.test(value), "Invalid account path") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const vmGecosSchema = z + .string() + .refine((value) => !/[:\r\n\0]/u.test(value), "Invalid GECOS field") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const passwdRecordBytes = (account: { + uid: number; + gid: number; + username: string; + homedir: string; + shell: string; + gecos?: string; +}) => + 7 + + utf8ByteLength(account.username) + + String(account.uid).length + + String(account.gid).length + + utf8ByteLength(account.gecos ?? "") + + utf8ByteLength(account.homedir) + + utf8ByteLength(account.shell); +const groupRecordBytes = (group: { + gid: number; + name: string; + members: string[]; +}) => + 4 + + utf8ByteLength(group.name) + + String(group.gid).length + + group.members.reduce((total, member) => total + utf8ByteLength(member), 0) + + Math.max(0, group.members.length - 1); const vmUserAccountSchema = z .object({ uid: vmIdSchema, @@ -24,33 +71,140 @@ const vmUserAccountSchema = z username: vmAccountNameSchema, homedir: vmGuestPathSchema, shell: vmGuestPathSchema, - gecos: z.string().optional(), + gecos: vmGecosSchema.optional(), supplementaryGids: z.array(vmIdSchema).max(64), }) - .strict(); + .strict() + .superRefine((account, context) => { + if (passwdRecordBytes(account) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered passwd record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + }); const vmGroupSchema = z .object({ gid: vmIdSchema, name: vmAccountNameSchema, - members: z.array(vmAccountNameSchema), + members: z.array(vmAccountNameSchema).max(maxGroupMembers), }) - .strict(); + .strict() + .superRefine((group, context) => { + if (groupRecordBytes(group) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered group record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + }); const vmUserConfigSchema = z .object({ uid: vmIdSchema.optional(), gid: vmIdSchema.optional(), euid: vmIdSchema.optional(), egid: vmIdSchema.optional(), - username: z.string().optional(), - homedir: z.string().optional(), - shell: z.string().optional(), - gecos: z.string().optional(), - groupName: z.string().optional(), + username: vmAccountNameSchema.optional(), + homedir: vmGuestPathSchema.optional(), + shell: vmGuestPathSchema.optional(), + gecos: vmGecosSchema.optional(), + groupName: vmAccountNameSchema.optional(), supplementaryGids: z.array(vmIdSchema).max(64).optional(), accounts: z.array(vmUserAccountSchema).max(64).optional(), groups: z.array(vmGroupSchema).max(128).optional(), }) - .strict(); + .strict() + .superRefine((user, context) => { + const primary = { + uid: user.uid ?? 1000, + gid: user.gid ?? 1000, + username: user.username ?? "agentos", + homedir: user.homedir ?? "/home/agentos", + shell: user.shell ?? "/bin/sh", + gecos: user.gecos, + supplementaryGids: user.supplementaryGids ?? [], + }; + if (passwdRecordBytes(primary) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered passwd record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + + const materialized = new Map(); + const explicitNames = new Map(); + for (const [index, group] of (user.groups ?? []).entries()) { + if (materialized.has(group.gid)) { + context.addIssue({ + code: "custom", + path: ["groups", index, "gid"], + message: `Duplicate user group gid ${group.gid}`, + }); + continue; + } + const previousGid = explicitNames.get(group.name); + if (previousGid !== undefined) { + context.addIssue({ + code: "custom", + path: ["groups", index, "name"], + message: `Duplicate user group name ${group.name}`, + }); + } + explicitNames.set(group.name, group.gid); + materialized.set(group.gid, { + name: group.name, + members: [...group.members], + }); + } + + if (!materialized.has(primary.gid)) { + materialized.set(primary.gid, { + name: user.groupName ?? primary.username, + members: [primary.username], + }); + } + const authoritativeGids = new Set(materialized.keys()); + const effectiveAccounts = new Map( + (user.accounts ?? []).map((account) => [account.uid, account] as const), + ); + effectiveAccounts.set(primary.uid, primary); + for (const account of effectiveAccounts.values()) { + for (const gid of [account.gid, ...account.supplementaryGids]) { + if (authoritativeGids.has(gid)) continue; + const group = materialized.get(gid) ?? { + name: `group${gid}`, + members: [], + }; + if (!group.members.includes(account.username)) { + group.members.push(account.username); + } + materialized.set(gid, group); + } + } + + const gidsByName = new Map(); + for (const [gid, group] of materialized) { + const previousGid = gidsByName.get(group.name); + if (previousGid !== undefined && previousGid !== gid) { + context.addIssue({ + code: "custom", + path: ["groups"], + message: `Materialized user group name ${group.name} maps to both gid ${previousGid} and gid ${gid}; synthesized group names must not collide`, + }); + } + gidsByName.set(group.name, gid); + if ( + group.members.length > maxGroupMembers || + groupRecordBytes({ gid, ...group }) > maxAccountRecordBytes + ) { + context.addIssue({ + code: "custom", + path: ["groups"], + message: `Materialized group exceeds the ${maxGroupMembers}-member or ${maxAccountRecordBytes}-byte account ABI limit`, + }); + } + } + }); const functionSchema = z.custom<(...args: any[]) => any>( (value) => typeof value === "function", { message: "Expected function" }, @@ -129,7 +283,6 @@ export const agentOsLimitsSchema = z maxProcessArgvBytes: nonNegativeInteger.optional(), maxProcessEnvBytes: nonNegativeInteger.optional(), maxReaddirEntries: nonNegativeInteger.optional(), - maxWasmFuel: nonNegativeInteger.optional(), maxWasmMemoryBytes: nonNegativeInteger.optional(), maxWasmStackBytes: nonNegativeInteger.optional(), }) @@ -220,7 +373,9 @@ export const agentOsLimitsSchema = z syncReadLimitBytes: positiveInteger.optional(), prewarmTimeoutMs: positiveInteger.optional(), runnerHeapLimitMb: positiveInteger.optional(), - runnerCpuTimeLimitMs: nonNegativeInteger.optional(), + activeCpuTimeLimitMs: nonNegativeInteger.optional(), + wallClockLimitMs: nonNegativeInteger.optional(), + deterministicFuel: nonNegativeInteger.optional(), }) .strict() .optional(), @@ -231,6 +386,8 @@ export const agentOsLimitsSchema = z pendingStdinBytes: positiveInteger.optional(), pendingEventCount: positiveInteger.optional(), pendingEventBytes: positiveInteger.optional(), + maxPendingChildSyncCount: positiveInteger.optional(), + maxPendingChildSyncBytes: positiveInteger.optional(), }) .strict() .optional(), diff --git a/packages/core/tests/options-schema.test.ts b/packages/core/tests/options-schema.test.ts index cd78c77ae4..591399d5b7 100644 --- a/packages/core/tests/options-schema.test.ts +++ b/packages/core/tests/options-schema.test.ts @@ -49,6 +49,73 @@ describe("AgentOsOptions validation", () => { ).toThrow(/filesystem/); }); + test("accepts the distinct WASM CPU fields and rejects removed aliases", () => { + expect( + agentOsOptionsSchema.safeParse({ + limits: { + wasm: { + activeCpuTimeLimitMs: 30_000, + wallClockLimitMs: 45_000, + deterministicFuel: 1_000_000, + }, + }, + }).success, + ).toBe(true); + expect(() => + agentOsOptionsSchema.parse({ + limits: { resources: { maxWasmFuel: 1 } }, + }), + ).toThrow(/maxWasmFuel/); + expect(() => + agentOsOptionsSchema.parse({ + limits: { wasm: { runnerCpuTimeLimitMs: 1 } }, + }), + ).toThrow(/runnerCpuTimeLimitMs/); + }); + + test("bounds and materializes Linux account records", () => { + const exactPasswdRecord = { + uid: 0, + gid: 0, + username: "u", + homedir: "/", + shell: "/", + gecos: "x".repeat(4083), + }; + expect( + agentOsOptionsSchema.safeParse({ user: exactPasswdRecord }).success, + ).toBe(true); + expect( + agentOsOptionsSchema.safeParse({ + user: { ...exactPasswdRecord, gecos: "😀".repeat(1021) }, + }).success, + ).toBe(false); + expect( + agentOsOptionsSchema.safeParse({ + user: { + uid: 0, + gid: 0, + username: "root", + supplementaryGids: [44], + groups: [{ gid: 99, name: "group44", members: [] }], + }, + }).success, + ).toBe(false); + expect( + agentOsOptionsSchema.safeParse({ + user: { + groups: [ + { + gid: 7, + name: "g", + members: Array.from({ length: 257 }, (_, index) => `m${index}`), + }, + ], + }, + }).success, + ).toBe(false); + }); + test("rejects create option factories on the one-shot core constructor", () => { expect(() => agentOsOptionsSchema.parse({ diff --git a/packages/runtime-benchmarks/src/leak.ts b/packages/runtime-benchmarks/src/leak.ts index e3d36bb6db..b952f96c27 100644 --- a/packages/runtime-benchmarks/src/leak.ts +++ b/packages/runtime-benchmarks/src/leak.ts @@ -82,6 +82,7 @@ await new Promise((resolve, reject) => { guestHeapRss: slope(samples, "guestHeapRss"), sidecarRss: slope(samples, "sidecarRss"), runningProcesses: slope(samples, "runningProcesses"), + stoppedProcesses: slope(samples, "stoppedProcesses"), exitedProcesses: slope(samples, "exitedProcesses"), openFds: slope(samples, "openFds"), sockets: slope(samples, "sockets"), diff --git a/packages/runtime-benchmarks/src/lib/memory.ts b/packages/runtime-benchmarks/src/lib/memory.ts index 6b9bc09e2c..baeb7b6aa2 100644 --- a/packages/runtime-benchmarks/src/lib/memory.ts +++ b/packages/runtime-benchmarks/src/lib/memory.ts @@ -8,6 +8,7 @@ export interface MemorySample { guestHeapRss: number; sidecarRss: number; runningProcesses: number; + stoppedProcesses: number; exitedProcesses: number; openFds: number; sockets: number; @@ -256,6 +257,7 @@ export async function sampleMemory(vm: BenchVm, cycle: number): Promise, }; +export type VmGroupConfig = { gid: number, name: string, +/** + * Authoritative `/etc/group` membership. Process supplementary gids are + * intentionally not merged into this list. + */ +members: Array, }; diff --git a/packages/runtime-core/src/generated/VmUserAccountConfig.ts b/packages/runtime-core/src/generated/VmUserAccountConfig.ts index 0570dd327d..0c304bf46d 100644 --- a/packages/runtime-core/src/generated/VmUserAccountConfig.ts +++ b/packages/runtime-core/src/generated/VmUserAccountConfig.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type VmUserAccountConfig = { uid: number, gid: number, username: string, homedir: string, shell: string, gecos?: string, supplementaryGids: Array, }; +export type VmUserAccountConfig = { uid: number, gid: number, username: string, homedir: string, shell: string, gecos?: string, +/** + * Initial process credentials only. These gids do not add the account to + * an explicit `/etc/group` record's member list. + */ +supplementaryGids: Array, }; diff --git a/packages/runtime-core/src/generated/VmUserConfig.ts b/packages/runtime-core/src/generated/VmUserConfig.ts index adb01c217a..82ce8ff924 100644 --- a/packages/runtime-core/src/generated/VmUserConfig.ts +++ b/packages/runtime-core/src/generated/VmUserConfig.ts @@ -5,4 +5,9 @@ import type { VmUserAccountConfig } from "./VmUserAccountConfig.js"; /** * Initial Linux-style credentials and account record for processes in a VM. */ -export type VmUserConfig = { uid?: number, gid?: number, euid?: number, egid?: number, username?: string, homedir?: string, shell?: string, gecos?: string, groupName?: string, supplementaryGids?: Array, accounts?: Array, groups?: Array, }; +export type VmUserConfig = { uid?: number, gid?: number, euid?: number, egid?: number, username?: string, homedir?: string, shell?: string, gecos?: string, groupName?: string, +/** + * Initial supplementary process credentials. An explicit group record is + * authoritative and is not given extra members from this list. + */ +supplementaryGids?: Array, accounts?: Array, groups?: Array, }; diff --git a/packages/runtime-core/src/generated/WasmLimitsConfig.ts b/packages/runtime-core/src/generated/WasmLimitsConfig.ts index 3ba3cc3833..7743ce23bd 100644 --- a/packages/runtime-core/src/generated/WasmLimitsConfig.ts +++ b/packages/runtime-core/src/generated/WasmLimitsConfig.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, runnerCpuTimeLimitMs?: number, }; +export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, activeCpuTimeLimitMs?: number, wallClockLimitMs?: number, deterministicFuel?: number, }; diff --git a/packages/runtime-core/src/node-runtime-options-schema.ts b/packages/runtime-core/src/node-runtime-options-schema.ts index 35611a26e7..989b9ea961 100644 --- a/packages/runtime-core/src/node-runtime-options-schema.ts +++ b/packages/runtime-core/src/node-runtime-options-schema.ts @@ -4,11 +4,58 @@ import type { NodeRuntimeCreateOptions } from "./node-runtime.js"; const permissionModeSchema = z.enum(["allow", "deny"]); const stringArray = z.array(z.string()); const vmIdSchema = z.number().int().min(0).max(0xffffffff); +const maxAccountRecordBytes = 4095; +const maxGroupMembers = 256; +const utf8Encoder = new TextEncoder(); +const utf8ByteLength = (value: string) => utf8Encoder.encode(value).byteLength; const vmAccountNameSchema = z .string() .min(1) - .refine((value) => !/[:\s\0]/u.test(value), "Invalid account name"); -const vmGuestPathSchema = z.string().startsWith("/"); + .refine((value) => !/[:,\s\0]/u.test(value), "Invalid account name") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const vmGuestPathSchema = z + .string() + .startsWith("/") + .refine((value) => !/[:\r\n\0]/u.test(value), "Invalid account path") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const vmGecosSchema = z + .string() + .refine((value) => !/[:\r\n\0]/u.test(value), "Invalid GECOS field") + .refine( + (value) => utf8ByteLength(value) <= maxAccountRecordBytes, + `Account text exceeds ${maxAccountRecordBytes} UTF-8 bytes`, + ); +const passwdRecordBytes = (account: { + uid: number; + gid: number; + username: string; + homedir: string; + shell: string; + gecos?: string; +}) => + 7 + + utf8ByteLength(account.username) + + String(account.uid).length + + String(account.gid).length + + utf8ByteLength(account.gecos ?? "") + + utf8ByteLength(account.homedir) + + utf8ByteLength(account.shell); +const groupRecordBytes = (group: { + gid: number; + name: string; + members: string[]; +}) => + 4 + + utf8ByteLength(group.name) + + String(group.gid).length + + group.members.reduce((total, member) => total + utf8ByteLength(member), 0) + + Math.max(0, group.members.length - 1); const vmUserAccountSchema = z .object({ uid: vmIdSchema, @@ -16,33 +63,140 @@ const vmUserAccountSchema = z username: vmAccountNameSchema, homedir: vmGuestPathSchema, shell: vmGuestPathSchema, - gecos: z.string().optional(), + gecos: vmGecosSchema.optional(), supplementaryGids: z.array(vmIdSchema).max(64), }) - .strict(); + .strict() + .superRefine((account, context) => { + if (passwdRecordBytes(account) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered passwd record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + }); const vmGroupSchema = z .object({ gid: vmIdSchema, name: vmAccountNameSchema, - members: z.array(vmAccountNameSchema), + members: z.array(vmAccountNameSchema).max(maxGroupMembers), }) - .strict(); + .strict() + .superRefine((group, context) => { + if (groupRecordBytes(group) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered group record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + }); const vmUserConfigSchema = z .object({ uid: vmIdSchema.optional(), gid: vmIdSchema.optional(), euid: vmIdSchema.optional(), egid: vmIdSchema.optional(), - username: z.string().optional(), - homedir: z.string().optional(), - shell: z.string().optional(), - gecos: z.string().optional(), - groupName: z.string().optional(), + username: vmAccountNameSchema.optional(), + homedir: vmGuestPathSchema.optional(), + shell: vmGuestPathSchema.optional(), + gecos: vmGecosSchema.optional(), + groupName: vmAccountNameSchema.optional(), supplementaryGids: z.array(vmIdSchema).max(64).optional(), accounts: z.array(vmUserAccountSchema).max(64).optional(), groups: z.array(vmGroupSchema).max(128).optional(), }) - .strict(); + .strict() + .superRefine((user, context) => { + const primary = { + uid: user.uid ?? 1000, + gid: user.gid ?? 1000, + username: user.username ?? "agentos", + homedir: user.homedir ?? "/home/agentos", + shell: user.shell ?? "/bin/sh", + gecos: user.gecos, + supplementaryGids: user.supplementaryGids ?? [], + }; + if (passwdRecordBytes(primary) > maxAccountRecordBytes) { + context.addIssue({ + code: "custom", + message: `Rendered passwd record exceeds ${maxAccountRecordBytes} bytes (the 4096-byte ABI buffer includes its terminating NUL)`, + }); + } + + const materialized = new Map(); + const explicitNames = new Map(); + for (const [index, group] of (user.groups ?? []).entries()) { + if (materialized.has(group.gid)) { + context.addIssue({ + code: "custom", + path: ["groups", index, "gid"], + message: `Duplicate user group gid ${group.gid}`, + }); + continue; + } + const previousGid = explicitNames.get(group.name); + if (previousGid !== undefined) { + context.addIssue({ + code: "custom", + path: ["groups", index, "name"], + message: `Duplicate user group name ${group.name}`, + }); + } + explicitNames.set(group.name, group.gid); + materialized.set(group.gid, { + name: group.name, + members: [...group.members], + }); + } + + if (!materialized.has(primary.gid)) { + materialized.set(primary.gid, { + name: user.groupName ?? primary.username, + members: [primary.username], + }); + } + const authoritativeGids = new Set(materialized.keys()); + const effectiveAccounts = new Map( + (user.accounts ?? []).map((account) => [account.uid, account] as const), + ); + effectiveAccounts.set(primary.uid, primary); + for (const account of effectiveAccounts.values()) { + for (const gid of [account.gid, ...account.supplementaryGids]) { + if (authoritativeGids.has(gid)) continue; + const group = materialized.get(gid) ?? { + name: `group${gid}`, + members: [], + }; + if (!group.members.includes(account.username)) { + group.members.push(account.username); + } + materialized.set(gid, group); + } + } + + const gidsByName = new Map(); + for (const [gid, group] of materialized) { + const previousGid = gidsByName.get(group.name); + if (previousGid !== undefined && previousGid !== gid) { + context.addIssue({ + code: "custom", + path: ["groups"], + message: `Materialized user group name ${group.name} maps to both gid ${previousGid} and gid ${gid}; synthesized group names must not collide`, + }); + } + gidsByName.set(group.name, gid); + if ( + group.members.length > maxGroupMembers || + groupRecordBytes({ gid, ...group }) > maxAccountRecordBytes + ) { + context.addIssue({ + code: "custom", + path: ["groups"], + message: `Materialized group exceeds the ${maxGroupMembers}-member or ${maxAccountRecordBytes}-byte account ABI limit`, + }); + } + } + }); const fsPermissionRuleSchema = z .object({ @@ -74,7 +228,10 @@ const patternRulePermissionsSchema = z }) .strict(); -const fsPermissionsSchema = z.union([permissionModeSchema, fsRulePermissionsSchema]); +const fsPermissionsSchema = z.union([ + permissionModeSchema, + fsRulePermissionsSchema, +]); const patternPermissionsSchema = z.union([ permissionModeSchema, patternRulePermissionsSchema, @@ -182,9 +339,7 @@ export const nodeRuntimeCreateOptionsSchema = z mounts: z.array(hostDirectoryMountSchema).optional(), nodeModules: z.union([z.string(), nodeModulesMountSchema]).optional(), bindings: z.record(z.string(), bindingDefinitionSchema).optional(), - loopbackExemptPorts: z - .array(z.number().int().min(0).max(65535)) - .optional(), + loopbackExemptPorts: z.array(z.number().int().min(0).max(65535)).optional(), jsRuntime: jsRuntimeSchema.optional(), }) .strict() as z.ZodType; diff --git a/packages/runtime-core/src/node-runtime.ts b/packages/runtime-core/src/node-runtime.ts index daead327d4..0bdf252eea 100644 --- a/packages/runtime-core/src/node-runtime.ts +++ b/packages/runtime-core/src/node-runtime.ts @@ -495,6 +495,7 @@ export interface NodeRuntimeProcess { export interface NodeRuntimeResourceSnapshot { runningProcesses: number; + stoppedProcesses: number; exitedProcesses: number; fdTables: number; openFds: number; diff --git a/packages/runtime-core/src/response-payloads.ts b/packages/runtime-core/src/response-payloads.ts index ba7c6fd44e..216ced2222 100644 --- a/packages/runtime-core/src/response-payloads.ts +++ b/packages/runtime-core/src/response-payloads.ts @@ -50,6 +50,7 @@ export interface LiveQueueSnapshotEntry { export interface LiveResourceSnapshot { running_processes: number; + stopped_processes: number; exited_processes: number; fd_tables: number; open_fds: number; @@ -432,6 +433,10 @@ export function fromGeneratedResponsePayload( payload.val.runningProcesses, "resource_snapshot.running_processes", ), + stopped_processes: bigIntToSafeNumber( + payload.val.stoppedProcesses, + "resource_snapshot.stopped_processes", + ), exited_processes: bigIntToSafeNumber( payload.val.exitedProcesses, "resource_snapshot.exited_processes", diff --git a/packages/runtime-core/src/sidecar-process.ts b/packages/runtime-core/src/sidecar-process.ts index f997f74750..f1995ae45b 100644 --- a/packages/runtime-core/src/sidecar-process.ts +++ b/packages/runtime-core/src/sidecar-process.ts @@ -132,6 +132,7 @@ export interface SidecarQueueSnapshotEntry { export interface SidecarResourceSnapshot { runningProcesses: number; + stoppedProcesses: number; exitedProcesses: number; fdTables: number; openFds: number; @@ -1441,6 +1442,7 @@ export class SidecarProcess { } return { runningProcesses: response.payload.running_processes, + stoppedProcesses: response.payload.stopped_processes, exitedProcesses: response.payload.exited_processes, fdTables: response.payload.fd_tables, openFds: response.payload.open_fds, diff --git a/packages/runtime-core/src/test-runtime.ts b/packages/runtime-core/src/test-runtime.ts index 8122008117..b18c8d052d 100644 --- a/packages/runtime-core/src/test-runtime.ts +++ b/packages/runtime-core/src/test-runtime.ts @@ -516,6 +516,7 @@ export interface Kernel extends KernelInterface { registerBindings(bindings: Record): Promise; getResourceSnapshot(): Promise<{ runningProcesses: number; + stoppedProcesses: number; exitedProcesses: number; fdTables: number; openFds: number; @@ -2077,9 +2078,9 @@ function createBootstrapEntries(commandNames: string[]): RootFilesystemEntry[] { ? 0o2755 : 0o755, uid: - entryPath === "/home/agentos" || entryPath === "/workspace" ? 1000 : 0, + entryPath === "/workspace" || entryPath === "/home/agentos" ? 1000 : 0, gid: - entryPath === "/home/agentos" || entryPath === "/workspace" ? 1000 : 0, + entryPath === "/workspace" || entryPath === "/home/agentos" ? 1000 : 0, })), { path: "/usr/bin/env", diff --git a/packages/runtime-core/tests/node-runtime-options-schema.test.ts b/packages/runtime-core/tests/node-runtime-options-schema.test.ts index c85fecb2c3..2e61818a0a 100644 --- a/packages/runtime-core/tests/node-runtime-options-schema.test.ts +++ b/packages/runtime-core/tests/node-runtime-options-schema.test.ts @@ -1,8 +1,5 @@ import { describe, expect, test } from "vitest"; -import { - NodeRuntime, - nodeRuntimeCreateOptionsSchema, -} from "../src/index.js"; +import { NodeRuntime, nodeRuntimeCreateOptionsSchema } from "../src/index.js"; import { createInMemoryFileSystem } from "../src/test-runtime.js"; describe("NodeRuntime create options validation", () => { @@ -25,4 +22,54 @@ describe("NodeRuntime create options validation", () => { }), ).toThrow(/filesystem/); }); + + test("bounds and materializes Linux account records", () => { + const filesystem = createInMemoryFileSystem(); + const exactPasswdRecord = { + uid: 0, + gid: 0, + username: "u", + homedir: "/", + shell: "/", + gecos: "x".repeat(4083), + }; + expect( + nodeRuntimeCreateOptionsSchema.safeParse({ + filesystem, + user: exactPasswdRecord, + }).success, + ).toBe(true); + expect( + nodeRuntimeCreateOptionsSchema.safeParse({ + filesystem, + user: { ...exactPasswdRecord, gecos: "😀".repeat(1021) }, + }).success, + ).toBe(false); + expect( + nodeRuntimeCreateOptionsSchema.safeParse({ + filesystem, + user: { + uid: 0, + gid: 0, + username: "root", + supplementaryGids: [44], + groups: [{ gid: 99, name: "group44", members: [] }], + }, + }).success, + ).toBe(false); + expect( + nodeRuntimeCreateOptionsSchema.safeParse({ + filesystem, + user: { + groups: [ + { + gid: 7, + name: "g", + members: Array.from({ length: 257 }, (_, index) => `m${index}`), + }, + ], + }, + }).success, + ).toBe(false); + }); }); diff --git a/packages/runtime-core/tests/response-payloads.test.ts b/packages/runtime-core/tests/response-payloads.test.ts index a4d67b88b5..23ade0c93a 100644 --- a/packages/runtime-core/tests/response-payloads.test.ts +++ b/packages/runtime-core/tests/response-payloads.test.ts @@ -210,6 +210,7 @@ describe("response payload conversion", () => { tag: "ResourceSnapshotResponse", val: { runningProcesses: 2n, + stoppedProcesses: 3n, exitedProcesses: 1n, fdTables: 2n, openFds: 6n, @@ -238,6 +239,7 @@ describe("response payload conversion", () => { ).toEqual({ type: "resource_snapshot", running_processes: 2, + stopped_processes: 3, exited_processes: 1, fd_tables: 2, open_fds: 6, diff --git a/packages/runtime-core/tests/test-runtime-bootstrap.test.ts b/packages/runtime-core/tests/test-runtime-bootstrap.test.ts new file mode 100644 index 0000000000..1d6fbc913a --- /dev/null +++ b/packages/runtime-core/tests/test-runtime-bootstrap.test.ts @@ -0,0 +1,48 @@ +import { resolve } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import { + allowAll, + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + type Kernel, +} from "../src/test-runtime.js"; + +describe("test runtime bootstrap ownership", () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }); + + test("matches production ownership for guest-writable home and workspace", async () => { + const filesystem = createInMemoryFileSystem(); + kernel = createKernel({ filesystem, permissions: allowAll }); + await kernel.mount( + createWasmVmRuntime({ + commandDirs: [resolve(import.meta.dirname, "../commands")], + }), + ); + + const root = await filesystem.stat("/"); + const home = await filesystem.stat("/home/agentos"); + const workspace = await filesystem.stat("/workspace"); + + expect({ uid: root.uid, gid: root.gid, mode: root.mode & 0o7777 }).toEqual({ + uid: 0, + gid: 0, + mode: 0o755, + }); + expect({ uid: home.uid, gid: home.gid, mode: home.mode & 0o7777 }).toEqual({ + uid: 1000, + gid: 1000, + mode: 0o2755, + }); + expect({ + uid: workspace.uid, + gid: workspace.gid, + mode: workspace.mode & 0o7777, + }).toEqual({ uid: 1000, gid: 1000, mode: 0o755 }); + }); +}); diff --git a/scripts/audit-wasm-imports.mjs b/scripts/audit-wasm-imports.mjs new file mode 100644 index 0000000000..5a53e1fceb --- /dev/null +++ b/scripts/audit-wasm-imports.mjs @@ -0,0 +1,228 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'; +import { basename, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(fileURLToPath(new URL('..', import.meta.url))); +const defaultCommandsDir = resolve( + root, + 'toolchain/target/wasm32-wasip1/release/commands', +); +const defaultManifestPath = resolve( + root, + 'crates/execution/assets/agentos-wasm-abi.json', +); +const expectedManifestSchemaVersion = 2; +const allowedImportStatuses = new Set(['canonical', 'compatibility']); + +const args = process.argv.slice(2); +const printObserved = args.includes('--print-observed'); +const printContract = args.includes('--print-contract'); +const jsonOutput = args.includes('--json'); + +function option(name, fallback) { + const index = args.indexOf(name); + if (index === -1) return fallback; + if (index + 1 >= args.length) throw new Error(`${name} requires a value`); + return resolve(process.cwd(), args[index + 1]); +} + +const commandsDir = option('--commands', defaultCommandsDir); +const manifestPath = option('--manifest', defaultManifestPath); + +function signatureFromImportLine(line, command) { + const header = line.match(/^\s*\(import\s+"([^"]+)"\s+"([^"]+)"\s+(.+)\)\s*$/); + if (!header) return null; + const [, module, name, declaration] = header; + if (!declaration.startsWith('(func ')) { + throw new Error( + `${command}: non-function import ${module}.${name} is outside the AgentOS ABI`, + ); + } + + const params = [...declaration.matchAll(/\(param(?:\s+\$[^\s)]+)?\s+([^)]+)\)/g)] + .flatMap((match) => match[1].trim().split(/\s+/)); + const results = [...declaration.matchAll(/\(result\s+([^)]+)\)/g)] + .flatMap((match) => match[1].trim().split(/\s+/)); + for (const type of [...params, ...results]) { + if (!['i32', 'i64', 'f32', 'f64', 'v128', 'funcref', 'externref'].includes(type)) { + throw new Error(`${command}: unsupported import value type ${type}`); + } + } + return { module, name, params, results }; +} + +function inspectCommand(path, command) { + const bytes = readFileSync(path); + if (bytes.length < 8 || bytes.subarray(0, 4).toString('hex') !== '0061736d') { + throw new Error(`${command}: expected a WebAssembly binary`); + } + const wat = execFileSync('wasm-dis', [path, '-o', '-'], { + encoding: 'utf8', + maxBuffer: 512 * 1024 * 1024, + }); + const imports = wat + .split('\n') + .filter((line) => /^\s*\(import\s/.test(line)) + .map((line) => signatureFromImportLine(line, command)); + if (imports.some((entry) => entry === null)) { + throw new Error(`${command}: failed to parse one or more WebAssembly imports`); + } + imports.sort((a, b) => + `${a.module}\0${a.name}`.localeCompare(`${b.module}\0${b.name}`), + ); + return { + command, + target: basename(realpathSync(path)), + sha256: createHash('sha256').update(bytes).digest('hex'), + imports, + }; +} + +function importKey(entry) { + return `${entry.module}.${entry.name}`; +} + +function signatureText(entry) { + return `(${entry.params.join(',')}) -> (${entry.results.join(',')})`; +} + +function collectCommands() { + let entries; + try { + entries = readdirSync(commandsDir, { withFileTypes: true }); + } catch (error) { + throw new Error( + `canonical command directory is unavailable (${relative(root, commandsDir)}); run just tools-rebuild first: ${error.message}`, + ); + } + const commandNames = entries + .filter((entry) => entry.isFile() || entry.isSymbolicLink()) + .map((entry) => entry.name) + .sort(); + if (commandNames.length === 0) { + throw new Error(`no commands found in ${relative(root, commandsDir)}`); + } + return commandNames.map((command) => { + const path = resolve(commandsDir, command); + if (!statSync(path).isFile()) { + throw new Error(`${command}: command target is not a file`); + } + return inspectCommand(path, command); + }); +} + +function collectObserved(commands) { + const observed = new Map(); + for (const command of commands) { + for (const entry of command.imports) { + const key = importKey(entry); + const prior = observed.get(key); + if (prior && signatureText(prior) !== signatureText(entry)) { + throw new Error( + `${key} has conflicting signatures: ${signatureText(prior)} vs ${signatureText(entry)} in ${command.command}`, + ); + } + const record = prior ?? { ...entry, commands: [] }; + record.commands.push(command.command); + observed.set(key, record); + } + } + return [...observed.values()].sort((a, b) => + importKey(a).localeCompare(importKey(b)), + ); +} + +function loadManifest() { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + if ( + manifest.schemaVersion !== expectedManifestSchemaVersion || + !Array.isArray(manifest.imports) + ) { + throw new Error( + `ABI manifest must have schemaVersion ${expectedManifestSchemaVersion} and an imports array`, + ); + } + const declared = new Map(); + for (const entry of manifest.imports) { + if ( + typeof entry.module !== 'string' || + typeof entry.name !== 'string' || + !Array.isArray(entry.params) || + !Array.isArray(entry.results) || + !allowedImportStatuses.has(entry.status) + ) { + throw new Error( + 'every ABI import requires a known status, module, name, params, and results', + ); + } + const key = importKey(entry); + if (declared.has(key)) throw new Error(`duplicate ABI declaration ${key}`); + declared.set(key, entry); + } + const aliases = new Map(Object.entries(manifest.moduleAliases ?? {})); + for (const [alias, canonical] of aliases) { + if (alias === canonical) throw new Error(`ABI module alias ${alias} is self-referential`); + } + return { manifest, declared, aliases }; +} + +function verifyObserved(observed, declared, aliases) { + const failures = []; + for (const entry of observed) { + const key = importKey(entry); + const canonicalModule = aliases.get(entry.module) ?? entry.module; + const contract = declared.get(`${canonicalModule}.${entry.name}`); + if (!contract) { + failures.push(`${key}: undeclared import (${signatureText(entry)})`); + continue; + } + if (signatureText(contract) !== signatureText(entry)) { + failures.push( + `${key}: expected ${signatureText(contract)}, observed ${signatureText(entry)}`, + ); + } + } + if (failures.length > 0) { + throw new Error(`WASM import audit failed:\n- ${failures.join('\n- ')}`); + } +} + +try { + const commands = collectCommands(); + const observed = collectObserved(commands); + if (printContract) { + const contract = observed.map(({ commands: _commands, ...entry }) => entry); + process.stdout.write(`${JSON.stringify(contract)}\n`); + process.exit(0); + } + if (printObserved) { + process.stdout.write(`${JSON.stringify(observed, null, 2)}\n`); + process.exit(0); + } + + const { manifest, declared, aliases } = loadManifest(); + verifyObserved(observed, declared, aliases); + const distinctTargets = new Set(commands.map((entry) => entry.target)).size; + const evidence = { + schemaVersion: manifest.schemaVersion, + abiVersion: manifest.abiVersion, + commandEntries: commands.length, + distinctModules: distinctTargets, + observedImports: observed.length, + commands, + }; + if (jsonOutput) { + process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`); + } else { + process.stdout.write( + `WASM import audit passed: ${commands.length} commands, ${distinctTargets} modules, ${observed.length} imports\n`, + ); + } +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +} diff --git a/scripts/generate-wasm-abi-manifest.mjs b/scripts/generate-wasm-abi-manifest.mjs new file mode 100644 index 0000000000..1d9b423e9e --- /dev/null +++ b/scripts/generate-wasm-abi-manifest.mjs @@ -0,0 +1,866 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(fileURLToPath(new URL('..', import.meta.url))); +const outputPath = resolve(root, 'crates/execution/assets/agentos-wasm-abi.json'); +const registryOutputPath = resolve(root, 'crates/execution/src/abi/generated.rs'); +const preview1WitxPath = resolve( + root, + 'crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx', +); +const preview1TypesPath = resolve( + root, + 'crates/execution/abi/wasi_snapshot_preview1/typenames.witx', +); + +const definitions = []; + +function define(module, name, signature, status = 'canonical') { + const [paramsText, resultsText = ''] = signature.split('->').map((part) => part.trim()); + definitions.push({ + module, + name, + params: paramsText === '' ? [] : paramsText.split(/\s+/), + results: resultsText === '' ? [] : resultsText.split(/\s+/), + status, + }); +} + +function defineMany(module, entries) { + for (const [name, signature, status] of entries) { + define(module, name, signature, status); + } +} + +const preview1Selection = [ + ['args_get'], + ['args_sizes_get'], + ['clock_res_get'], + ['clock_time_get'], + ['environ_get'], + ['environ_sizes_get'], + ['fd_allocate', 'compatibility'], + ['fd_close'], + ['fd_datasync'], + ['fd_fdstat_get'], + ['fd_fdstat_set_flags'], + ['fd_filestat_get'], + ['fd_filestat_set_size'], + ['fd_filestat_set_times', 'compatibility'], + ['fd_pread'], + ['fd_prestat_dir_name'], + ['fd_prestat_get'], + ['fd_pwrite'], + ['fd_read'], + ['fd_readdir'], + ['fd_renumber', 'compatibility'], + ['fd_seek'], + ['fd_sync'], + ['fd_tell'], + ['fd_write'], + ['path_create_directory'], + ['path_filestat_get'], + ['path_filestat_set_times', 'compatibility'], + ['path_link'], + ['path_open'], + ['path_readlink'], + ['path_remove_directory'], + ['path_rename'], + ['path_symlink'], + ['path_unlink_file'], + ['poll_oneoff'], + ['proc_exit'], + ['random_get'], + ['sched_yield'], + ['sock_shutdown', 'compatibility'], +]; + +const loweredPreview1 = JSON.parse( + execFileSync( + 'cargo', + [ + 'run', + '--quiet', + '-p', + 'agentos-wasm-abi-generator', + '--', + preview1WitxPath, + ], + { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, + ), +); +if (loweredPreview1.module !== 'wasi_snapshot_preview1') { + throw new Error(`unexpected pinned WITX module ${loweredPreview1.module}`); +} +const loweredPreview1Imports = new Map( + loweredPreview1.imports.map((entry) => [entry.name, entry]), +); +for (const [name, status = 'canonical'] of preview1Selection) { + const entry = loweredPreview1Imports.get(name); + if (entry == null) { + throw new Error(`pinned Preview1 WITX is missing selected import ${name}`); + } + definitions.push({ + module: 'wasi_snapshot_preview1', + name, + params: entry.params, + results: entry.results, + status, + }); +} + +defineMany('host_fs', [ + ['open_tmpfile', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_link', 'i32 i32 i32 i32 -> i32'], + ['remount', 'i32 i32 i32 i32 -> i32'], + ['path_mknod', 'i32 i32 i32 i32 i64 -> i32'], + ['path_renameat2', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['path_statfs', 'i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_fiemap', 'i32 i32 i32 i32 i32 -> i32'], + ['fd_punch_hole', 'i32 i64 i64 -> i32'], + ['fd_zero_range', 'i32 i64 i64 i32 -> i32'], + ['fd_insert_range', 'i32 i64 i64 -> i32'], + ['fd_collapse_range', 'i32 i64 i64 -> i32'], + ['set_open_mode', 'i32 -> i32', 'compatibility'], + ['set_open_direct', 'i32 -> i32', 'compatibility'], + ['path_owner', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['path_mode', 'i32 i32 i32 i32 -> i32'], + ['path_size', 'i32 i32 i32 i32 -> i64'], + ['path_blocks', 'i32 i32 i32 i32 -> i64'], + ['path_rdev', 'i32 i32 i32 i32 -> i64'], + ['fd_owner', 'i32 i32 i32 -> i32'], + ['fd_mode', 'i32 -> i32'], + ['fd_size', 'i32 -> i64'], + ['fd_blocks', 'i32 -> i64'], + ['path_access', 'i32 i32 i32 i32 i32 -> i32'], + ['path_chown', 'i32 i32 i32 i32 i32 i32 -> i32', 'compatibility'], + ['fd_chown', 'i32 i32 i32 -> i32', 'compatibility'], + ['chown', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['fchown', 'i32 i32 i32 -> i32'], + ['chmod', 'i32 i32 i32 i32 -> i32'], + ['fchmod', 'i32 i32 -> i32'], + ['path_getxattr', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['path_listxattr', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['path_setxattr', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['path_removexattr', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_getxattr', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_listxattr', 'i32 i32 i32 i32 -> i32'], + ['fd_setxattr', 'i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_removexattr', 'i32 i32 i32 -> i32'], + ['ftruncate', 'i32 i64 -> i32', 'compatibility'], +]); + +defineMany('host_net', [ + ['net_socket', 'i32 i32 i32 i32 -> i32'], + ['net_set_nonblock', 'i32 i32 -> i32'], + ['net_connect', 'i32 i32 i32 -> i32'], + ['net_getaddrinfo', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['net_dns_query_rr_v1', 'i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['net_bind', 'i32 i32 i32 -> i32'], + ['net_listen', 'i32 i32 -> i32'], + ['net_accept', 'i32 i32 i32 i32 -> i32'], + ['net_validate_socket', 'i32 -> i32', 'compatibility'], + ['net_validate_accept', 'i32 -> i32', 'compatibility'], + ['net_getsockname', 'i32 i32 i32 -> i32'], + ['net_getpeername', 'i32 i32 i32 -> i32'], + ['net_send', 'i32 i32 i32 i32 i32 -> i32'], + ['net_recv', 'i32 i32 i32 i32 i32 -> i32'], + ['net_sendto', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['net_recvfrom', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['net_setsockopt', 'i32 i32 i32 i32 i32 -> i32'], + ['net_getsockopt', 'i32 i32 i32 i32 i32 -> i32'], + ['net_poll', 'i32 i32 i32 i32 -> i32'], + ['net_close', 'i32 -> i32', 'compatibility'], + ['net_tls_connect', 'i32 i32 i32 -> i32'], +]); + +defineMany('host_process', [ + ['proc_spawn', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32', 'compatibility'], + ['proc_spawn_v2', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32', 'compatibility'], + ['proc_spawn_v3', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32', 'compatibility'], + ['proc_spawn_v4', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['proc_exec', 'i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['proc_fexec', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['proc_waitpid', 'i32 i32 i32 i32 -> i32', 'compatibility'], + ['proc_waitpid_v2', 'i32 i32 i32 i32 i32 i32 -> i32', 'compatibility'], + ['proc_waitpid_v3', 'i32 i32 i32 i32 -> i32'], + ['proc_kill', 'i32 i32 -> i32'], + ['proc_getpid', 'i32 -> i32'], + ['proc_getppid', 'i32 -> i32'], + ['proc_getrlimit', 'i32 i32 i32 -> i32'], + ['proc_setrlimit', 'i32 i64 i64 -> i32'], + ['proc_umask', 'i32 i32 -> i32'], + ['umask', 'i32 i32 i32 -> i32', 'compatibility'], + ['proc_itimer_real', 'i32 i64 i64 i32 i32 -> i32'], + ['proc_getpgid', 'i32 i32 -> i32'], + ['proc_setpgid', 'i32 i32 -> i32'], + ['fd_pipe', 'i32 i32 -> i32'], + ['fd_dup', 'i32 i32 -> i32'], + ['fd_dup2', 'i32 i32 -> i32'], + ['fd_dup_min', 'i32 i32 i32 -> i32'], + ['fd_getfd', 'i32 i32 -> i32'], + ['fd_setfd', 'i32 i32 -> i32'], + ['fd_flock', 'i32 i32 -> i32'], + ['fd_record_lock', 'i32 i32 i32 i64 i64 i32 i32 i32 i32 -> i32'], + ['proc_closefrom', 'i32 -> i32'], + ['fd_socketpair', 'i32 i32 i32 i32 i32 -> i32'], + ['fd_sendmsg_rights', 'i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['fd_recvmsg_rights', 'i32 i32 i32 i32 i32 i32 i32 i32 i32 -> i32'], + ['sleep_ms', 'i32 -> i32', 'compatibility'], + ['pty_open', 'i32 i32 -> i32'], + ['proc_sigaction', 'i32 i32 i32 i32 i32 -> i32'], + ['proc_signal_mask_v2', 'i32 i32 i32 i32 i32 -> i32'], + ['proc_ppoll_v1', 'i32 i32 i64 i64 i32 i32 i32 i32 -> i32'], +]); + +defineMany('host_tty', [ + ['read', 'i32 i32 i32 -> i32', 'compatibility'], + ['isatty', 'i32 -> i32', 'compatibility'], + ['get_size', 'i32 i32 i32 -> i32', 'compatibility'], + ['set_size', 'i32 i32 i32 -> i32'], + ['get_attr', 'i32 i32 i32 -> i32'], + ['set_attr', 'i32 i32 i32 -> i32'], + ['get_pgrp', 'i32 i32 -> i32'], + ['set_pgrp', 'i32 i32 -> i32'], + ['get_sid', 'i32 i32 -> i32'], + ['set_raw_mode', 'i32 -> i32', 'compatibility'], +]); + +defineMany('host_user', [ + ['getuid', 'i32 -> i32'], + ['getgid', 'i32 -> i32'], + ['geteuid', 'i32 -> i32'], + ['getegid', 'i32 -> i32'], + ['getresuid', 'i32 i32 i32 -> i32'], + ['getresgid', 'i32 i32 i32 -> i32'], + ['setuid', 'i32 -> i32'], + ['seteuid', 'i32 -> i32'], + ['setreuid', 'i32 i32 -> i32'], + ['setresuid', 'i32 i32 i32 -> i32'], + ['setgid', 'i32 -> i32'], + ['setegid', 'i32 -> i32'], + ['setregid', 'i32 i32 -> i32'], + ['setresgid', 'i32 i32 i32 -> i32'], + ['getgroups', 'i32 i32 i32 -> i32'], + ['setgroups', 'i32 i32 -> i32'], + ['getpwuid', 'i32 i32 i32 i32 -> i32'], + ['getpwnam', 'i32 i32 i32 i32 i32 -> i32'], + ['getpwent', 'i32 i32 i32 i32 -> i32'], + ['getgrgid', 'i32 i32 i32 i32 -> i32'], + ['getgrnam', 'i32 i32 i32 i32 i32 -> i32'], + ['getgrent', 'i32 i32 i32 i32 -> i32'], + ['isatty', 'i32 i32 -> i32', 'compatibility'], +]); + +defineMany('host_system', [ + ['get_identity', 'i32 i32 i32 -> i32'], +]); + +definitions.sort((a, b) => `${a.module}\0${a.name}`.localeCompare(`${b.module}\0${b.name}`)); + +const allPermissionTiers = ['isolated', 'read-only', 'read-write', 'full']; +const moduleAliases = { + wasi_unstable: 'wasi_snapshot_preview1', +}; +const modulePolicy = { + wasi_snapshot_preview1: allPermissionTiers, + wasi_unstable: allPermissionTiers, + host_fs: allPermissionTiers, + host_user: allPermissionTiers, + host_tty: allPermissionTiers, + host_system: allPermissionTiers, + host_net: ['full'], + host_process: ['full'], +}; +const importPolicyOverrides = { + 'host_process.fd_dup_min': ['read-only', 'read-write', 'full'], + 'host_process.fd_flock': ['read-only', 'read-write', 'full'], + 'host_process.fd_getfd': ['read-only', 'read-write', 'full'], + 'host_process.fd_record_lock': ['read-only', 'read-write', 'full'], + 'host_process.fd_setfd': ['read-only', 'read-write', 'full'], + 'host_process.proc_getrlimit': ['read-only', 'read-write', 'full'], + 'host_process.proc_setrlimit': ['read-only', 'read-write', 'full'], + 'host_process.proc_umask': ['read-only', 'read-write', 'full'], + 'host_process.umask': ['read-only', 'read-write', 'full'], +}; + +function importKey(module, name) { + return `${module}.${name}`; +} + +function importKeys(module, names) { + return names.map((name) => importKey(module, name)); +} + +function pascalCase(value) { + return value + .split(/[^A-Za-z0-9]+/u) + .filter(Boolean) + .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`) + .join(''); +} + +function coreValueRun(values, empty) { + if (values.length === 0) return empty; + if (values.every((value) => value === values[0])) { + const value = values[0].toUpperCase(); + return values.length === 1 ? value : `${value}x${values.length}`; + } + return values.map((value) => value.toUpperCase()).join(''); +} + +function coreSignatureId(params, results) { + return `${coreValueRun(params, 'NoParams')}To${coreValueRun(results, 'NoResults')}`; +} + +function groupMap(groups, defaultId) { + const values = new Map(); + for (const [id, keys] of groups) { + for (const key of keys) { + if (values.has(key)) { + throw new Error(`semantic binding ${key} appears in multiple groups`); + } + values.set(key, id); + } + } + return (definition) => values.get(importKey(definition.module, definition.name)) ?? defaultId(definition); +} + +const handlerId = groupMap([ + ['ProcessArguments', importKeys('wasi_snapshot_preview1', ['args_get', 'args_sizes_get'])], + ['ProcessEnvironment', importKeys('wasi_snapshot_preview1', ['environ_get', 'environ_sizes_get'])], + ['ClockSnapshot', importKeys('wasi_snapshot_preview1', ['clock_res_get', 'clock_time_get'])], + ['DescriptorClose', [ + importKey('wasi_snapshot_preview1', 'fd_close'), + importKey('host_net', 'net_close'), + ]], + ['DescriptorSync', importKeys('wasi_snapshot_preview1', ['fd_datasync', 'fd_sync'])], + ['DescriptorStatusFlags', [ + importKey('wasi_snapshot_preview1', 'fd_fdstat_get'), + importKey('wasi_snapshot_preview1', 'fd_fdstat_set_flags'), + importKey('host_net', 'net_set_nonblock'), + ]], + ['DescriptorMetadata', [ + importKey('wasi_snapshot_preview1', 'fd_filestat_get'), + ...importKeys('host_fs', ['fd_owner', 'fd_mode', 'fd_size', 'fd_blocks']), + ]], + ['DescriptorSetLength', [ + importKey('wasi_snapshot_preview1', 'fd_filestat_set_size'), + importKey('host_fs', 'ftruncate'), + ]], + ['MetadataSetTimes', importKeys('wasi_snapshot_preview1', [ + 'fd_filestat_set_times', + 'path_filestat_set_times', + ])], + ['DescriptorRead', importKeys('wasi_snapshot_preview1', ['fd_pread', 'fd_read'])], + ['DescriptorWrite', importKeys('wasi_snapshot_preview1', ['fd_pwrite', 'fd_write'])], + ['DescriptorSeek', importKeys('wasi_snapshot_preview1', ['fd_seek', 'fd_tell'])], + ['Preopen', importKeys('wasi_snapshot_preview1', ['fd_prestat_dir_name', 'fd_prestat_get'])], + ['ExtentRange', [ + importKey('wasi_snapshot_preview1', 'fd_allocate'), + ...importKeys('host_fs', [ + 'fd_punch_hole', + 'fd_zero_range', + 'fd_insert_range', + 'fd_collapse_range', + ]), + ]], + ['PathMetadata', [ + importKey('wasi_snapshot_preview1', 'path_filestat_get'), + ...importKeys('host_fs', ['path_owner', 'path_mode', 'path_size', 'path_blocks', 'path_rdev']), + ]], + ['PathRemove', importKeys('wasi_snapshot_preview1', ['path_remove_directory', 'path_unlink_file'])], + ['PathRename', [ + importKey('wasi_snapshot_preview1', 'path_rename'), + importKey('host_fs', 'path_renameat2'), + ]], + ['ProcessPoll', [ + importKey('wasi_snapshot_preview1', 'poll_oneoff'), + importKey('host_net', 'net_poll'), + importKey('host_process', 'proc_ppoll_v1'), + ]], + ['ProcessSpawn', importKeys('host_process', ['proc_spawn', 'proc_spawn_v2', 'proc_spawn_v3', 'proc_spawn_v4'])], + ['ProcessExec', importKeys('host_process', ['proc_exec', 'proc_fexec'])], + ['ProcessWait', importKeys('host_process', ['proc_waitpid', 'proc_waitpid_v2', 'proc_waitpid_v3'])], + ['ProcessUmask', importKeys('host_process', ['proc_umask', 'umask'])], + ['ProcessGroup', importKeys('host_process', ['proc_getpgid', 'proc_setpgid'])], + ['DescriptorDuplicate', importKeys('host_process', ['fd_dup', 'fd_dup2', 'fd_dup_min'])], + ['DescriptorFlags', importKeys('host_process', ['fd_getfd', 'fd_setfd'])], + ['DescriptorLock', importKeys('host_process', ['fd_flock', 'fd_record_lock'])], + ['DescriptorRights', importKeys('host_process', ['fd_sendmsg_rights', 'fd_recvmsg_rights'])], + ['NetworkValidate', importKeys('host_net', ['net_validate_socket', 'net_validate_accept'])], + ['NetworkAddress', importKeys('host_net', ['net_getsockname', 'net_getpeername'])], + ['NetworkSend', importKeys('host_net', ['net_send', 'net_sendto'])], + ['NetworkReceive', importKeys('host_net', ['net_recv', 'net_recvfrom'])], + ['NetworkOption', importKeys('host_net', ['net_setsockopt', 'net_getsockopt'])], + ['MetadataOwnership', importKeys('host_fs', ['path_chown', 'fd_chown', 'chown', 'fchown'])], + ['MetadataMode', importKeys('host_fs', ['chmod', 'fchmod'])], + ['PathXattr', importKeys('host_fs', [ + 'path_getxattr', + 'path_listxattr', + 'path_setxattr', + 'path_removexattr', + ])], + ['DescriptorXattr', importKeys('host_fs', [ + 'fd_getxattr', + 'fd_listxattr', + 'fd_setxattr', + 'fd_removexattr', + ])], + ['IdentitySnapshot', importKeys('host_user', [ + 'getuid', + 'getgid', + 'geteuid', + 'getegid', + 'getresuid', + 'getresgid', + ])], + ['IdentityCredentials', importKeys('host_user', [ + 'setuid', + 'seteuid', + 'setreuid', + 'setresuid', + 'setgid', + 'setegid', + 'setregid', + 'setresgid', + ])], + ['AccountPassword', importKeys('host_user', ['getpwuid', 'getpwnam', 'getpwent'])], + ['AccountGroup', importKeys('host_user', ['getgrgid', 'getgrnam', 'getgrent'])], + ['TerminalIsatty', [importKey('host_user', 'isatty'), importKey('host_tty', 'isatty')]], + ['TerminalSize', importKeys('host_tty', ['get_size', 'set_size'])], + ['TerminalAttributes', importKeys('host_tty', ['get_attr', 'set_attr'])], + ['TerminalProcessGroup', importKeys('host_tty', ['get_pgrp', 'set_pgrp'])], +], (definition) => pascalCase(`${definition.module}_${definition.name}`)); + +const decodeId = groupMap([ + ['Fd', [ + importKey('wasi_snapshot_preview1', 'fd_close'), + importKey('wasi_snapshot_preview1', 'fd_datasync'), + importKey('wasi_snapshot_preview1', 'fd_sync'), + importKey('host_net', 'net_close'), + importKey('host_net', 'net_validate_socket'), + importKey('host_net', 'net_validate_accept'), + importKey('host_tty', 'isatty'), + ]], + ['FdSetLength', [ + importKey('wasi_snapshot_preview1', 'fd_filestat_set_size'), + importKey('host_fs', 'ftruncate'), + ]], + ['PathOwnership', importKeys('host_fs', ['path_chown', 'chown'])], + ['DescriptorOwnership', importKeys('host_fs', ['fd_chown', 'fchown'])], + ['NetworkAddressOutput', importKeys('host_net', ['net_getsockname', 'net_getpeername'])], + ['IdentityScalarOutput', importKeys('host_user', ['getuid', 'getgid', 'geteuid', 'getegid'])], + ['IdentityTripleOutput', importKeys('host_user', ['getresuid', 'getresgid'])], + ['IdentitySetOne', importKeys('host_user', ['setuid', 'seteuid', 'setgid', 'setegid'])], + ['IdentitySetTwo', importKeys('host_user', ['setreuid', 'setregid'])], + ['IdentitySetThree', importKeys('host_user', ['setresuid', 'setresgid'])], + ['AccountById', importKeys('host_user', ['getpwuid', 'getgrgid'])], + ['AccountByName', importKeys('host_user', ['getpwnam', 'getgrnam'])], + ['AccountByIndex', importKeys('host_user', ['getpwent', 'getgrent'])], + ['TerminalU32Output', importKeys('host_tty', ['get_pgrp', 'get_sid'])], +], (definition) => pascalCase(`${definition.module}_${definition.name}`)); + +const prevalidateOutputKeys = new Set([ + ...importKeys('wasi_snapshot_preview1', [ + 'args_get', + 'args_sizes_get', + 'clock_res_get', + 'clock_time_get', + 'environ_get', + 'environ_sizes_get', + 'fd_fdstat_get', + 'fd_filestat_get', + 'fd_pread', + 'fd_prestat_dir_name', + 'fd_prestat_get', + 'fd_pwrite', + 'fd_read', + 'fd_readdir', + 'fd_seek', + 'fd_tell', + 'fd_write', + 'path_filestat_get', + 'path_open', + 'path_readlink', + 'poll_oneoff', + 'random_get', + ]), + ...importKeys('host_fs', [ + 'open_tmpfile', + 'path_statfs', + 'fd_fiemap', + 'path_owner', + 'fd_owner', + 'path_getxattr', + 'path_listxattr', + 'fd_getxattr', + 'fd_listxattr', + ]), + ...importKeys('host_net', [ + 'net_socket', + 'net_getaddrinfo', + 'net_dns_query_rr_v1', + 'net_accept', + 'net_getsockname', + 'net_getpeername', + 'net_send', + 'net_recv', + 'net_sendto', + 'net_recvfrom', + 'net_getsockopt', + 'net_poll', + ]), + ...importKeys('host_process', [ + 'proc_spawn', + 'proc_spawn_v2', + 'proc_spawn_v3', + 'proc_spawn_v4', + 'proc_waitpid', + 'proc_waitpid_v2', + 'proc_waitpid_v3', + 'proc_getpid', + 'proc_getppid', + 'proc_getrlimit', + 'proc_umask', + 'umask', + 'proc_itimer_real', + 'proc_getpgid', + 'fd_pipe', + 'fd_dup', + 'fd_dup_min', + 'fd_getfd', + 'fd_record_lock', + 'fd_socketpair', + 'fd_sendmsg_rights', + 'fd_recvmsg_rights', + 'pty_open', + 'proc_signal_mask_v2', + 'proc_ppoll_v1', + ]), + ...importKeys('host_tty', ['read', 'get_size', 'get_attr', 'get_pgrp', 'get_sid']), + ...importKeys('host_user', [ + 'getuid', + 'getgid', + 'geteuid', + 'getegid', + 'getresuid', + 'getresgid', + 'getgroups', + 'getpwuid', + 'getpwnam', + 'getpwent', + 'getgrgid', + 'getgrnam', + 'getgrent', + 'isatty', + ]), + importKey('host_system', 'get_identity'), +]); + +const transactionalKeys = new Set([ + ...importKeys('wasi_snapshot_preview1', [ + 'fd_renumber', + 'fd_read', + 'fd_write', + 'fd_pwrite', + 'fd_seek', + 'path_open', + 'proc_exit', + 'random_get', + ]), + importKey('host_fs', 'open_tmpfile'), + ...importKeys('host_net', ['net_socket', 'net_accept', 'net_send', 'net_recv', 'net_sendto', 'net_recvfrom']), + ...importKeys('host_process', [ + 'proc_closefrom', + 'proc_exec', + 'proc_fexec', + 'proc_spawn', + 'proc_spawn_v2', + 'proc_spawn_v3', + 'proc_spawn_v4', + 'proc_waitpid', + 'proc_waitpid_v2', + 'proc_waitpid_v3', + 'proc_umask', + 'umask', + 'proc_itimer_real', + 'fd_pipe', + 'fd_dup', + 'fd_dup_min', + 'fd_record_lock', + 'fd_socketpair', + 'fd_sendmsg_rights', + 'fd_recvmsg_rights', + 'pty_open', + 'proc_signal_mask_v2', + 'proc_ppoll_v1', + ]), + importKey('host_tty', 'read'), +]); + +const waitKeys = new Set([ + ...importKeys('wasi_snapshot_preview1', ['fd_read', 'fd_write', 'path_open', 'poll_oneoff']), + ...importKeys('host_net', [ + 'net_connect', + 'net_getaddrinfo', + 'net_dns_query_rr_v1', + 'net_bind', + 'net_accept', + 'net_send', + 'net_recv', + 'net_sendto', + 'net_recvfrom', + 'net_poll', + 'net_close', + 'net_tls_connect', + ]), + ...importKeys('host_process', [ + 'proc_waitpid', + 'proc_waitpid_v2', + 'proc_waitpid_v3', + 'fd_flock', + 'fd_record_lock', + 'fd_sendmsg_rights', + 'fd_recvmsg_rights', + 'sleep_ms', + 'proc_ppoll_v1', + ]), + importKey('host_tty', 'read'), +]); + +const restartableKeys = new Set([ + ...importKeys('wasi_snapshot_preview1', ['fd_read', 'fd_write', 'path_open']), + ...importKeys('host_net', ['net_accept', 'net_send', 'net_recv', 'net_sendto', 'net_recvfrom']), + ...importKeys('host_process', [ + 'proc_waitpid', + 'proc_waitpid_v2', + 'proc_waitpid_v3', + 'fd_flock', + 'fd_record_lock', + 'fd_sendmsg_rights', + 'fd_recvmsg_rights', + ]), + importKey('host_tty', 'read'), +]); + +const bootstrapKeys = new Set([ + ...importKeys('wasi_snapshot_preview1', [ + 'args_get', + 'args_sizes_get', + 'environ_get', + 'environ_sizes_get', + 'fd_prestat_dir_name', + 'fd_prestat_get', + ]), +]); +const localKeys = new Set([ + importKey('wasi_snapshot_preview1', 'sched_yield'), + ...importKeys('host_fs', ['set_open_mode', 'set_open_direct']), +]); +const terminalKeys = new Set([ + importKey('wasi_snapshot_preview1', 'proc_exit'), + ...importKeys('host_process', ['proc_exec', 'proc_fexec']), +]); +const scalarI32Keys = new Set([ + ...importKeys('host_fs', ['fd_mode', 'path_mode']), + ...importKeys('host_tty', ['read', 'isatty']), +]); +const scalarI64Keys = new Set([ + ...importKeys('host_fs', ['fd_size', 'fd_blocks', 'path_size', 'path_blocks', 'path_rdev']), +]); +const scalarI64ZeroOnErrorKeys = new Set([ + importKey('host_fs', 'path_rdev'), +]); + +function returnKind(key, definition) { + if (definition.results.length === 0) return 'Void'; + if (scalarI32Keys.has(key)) return 'ScalarI32'; + if (scalarI64Keys.has(key)) return 'ScalarI64'; + return 'WasiErrno'; +} + +function executionClass(key) { + if (bootstrapKeys.has(key)) return 'Bootstrap'; + if (terminalKeys.has(key)) return 'Terminal'; + if (localKeys.has(key)) return 'Local'; + if (waitKeys.has(key)) return 'Wait'; + return 'Host'; +} + +const encodeOverrides = groupMap([ + ['U64Output', importKeys('wasi_snapshot_preview1', ['clock_res_get', 'clock_time_get'])], + ['DescriptorReadOutput', importKeys('wasi_snapshot_preview1', ['fd_pread', 'fd_read'])], + ['DescriptorWriteOutput', importKeys('wasi_snapshot_preview1', ['fd_pwrite', 'fd_write'])], + ['DescriptorOffsetOutput', importKeys('wasi_snapshot_preview1', ['fd_seek', 'fd_tell'])], + ['ProcessIdOutput', importKeys('host_process', ['proc_spawn', 'proc_spawn_v2', 'proc_spawn_v3', 'proc_spawn_v4'])], + ['NetworkAddressOutput', importKeys('host_net', ['net_getsockname', 'net_getpeername'])], + ['IdentityScalarOutput', importKeys('host_user', ['getuid', 'getgid', 'geteuid', 'getegid'])], + ['IdentityTripleOutput', importKeys('host_user', ['getresuid', 'getresgid'])], + ['AccountRecordOutput', importKeys('host_user', [ + 'getpwuid', + 'getpwnam', + 'getpwent', + 'getgrgid', + 'getgrnam', + 'getgrent', + ])], + ['TerminalU32Output', importKeys('host_tty', ['get_pgrp', 'get_sid'])], +], (definition) => { + const key = importKey(definition.module, definition.name); + const kind = returnKind(key, definition); + if (!prevalidateOutputKeys.has(key)) { + if (kind === 'ScalarI32') return 'ScalarI32ZeroOnError'; + if (kind === 'ScalarI64') { + return scalarI64ZeroOnErrorKeys.has(key) + ? 'ScalarI64ZeroOnError' + : 'ScalarI64MaxOnError'; + } + return kind; + } + return `${pascalCase(`${definition.module}_${definition.name}`)}Output`; +}); + +const signatureMap = new Map(); +for (const definition of definitions) { + const shape = `${definition.params.join(',')}->${definition.results.join(',')}`; + const id = coreSignatureId(definition.params, definition.results); + const previous = signatureMap.get(shape); + if (previous != null && previous.id !== id) { + throw new Error(`core signature ${shape} has conflicting ids ${previous.id} and ${id}`); + } + signatureMap.set(shape, { id, params: definition.params, results: definition.results }); +} +const coreSignatures = [...signatureMap.values()].sort((a, b) => a.id.localeCompare(b.id)); +if (new Set(coreSignatures.map((signature) => signature.id)).size !== coreSignatures.length) { + throw new Error('generated core signature ids are not unique'); +} + +const definitionKeys = new Set(definitions.map((definition) => importKey(definition.module, definition.name))); +for (const keys of [ + prevalidateOutputKeys, + transactionalKeys, + waitKeys, + restartableKeys, + bootstrapKeys, + localKeys, + terminalKeys, + scalarI32Keys, + scalarI64Keys, + scalarI64ZeroOnErrorKeys, +]) { + for (const key of keys) { + if (!definitionKeys.has(key)) throw new Error(`semantic metadata references unknown import ${key}`); + } +} + +const enrichedImports = definitions.map((definition) => { + const key = importKey(definition.module, definition.name); + const shape = `${definition.params.join(',')}->${definition.results.join(',')}`; + const tiers = importPolicyOverrides[key] ?? modulePolicy[definition.module]; + if (tiers == null) throw new Error(`missing permission policy for ${key}`); + return { + id: pascalCase(`${definition.module}_${definition.name}`), + ...definition, + coreSignature: signatureMap.get(shape).id, + binding: { + handler: handlerId(definition), + decode: decodeId(definition), + encode: encodeOverrides(definition), + returnKind: returnKind(key, definition), + executionClass: executionClass(key), + restartability: restartableKeys.has(key) ? 'SignalRestartable' : 'Never', + transactional: transactionalKeys.has(key), + prevalidateOutputs: prevalidateOutputKeys.has(key), + permissionTiers: tiers, + }, + }; +}); +const bindings = Object.fromEntries(enrichedImports.map((definition) => { + const key = importKey(definition.module, definition.name); + return [key, { + id: definition.id, + status: definition.status, + coreSignature: definition.coreSignature, + ...definition.binding, + }]; +})); + +const manifest = { + schemaVersion: 2, + abiVersion: 'agentos-wasm-host-v1', + source: { + preview1Module: 'wasi_snapshot_preview1', + preview1CompatibilityAlias: 'wasi_unstable', + preview1WitxCommit: 'd4d3df3072b65ce43cb01c1add72b402d69a79d1', + preview1Witx: [ + { + path: 'crates/execution/abi/wasi_snapshot_preview1/typenames.witx', + sha256: createHash('sha256').update(readFileSync(preview1TypesPath)).digest('hex'), + }, + { + path: 'crates/execution/abi/wasi_snapshot_preview1/wasi_snapshot_preview1.witx', + sha256: createHash('sha256').update(readFileSync(preview1WitxPath)).digest('hex'), + }, + ], + preview1Generator: 'agentos-wasm-abi-generator@0.0.1 (witx=0.9.1)', + wasiLibcCommit: '574b88da481569b65a237cb80daf9a2d5aeaf82d', + customAbiInventory: 'docs/design/wasmtime-phase-0.md', + }, + moduleAliases, + representation: { + byteOrder: 'little', + pointerBits: 32, + sizeBits: 32, + layouts: loweredPreview1.layouts, + }, + modulePolicy, + importPolicyOverrides, + coreSignatures, + bindings, + imports: definitions, +}; + +const output = `${JSON.stringify(manifest, null, 2)}\n`; +const rawRegistryOutput = execFileSync( + 'cargo', + ['run', '--quiet', '-p', 'agentos-wasm-abi-generator', '--', '--render-registry'], + { cwd: root, encoding: 'utf8', input: output, maxBuffer: 32 * 1024 * 1024 }, +); +const registryOutput = execFileSync( + 'rustfmt', + ['--edition', '2021', '--emit', 'stdout'], + { cwd: root, encoding: 'utf8', input: rawRegistryOutput, maxBuffer: 32 * 1024 * 1024 }, +); +if (process.argv.includes('--write')) { + writeFileSync(outputPath, output); + writeFileSync(registryOutputPath, registryOutput); + process.stdout.write(`wrote ${outputPath}\nwrote ${registryOutputPath}\n`); +} else { + let currentManifest; + let currentRegistry; + try { + currentManifest = readFileSync(outputPath, 'utf8'); + } catch { + process.stderr.write(`missing generated ABI manifest: ${outputPath}\n`); + process.exit(1); + } + try { + currentRegistry = readFileSync(registryOutputPath, 'utf8'); + } catch { + process.stderr.write(`missing generated ABI Rust registry: ${registryOutputPath}\n`); + process.exit(1); + } + if (currentManifest !== output || currentRegistry !== registryOutput) { + process.stderr.write('generated WASM ABI manifest is stale; run node scripts/generate-wasm-abi-manifest.mjs --write\n'); + process.exit(1); + } + process.stdout.write( + `WASM ABI manifest and Rust registry are current (${definitions.length} functions, ${coreSignatures.length} signatures)\n`, + ); +} diff --git a/software/acl/package.json b/software/acl/package.json index 1f9c33ac2d..ccc1d6b705 100644 --- a/software/acl/package.json +++ b/software/acl/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", "check-types": "tsc --noEmit", - "test": "vitest run test/ --passWithNoTests" + "test": "vitest run test/" }, "devDependencies": { "@agentos-software/manifest": "workspace:*", diff --git a/software/acl/test/acl.test.ts b/software/acl/test/acl.test.ts new file mode 100644 index 0000000000..ef6c3ba0a9 --- /dev/null +++ b/software/acl/test/acl.test.ts @@ -0,0 +1,131 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + type Kernel, +} from "@rivet-dev/agentos-test-harness"; +import { afterEach, beforeEach, expect, it } from "vitest"; + +const ACL_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const ACL_COMMANDS = ["chacl", "getfacl", "setfacl"]; +const hasAclCommands = ACL_COMMANDS.every((command) => + existsSync(join(ACL_COMMAND_DIR, command)), +); + +describeIf(hasAclCommands, "ACL commands", { timeout: 30_000 }, () => { + let filesystem: ReturnType; + let kernel: Kernel | undefined; + + beforeEach(async () => { + filesystem = createInMemoryFileSystem(); + await filesystem.writeFile("/workspace/acl.txt", "acl metadata\n"); + await filesystem.chown("/workspace/acl.txt", 1000, 1000); + await filesystem.chmod("/workspace/acl.txt", 0o640); + await filesystem.mkdir("/workspace/defaults", { recursive: true }); + await filesystem.chown("/workspace/defaults", 1000, 1000); + await filesystem.chmod("/workspace/defaults", 0o750); + kernel = createKernel({ filesystem }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [ACL_COMMAND_DIR] })); + }, 60_000); + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }, 60_000); + + async function run(command: string, args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await process.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { exitCode, stdout, stderr }; + } + + it("sets an extended access ACL and synchronizes the mode mask", async () => { + const path = "/workspace/acl.txt"; + const set = await run("setfacl", ["-m", "u:2000:rwx", path]); + expect(set.exitCode, set.stderr).toBe(0); + expect(set.stdout).toBe(""); + expect(set.stderr).toBe(""); + + const get = await run("getfacl", [ + "-n", + "--absolute-names", + path, + ]); + expect(get.exitCode, get.stderr).toBe(0); + expect(get.stdout).toContain("# file: /workspace/acl.txt"); + expect(get.stdout).toContain("user::rw-"); + expect(get.stdout).toContain("user:2000:rwx"); + expect(get.stdout).toContain("group::r--"); + expect(get.stdout).toContain("mask::rwx"); + expect(get.stdout).toContain("other::---"); + expect(get.stderr).toBe(""); + + const stat = await filesystem.stat(path); + expect(stat.mode & 0o777).toBe(0o670); + }); + + it("stores a default directory ACL with an automatically calculated mask", async () => { + const path = "/workspace/defaults"; + const set = await run("setfacl", [ + "-d", + "-m", + "u::rwx,u:2000:r--,g::r-x,o::---", + path, + ]); + expect(set.exitCode, set.stderr).toBe(0); + expect(set.stderr).toBe(""); + + const get = await run("getfacl", [ + "-n", + "--absolute-names", + path, + ]); + expect(get.exitCode, get.stderr).toBe(0); + expect(get.stdout).toContain("default:user::rwx"); + expect(get.stdout).toContain("default:user:2000:r--"); + expect(get.stdout).toContain("default:group::r-x"); + expect(get.stdout).toContain("default:mask::r-x"); + expect(get.stdout).toContain("default:other::---"); + }); + + it("sets, lists, and removes ACL state through chacl and setfacl", async () => { + const path = "/workspace/acl.txt"; + const set = await run("chacl", ["u::rw-,g::r--,o::---", path]); + expect(set.exitCode, set.stderr).toBe(0); + expect(set.stderr).toBe(""); + + const list = await run("chacl", ["-l", path]); + expect(list.exitCode, list.stderr).toBe(0); + expect(list.stdout.trim()).toBe( + "/workspace/acl.txt [u::rw-,g::r--,o::---]", + ); + + const addNamed = await run("setfacl", ["-m", "u:2000:r--", path]); + expect(addNamed.exitCode, addNamed.stderr).toBe(0); + const removeAll = await run("setfacl", ["-b", path]); + expect(removeAll.exitCode, removeAll.stderr).toBe(0); + + const get = await run("getfacl", ["-n", path]); + expect(get.exitCode, get.stderr).toBe(0); + expect(get.stdout).not.toContain("user:2000:"); + expect(get.stdout).not.toContain("mask::"); + expect(get.stdout).toContain("user::rw-"); + expect(get.stdout).toContain("group::r--"); + expect(get.stdout).toContain("other::---"); + }); +}); diff --git a/software/attr/package.json b/software/attr/package.json index e5e158ca46..d6449ebd0b 100644 --- a/software/attr/package.json +++ b/software/attr/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", "check-types": "tsc --noEmit", - "test": "vitest run test/ --passWithNoTests" + "test": "vitest run test/" }, "devDependencies": { "@agentos-software/manifest": "workspace:*", diff --git a/software/attr/test/attr.test.ts b/software/attr/test/attr.test.ts new file mode 100644 index 0000000000..c99615ab54 --- /dev/null +++ b/software/attr/test/attr.test.ts @@ -0,0 +1,183 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + type Kernel, +} from "@rivet-dev/agentos-test-harness"; +import { afterEach, beforeEach, expect, it } from "vitest"; + +const ATTR_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const ATTR_COMMANDS = ["attr", "getfattr", "setfattr"]; +const hasAttrCommands = ATTR_COMMANDS.every((command) => + existsSync(join(ATTR_COMMAND_DIR, command)), +); + +describeIf(hasAttrCommands, "attr commands", { timeout: 30_000 }, () => { + let kernel: Kernel | undefined; + + beforeEach(async () => { + const filesystem = createInMemoryFileSystem(); + await filesystem.writeFile("/workspace/metadata.txt", "metadata\n"); + await filesystem.chown("/workspace/metadata.txt", 1000, 1000); + kernel = createKernel({ filesystem }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [ATTR_COMMAND_DIR] }), + ); + }, 60_000); + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }, 60_000); + + async function run(command: string, args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await process.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { exitCode, stdout, stderr }; + } + + it("round-trips text and binary user xattrs through the kernel", async () => { + const path = "/workspace/metadata.txt"; + const setText = await run("setfattr", [ + "-n", + "user.agentos", + "-v", + "phase-one", + path, + ]); + expect(setText.exitCode, setText.stderr).toBe(0); + expect(setText.stdout).toBe(""); + expect(setText.stderr).toBe(""); + + const getText = await run("getfattr", [ + "--only-values", + "-n", + "user.agentos", + path, + ]); + expect(getText.exitCode, getText.stderr).toBe(0); + expect(getText.stdout).toBe("phase-one"); + expect(getText.stderr).toBe(""); + + const setBinary = await run("setfattr", [ + "-n", + "user.binary", + "-v", + "0x0001ff", + path, + ]); + expect(setBinary.exitCode, setBinary.stderr).toBe(0); + + const getBinary = await run("getfattr", [ + "--absolute-names", + "-n", + "user.binary", + "-e", + "hex", + path, + ]); + expect(getBinary.exitCode, getBinary.stderr).toBe(0); + expect(getBinary.stdout).toContain("# file: /workspace/metadata.txt"); + expect(getBinary.stdout).toContain("user.binary=0x0001ff"); + expect(getBinary.stderr).toBe(""); + }); + + it("lists and removes xattrs with the legacy attr interface", async () => { + const path = "/workspace/metadata.txt"; + const set = await run("attr", ["-s", "phase", "-V", "ready", path]); + expect(set.exitCode, set.stderr).toBe(0); + expect(set.stdout).toContain('Attribute "phase" set to a 5 byte value'); + expect(set.stdout).toContain("ready"); + + const get = await run("attr", ["-g", "phase", path]); + expect(get.exitCode, get.stderr).toBe(0); + expect(get.stdout).toContain('Attribute "phase" had a 5 byte value'); + expect(get.stdout).toContain("ready"); + + const list = await run("attr", ["-l", path]); + expect(list.exitCode, list.stderr).toBe(0); + expect(list.stdout).toContain( + 'Attribute "phase" has a 5 byte value for /workspace/metadata.txt', + ); + + const remove = await run("attr", ["-r", "phase", path]); + expect(remove.exitCode, remove.stderr).toBe(0); + expect(remove.stderr).toBe(""); + + const missing = await run("attr", ["-g", "phase", path]); + expect(missing.exitCode).toBe(1); + expect(missing.stdout).toBe(""); + expect(missing.stderr).toContain( + 'Could not get "phase" for /workspace/metadata.txt', + ); + }); + + it("dumps and restores multiple attributes without losing values", async () => { + const path = "/workspace/metadata.txt"; + for (const [name, value] of [ + ["user.alpha", "first"], + ["user.beta", "0x0002fe"], + ] as const) { + const result = await run("setfattr", ["-n", name, "-v", value, path]); + expect(result.exitCode, result.stderr).toBe(0); + } + + const dump = await run("getfattr", [ + "--absolute-names", + "-d", + "-e", + "hex", + path, + ]); + expect(dump.exitCode, dump.stderr).toBe(0); + expect(dump.stdout).toContain("user.alpha=0x6669727374"); + expect(dump.stdout).toContain("user.beta=0x0002fe"); + + for (const name of ["user.alpha", "user.beta"]) { + const result = await run("setfattr", ["-x", name, path]); + expect(result.exitCode, result.stderr).toBe(0); + } + + if (!kernel) throw new Error("kernel not mounted"); + await kernel.writeFile("/workspace/attrs.dump", dump.stdout); + const restore = await run("setfattr", [ + "--restore", + "/workspace/attrs.dump", + ]); + expect(restore.exitCode, restore.stderr).toBe(0); + + const restoredAlpha = await run("getfattr", [ + "--only-values", + "-n", + "user.alpha", + path, + ]); + expect(restoredAlpha.exitCode, restoredAlpha.stderr).toBe(0); + expect(restoredAlpha.stdout).toBe("first"); + + const restoredBeta = await run("getfattr", [ + "-n", + "user.beta", + "-e", + "hex", + path, + ]); + expect(restoredBeta.exitCode, restoredBeta.stderr).toBe(0); + expect(restoredBeta.stdout).toContain("user.beta=0x0002fe"); + }); +}); diff --git a/software/coreutils/test/kill.nightly.test.ts b/software/coreutils/test/kill.nightly.test.ts index 14839ad656..5f8540e234 100644 --- a/software/coreutils/test/kill.nightly.test.ts +++ b/software/coreutils/test/kill.nightly.test.ts @@ -59,7 +59,7 @@ describeIf(hasWasmBinaries, "upstream kill", () => { const nativeProbe = await runNative("sh", ["-c", "env kill -0 -- $$"]); const wasmProbe = await vm.exec("sh -c 'env kill -0 -- $$'"); - expect(wasmProbe.exitCode).toBe(nativeProbe.exitCode); + expect(wasmProbe.exitCode, wasmProbe.stderr).toBe(nativeProbe.exitCode); expect(wasmProbe.exitCode).toBe(0); }, 20_000); diff --git a/software/coreutils/test/shell-redirect.nightly.test.ts b/software/coreutils/test/shell-redirect.nightly.test.ts index 8afa4361c5..85e2ae4ed9 100644 --- a/software/coreutils/test/shell-redirect.nightly.test.ts +++ b/software/coreutils/test/shell-redirect.nightly.test.ts @@ -28,7 +28,10 @@ describeIf(hasWasmBinaries, "wasmvm shell redirects", () => { await (vfs as any).chmod("/", 0o1777); await vfs.mkdir("/tmp", { recursive: true }); await (vfs as any).chmod("/tmp", 0o1777); - kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + kernel = createKernel({ + filesystem: vfs, + syncFilesystemOnDispose: false, + }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); const result = await kernel.exec( @@ -89,6 +92,27 @@ describeIf(hasWasmBinaries, "wasmvm shell redirects", () => { expect(wasm.stdout).toBe("custom-zero|custom-one|from-exec\n"); }, 30_000); + it("executes an execute-only WASM image without requiring read permission", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const executeOnlyPath = `/tmp/agentos-exec-only-${process.pid}`; + await kernel.writeFile(executeOnlyPath, readFileSync(`${COMMANDS_DIR}/sh`)); + await (vfs as any).chmod(executeOnlyPath, 0o111); + const script = + `exec ${executeOnlyPath} -c ` + + `'printf "execute-only\\n"'`; + const wasm = await kernel.exec(`sh -c ${shellQuote(script)}`); + + expect(wasm.exitCode, wasm.stderr).toBe(0); + expect(wasm.stdout).toBe("execute-only\n"); + expect(wasm.stderr).toBe(""); + }, 30_000); + it("matches native exec redirections and inherited descriptors", async () => { const vfs = createInMemoryFileSystem(); await (vfs as any).chmod("/", 0o1777); diff --git a/software/coreutils/test/shell-terminal.nightly.test.ts b/software/coreutils/test/shell-terminal.nightly.test.ts index 9c5a3788d1..dd79d354c8 100644 --- a/software/coreutils/test/shell-terminal.nightly.test.ts +++ b/software/coreutils/test/shell-terminal.nightly.test.ts @@ -6,14 +6,34 @@ * Registers only when the WASM shell binary is available. */ -import { describe, it, expect, afterEach } from "vitest"; -import { TerminalHarness } from '@rivet-dev/agentos-test-harness'; +import { describe, it, expect, afterEach, vi } from "vitest"; +import { TerminalHarness as BaseTerminalHarness } from '@rivet-dev/agentos-test-harness'; import { createWasmVmRuntime } from '@rivet-dev/agentos-test-harness'; import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@rivet-dev/agentos-test-harness'; import type { Kernel } from '@rivet-dev/agentos-test-harness'; /** brush-shell interactive prompt (captured empirically). */ const PROMPT = "sh-0.4$ "; +const TERMINAL_TEST_TIMEOUT_MS = 15_000; + +// Starting and driving a real sidecar-backed shell can exceed Vitest's 5s +// unit-test default under the package's parallel file load. Keep this +// integration suite bounded without weakening any runtime deadline or screen +// assertion. +vi.setConfig({ + testTimeout: TERMINAL_TEST_TIMEOUT_MS, + hookTimeout: TERMINAL_TEST_TIMEOUT_MS, +}); + +class TerminalHarness extends BaseTerminalHarness { + override waitFor( + text: string, + occurrence: number = 1, + timeoutMs: number = TERMINAL_TEST_TIMEOUT_MS, + ): Promise { + return super.waitFor(text, occurrence, timeoutMs); + } +} // --------------------------------------------------------------------------- // Simple in-memory VFS for kernel tests diff --git a/software/curl/test/curl.nightly.test.ts b/software/curl/test/curl.nightly.test.ts index b79f4428d8..d6ddad43ab 100644 --- a/software/curl/test/curl.nightly.test.ts +++ b/software/curl/test/curl.nightly.test.ts @@ -54,7 +54,7 @@ import { brotliCompressSync, gzipSync, zstdCompressSync } from 'node:zlib'; // The upstream curl parity assertions below only hold for the C-built curl // artifact; the Rust fallback in COMMANDS_DIR intentionally supports a smaller // flag surface and should not be used for these cases. -const hasHttpGetTest = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'http_get_test')); +const hasHttpGetTest = hasWasmBinaries && existsSync(resolve(C_BUILD_DIR, 'http_get_test')); const hasCurl = hasCWasmBinaries('curl'); const runExternalNetwork = process.env.AGENTOS_E2E_NETWORK === '1'; const EXTERNAL_HOST = 'example.com'; diff --git a/software/duckdb/test/duckdb.nightly.test.ts b/software/duckdb/test/duckdb.nightly.test.ts index 51b6c3837d..e9af2e9997 100644 --- a/software/duckdb/test/duckdb.nightly.test.ts +++ b/software/duckdb/test/duckdb.nightly.test.ts @@ -39,6 +39,9 @@ async function mountKernel( filesystem: ReturnType, options: { loopbackExemptPorts?: number[] } = {}, ) { + // Keep /tmp out of the supplied snapshot: the kernel bootstrap owns its + // Linux 01777 temp directory, and generated database files must not be + // mirrored through the bounded host bridge. const kernel = createKernel({ filesystem, cwd: '/tmp', @@ -69,12 +72,12 @@ function closeServer(server: Server) { } async function waitForFilesystemPath( - filesystem: ReturnType, + kernel: Kernel, path: string, timeoutMs = 30_000, ) { const start = Date.now(); - while (!(await filesystem.exists(path))) { + while (!(await kernel.exists(path))) { if (Date.now() - start >= timeoutMs) { throw new Error(`timed out waiting for ${path}`); } @@ -93,7 +96,6 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('executes basic SQL against an in-memory database', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); const result = await kernel.exec('duckdb -csv -c "SELECT 41 + 1 AS answer"'); @@ -101,23 +103,17 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { expect(result.stdout.trim()).toBe('answer\n42'); }); - it('persists database files on the shared VFS and reopens them in a new process', async () => { + it('persists database files on the kernel VFS and reopens them in a new process', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); - await filesystem.writeFile('/tmp/input.csv', 'name,value\nalpha,1\nbeta,2\n'); - kernel = await mountKernel(filesystem); + await kernel.writeFile('/tmp/input.csv', 'name,value\nalpha,1\nbeta,2\n'); let result = await kernel.exec( `duckdb -csv /tmp/app.duckdb -c "CREATE TABLE items AS SELECT * FROM read_csv_auto('/tmp/input.csv');"` ); expect(result.exitCode).toBe(0); - await kernel.dispose(); - kernel = undefined; + expect(await kernel.exists('/tmp/app.duckdb')).toBe(true); + expect((await kernel.stat('/tmp/app.duckdb')).size).toBeGreaterThan(0); - expect(await filesystem.exists('/tmp/app.duckdb')).toBe(true); - expect((await filesystem.stat('/tmp/app.duckdb')).size).toBeGreaterThan(0); - - kernel = await mountKernel(filesystem); result = await kernel.exec( `duckdb -csv /tmp/app.duckdb -c "SELECT name, value FROM items ORDER BY value;"` ); @@ -127,7 +123,6 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('persists inserted and updated rows across process reopens', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); let result = await kernel.exec( @@ -144,7 +139,6 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('supports joins and indexes on file-backed tables', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); const result = await kernel.exec( @@ -156,7 +150,6 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('keeps temp tables scoped to a single DuckDB process', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); let result = await kernel.exec( @@ -174,7 +167,6 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('drops uncommitted rows after a hard-killed process is reopened', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); let result = await kernel.exec( @@ -189,7 +181,7 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { "BEGIN; INSERT INTO items VALUES (42); COPY (SELECT COUNT(*) AS rows_in_tx FROM items) TO '/tmp/tx-ready.csv' (HEADER, DELIMITER ','); SELECT SUM(i) FROM range(100000000000) tbl(i);", ]); - await waitForFilesystemPath(filesystem, '/tmp/tx-ready.csv'); + await waitForFilesystemPath(kernel, '/tmp/tx-ready.csv'); proc.kill(9); await proc.wait().catch(() => undefined); @@ -203,23 +195,21 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { it('handles large sorted exports with a configured temp directory under constrained memory', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); kernel = await mountKernel(filesystem); const result = await kernel.exec( `duckdb -csv /tmp/spill.duckdb -c "PRAGMA temp_directory='/tmp/duckdb-spill'; SET threads=1; SET preserve_insertion_order=false; SET memory_limit='64MB'; COPY (SELECT i, repeat('x', 256) AS payload FROM range(200000) tbl(i) ORDER BY i DESC) TO '/tmp/spilled.csv' (HEADER, DELIMITER ',');"` ); expect(result.exitCode).toBe(0); - expect(await filesystem.exists('/tmp/spilled.csv')).toBe(true); - expect((await filesystem.stat('/tmp/spilled.csv')).size).toBeGreaterThan(50_000_000); + expect(await kernel.exists('/tmp/spilled.csv')).toBe(true); + expect((await kernel.stat('/tmp/spilled.csv')).size).toBeGreaterThan(50_000_000); }); itIf( hasWasmCurl, - 'queries data fetched over the network through the shared VFS', + 'queries data fetched over the network through the kernel VFS', async () => { const filesystem = createInMemoryFileSystem(); - await filesystem.mkdir('/tmp'); const server = createServer((req: IncomingMessage, res: ServerResponse) => { if (req.url === '/' || req.url === '/remote.csv') { @@ -248,7 +238,9 @@ describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { ); expect(result.exitCode).toBe(0); - expect(await filesystem.readTextFile('/tmp/remote.csv')).toContain('city,value'); + expect(new TextDecoder().decode(await kernel.readFile('/tmp/remote.csv'))).toContain( + 'city,value' + ); result = await kernel.exec( `duckdb -csv -c "SELECT SUM(value) AS total FROM read_csv_auto('/tmp/remote.csv');"` diff --git a/software/envsubst/test/envsubst.nightly.test.ts b/software/envsubst/test/envsubst.nightly.test.ts index cc3870e730..b9392c6e8b 100644 --- a/software/envsubst/test/envsubst.nightly.test.ts +++ b/software/envsubst/test/envsubst.nightly.test.ts @@ -103,7 +103,7 @@ class SimpleVFS { } } -describeIf(hasCWasmBinaries('envsubst'), 'envsubst command', () => { +describeIf(hasCWasmBinaries('envsubst'), 'envsubst command', { timeout: 10_000 }, () => { let kernel: Kernel; afterEach(async () => { diff --git a/software/git/test/git.nightly.test.ts b/software/git/test/git.nightly.test.ts index c42be71d67..f1846bdeb6 100644 --- a/software/git/test/git.nightly.test.ts +++ b/software/git/test/git.nightly.test.ts @@ -24,7 +24,9 @@ import { } from '@rivet-dev/agentos-test-harness'; import type { Kernel } from '@rivet-dev/agentos-test-harness'; -vi.setConfig({ testTimeout: 30_000 }); +// Several integration cases intentionally sequence multiple fresh WASM Git +// executions, so their aggregate harness budget must cover all cold starts. +vi.setConfig({ testTimeout: 60_000 }); /** Check git binary exists in addition to base WASM binaries */ const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git')); diff --git a/software/unzip/test/unzip.nightly.test.ts b/software/unzip/test/unzip.nightly.test.ts index 56374841eb..748d56c53b 100644 --- a/software/unzip/test/unzip.nightly.test.ts +++ b/software/unzip/test/unzip.nightly.test.ts @@ -85,12 +85,16 @@ describeIf( createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const zipResult = await kernel.exec("zip /archive.zip /hello.txt"); + const zipResult = await kernel.exec( + "zip /workspace/archive.zip /hello.txt", + ); expect(zipResult.exitCode, zipResult.stderr).toBe(0); - const unzipResult = await kernel.exec("unzip -d /extracted /archive.zip"); + const unzipResult = await kernel.exec( + "unzip -d /workspace/extracted /workspace/archive.zip", + ); expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - expect(await vfs.readTextFile("/extracted/hello.txt")).toBe( + expect(await vfs.readTextFile("/workspace/extracted/hello.txt")).toBe( "Hello, World!\n", ); }); @@ -104,10 +108,14 @@ describeIf( createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const zipResult = await kernel.exec("zip /list-test.zip /data.txt"); + const zipResult = await kernel.exec( + "zip /workspace/list-test.zip /data.txt", + ); expect(zipResult.exitCode, zipResult.stderr).toBe(0); - const listResult = await kernel.exec("unzip -l /list-test.zip"); + const listResult = await kernel.exec( + "unzip -l /workspace/list-test.zip", + ); expect(listResult.exitCode, listResult.stderr).toBe(0); expect(listResult.stdout).toContain("data.txt"); expect(listResult.stdout).toContain("18"); @@ -125,13 +133,17 @@ describeIf( createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const zipResult = await kernel.exec("zip /roundtrip.zip /binary.bin"); + const zipResult = await kernel.exec( + "zip /workspace/roundtrip.zip /binary.bin", + ); expect(zipResult.exitCode, zipResult.stderr).toBe(0); - const unzipResult = await kernel.exec("unzip -d /rt-out /roundtrip.zip"); + const unzipResult = await kernel.exec( + "unzip -d /workspace/rt-out /workspace/roundtrip.zip", + ); expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - const extracted = await vfs.readFile("/rt-out/binary.bin"); + const extracted = await vfs.readFile("/workspace/rt-out/binary.bin"); expect(extracted.length).toBe(256); for (let i = 0; i < 256; i++) { expect(extracted[i]).toBe(i); @@ -156,10 +168,12 @@ describeIf( createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const result = await kernel.exec("unzip -d /out /evil.zip"); + const result = await kernel.exec( + "unzip -d /workspace/out /evil.zip", + ); expect(result.exitCode, result.stderr).not.toBe(0); expect(result.stderr).toMatch(/error/); - expect(await vfs.exists("/out/evil.txt")).toBe(false); + expect(await vfs.exists("/workspace/out/evil.txt")).toBe(false); }); it("rejects an entry whose normalized name is empty", async () => { @@ -210,10 +224,12 @@ describeIf( createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const result = await kernel.exec("unzip -d /cap-out /big.zip"); + const result = await kernel.exec( + "unzip -d /workspace/cap-out /big.zip", + ); expect(result.exitCode, result.stderr).not.toBe(0); expect(result.stderr).toMatch(/error/); - expect(await vfs.exists("/cap-out/big.bin")).toBe(false); + expect(await vfs.exists("/workspace/cap-out/big.bin")).toBe(false); }); }, ); diff --git a/software/wget/test/wget.nightly.test.ts b/software/wget/test/wget.nightly.test.ts index 2e031c8448..839728fd5b 100644 --- a/software/wget/test/wget.nightly.test.ts +++ b/software/wget/test/wget.nightly.test.ts @@ -509,12 +509,12 @@ describeIf(hasWgetBinary, "wget command", () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget -O /tmp/output.txt http://127.0.0.1:${port}/data.json`, + `wget -O /workspace/output.txt http://127.0.0.1:${port}/data.json`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/output.txt")).toContain( + expect(await filesystem.readTextFile("/workspace/output.txt")).toContain( '"status":"ok"', ); }, 15_000); @@ -523,13 +523,13 @@ describeIf(hasWgetBinary, "wget command", () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget -q -O /tmp/quiet.txt http://127.0.0.1:${port}/file.txt`, + `wget -q -O /workspace/quiet.txt http://127.0.0.1:${port}/file.txt`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); expect(result.stderr).toBe(""); - expect(await filesystem.readTextFile("/tmp/quiet.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/quiet.txt")).toBe( "downloaded content", ); }, 15_000); @@ -550,12 +550,12 @@ describeIf(hasWgetBinary, "wget command", () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget -O /tmp/redirected.txt http://127.0.0.1:${port}/redirect`, + `wget -O /workspace/redirected.txt http://127.0.0.1:${port}/redirect`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/redirected.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/redirected.txt")).toBe( "arrived after redirect", ); }, 15_000); @@ -578,12 +578,12 @@ describeIf(hasWgetBinary, "wget command", () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --compression=auto -O /tmp/gz.txt http://127.0.0.1:${port}/gzip`, + `wget --compression=auto -O /workspace/gz.txt http://127.0.0.1:${port}/gzip`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/gz.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/gz.txt")).toBe( COMPRESSION_PAYLOAD, ); }, 15_000); @@ -592,7 +592,7 @@ describeIf(hasWgetBinary, "wget command", () => { await mountKernel(); const result = await kernel.exec( - `wget --tries=1 --connect-timeout=1 --read-timeout=1 --no-check-certificate -O /tmp/handshake-timeout.txt https://127.0.0.1:${handshakeStallPort}/`, + `wget --tries=1 --connect-timeout=1 --read-timeout=1 --no-check-certificate -O /workspace/handshake-timeout.txt https://127.0.0.1:${handshakeStallPort}/`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -607,7 +607,7 @@ describeIf(hasWgetBinary, "wget command", () => { await mountKernel(); const result = await kernel.exec( - `wget --tries=1 --read-timeout=1 -O /tmp/truncated.txt https://127.0.0.1:${readStallPort}/`, + `wget --tries=1 --read-timeout=1 -O /workspace/truncated.txt https://127.0.0.1:${readStallPort}/`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -623,12 +623,12 @@ describeIf(hasWgetBinary, "wget command", () => { // No --no-check-certificate, no --ca-certificate: trust comes solely // from the seeded /etc/ssl/certs/ca-certificates.crt, like Debian wget. const result = await kernel.exec( - `wget -O /tmp/secure.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget -O /workspace/secure.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/secure.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/secure.txt")).toBe( "verified https content", ); }, 15_000); @@ -637,7 +637,7 @@ describeIf(hasWgetBinary, "wget command", () => { await mountKernel(); const result = await kernel.exec( - `wget -O /tmp/nope.txt https://127.0.0.1:${selfSignedPort}/file`, + `wget -O /workspace/nope.txt https://127.0.0.1:${selfSignedPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -650,12 +650,12 @@ describeIf(hasWgetBinary, "wget command", () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --no-check-certificate -O /tmp/insecure.txt https://127.0.0.1:${selfSignedPort}/file`, + `wget --no-check-certificate -O /workspace/insecure.txt https://127.0.0.1:${selfSignedPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/insecure.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/insecure.txt")).toBe( "self-signed secure content", ); }, 15_000); @@ -667,12 +667,12 @@ describeIf(hasWgetBinary, "wget command", () => { // --ca-certificate is honored (real file read + chain build in-guest). await kernel.writeFile("/tmp/cacert-only.pem", caOnlyPem); const result = await kernel.exec( - `wget --ca-certificate=/tmp/cacert-only.pem -O /tmp/cacert.txt https://127.0.0.1:${caHttpsPort}/file`, + `wget --ca-certificate=/tmp/cacert-only.pem -O /workspace/cacert.txt https://127.0.0.1:${caHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/cacert.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/cacert.txt")).toBe( "cacert https content", ); }, 15_000); @@ -685,12 +685,12 @@ describeIf(hasWgetBinary, "wget command", () => { // fail verification. await kernel.writeFile("/tmp/additional-ca.pem", caOnlyPem); const result = await kernel.exec( - `wget --ca-certificate=/tmp/additional-ca.pem -O /tmp/system-trust.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget --ca-certificate=/tmp/additional-ca.pem -O /workspace/system-trust.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/system-trust.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/system-trust.txt")).toBe( "verified https content", ); }, 15_000); @@ -702,7 +702,7 @@ describeIf(hasWgetBinary, "wget command", () => { // caHttpsServer's leaf. await kernel.writeFile("/tmp/wrong-ca.pem", seededCaPem); const result = await kernel.exec( - `wget --ca-certificate=/tmp/wrong-ca.pem -O /tmp/wrong.txt https://127.0.0.1:${caHttpsPort}/file`, + `wget --ca-certificate=/tmp/wrong-ca.pem -O /workspace/wrong.txt https://127.0.0.1:${caHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -713,12 +713,12 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "--secure-protocol=TLSv1_2 remains a minimum and permits TLS 1.3", async () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --secure-protocol=TLSv1_2 -O /tmp/tls13.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget --secure-protocol=TLSv1_2 -O /workspace/tls13.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/tls13.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/tls13.txt")).toBe( "verified https content", ); }, 15_000); @@ -726,7 +726,7 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "rejects unavailable SSLv3 instead of silently upgrading to TLS", async () => { await mountKernel(); const result = await kernel.exec( - `wget --secure-protocol=SSLv3 -O /tmp/old.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget --secure-protocol=SSLv3 -O /workspace/old.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -737,12 +737,12 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "honors common OpenSSL HIGH/exclusion cipher policy syntax", async () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --ciphers='HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK' -O /tmp/cipher.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget --ciphers='HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK' -O /workspace/cipher.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/cipher.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/cipher.txt")).toBe( "verified https content", ); }, 15_000); @@ -752,12 +752,12 @@ describeIf(hasWgetBinary, "wget command", () => { await kernel.writeFile("/tmp/cipher-ca.pem", caOnlyPem); const result = await kernel.exec( `wget --ciphers=ECDHE-RSA-AES128-GCM-SHA256 --ca-certificate=/tmp/cipher-ca.pem ` + - `-O /tmp/explicit-cipher.txt https://127.0.0.1:${caHttpsPort}/file`, + `-O /workspace/explicit-cipher.txt https://127.0.0.1:${caHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/explicit-cipher.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/explicit-cipher.txt")).toBe( "cacert https content", ); }, 15_000); @@ -765,13 +765,13 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "leaves TLS 1.3 enabled when --ciphers names a TLS 1.2 suite", async () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --ciphers=ECDHE-RSA-AES128-GCM-SHA256 -O /tmp/tls13-cipher.txt ` + + `wget --ciphers=ECDHE-RSA-AES128-GCM-SHA256 -O /workspace/tls13-cipher.txt ` + `https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/tls13-cipher.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/tls13-cipher.txt")).toBe( "verified https content", ); }, 15_000); @@ -779,7 +779,7 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "fails explicitly for an unsupported cipher policy token", async () => { await mountKernel(); const result = await kernel.exec( - `wget --ciphers=NOT-A-CIPHER -O /tmp/bad-cipher.txt https://127.0.0.1:${validHttpsPort}/file`, + `wget --ciphers=NOT-A-CIPHER -O /workspace/bad-cipher.txt https://127.0.0.1:${validHttpsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); @@ -794,12 +794,12 @@ describeIf(hasWgetBinary, "wget command", () => { await kernel.writeFile("/tmp/client.key", clientKeyPem); const result = await kernel.exec( `wget --ca-certificate=/tmp/mutual-ca.pem --certificate=/tmp/client.crt ` + - `--private-key=/tmp/client.key -O /tmp/mutual.txt https://127.0.0.1:${mutualTlsPort}/file`, + `--private-key=/tmp/client.key -O /workspace/mutual.txt https://127.0.0.1:${mutualTlsPort}/file`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/mutual.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/mutual.txt")).toBe( "mutual tls content", ); }, 15_000); @@ -807,12 +807,12 @@ describeIf(hasWgetBinary, "wget command", () => { itIf(hasOpenssl, "resumes the FTPS control session on the protected data channel", async () => { const filesystem = await mountKernel(); const result = await kernel.exec( - `wget --ftps-implicit -O /tmp/ftps.txt ftps://127.0.0.1:${ftpsControlPort}/file.txt`, + `wget --ftps-implicit -O /workspace/ftps.txt ftps://127.0.0.1:${ftpsControlPort}/file.txt`, { timeout: WGET_EXEC_TIMEOUT_MS }, ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(await filesystem.readTextFile("/tmp/ftps.txt")).toBe( + expect(await filesystem.readTextFile("/workspace/ftps.txt")).toBe( "resumed ftps content\n", ); expect(ftpsDataSessionReused).toBe(true); diff --git a/software/xfsprogs/package.json b/software/xfsprogs/package.json index 923406435f..b1fff71ef6 100644 --- a/software/xfsprogs/package.json +++ b/software/xfsprogs/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", "check-types": "tsc --noEmit", - "test": "vitest run test/ --passWithNoTests" + "test": "vitest run test/" }, "devDependencies": { "@agentos-software/manifest": "workspace:*", diff --git a/software/xfsprogs/test/xfs-io.test.ts b/software/xfsprogs/test/xfs-io.test.ts new file mode 100644 index 0000000000..de881471fa --- /dev/null +++ b/software/xfsprogs/test/xfs-io.test.ts @@ -0,0 +1,137 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + type Kernel, +} from "@rivet-dev/agentos-test-harness"; +import { afterEach, beforeEach, expect, it } from "vitest"; + +const XFS_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasXfsIo = existsSync(join(XFS_COMMAND_DIR, "xfs_io")); + +describeIf(hasXfsIo, "xfs_io command", { timeout: 30_000 }, () => { + let filesystem: ReturnType; + let kernel: Kernel | undefined; + + beforeEach(async () => { + filesystem = createInMemoryFileSystem(); + await filesystem.mkdir("/workspace", { recursive: true }); + await filesystem.chown("/workspace", 1000, 1000); + kernel = createKernel({ filesystem }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [XFS_COMMAND_DIR] })); + }, 60_000); + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }, 60_000); + + async function run(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const process = kernel.spawn("xfs_io", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await process.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { exitCode, stdout, stderr }; + } + + it("writes, truncates, and reports metadata for a kernel-backed file", async () => { + const path = "/workspace/data.bin"; + const result = await run([ + "-f", + "-c", + "pwrite -q -S 0x41 0 8", + "-c", + "truncate 12", + "-c", + "stat", + path, + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain(`fd.path = "${path}"`); + expect(result.stdout).toContain("stat.size = 12"); + + const bytes = await filesystem.readFile(path); + expect(Array.from(bytes)).toEqual([ + 0x41, + 0x41, + 0x41, + 0x41, + 0x41, + 0x41, + 0x41, + 0x41, + 0, + 0, + 0, + 0, + ]); + }); + + it("punches a complete extent and exposes the resulting hole", async () => { + const path = "/workspace/sparse.bin"; + const result = await run([ + "-f", + "-c", + "pwrite -q -S 0x7a 0 1536", + "-c", + "fpunch 512 512", + "-c", + "fiemap -v", + path, + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("hole"); + + const bytes = await filesystem.readFile(path); + expect(bytes).toHaveLength(1536); + expect(bytes.slice(0, 512).every((byte) => byte === 0x7a)).toBe(true); + expect(bytes.slice(512, 1024).every((byte) => byte === 0)).toBe(true); + expect(bytes.slice(1024).every((byte) => byte === 0x7a)).toBe(true); + }); + + it("links the open description and persists nanosecond timestamps", async () => { + const path = "/workspace/original.bin"; + const linkedPath = "/workspace/linked.bin"; + const result = await run([ + "-f", + "-c", + "pwrite -q -S 0x2a 0 4", + "-c", + "utimes 123 456000000 789 123000000", + "-c", + `flink ${linkedPath}`, + path, + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe(""); + + const originalStat = await filesystem.stat(path); + const linkedStat = await filesystem.stat(linkedPath); + expect(linkedStat.ino).toBe(originalStat.ino); + expect(originalStat.nlink).toBe(2); + expect(linkedStat.nlink).toBe(2); + expect(originalStat.atimeMs).toBe(123_456); + expect(originalStat.mtimeMs).toBe(789_123); + + const original = await filesystem.readFile(path); + const linked = await filesystem.readFile(linkedPath); + expect(Array.from(original)).toEqual([0x2a, 0x2a, 0x2a, 0x2a]); + expect(Array.from(linked)).toEqual(Array.from(original)); + }); +}); diff --git a/software/zip/test/zip.nightly.test.ts b/software/zip/test/zip.nightly.test.ts index da80079ba1..4978cdfaab 100644 --- a/software/zip/test/zip.nightly.test.ts +++ b/software/zip/test/zip.nightly.test.ts @@ -31,10 +31,10 @@ describeIf(hasCWasmBinaries("zip"), "zip command", { timeout: 10_000 }, () => { createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const result = await kernel.exec("zip /archive.zip /hello.txt"); + const result = await kernel.exec("zip /workspace/archive.zip /hello.txt"); expect(result.exitCode, result.stderr).toBe(0); - const archive = await vfs.readFile("/archive.zip"); + const archive = await vfs.readFile("/workspace/archive.zip"); expect(archive.length).toBeGreaterThan(0); expect(Array.from(archive.slice(0, 2))).toEqual([0x50, 0x4b]); }); @@ -50,8 +50,8 @@ describeIf(hasCWasmBinaries("zip"), "zip command", { timeout: 10_000 }, () => { createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), ); - const result = await kernel.exec("zip -r /dir.zip /mydir"); + const result = await kernel.exec("zip -r /workspace/dir.zip /mydir"); expect(result.exitCode, result.stderr).toBe(0); - expect(await vfs.exists("/dir.zip")).toBe(true); + expect(await vfs.exists("/workspace/dir.zip")).toBe(true); }); }); diff --git a/toolchain/c/Makefile b/toolchain/c/Makefile index 3017321553..ed2125b51f 100644 --- a/toolchain/c/Makefile +++ b/toolchain/c/Makefile @@ -78,7 +78,7 @@ COMMAND_ALIASES := chacl:acl_tools getfacl:acl_tools setfacl:acl_tools \ mkfifo:mknod # Programs requiring patched sysroot (Tier 2+ custom host imports) -PATCHED_PROGRAMS := http_get_test isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test closefrom_test spawn_child spawn_contract spawn_exit_code exec_edge exec_variants pipeline itimer_contract kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test libc_compat_contract libc_bounds_contract chown_contract signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge select_edge socket_flags socketpair_rights ssh_proxy_helper ssh_sk_helper_contract tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler dns_lookup getaddrinfo_connect getnameinfo_contract ppoll_contract open_flags record_lock mlock_contract sqlite3 sqlite3_mem curl wget grep tree zip unzip fs_probe acl_tools xattr_tools xfs_io mknod credentials_test fifo_test flock_test mmap_test openat_test pwritev_test self_stop_status sync_test waitpid_status xattr_test +PATCHED_PROGRAMS := http_get_test isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test closefrom_test spawn_child spawn_contract spawn_exit_code exec_edge exec_variants pipeline itimer_contract kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test libc_compat_contract libc_bounds_contract getgrouplist_bounds chown_contract signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge select_edge socket_flags socketpair_rights ssh_proxy_helper ssh_sk_helper_contract tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler dns_lookup getaddrinfo_connect getnameinfo_contract ppoll_contract open_flags record_lock mlock_contract sqlite3 sqlite3_mem curl wget grep tree zip unzip fs_probe acl_tools xattr_tools xfs_io mknod credentials_test fifo_test flock_test mmap_test openat_test pwritev_test self_stop_status sync_test waitpid_status xattr_test # Discover all package command and test-program C source files. ALL_SOURCES := $(foreach dir,$(C_SOURCE_DIRS),$(wildcard $(dir)/*.c)) diff --git a/toolchain/conformance/c-parity.test.ts b/toolchain/conformance/c-parity.test.ts index f5ea23e470..25c657407f 100644 --- a/toolchain/conformance/c-parity.test.ts +++ b/toolchain/conformance/c-parity.test.ts @@ -27,6 +27,12 @@ import { createServer as createTcpServer } from 'node:net'; import { createServer as createHttpServer } from 'node:http'; const NATIVE_DIR = join(C_BUILD_DIR, 'native'); +const NATIVE_FIXTURE_NAMES: Readonly> = { + cat: 'c-cat', + env: 'c-env', + sort: 'c-sort', + wc: 'c-wc', +}; const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'hello')); const hasNativeBinaries = existsSync(join(NATIVE_DIR, 'hello')); @@ -44,8 +50,9 @@ function runNative( args: string[] = [], options?: { input?: string; env?: Record }, ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return new Promise((res) => { - const proc = spawn(join(NATIVE_DIR, name), args, { + return new Promise((res, reject) => { + const fixtureName = NATIVE_FIXTURE_NAMES[name] ?? name; + const proc = spawn(join(NATIVE_DIR, fixtureName), args, { env: options?.env, stdio: ['pipe', 'pipe', 'pipe'], }); @@ -54,6 +61,7 @@ function runNative( proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); if (options?.input !== undefined) { proc.stdin.write(options.input); @@ -70,7 +78,7 @@ function runNativeWithHosts( name: string, hostsFile: string, ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return new Promise((res) => { + return new Promise((res, reject) => { const proc = spawn('unshare', [ '-Urm', 'sh', @@ -85,6 +93,7 @@ function runNativeWithHosts( proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); proc.on('close', (code) => res({ exitCode: code ?? 0, stdout, stderr })); }); } @@ -95,7 +104,7 @@ function runNativeWithNetworkFiles( servicesFile: string, args: string[], ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return new Promise((res) => { + return new Promise((res, reject) => { const proc = spawn('unshare', [ '-Urm', 'sh', @@ -111,6 +120,7 @@ function runNativeWithNetworkFiles( let stderr = ''; proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); proc.on('close', (code) => res({ exitCode: code ?? 0, stdout, stderr })); }); } @@ -122,7 +132,7 @@ function runNativeWithLibcFiles( passwdFile: string, args: string[], ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return new Promise((res) => { + return new Promise((res, reject) => { const proc = spawn('unshare', [ '-Urm', 'sh', @@ -139,6 +149,7 @@ function runNativeWithLibcFiles( let stderr = ''; proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); proc.on('close', (code) => res({ exitCode: code ?? 0, stdout, stderr })); }); } @@ -343,7 +354,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('hello'); const wasm = await kernel.exec('hello'); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.stdout).toBe(native.stdout); }); @@ -393,7 +407,8 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const wasm = await kernel.exec('wc', { stdin: input }); expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); + const counts = (output: string) => output.trim().split(/\s+/).map(Number); + expect(counts(wasm.stdout)).toEqual(counts(native.stdout)); }); it('fread: file contents match', async () => { @@ -537,8 +552,11 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('getpwuid_test'); const wasm = await kernel.exec('getpwuid_test'); - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); + const diagnostic = + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}` + + `\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`; + expect(wasm.exitCode, diagnostic).toBe(native.exitCode); + expect(wasm.exitCode, diagnostic).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); // Both should get valid passwd entries expect(wasm.stdout).toContain('getpwuid: ok'); @@ -588,7 +606,12 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => (_, index) => `198.51.100.${index + 1} many.test`, ).join('\n') + '\n'; const services = `oversvc 12345/tcp ${'alias'.repeat(240)}\n`; - const passwd = `oversuser:x:123:456:${'gecos'.repeat(240)}:/home/oversuser:/bin/sh\n`; + const passwdPrefix = 'oversuser:x:123:456:'; + const passwdSuffix = ':/home/oversuser:/bin/sh'; + const passwd = `${passwdPrefix}${'g'.repeat( + 4096 - passwdPrefix.length - passwdSuffix.length, + )}${passwdSuffix}\n`; + expect(Buffer.byteLength(passwd.slice(0, -1))).toBe(4096); await vfs.writeFile('/etc/hosts', hosts); await vfs.writeFile('/etc/services', services); await vfs.writeFile('/etc/passwd', passwd); @@ -596,8 +619,8 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const wasm = await kernel.exec('libc_bounds_contract many.test oversvc oversuser'); expect(wasm.exitCode, `${wasm.stderr}\n${wasm.stdout}`).toBe(0); expect(wasm.stderr).toBe(''); - expect(wasm.stdout).toContain('nofile_soft=256\n'); - expect(wasm.stdout).toContain('nofile_hard=256\n'); + expect(wasm.stdout).toContain('nofile_soft=1024\n'); + expect(wasm.stdout).toContain('nofile_hard=1024\n'); expect(wasm.stdout).toContain('host_addresses=20\n'); expect(wasm.stdout).toContain('service_found=no\nservice_erange=yes\n'); expect(wasm.stdout).toContain('passwd_found=no\npasswd_erange=yes\n'); @@ -663,6 +686,70 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => } }); + itIf(!tier2Skip, 'libc group bounds: exact capacities succeed and overflow is explicit without overwrite', async () => { + const members = Array.from({ length: 256 }, (_, index) => `u${index}`); + await vfs.writeFile('/etc/group', `membercap:x:2000:${members.join(',')}\n`); + const exactMembers = await kernel.exec('getgrouplist_bounds group-members'); + expect(exactMembers.exitCode, `${exactMembers.stderr}\n${exactMembers.stdout}`).toBe(0); + expect(exactMembers.stderr).toBe(''); + expect(exactMembers.stdout).toBe( + 'group_found=yes\ngroup_members=256\ngroup_overflow=no\n', + ); + + await vfs.writeFile( + '/etc/group', + `membercap:x:2000:${[...members, 'overflow'].join(',')}\n`, + ); + const overflowMembers = await kernel.exec('getgrouplist_bounds group-members'); + expect( + overflowMembers.exitCode, + `${overflowMembers.stderr}\n${overflowMembers.stdout}`, + ).toBe(0); + expect(overflowMembers.stderr).toBe(''); + expect(overflowMembers.stdout).toBe( + 'group_found=no\ngroup_members=0\ngroup_overflow=yes\n', + ); + + const matchingGroups = (count: number) => + Array.from( + { length: count }, + (_, index) => `g${index}:x:${2000 + index}:boundsuser`, + ).join('\n') + '\n'; + await vfs.writeFile('/etc/group', matchingGroups(255)); + const exactList = await kernel.exec('getgrouplist_bounds grouplist'); + expect(exactList.exitCode, `${exactList.stderr}\n${exactList.stdout}`).toBe(0); + expect(exactList.stderr).toBe(''); + expect(exactList.stdout).toBe( + 'grouplist_result=256\ngrouplist_count=256\ngrouplist_overflow=no\ngrouplist_canary=yes\n', + ); + + await vfs.writeFile('/etc/group', matchingGroups(256)); + const matchingOverflow = await kernel.exec('getgrouplist_bounds grouplist'); + expect( + matchingOverflow.exitCode, + `${matchingOverflow.stderr}\n${matchingOverflow.stdout}`, + ).toBe(0); + expect(matchingOverflow.stderr).toBe(''); + expect(matchingOverflow.stdout).toContain('grouplist_result=-1\n'); + expect(matchingOverflow.stdout).toContain('grouplist_overflow=yes\n'); + expect(matchingOverflow.stdout).toContain('grouplist_canary=yes\n'); + + const nonmatchingGroups = Array.from( + { length: 257 }, + (_, index) => `g${index}:x:${2000 + index}:someoneelse`, + ).join('\n') + '\n'; + await vfs.writeFile('/etc/group', nonmatchingGroups); + const databaseOverflow = await kernel.exec('getgrouplist_bounds grouplist'); + expect( + databaseOverflow.exitCode, + `${databaseOverflow.stderr}\n${databaseOverflow.stdout}`, + ).toBe(0); + expect(databaseOverflow.stderr).toBe(''); + expect(databaseOverflow.stdout).toContain('grouplist_result=-1\n'); + expect(databaseOverflow.stdout).toContain('grouplist_overflow=yes\n'); + expect(databaseOverflow.stdout).toContain('grouplist_canary=yes\n'); + }); + itIf(!tier2Skip, 'chown family matches Linux ownership, fd, and symlink semantics', async () => { const native = await runNative('chown_contract'); const wasm = await kernel.exec('chown_contract'); @@ -696,20 +783,22 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('dup_test'); const wasm = await kernel.exec('dup_test'); - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); + const diagnostic = `WASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`; + expect(wasm.exitCode, diagnostic).toBe(native.exitCode); + expect(wasm.stdout, diagnostic).toBe(native.stdout); + expect(normalizeStderr(wasm.stderr), diagnostic).toBe(normalizeStderr(native.stderr)); }); itIf(!tier2Skip, 'closefrom_test: closes high virtual descriptors', async () => { const native = await runNative('closefrom_test'); const wasm = await kernel.exec('closefrom_test'); - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(wasm.stdout).toContain('closefrom_closed=yes'); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); + const diagnostic = `WASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`; + expect(wasm.exitCode, diagnostic).toBe(native.exitCode); + expect(wasm.exitCode, diagnostic).toBe(0); + expect(wasm.stdout, diagnostic).toBe(native.stdout); + expect(wasm.stdout, diagnostic).toContain('closefrom_closed=yes'); + expect(normalizeStderr(wasm.stderr), diagnostic).toBe(normalizeStderr(native.stderr)); }); it('sleep_test: nanosleep completes successfully', async () => { @@ -1076,7 +1165,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('sigaction_behavior', [], { env }); const wasm = await kernel.exec('sigaction_behavior'); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `WASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(wasm.stdout).toBe(native.stdout); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); @@ -1321,8 +1413,11 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('socket_flags'); const wasm = await kernel.exec('socket_flags'); - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); + const diagnostic = + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}` + + `\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`; + expect(wasm.exitCode, diagnostic).toBe(native.exitCode); + expect(wasm.exitCode, diagnostic).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); expect(wasm.stdout).toBe(native.stdout); expect(wasm.stdout).toContain('socket_nonblock=yes'); @@ -1428,7 +1523,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('exec_edge'); const wasm = await kernel.exec('exec_edge'); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); expect(wasm.stdout).toBe(native.stdout); @@ -1455,7 +1553,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('exec_variants', [mode]); const wasm = await kernel.exec(`exec_variants ${mode}`); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); expect(wasm.stdout).toBe(native.stdout); @@ -1717,7 +1818,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('tcp_echo', [String(port)]); const wasm = await kernel.exec(`tcp_echo ${port}`); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(wasm.stdout).toBe(native.stdout); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); diff --git a/toolchain/crates/wasi-ext/src/lib.rs b/toolchain/crates/wasi-ext/src/lib.rs index 8d45e57b2e..32193931d9 100644 --- a/toolchain/crates/wasi-ext/src/lib.rs +++ b/toolchain/crates/wasi-ext/src/lib.rs @@ -21,6 +21,8 @@ pub const ERRNO_BADF: Errno = 8; pub const ERRNO_INVAL: Errno = 28; pub const ERRNO_IO: Errno = 29; pub const ERRNO_NOSYS: Errno = 52; +pub const ERRNO_NOTSUP: Errno = 58; +pub const ERRNO_PROTONOSUPPORT: Errno = 66; pub const ERRNO_NOENT: Errno = 44; pub const ERRNO_SRCH: Errno = 71; // No such process pub const ERRNO_CHILD: Errno = 12; // No child processes @@ -205,10 +207,11 @@ extern "C" { /// /// On success, the two socket FDs are written to `ret_fd0` and `ret_fd1`. /// Returns errno. - fn fd_socketpair( - domain: u32, - sock_type: u32, - protocol: u32, + #[link_name = "fd_socketpair"] + fn fd_socketpair_import( + socket_kind: u32, + nonblocking: u32, + close_on_exec: u32, ret_fd0: *mut u32, ret_fd1: *mut u32, ) -> Errno; @@ -398,19 +401,22 @@ pub fn spawn( // Encode them in the same ordered action stream used by libc posix_spawn. let mut actions = [0_u8; (ACTION_BYTES * 3) + (b"/dev/null".len() * 3)]; let mut actions_len = 0; - for (source, target) in [ - (stdin_fd, 0_u32), - (stdout_fd, 1_u32), - (stderr_fd, 2_u32), - ] - .into_iter() + for (source, target) in [(stdin_fd, 0_u32), (stdout_fd, 1_u32), (stderr_fd, 2_u32)].into_iter() { if source == target { continue; } - let path = if source == u32::MAX { b"/dev/null".as_slice() } else { &[] }; + let path = if source == u32::MAX { + b"/dev/null".as_slice() + } else { + &[] + }; let base = actions_len; - let command = if path.is_empty() { FDOP_DUP2 } else { FDOP_OPEN }; + let command = if path.is_empty() { + FDOP_DUP2 + } else { + FDOP_OPEN + }; actions[base..base + 4].copy_from_slice(&command.to_le_bytes()); actions[base + 4..base + 8].copy_from_slice(&target.to_le_bytes()); if command == FDOP_DUP2 { @@ -418,10 +424,8 @@ pub fn spawn( } else if target != 0 { actions[base + 12..base + 16].copy_from_slice(&O_WRONLY.to_le_bytes()); } - actions[base + 20..base + 24] - .copy_from_slice(&(path.len() as u32).to_le_bytes()); - actions[base + ACTION_BYTES..base + ACTION_BYTES + path.len()] - .copy_from_slice(path); + actions[base + 20..base + 24].copy_from_slice(&(path.len() as u32).to_le_bytes()); + actions[base + ACTION_BYTES..base + ACTION_BYTES + path.len()].copy_from_slice(path); actions_len += ACTION_BYTES + path.len(); } let (sigmask_lo, sigmask_hi) = signal_mask(3, 0, 0)?; @@ -462,9 +466,7 @@ pub fn spawn( pub fn signal_mask(how: u32, set_lo: u32, set_hi: u32) -> Result<(u32, u32), Errno> { let mut old_lo = 0; let mut old_hi = 0; - let errno = unsafe { - proc_signal_mask_v2(how, set_lo, set_hi, &mut old_lo, &mut old_hi) - }; + let errno = unsafe { proc_signal_mask_v2(how, set_lo, set_hi, &mut old_lo, &mut old_hi) }; if errno == ERRNO_SUCCESS { Ok((old_lo, old_hi)) } else { @@ -701,9 +703,40 @@ pub fn closefrom(low_fd: u32) -> Result<(), Errno> { /// /// Returns `Ok((fd0, fd1))` on success, `Err(errno)` on failure. pub fn socketpair(domain: u32, sock_type: u32, protocol: u32) -> Result<(u32, u32), Errno> { + const AF_UNIX: u32 = 1; + const SOCK_TYPE_MASK: u32 = 0x0f; + const SOCK_STREAM: u32 = 1; + const SOCK_DGRAM: u32 = 2; + const SOCK_SEQPACKET: u32 = 5; + const SOCK_NONBLOCK: u32 = 0o4000; + const SOCK_CLOEXEC: u32 = 0o2000000; + + if domain != AF_UNIX { + return Err(ERRNO_NOTSUP); + } + if protocol != 0 && protocol != AF_UNIX { + return Err(ERRNO_PROTONOSUPPORT); + } + if sock_type & !(SOCK_TYPE_MASK | SOCK_NONBLOCK | SOCK_CLOEXEC) != 0 { + return Err(ERRNO_INVAL); + } + let socket_kind = match sock_type & SOCK_TYPE_MASK { + SOCK_STREAM => 1, + SOCK_DGRAM => 2, + SOCK_SEQPACKET => 3, + _ => return Err(ERRNO_NOTSUP), + }; let mut fd0 = 0; let mut fd1 = 0; - let errno = unsafe { fd_socketpair(domain, sock_type, protocol, &mut fd0, &mut fd1) }; + let errno = unsafe { + fd_socketpair_import( + socket_kind, + u32::from(sock_type & SOCK_NONBLOCK != 0), + u32::from(sock_type & SOCK_CLOEXEC != 0), + &mut fd0, + &mut fd1, + ) + }; if errno == ERRNO_SUCCESS { Ok((fd0, fd1)) } else { @@ -1448,9 +1481,7 @@ pub fn get_groups() -> Result, Errno> { } pub fn set_groups(groups: &[u32]) -> Result<(), Errno> { - errno_result(unsafe { - host_setgroups(checked_u32_len(groups.len())?, groups.as_ptr()) - }) + errno_result(unsafe { host_setgroups(checked_u32_len(groups.len())?, groups.as_ptr()) }) } pub fn path_ids(path: &str, follow_symlinks: bool) -> Result<(u32, u32), Errno> { @@ -1545,7 +1576,12 @@ pub fn get_pwnam(name: &str, buf: &mut [u8]) -> Result { pub fn get_pwent(index: u32, buf: &mut [u8]) -> Result { let mut len = 0; let errno = unsafe { - host_getpwent(index, buf.as_mut_ptr(), checked_u32_len(buf.len())?, &mut len) + host_getpwent( + index, + buf.as_mut_ptr(), + checked_u32_len(buf.len())?, + &mut len, + ) }; if errno == ERRNO_SUCCESS { validate_returned_len(len, buf.len()) @@ -1556,9 +1592,8 @@ pub fn get_pwent(index: u32, buf: &mut [u8]) -> Result { pub fn get_grgid(gid: u32, buf: &mut [u8]) -> Result { let mut len = 0; - let errno = unsafe { - host_getgrgid(gid, buf.as_mut_ptr(), checked_u32_len(buf.len())?, &mut len) - }; + let errno = + unsafe { host_getgrgid(gid, buf.as_mut_ptr(), checked_u32_len(buf.len())?, &mut len) }; if errno == ERRNO_SUCCESS { validate_returned_len(len, buf.len()) } else { @@ -1587,7 +1622,12 @@ pub fn get_grnam(name: &str, buf: &mut [u8]) -> Result { pub fn get_grent(index: u32, buf: &mut [u8]) -> Result { let mut len = 0; let errno = unsafe { - host_getgrent(index, buf.as_mut_ptr(), checked_u32_len(buf.len())?, &mut len) + host_getgrent( + index, + buf.as_mut_ptr(), + checked_u32_len(buf.len())?, + &mut len, + ) }; if errno == ERRNO_SUCCESS { validate_returned_len(len, buf.len()) diff --git a/toolchain/std-patches/crates/uu_uname/0001-wasi-agentos-linux-identity.patch b/toolchain/std-patches/crates/uu_uname/0001-wasi-agentos-linux-identity.patch deleted file mode 100644 index 0724aebb0a..0000000000 --- a/toolchain/std-patches/crates/uu_uname/0001-wasi-agentos-linux-identity.patch +++ /dev/null @@ -1,34 +0,0 @@ ---- a/src/uname.rs -+++ b/src/uname.rs -@@ -76,12 +76,31 @@ impl UNameOutput { - - let nodename = (opts.nodename || opts.all).then(|| uname.nodename().to_owned()); - -+ #[cfg(target_os = "wasi")] -+ let kernel_name = (opts.kernel_name || opts.all || none) -+ .then(|| OsString::from("Linux")); -+ -+ #[cfg(target_os = "wasi")] -+ let nodename = (opts.nodename || opts.all) -+ .then(|| OsString::from("agentos")); -+ - let kernel_release = (opts.kernel_release || opts.all).then(|| uname.release().to_owned()); - - let kernel_version = (opts.kernel_version || opts.all).then(|| uname.version().to_owned()); - - let machine = (opts.machine || opts.all).then(|| uname.machine().to_owned()); - - let os = (opts.os || opts.all).then(|| uname.osname().to_owned()); -+ -+ #[cfg(target_os = "wasi")] -+ let kernel_release = (opts.kernel_release || opts.all) -+ .then(|| OsString::from("0.0.1-agentos")); -+ #[cfg(target_os = "wasi")] -+ let kernel_version = (opts.kernel_version || opts.all) -+ .then(|| OsString::from("#1 AgentOS Linux-in-WASM")); -+ #[cfg(target_os = "wasi")] -+ let machine = (opts.machine || opts.all).then(|| OsString::from("wasm32")); -+ #[cfg(target_os = "wasi")] -+ let os = (opts.os || opts.all).then(|| OsString::from("GNU/Linux")); - - // This option is unsupported on modern Linux systems diff --git a/toolchain/std-patches/wasi-libc/0008-sockets.patch b/toolchain/std-patches/wasi-libc/0008-sockets.patch index 8c2e33fb0a..847b9a972e 100644 --- a/toolchain/std-patches/wasi-libc/0008-sockets.patch +++ b/toolchain/std-patches/wasi-libc/0008-sockets.patch @@ -136,7 +136,7 @@ new file mode 100644 index 0000000..975e62a --- /dev/null +++ b/libc-bottom-half/sources/host_socket.c -@@ -0,0 +1,1195 @@ +@@ -0,0 +1,1205 @@ +// Socket API via wasmVM host_net imports. +// +// Replaces wasi-libc's ENOSYS stubs with calls to our custom WASM imports: @@ -200,6 +200,10 @@ index 0000000..975e62a +#define WASM_IMPORT(mod, fn) \ + __attribute__((__import_module__(mod), __import_name__(fn))) + ++// host_system.get_identity(field, buffer, length) -> errno ++WASM_IMPORT("host_system", "get_identity") ++uint32_t __host_system_get_identity(uint32_t field, char *buffer, uint32_t length); ++ +// host_net.net_socket(domain: u32, type: u32, protocol: u32, ret_fd: *mut u32) -> errno +WASM_IMPORT("host_net", "net_socket") +uint32_t __host_net_socket(uint32_t domain, uint32_t type, uint32_t protocol, uint32_t *ret_fd); @@ -804,13 +808,19 @@ index 0000000..975e62a +} + +int gethostname(char *name, size_t len) { -+ const char *hostname = "sandbox"; -+ size_t hlen = strlen(hostname); -+ if (hlen >= len) { -+ errno = ENAMETOOLONG; ++ if (name == NULL) { ++ errno = EFAULT; ++ return -1; ++ } ++ if (len > UINT32_MAX) { ++ errno = EOVERFLOW; ++ return -1; ++ } ++ uint32_t err = __host_system_get_identity(0, name, (uint32_t)len); ++ if (err != 0) { ++ errno = (int)err; + return -1; + } -+ memcpy(name, hostname, hlen + 1); + return 0; +} + diff --git a/toolchain/std-patches/wasi-libc/0018-posix-spawn-and-terminal-headers.patch b/toolchain/std-patches/wasi-libc/0018-posix-spawn-and-terminal-headers.patch index a792103b40..6342903acc 100644 --- a/toolchain/std-patches/wasi-libc/0018-posix-spawn-and-terminal-headers.patch +++ b/toolchain/std-patches/wasi-libc/0018-posix-spawn-and-terminal-headers.patch @@ -11,7 +11,7 @@ new file mode 100644 index 0000000..3b70693 --- /dev/null +++ b/libc-bottom-half/sources/host_terminal_compat.c -@@ -0,0 +1,217 @@ +@@ -0,0 +1,281 @@ +// Terminal/process-group compatibility for AgentOS WASI commands. +// +// The kernel owns PTY state and exposes it through host_tty imports. Keep the @@ -42,8 +42,30 @@ index 0000000..3b70693 +WASM_IMPORT("host_tty", "set_raw_mode") +uint32_t __host_tty_set_raw_mode(uint32_t enabled); + -+static struct termios g_shadow; -+static int g_shadow_valid = 0; ++WASM_IMPORT("host_tty", "set_size") ++uint32_t __host_tty_set_size(uint32_t fd, uint32_t cols, uint32_t rows); ++ ++WASM_IMPORT("host_tty", "get_attr") ++uint32_t __host_tty_get_attr(uint32_t fd, uint32_t *flags, uint8_t *cc); ++ ++WASM_IMPORT("host_tty", "set_attr") ++uint32_t __host_tty_set_attr(uint32_t fd, uint32_t flags, const uint8_t *cc); ++ ++WASM_IMPORT("host_tty", "get_pgrp") ++uint32_t __host_tty_get_pgrp(uint32_t fd, uint32_t *pgrp); ++ ++WASM_IMPORT("host_tty", "set_pgrp") ++uint32_t __host_tty_set_pgrp(uint32_t fd, uint32_t pgrp); ++ ++WASM_IMPORT("host_tty", "get_sid") ++uint32_t __host_tty_get_sid(uint32_t fd, uint32_t *sid); ++ ++#define HOST_TTY_ICRNL (1u << 0) ++#define HOST_TTY_OPOST (1u << 1) ++#define HOST_TTY_ONLCR (1u << 2) ++#define HOST_TTY_ICANON (1u << 3) ++#define HOST_TTY_ECHO (1u << 4) ++#define HOST_TTY_ISIG (1u << 5) + +static void cooked_defaults(struct termios *t) { + memset(t, 0, sizeof(*t)); @@ -69,13 +91,6 @@ index 0000000..3b70693 + t->__c_ospeed = B38400; +} + -+static void ensure_shadow(void) { -+ if (!g_shadow_valid) { -+ cooked_defaults(&g_shadow); -+ g_shadow_valid = 1; -+ } -+} -+ +pid_t fork(void) { + errno = ENOSYS; + return -1; @@ -91,13 +106,25 @@ index 0000000..3b70693 +} + +pid_t tcgetpgrp(int fd) { -+ (void)fd; -+ return 1; ++ uint32_t pgrp = 0; ++ uint32_t rc = __host_tty_get_pgrp((uint32_t)fd, &pgrp); ++ if (rc != 0) { ++ errno = (int)rc; ++ return -1; ++ } ++ return (pid_t)pgrp; +} + +int tcsetpgrp(int fd, pid_t pgrp) { -+ (void)fd; -+ (void)pgrp; ++ if (pgrp < 0) { ++ errno = EINVAL; ++ return -1; ++ } ++ uint32_t rc = __host_tty_set_pgrp((uint32_t)fd, (uint32_t)pgrp); ++ if (rc != 0) { ++ errno = (int)rc; ++ return -1; ++ } + return 0; +} + @@ -106,12 +133,27 @@ index 0000000..3b70693 + errno = EFAULT; + return -1; + } -+ if (!__host_tty_isatty((uint32_t)fd)) { -+ errno = ENOTTY; ++ uint32_t flags = 0; ++ uint8_t cc[7]; ++ uint32_t rc = __host_tty_get_attr((uint32_t)fd, &flags, cc); ++ if (rc != 0) { ++ errno = (int)rc; + return -1; + } -+ ensure_shadow(); -+ *termios_p = g_shadow; ++ cooked_defaults(termios_p); ++ if (!(flags & HOST_TTY_ICRNL)) termios_p->c_iflag &= ~ICRNL; ++ if (!(flags & HOST_TTY_OPOST)) termios_p->c_oflag &= ~OPOST; ++ if (!(flags & HOST_TTY_ONLCR)) termios_p->c_oflag &= ~ONLCR; ++ if (!(flags & HOST_TTY_ICANON)) termios_p->c_lflag &= ~ICANON; ++ if (!(flags & HOST_TTY_ECHO)) termios_p->c_lflag &= ~ECHO; ++ if (!(flags & HOST_TTY_ISIG)) termios_p->c_lflag &= ~ISIG; ++ termios_p->c_cc[VINTR] = cc[0]; ++ termios_p->c_cc[VQUIT] = cc[1]; ++ termios_p->c_cc[VSUSP] = cc[2]; ++ termios_p->c_cc[VEOF] = cc[3]; ++ termios_p->c_cc[VERASE] = cc[4]; ++ termios_p->c_cc[VKILL] = cc[5]; ++ termios_p->c_cc[VWERASE] = cc[6]; + return 0; +} + @@ -121,14 +163,25 @@ index 0000000..3b70693 + errno = EFAULT; + return -1; + } -+ if (!__host_tty_isatty((uint32_t)fd)) { -+ errno = ENOTTY; ++ if (optional_actions != TCSANOW && optional_actions != TCSADRAIN && ++ optional_actions != TCSAFLUSH) { ++ errno = EINVAL; + return -1; + } -+ g_shadow = *termios_p; -+ g_shadow_valid = 1; -+ uint32_t raw = ((termios_p->c_lflag & ICANON) && (termios_p->c_lflag & ECHO)) ? 0u : 1u; -+ uint32_t rc = __host_tty_set_raw_mode(raw); ++ uint32_t flags = 0; ++ if (termios_p->c_iflag & ICRNL) flags |= HOST_TTY_ICRNL; ++ if (termios_p->c_oflag & OPOST) flags |= HOST_TTY_OPOST; ++ if (termios_p->c_oflag & ONLCR) flags |= HOST_TTY_ONLCR; ++ if (termios_p->c_lflag & ICANON) flags |= HOST_TTY_ICANON; ++ if (termios_p->c_lflag & ECHO) flags |= HOST_TTY_ECHO; ++ if (termios_p->c_lflag & ISIG) flags |= HOST_TTY_ISIG; ++ uint8_t cc[7] = { ++ termios_p->c_cc[VINTR], termios_p->c_cc[VQUIT], ++ termios_p->c_cc[VSUSP], termios_p->c_cc[VEOF], ++ termios_p->c_cc[VERASE], termios_p->c_cc[VKILL], ++ termios_p->c_cc[VWERASE], ++ }; ++ uint32_t rc = __host_tty_set_attr((uint32_t)fd, flags, cc); + if (rc != 0) { + errno = (int)rc; + return -1; @@ -137,8 +190,13 @@ index 0000000..3b70693 +} + +pid_t tcgetsid(int fd) { -+ (void)fd; -+ return 1; ++ uint32_t sid = 0; ++ uint32_t rc = __host_tty_get_sid((uint32_t)fd, &sid); ++ if (rc != 0) { ++ errno = (int)rc; ++ return -1; ++ } ++ return (pid_t)sid; +} + +int tcgetwinsize(int fd, struct winsize *ws) { @@ -161,10 +219,16 @@ index 0000000..3b70693 +} + +int tcsetwinsize(int fd, const struct winsize *ws) { -+ (void)fd; -+ (void)ws; -+ errno = ENOSYS; -+ return -1; ++ if (ws == NULL) { ++ errno = EFAULT; ++ return -1; ++ } ++ uint32_t rc = __host_tty_set_size((uint32_t)fd, ws->ws_col, ws->ws_row); ++ if (rc != 0) { ++ errno = (int)rc; ++ return -1; ++ } ++ return 0; +} + +void cfmakeraw(struct termios *t) { diff --git a/toolchain/std-patches/wasi-libc/0037-user-account-database.patch b/toolchain/std-patches/wasi-libc/0037-user-account-database.patch index 843c5b1877..1f8fe89398 100644 --- a/toolchain/std-patches/wasi-libc/0037-user-account-database.patch +++ b/toolchain/std-patches/wasi-libc/0037-user-account-database.patch @@ -1,15 +1,16 @@ Implement the POSIX passwd and group database through host_user. -The VM account database is kernel-owned and may contain multiple configured -users and groups. Route name/id lookups, iteration, reentrant APIs, and -getgrouplist through that database instead of consulting guest /etc files. +The VM account database is kernel-owned and may contain multiple live or +configured users and groups. Route name/id lookups, iteration, reentrant APIs, +and getgrouplist through the kernel host service so `/etc/passwd` and +`/etc/group` remain live process-visible sources without executor-local state. diff --git a/libc-bottom-half/sources/host_user_accounts.c b/libc-bottom-half/sources/host_user_accounts.c new file mode 100644 index 0000000..a1f4e3d --- /dev/null +++ b/libc-bottom-half/sources/host_user_accounts.c -@@ -0,0 +1,292 @@ +@@ -0,0 +1,310 @@ +#define _GNU_SOURCE +#include +#include @@ -22,7 +23,8 @@ index 0000000..a1f4e3d +#define WASM_IMPORT(mod, fn) \ + __attribute__((__import_module__(mod), __import_name__(fn))) +#define ACCOUNT_TEXT_MAX 4096 -+#define ACCOUNT_MEMBERS_MAX 256 ++#define ACCOUNT_GROUP_MEMBERS_MAX 256 ++#define ACCOUNT_GROUPS_MAX 256 + +WASM_IMPORT("host_user", "getpwnam") +uint32_t __host_getpwnam(const uint8_t *, uint32_t, uint8_t *, uint32_t, @@ -194,11 +196,16 @@ index 0000000..a1f4e3d + temporary[length] = '\0'; + size_t member_count = group_member_count(temporary, length); + if (member_count == SIZE_MAX) return EIO; -+ uintptr_t strings_end = (uintptr_t)buffer + length + 1; -+ uintptr_t aligned = (strings_end + sizeof(char *) - 1) & ~(sizeof(char *) - 1); -+ if (aligned < (uintptr_t)buffer || -+ aligned + (member_count + 1) * sizeof(char *) > (uintptr_t)buffer + size) ++ if (member_count > ACCOUNT_GROUP_MEMBERS_MAX) return EOVERFLOW; ++ size_t strings_size = (size_t)length + 1; ++ size_t alignment_padding = ++ (sizeof(char *) - strings_size % sizeof(char *)) % sizeof(char *); ++ size_t members_size = (member_count + 1) * sizeof(char *); ++ if (strings_size > size || alignment_padding > size - strings_size || ++ members_size > size - strings_size - alignment_padding) + return ERANGE; ++ uintptr_t aligned = ++ (uintptr_t)(buffer + strings_size + alignment_padding); + memcpy(buffer, temporary, length + 1); + int parsed = parse_group(buffer, length, entry, (char **)aligned, + member_count); @@ -228,7 +235,7 @@ index 0000000..a1f4e3d + +static struct group group_entry; +static char group_buffer[ACCOUNT_TEXT_MAX + -+ (ACCOUNT_MEMBERS_MAX + 1) * sizeof(char *)]; ++ (ACCOUNT_GROUP_MEMBERS_MAX + 1) * sizeof(char *)]; +static uint32_t group_index; + +struct group *getgrgid(gid_t gid) { @@ -282,19 +289,31 @@ index 0000000..a1f4e3d + +int getgrouplist(const char *user, gid_t primary, gid_t *groups, int *ngroups) { + if (!user || !ngroups || *ngroups < 0) { errno = EINVAL; return -1; } -+ gid_t found[ACCOUNT_MEMBERS_MAX]; ++ gid_t found[ACCOUNT_GROUPS_MAX]; + size_t count = 0; + found[count++] = primary; -+ for (uint32_t index = 0; index < ACCOUNT_MEMBERS_MAX; index++) { ++ int database_ended = 0; ++ for (uint32_t index = 0; index < ACCOUNT_GROUPS_MAX; index++) { + struct group *entry; + int error = group_by_id(__host_getgrent, index, &group_entry, + group_buffer, sizeof group_buffer, &entry); + if (error != 0) { errno = error; return -1; } -+ if (!entry) break; ++ if (!entry) { database_ended = 1; break; } + if (!group_has_member(entry, user)) continue; + size_t existing = 0; + while (existing < count && found[existing] != entry->gr_gid) existing++; -+ if (existing == count) found[count++] = entry->gr_gid; ++ if (existing == count) { ++ if (count == ACCOUNT_GROUPS_MAX) { errno = EOVERFLOW; return -1; } ++ found[count++] = entry->gr_gid; ++ } ++ } ++ if (!database_ended) { ++ struct group *extra; ++ int error = group_by_id(__host_getgrent, ACCOUNT_GROUPS_MAX, ++ &group_entry, group_buffer, ++ sizeof group_buffer, &extra); ++ if (error != 0) { errno = error; return -1; } ++ if (extra) { errno = EOVERFLOW; return -1; } + } + int capacity = *ngroups; + int copied = capacity < (int)count ? capacity : (int)count; diff --git a/toolchain/std-patches/wasi-libc/0047-host-system-identity.patch b/toolchain/std-patches/wasi-libc/0047-host-system-identity.patch new file mode 100644 index 0000000000..bc849a575e --- /dev/null +++ b/toolchain/std-patches/wasi-libc/0047-host-system-identity.patch @@ -0,0 +1,57 @@ +--- a/libc-top-half/musl/src/misc/uname.c ++++ b/libc-top-half/musl/src/misc/uname.c +@@ -1,32 +1,40 @@ + #include + #ifdef __wasilibc_unmodified_upstream // Implement uname with placeholders + #include "syscall.h" + #else ++#include ++#include + #include ++ ++#define WASM_IMPORT(mod, fn) \ ++ __attribute__((__import_module__(mod), __import_name__(fn))) ++WASM_IMPORT("host_system", "get_identity") ++uint32_t __host_system_get_identity(uint32_t field, char *buffer, uint32_t length); + #endif + + int uname(struct utsname *uts) + { + #ifdef __wasilibc_unmodified_upstream // Implement uname with placeholders + return syscall(SYS_uname, uts); + #else +- // Just fill in the fields with placeholder values. +- strcpy(uts->sysname, "wasi"); +- strcpy(uts->nodename, "(none)"); +- strcpy(uts->release, "0.0.0"); +- strcpy(uts->version, "0.0.0"); +-#if defined(__wasm32__) +- strcpy(uts->machine, "wasm32"); +-#elif defined(__wasm64__) +- strcpy(uts->machine, "wasm64"); +-#else +- strcpy(uts->machine, "unknown"); +-#endif ++ if (uts == NULL) { ++ errno = EFAULT; ++ return -1; ++ } ++ uint32_t err = __host_system_get_identity(1, uts->sysname, sizeof(uts->sysname)); ++ if (err == 0) err = __host_system_get_identity(0, uts->nodename, sizeof(uts->nodename)); ++ if (err == 0) err = __host_system_get_identity(2, uts->release, sizeof(uts->release)); ++ if (err == 0) err = __host_system_get_identity(3, uts->version, sizeof(uts->version)); ++ if (err == 0) err = __host_system_get_identity(4, uts->machine, sizeof(uts->machine)); + #ifdef _GNU_SOURCE +- strcpy(uts->domainname, "(none)"); ++ if (err == 0) err = __host_system_get_identity(5, uts->domainname, sizeof(uts->domainname)); + #else +- strcpy(uts->__domainname, "(none)"); ++ if (err == 0) err = __host_system_get_identity(5, uts->__domainname, sizeof(uts->__domainname)); + #endif ++ if (err != 0) { ++ errno = (int)err; ++ return -1; ++ } + return 0; + #endif + } diff --git a/toolchain/test-programs/getgrouplist_bounds.c b/toolchain/test-programs/getgrouplist_bounds.c new file mode 100644 index 0000000000..b2d3a06821 --- /dev/null +++ b/toolchain/test-programs/getgrouplist_bounds.c @@ -0,0 +1,54 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#define GROUP_CAP 256 +#define CANARY UINT64_C(0x51a7cafe93d40b62) + +static int group_member_mode(void) { + errno = 0; + struct group *group = getgrnam("membercap"); + int saved_errno = errno; + size_t count = 0; + if (group != NULL) { + while (group->gr_mem[count] != NULL) count++; + } + printf("group_found=%s\n", group != NULL ? "yes" : "no"); + printf("group_members=%zu\n", count); + printf("group_overflow=%s\n", saved_errno == EOVERFLOW ? "yes" : "no"); + return 0; +} + +static int grouplist_mode(void) { + struct { + gid_t groups[GROUP_CAP]; + uint64_t canary; + } output; + memset(&output, 0xa5, sizeof output); + output.canary = CANARY; + int count = GROUP_CAP; + errno = 0; + int result = getgrouplist("boundsuser", 1000, output.groups, &count); + int saved_errno = errno; + + printf("grouplist_result=%d\n", result); + printf("grouplist_count=%d\n", count); + printf("grouplist_overflow=%s\n", saved_errno == EOVERFLOW ? "yes" : "no"); + printf("grouplist_canary=%s\n", output.canary == CANARY ? "yes" : "no"); + return 0; +} + +int main(int argc, char **argv) { + if (argc != 2) { + fputs("usage: getgrouplist_bounds group-members|grouplist\n", stderr); + return 2; + } + if (strcmp(argv[1], "group-members") == 0) return group_member_mode(); + if (strcmp(argv[1], "grouplist") == 0) return grouplist_mode(); + fprintf(stderr, "unknown mode: %s\n", argv[1]); + return 2; +} diff --git a/toolchain/test-programs/pipe_edge.c b/toolchain/test-programs/pipe_edge.c index 22cf02ba5c..c598d5182a 100644 --- a/toolchain/test-programs/pipe_edge.c +++ b/toolchain/test-programs/pipe_edge.c @@ -4,10 +4,7 @@ #include #include #include - -#ifndef __wasi__ #include -#endif #define LARGE_SIZE (128 * 1024) /* 128KB > 64KB pipe buffer */ #define CHUNK_SIZE 32768 /* 32KB per chunk — fits in pipe buffer */ @@ -103,9 +100,7 @@ int main(void) { /* Test 2: broken pipe — close read end, write to write end -> EPIPE */ { -#ifndef __wasi__ signal(SIGPIPE, SIG_IGN); -#endif int p[2]; if (pipe(p) != 0) { printf("broken_pipe: FAIL (pipe creation failed)\n"); diff --git a/toolchain/test-programs/ppoll_contract.c b/toolchain/test-programs/ppoll_contract.c index d167d41e20..9c059a9f1d 100644 --- a/toolchain/test-programs/ppoll_contract.c +++ b/toolchain/test-programs/ppoll_contract.c @@ -24,10 +24,13 @@ static int poll_abi_matches_linux(void) { static int normal_read_write_aliases(void) { int descriptors[2]; - if (pipe(descriptors) != 0) + if (pipe(descriptors) != 0) { + fprintf(stderr, "normal aliases: pipe errno=%d\n", errno); return 0; + } char byte = 'x'; if (write(descriptors[1], &byte, 1) != 1) { + fprintf(stderr, "normal aliases: write errno=%d\n", errno); close(descriptors[0]); close(descriptors[1]); return 0; @@ -42,6 +45,11 @@ static int normal_read_write_aliases(void) { close(descriptors[0]); close(descriptors[1]); printf("poll_normal_aliases=%s\n", ok ? "yes" : "no"); + if (!ok) + fprintf(stderr, + "normal aliases: rc=%d errno=%d read_revents=0x%x " + "write_revents=0x%x\n", + result, errno, fds[0].revents, fds[1].revents); return ok; } diff --git a/website/public/docs/docs/resource-limits.md b/website/public/docs/docs/resource-limits.md index 2ebe589d00..30e1cb6937 100644 --- a/website/public/docs/docs/resource-limits.md +++ b/website/public/docs/docs/resource-limits.md @@ -4,7 +4,7 @@ Cap per-VM resources, JavaScript CPU/wall-clock time, Python execution, and WASM Every agentOS VM runs with **per-VM resource and runtime caps**. These caps contain runaway or malicious guest work to its VM and give the host an explicit failure instead of silent data loss. -- **Secure defaults**: unset fields fall back to built-in defaults that match the runtime's historical constants. Optional execution budgets such as WASM fuel explicitly document when their default has no additional budget. +- **Secure defaults**: unset fields fall back to built-in defaults. Standalone WASM gets a 30-second active-CPU safeguard, while elapsed wall-clock and deterministic-fuel budgets remain opt-in. - **Per-VM**: every VM gets its own budget. Limits are not shared across VMs. - **Enforced by the sidecar/runtime**: a guest that exceeds a cap fails inside the VM (out-of-memory, `EMFILE`, `EAGAIN`, runtime timeout, etc.) instead of consuming past the configured budget. - **Operator-raisable**: the operator (the trusted process that creates the VM) may raise any cap for trusted workloads. Guest code can never raise its own caps. @@ -22,7 +22,6 @@ Set caps on the `limits` object in the `agentOS` config. Limits are grouped by s | `resources.maxSockets` | Open sockets in the socket table | Bounds concurrent connections; excess `connect`/`accept` fail. | | `resources.maxFilesystemBytes` | Total bytes stored in the virtual filesystem | Bounds VFS storage; writes past the budget fail with a no-space error. | | `resources.maxInodeCount` | Inodes retained by the virtual filesystem | Default is `16384`; creating another file or directory fails with a no-space error. This is the expected upper bound for filesystem-schema sizing and benchmarks. | -| `resources.maxWasmFuel` | WASM execution budget | Bounds WASM execution work; unset means no explicit fuel budget. | | `resources.maxWasmMemoryBytes` | WASM linear memory, in bytes | Default is `128 MiB`. | | `resources.maxWasmStackBytes` | Maximum WASM call-stack size, in bytes | Deep recursion fails with a stack overflow instead of crashing the VM. | | `resources.maxBlockingReadMs` | AgentOS safety backstop for otherwise-blocking guest operations | Default is `30000`. Socket waits, poll, and contended `F_SETLKW` warn near the limit and fail with `ETIMEDOUT` if it expires; raise it for workloads that intentionally wait longer. Linux has no equivalent global backstop. | @@ -36,6 +35,8 @@ Set caps on the `limits` object in the `agentOS` config. Limits are grouped by s | `acp.maxPendingPermissionsPerVm` | Actionable ACP permission requests across one VM | Default is `10000`. | | `acp.maxPermissionOutcomesPerSession` | Terminal ACP permission outcomes retained for one session | Default is `10000`; it must not exceed `acp.maxPermissionOutcomesPerVm`. | | `acp.maxPermissionOutcomesPerVm` | Terminal ACP permission outcomes retained across one VM | Default is `100000`. Old outcomes are bounded independently from pending requests. | +| `process.maxPendingChildSyncCount` | Concurrent `spawnSync` and Python synchronous subprocess calls retained across one VM | Default is `64`. Admission fails before the child is spawned and reports `limits.process.maxPendingChildSyncCount`. | +| `process.maxPendingChildSyncBytes` | Aggregate input plus stdout/stderr capture capacity reserved by synchronous child-process calls across one VM | Default is `64 MiB`. Capacity is reclaimed on rejection, child completion, or process teardown. | | `jsRuntime.v8HeapLimitMb` | Guest JavaScript V8 heap, in MiB | Default is `128`. | | `jsRuntime.cpuTimeLimitMs` | Active JavaScript CPU time | Default is `30000`; `0` disables the CPU watchdog. | | `jsRuntime.wallClockLimitMs` | JavaScript elapsed wall-clock backstop | Default is `0`, disabled. Use this for finite commands, not long-lived adapters. | @@ -46,12 +47,16 @@ Set caps on the `limits` object in the `agentOS` config. Limits are grouped by s | `wasm.prewarmTimeoutMs` | WASM compile-cache warmup timeout | Default is `30000`. | | `wasm.runnerHeapLimitMb` | Trusted WASI/WASM runner V8 heap, in MiB | Default is `2048`; this is not guest linear memory. | | `wasm.runnerCpuTimeLimitMs` | Trusted WASI/WASM runner active-CPU budget | Default is `30000`; `0` disables this budget for trusted configurations. | +| `wasm.activeCpuTimeLimitMs` | Active standalone-WASM CPU time | Default is `30000`; `0` disables this safeguard for trusted configurations. Time blocked on terminal, network, filesystem, child, or timer waits does not consume it. | +| `wasm.wallClockLimitMs` | Standalone-WASM elapsed wall-clock backstop | Optional and disabled when omitted. It includes time spent blocked or awaiting host work. | +| `wasm.deterministicFuel` | Deterministic standalone-WASM instruction budget | Optional. The V8 compatibility backend rejects an explicit value with `ENOTSUP` because V8 cannot meter deterministic fuel. | | `process.maxSpawnFileActions` | File actions decoded for one `posix_spawn` call | Default is `4096`; excess actions fail with `E2BIG`. | | `process.maxSpawnFileActionBytes` | Serialized file-action bytes for one `posix_spawn` call | Default is `1 MiB`; excess input fails with `E2BIG`. | ## Behavior at the limit - **WASM stack**: deep recursion throws a stack-overflow error in the guest, never a host crash. +- **WASM CPU time**: CPU-bound modules terminate after `wasm.activeCpuTimeLimitMs`, while idle interactive commands do not consume that budget. `wasm.wallClockLimitMs` is the independent opt-in elapsed deadline. - **JavaScript CPU time**: CPU-bound loops terminate with a CPU-budget error once active JS CPU exceeds `jsRuntime.cpuTimeLimitMs`. - **JavaScript wall time**: awaiting or blocked JS terminates only when you set `jsRuntime.wallClockLimitMs`; the default is disabled for long-lived adapters. - **Filesystem bytes**: writing past the VFS budget fails with a no-space error to the guest. @@ -102,4 +107,4 @@ bounded queues are tracked in a central limit registry that: or `AGENTOS_LOG=debug` for live per-limit usage snapshots. See [Limits & Observability](/docs/architecture/limits-and-observability) for the -full architecture. \ No newline at end of file +full architecture. diff --git a/website/src/content/docs/docs/resource-limits.mdx b/website/src/content/docs/docs/resource-limits.mdx index 1f6d5f59a6..c148900af1 100644 --- a/website/src/content/docs/docs/resource-limits.mdx +++ b/website/src/content/docs/docs/resource-limits.mdx @@ -6,7 +6,7 @@ skill: true Every agentOS VM runs with **per-VM resource and runtime caps**. These caps contain runaway or malicious guest work to its VM and give the host an explicit failure instead of silent data loss. -- **Secure defaults**: unset fields fall back to built-in defaults that match the runtime's historical constants. Optional execution budgets such as WASM fuel explicitly document when their default has no additional budget. +- **Secure defaults**: unset fields fall back to built-in defaults. Standalone WASM gets a 30-second active-CPU safeguard, while elapsed wall-clock and deterministic-fuel budgets remain opt-in. - **Per-VM**: every VM gets its own budget. Limits are not shared across VMs. - **Enforced by the sidecar/runtime**: a guest that exceeds a cap fails inside the VM (out-of-memory, `EMFILE`, `EAGAIN`, runtime timeout, etc.) instead of consuming past the configured budget. - **Operator-raisable**: the operator (the trusted process that creates the VM) may raise any cap for trusted workloads. Guest code can never raise its own caps. @@ -26,13 +26,14 @@ Set caps on the `limits` object in the `agentOS` config. Limits are grouped by s | `resources.maxSockets` | Open sockets in the socket table | Bounds concurrent connections; excess `connect`/`accept` fail. | | `resources.maxFilesystemBytes` | Total bytes stored in the virtual filesystem | Bounds VFS storage; writes past the budget fail with a no-space error. | | `resources.maxInodeCount` | Inodes retained by the virtual filesystem | Default is `16384`; creating another file or directory fails with a no-space error. This is the expected upper bound for filesystem-schema sizing and benchmarks. | -| `resources.maxWasmFuel` | WASM execution budget | Bounds WASM execution work; unset means no explicit fuel budget. | | `resources.maxWasmMemoryBytes` | WASM linear memory, in bytes | Default is `128 MiB`. | | `resources.maxWasmStackBytes` | Maximum WASM call-stack size, in bytes | Deep recursion fails with a stack overflow instead of crashing the VM. | | `resources.maxBlockingReadMs` | AgentOS safety backstop for otherwise-blocking guest operations | Default is `30000`. Socket waits, poll, and contended `F_SETLKW` warn near the limit and fail with `ETIMEDOUT` if it expires; raise it for workloads that intentionally wait longer. Linux has no equivalent global backstop. | | `process.pendingStdinBytes` | Stdin accepted by the sidecar but not yet written into kernel pipes | Default is `64 MiB` per process and across the VM. Sibling processes share the same aggregate envelope, so this is a tighter bound for multi-process workloads. A non-draining process rejects further writes with an error naming `limits.process.pendingStdinBytes`. | | `process.pendingEventCount` | Event count at each bounded VM/process delivery-queue stage | Default is `10000`. The crossing event is rejected with an error naming `limits.process.pendingEventCount`; it is never silently dropped. | | `process.pendingEventBytes` | Retained process-event bytes at each bounded delivery-queue stage | Default is `64 MiB` per process and across all process queues in the VM. Sibling processes share the VM-wide envelope. Large stdout/stderr bursts are rejected with an error naming `limits.process.pendingEventBytes`, independently of event count. | +| `process.maxPendingChildSyncCount` | Concurrent `spawnSync` and Python synchronous subprocess calls retained across one VM | Default is `64`. Admission fails before the child is spawned and reports `limits.process.maxPendingChildSyncCount`. | +| `process.maxPendingChildSyncBytes` | Aggregate input plus stdout/stderr capture capacity reserved by synchronous child-process calls across one VM | Default is `64 MiB`. Capacity is reclaimed on rejection, child completion, or process teardown. | | `acp.maxSessionsPerVm` | Durable sessions retained in one VM SQLite database | Default is `10000`. Opening another session fails with a typed error naming this field. | | `acp.maxPromptsPerSession` | Prompt and idempotency records retained for one durable session | Default is `100000`; it must not exceed `acp.maxPromptsPerVm`. | | `acp.maxPromptsPerVm` | Prompt and idempotency records retained across one VM | Default is `1000000`. | @@ -49,13 +50,16 @@ Set caps on the `limits` object in the `agentOS` config. Limits are grouped by s | `python.maxOldSpaceMb` | Pyodide runner V8 old-space heap, in MiB | Default is `0`, which keeps the engine default. | | `wasm.prewarmTimeoutMs` | WASM compile-cache warmup timeout | Default is `30000`. | | `wasm.runnerHeapLimitMb` | Trusted WASI/WASM runner V8 heap, in MiB | Default is `2048`; this is not guest linear memory. | -| `wasm.runnerCpuTimeLimitMs` | Trusted WASI/WASM runner active-CPU budget | Default is `30000`; `0` disables this budget for trusted configurations. | +| `wasm.activeCpuTimeLimitMs` | Active standalone-WASM CPU time | Default is `30000`; `0` disables this safeguard for trusted configurations. Time blocked on terminal, network, filesystem, child, or timer waits does not consume it. | +| `wasm.wallClockLimitMs` | Standalone-WASM elapsed wall-clock backstop | Optional and disabled when omitted. It includes time spent blocked or awaiting host work. | +| `wasm.deterministicFuel` | Deterministic standalone-WASM instruction budget | Optional. The V8 compatibility backend rejects an explicit value with `ENOTSUP` because V8 cannot meter deterministic fuel. | | `process.maxSpawnFileActions` | File actions decoded for one `posix_spawn` call | Default is `4096`; excess actions fail with `E2BIG`. | | `process.maxSpawnFileActionBytes` | Serialized file-action bytes for one `posix_spawn` call | Default is `1 MiB`; excess input fails with `E2BIG`. | ## Behavior at the limit - **WASM stack**: deep recursion throws a stack-overflow error in the guest, never a host crash. +- **WASM CPU time**: CPU-bound modules terminate after `wasm.activeCpuTimeLimitMs`, while idle interactive commands do not consume that budget. `wasm.wallClockLimitMs` is the independent opt-in elapsed deadline. - **JavaScript CPU time**: CPU-bound loops terminate with a CPU-budget error once active JS CPU exceeds `jsRuntime.cpuTimeLimitMs`. - **JavaScript wall time**: awaiting or blocked JS terminates only when you set `jsRuntime.wallClockLimitMs`; the default is disabled for long-lived adapters. - **Filesystem bytes**: writing past the VFS budget fails with a no-space error to the guest. From 84636cb2af8030d2e81e99cbc646e949a4448258 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Mon, 20 Jul 2026 17:28:34 -0700 Subject: [PATCH 3/6] feat: add Wasmtime executor --- .github/workflows/publish.yaml | 39 +- Cargo.lock | 601 +++++- Cargo.toml | 9 + crates/agentos-sidecar/src/acp/mod.rs | 9 +- crates/agentos-sidecar/src/acp/restore.rs | 1 + crates/agentos-sidecar/src/acp/runtime.rs | 1 + crates/client/src/lib.rs | 4 +- crates/client/src/process.rs | 25 + crates/client/src/shell.rs | 3 + crates/execution/Cargo.toml | 5 +- .../execution/assets/runners/wasm-runner.mjs | 52 +- crates/execution/src/backend/event.rs | 1 + crates/execution/src/backend/reply.rs | 36 +- crates/execution/src/backend/submission.rs | 8 +- crates/execution/src/host/filesystem.rs | 6 + crates/execution/src/host/mod.rs | 1 + crates/execution/src/host/process.rs | 4 + crates/execution/src/lib.rs | 5 +- crates/execution/src/node_import_cache.rs | 2 +- crates/execution/src/wasm.rs | 698 ++++++- crates/execution/src/wasm/profile.rs | 66 + crates/execution/src/wasm/wasmtime/cache.rs | 238 +++ crates/execution/src/wasm/wasmtime/engine.rs | 352 ++++ crates/execution/src/wasm/wasmtime/error.rs | 165 ++ .../execution/src/wasm/wasmtime/lifecycle.rs | 1286 ++++++++++++ crates/execution/src/wasm/wasmtime/limits.rs | 117 ++ .../src/wasm/wasmtime/linker/filesystem.rs | 875 +++++++++ .../execution/src/wasm/wasmtime/linker/mod.rs | 572 ++++++ .../src/wasm/wasmtime/linker/network.rs | 1067 ++++++++++ .../src/wasm/wasmtime/linker/preview1.rs | 1356 +++++++++++++ .../src/wasm/wasmtime/linker/process.rs | 1722 +++++++++++++++++ .../src/wasm/wasmtime/linker/terminal.rs | 318 +++ .../src/wasm/wasmtime/linker/user.rs | 346 ++++ crates/execution/src/wasm/wasmtime/memory.rs | 159 ++ crates/execution/src/wasm/wasmtime/mod.rs | 26 + crates/execution/src/wasm/wasmtime/module.rs | 24 + crates/execution/src/wasm/wasmtime/store.rs | 635 ++++++ crates/execution/tests/wasm.rs | 15 + crates/kernel/src/fd_table.rs | 186 +- crates/kernel/src/kernel.rs | 22 + crates/kernel/src/process_table.rs | 1 + crates/native-sidecar-core/src/limits.rs | 3 +- crates/native-sidecar-core/src/root_fs.rs | 4 +- .../src/execution/child_process.rs | 93 +- .../src/execution/host_dispatch/filesystem.rs | 127 +- .../src/execution/host_dispatch/mod.rs | 6 +- .../execution/host_dispatch/network_compat.rs | 14 +- .../src/execution/host_dispatch/process.rs | 5 + .../src/execution/javascript/rpc.rs | 3 +- crates/native-sidecar/src/execution/launch.rs | 25 +- crates/native-sidecar/src/execution/mod.rs | 17 +- .../src/execution/network/tcp.rs | 8 +- .../src/execution/network/unix.rs | 16 +- .../native-sidecar/src/execution/process.rs | 77 +- .../src/execution/process_events.rs | 45 +- crates/native-sidecar/src/filesystem.rs | 5 +- crates/native-sidecar/src/service.rs | 11 +- crates/native-sidecar/src/state.rs | 72 +- crates/native-sidecar/src/vm.rs | 16 +- .../tests/architecture_guards.rs | 132 +- .../tests/builtin_conformance.rs | 1 + crates/native-sidecar/tests/extension.rs | 2 + crates/native-sidecar/tests/filesystem.rs | 3 + .../tests/fixtures/limits-inventory.json | 161 ++ crates/native-sidecar/tests/guest_identity.rs | 44 +- .../native-sidecar/tests/posix_compliance.rs | 2 + crates/native-sidecar/tests/python.rs | 4 + .../tests/security_hardening.rs | 4 + crates/native-sidecar/tests/service.rs | 18 +- crates/native-sidecar/tests/signal.rs | 1 + crates/native-sidecar/tests/stdio_binary.rs | 1 + crates/native-sidecar/tests/support/mod.rs | 10 + crates/native-sidecar/tests/wasm_raw_abi.rs | 32 +- .../tests/wasm_software_parity.rs | 422 ++++ .../native-sidecar/tests/wasmtime_safety.rs | 305 +++ .../tests/xfstests_correctness.rs | 1 + crates/resource/src/lib.rs | 5 +- crates/runtime/src/accounting.rs | 3 + crates/runtime/src/lib.rs | 16 + .../protocol/agentos_sidecar_v1.bare | 6 + crates/sidecar-protocol/src/protocol.rs | 1 + crates/sidecar-protocol/src/wire.rs | 1 + crates/vm-config/src/lib.rs | 2 +- docker/build/darwin.Dockerfile | 2 +- docker/build/linux-gnu.Dockerfile | 3 +- docs/design/wasmtime-executor.md | 96 +- packages/core/src/runtime-compat.ts | 2 + packages/core/src/runtime.ts | 2 + packages/core/src/sidecar/rpc-client.ts | 3 + .../runtime-core/src/generated-protocol.ts | 163 +- packages/runtime-core/src/kernel-proxy.ts | 3 + packages/runtime-core/src/protocol-maps.ts | 12 + packages/runtime-core/src/request-payloads.ts | 7 + packages/runtime-core/src/sidecar-process.ts | 5 + packages/runtime-core/src/test-runtime.ts | 1 + .../tests/request-payloads.test.ts | 1 + 96 files changed, 12709 insertions(+), 373 deletions(-) create mode 100644 crates/execution/src/wasm/profile.rs create mode 100644 crates/execution/src/wasm/wasmtime/cache.rs create mode 100644 crates/execution/src/wasm/wasmtime/engine.rs create mode 100644 crates/execution/src/wasm/wasmtime/error.rs create mode 100644 crates/execution/src/wasm/wasmtime/lifecycle.rs create mode 100644 crates/execution/src/wasm/wasmtime/limits.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/filesystem.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/mod.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/network.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/preview1.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/process.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/terminal.rs create mode 100644 crates/execution/src/wasm/wasmtime/linker/user.rs create mode 100644 crates/execution/src/wasm/wasmtime/memory.rs create mode 100644 crates/execution/src/wasm/wasmtime/mod.rs create mode 100644 crates/execution/src/wasm/wasmtime/module.rs create mode 100644 crates/execution/src/wasm/wasmtime/store.rs create mode 100644 crates/native-sidecar/tests/wasm_software_parity.rs create mode 100644 crates/native-sidecar/tests/wasmtime_safety.rs diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index be44656971..b0d7a39cc6 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,7 +21,9 @@ env: R2_BUCKET: rivet-releases R2_ENDPOINT: https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com SIDECAR_PLATFORMS: "linux-x64-gnu linux-arm64-gnu darwin-x64 darwin-arm64" - RUST_TOOLCHAIN: "1.91.1" + # Wasmtime 46's reviewed MSRV. Keep the Docker defaults in sync so release + # artifacts cannot accidentally build with an older unsupported compiler. + RUST_TOOLCHAIN: "1.94.0" LINUX_GNU_LLVM_VERSION: "22" LINUX_GNU_SYSROOT_TAG: "sysroot-20250207" @@ -101,6 +103,33 @@ jobs: path: software/codex/wasm if-no-files-found: error + wasmtime-darwin-smoke: + needs: [context] + name: "Wasmtime smoke (${{ matrix.platform }})" + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-x64 + runner: macos-15-intel + architecture: x86_64 + - platform: darwin-arm64 + runner: macos-15 + architecture: arm64 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - uses: Swatinem/rust-cache@v2 + with: + key: wasmtime-${{ matrix.platform }} + - name: Verify native runner architecture + run: test "$(uname -m)" = "${{ matrix.architecture }}" + - name: Compile and execute Wasmtime embedding smoke tests + run: cargo test -p agentos-execution wasmtime --lib + build-sidecar: needs: [context] name: "Build linux native artifacts (${{ matrix.platform }})" @@ -244,9 +273,9 @@ jobs: if-no-files-found: error publish-npm: - needs: [context, wasm-commands, codex-wasm, build-sidecar, build-sidecar-darwin] + needs: [context, wasm-commands, codex-wasm, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin] name: Publish npm - if: ${{ !cancelled() && needs.wasm-commands.result == 'success' && needs.codex-wasm.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} + if: ${{ !cancelled() && needs.wasm-commands.result == 'success' && needs.codex-wasm.result == 'success' && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -328,9 +357,9 @@ jobs: ${{ needs.context.outputs.trigger == 'release' && '--release-mode' || '' }} release-assets: - needs: [context, build-sidecar, build-sidecar-darwin] + needs: [context, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin] name: Release assets - if: ${{ !cancelled() && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} + if: ${{ !cancelled() && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} runs-on: ubuntu-latest permissions: contents: write diff --git a/Cargo.lock b/Cargo.lock index 4818196752..73515a230e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -136,9 +145,12 @@ dependencies = [ "nix 0.29.0", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "tokio", "tracing", + "wasmparser 0.251.0", + "wasmtime", "wat", ] @@ -478,6 +490,12 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.9.2" @@ -1045,13 +1063,13 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex 1.3.0", "syn", ] @@ -1108,6 +1126,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytes" @@ -1246,6 +1267,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "combine" version = "4.6.7" @@ -1329,6 +1359,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1347,6 +1386,152 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2e9b7adf77fa02204d4d523ae4f171b6591c2632b030865336335c7d7b420e" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f09d9f397eae612ac15becf0e0b2165d2232003f4f2a1549572a724d1c48c5" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e004cf1270abc82f7b9fb32d1a9d73a1cc0d5a0215b97db8c32658748e78b01" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.3", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f32034641f96b123e4fdb5666e726d9f252e222638fc8fdd905b4ff18c3c4e" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" + +[[package]] +name = "cranelift-control" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591abe6f5312bd2c4220f1b3bead56c2ad00257c52668015ba013b85dcf2a17a" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341d5e1e071320505ebbc8194a8eb61fa94394b1ed93ba3cbec3be5fa1c3c1ce" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c6f3e2419ecb54a5503994d35421554c5e487d0493bc86ea96907bab51fd29" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21aa47a5e0b1e9fb2c9348459088d5dbb872ebe17781ada1270d8364c7e73257" + +[[package]] +name = "cranelift-native" +version = "0.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3054a03ac285b170662ec9530602b01cd394a7428cd753b48d9cae51f05249a" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ed47e602652e3410f9387fc0db70fefadcee4d78a78881421aabcab4e26b89" + [[package]] name = "crc-fast" version = "1.10.0" @@ -1622,6 +1807,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.2" @@ -1901,6 +2098,18 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -2007,6 +2216,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "hashlink" @@ -2407,6 +2621,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2506,6 +2726,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2630,6 +2859,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2683,6 +2918,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "matchers" version = "0.2.0" @@ -2718,6 +2959,15 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "memmap2" version = "0.9.11" @@ -2882,6 +3132,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3137,6 +3399,18 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -3191,6 +3465,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "047bda68096e5f290619ce037b7c3fa352d11f08edf0fce3030c351adc0f8ec0" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83258a9bcc97d3fb15bf4e1c43bc2cc85c718e3563a697f145f245e651f2828f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.46" @@ -3261,6 +3558,21 @@ dependencies = [ "syn", ] +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash 2.1.3", + "serde", + "smallvec", +] + [[package]] name = "regex" version = "1.12.4" @@ -3382,12 +3694,24 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3664,6 +3988,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -3901,6 +4229,9 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -4058,6 +4389,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -4614,6 +4951,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser 0.251.0", +] + [[package]] name = "wasm-encoder" version = "0.252.0" @@ -4621,7 +4968,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "semver", + "serde", ] [[package]] @@ -4635,6 +4995,220 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmprinter" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.251.0", +] + +[[package]] +name = "wasmtime" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08dd5caf09eda1d523261a0dd421a54e3bb165cf6f5b02c82327e712d49e3cf4" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix 1.1.4", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.251.0", + "wasmtime-environ", + "wasmtime-internal-component-macro", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f3aa689689cf295568aa4d24ab636579fb3b36dfa7cea35020d87064fc88ec" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de4451ab437d7b2d41e637a4379e87ff76aeeb051236064dcc75c1855714ee58" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b5e357002645964342b99a6fe8405cda7dd031f498927992c9d99d5012daa7" + +[[package]] +name = "wasmtime-internal-core" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6ce74d60a8ed870548e7efa9710c54f982bbfcf80e5ee5eee8498318616483" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24204583d847b7ce3d770f7a3456739d5d43f65c884cb47111dbe27b26ebbe6" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.251.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecaeb13bf3eb94a02e32ea3842c8fda3ea6897c299745100c1230b65769d2d91" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5090e9a302bb5729a84766e4af07b6b10efe733e437bc266dd954a957114df63" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bba33d9d951a9a974a866e80c864a9a46b28086e369582e0caa78e14a9f29e4" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695063a19bac17895f95c0c0f2f9544e85a277763cd77b5778f0cce6a971073e" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc77f7513209b76e8f4772af640819f47fca3a5425b4417ef9c20b888334063c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859103d8336304b8beebbf89a620c2f53a118fd5fbce41bc572cf4bacc8111d" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap 2.14.0", + "wit-parser", +] + [[package]] name = "wast" version = "35.0.2" @@ -4654,7 +5228,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.252.0", ] [[package]] @@ -4929,6 +5503,25 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-parser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.251.0", +] + [[package]] name = "witx" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index af7a155ff1..baa2e2ae44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,3 +81,12 @@ agentos-vm-config = { path = "crates/vm-config", version = "0.0.1" } vfs = { package = "agentos-vfs-core", path = "crates/vfs", version = "0.0.1" } vbare = "0.0.4" vbare-compiler = { package = "rivet-vbare-compiler", version = "0.0.5" } +# Keep parallel-compilation disabled: Wasmtime implements it with Rayon's +# host-sized global pool, outside AgentOS's fixed reviewed thread census. +wasmtime = { version = "=46.0.0", default-features = false, features = [ + "async", + "cranelift", + "runtime", + "std", +] } +wasmparser = "=0.251.0" diff --git a/crates/agentos-sidecar/src/acp/mod.rs b/crates/agentos-sidecar/src/acp/mod.rs index 8d574b4b8b..1907500e99 100644 --- a/crates/agentos-sidecar/src/acp/mod.rs +++ b/crates/agentos-sidecar/src/acp/mod.rs @@ -1373,7 +1373,7 @@ mod tests { #[test] fn configured_acp_limit_errors_preserve_stable_wire_codes() { - let mut limits = AcpLimits { + let limits = AcpLimits { max_prompt_bytes: 3, ..AcpLimits::default() }; @@ -1381,8 +1381,11 @@ mod tests { .expect_err("prompt bytes must be bounded"); assert_eq!(error_code(&bytes_error), "acp_prompt_bytes_limit"); - limits.max_prompt_bytes = 1024; - limits.max_prompt_blocks = 1; + let limits = AcpLimits { + max_prompt_bytes: 1024, + max_prompt_blocks: 1, + ..AcpLimits::default() + }; let blocks_error = parse_content_blocks("[{},{}]", "main", &limits) .expect_err("prompt blocks must be bounded"); assert_eq!(error_code(&blocks_error), "acp_prompt_blocks_limit"); diff --git a/crates/agentos-sidecar/src/acp/restore.rs b/crates/agentos-sidecar/src/acp/restore.rs index be709a57b4..730312d027 100644 --- a/crates/agentos-sidecar/src/acp/restore.rs +++ b/crates/agentos-sidecar/src/acp/restore.rs @@ -236,6 +236,7 @@ impl AcpExtension { env: env.into_iter().collect(), cwd: Some(create_like.cwd.clone()), wasm_permission_tier: None, + wasm_backend: None, }) .await { diff --git a/crates/agentos-sidecar/src/acp/runtime.rs b/crates/agentos-sidecar/src/acp/runtime.rs index 6bdda91588..09c882826a 100644 --- a/crates/agentos-sidecar/src/acp/runtime.rs +++ b/crates/agentos-sidecar/src/acp/runtime.rs @@ -46,6 +46,7 @@ impl AcpExtension { env: env.into_iter().collect(), cwd: Some(request.cwd.clone()), wasm_permission_tier: None, + wasm_backend: None, }) .await { diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index c0389caab2..b276779392 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -72,8 +72,8 @@ pub use config::{ pub use process::{ ExecOptions, ExecResult, ProcessExit, ProcessInfo, ProcessOutput, ProcessStatus, ProcessStream, - ProcessTreeNode, SpawnHandle, SpawnOptions, SpawnStdio, SpawnedProcessInfo, StdinInput, - TimingMitigation, + ProcessTreeNode, SpawnHandle, SpawnOptions, SpawnStdio, SpawnedProcessInfo, + StandaloneWasmBackend, StdinInput, TimingMitigation, }; pub use net::{HttpRequest, HttpResponse}; diff --git a/crates/client/src/process.rs b/crates/client/src/process.rs index 604c058830..50d2ac33c2 100644 --- a/crates/client/src/process.rs +++ b/crates/client/src/process.rs @@ -55,6 +55,24 @@ pub enum TimingMitigation { Freeze, } +/// Engine override for standalone WebAssembly commands. JavaScript and its +/// `WebAssembly.*` APIs always remain on V8. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StandaloneWasmBackend { + V8, + Wasmtime, +} + +impl From for wire::StandaloneWasmBackend { + fn from(value: StandaloneWasmBackend) -> Self { + match value { + StandaloneWasmBackend::V8 => Self::V8, + StandaloneWasmBackend::Wasmtime => Self::Wasmtime, + } + } +} + /// `stdin` value: a string or raw bytes. #[derive(Debug, Clone, PartialEq, Eq)] pub enum StdinInput { @@ -82,6 +100,7 @@ pub struct ExecOptions { pub file_path: Option, pub cpu_time_limit_ms: Option, pub timing_mitigation: Option, + pub wasm_backend: Option, } impl Default for ExecOptions { @@ -97,6 +116,7 @@ impl Default for ExecOptions { file_path: None, cpu_time_limit_ms: None, timing_mitigation: None, + wasm_backend: None, } } } @@ -128,6 +148,7 @@ pub struct SpawnOptions { pub stdout_fd: Option, pub stderr_fd: Option, pub stream_stdin: Option, + pub wasm_backend: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -251,6 +272,7 @@ impl AgentOs { resolved_args, options.env.clone(), options.cwd.clone(), + options.wasm_backend, ) .await .context("exec: Execute request failed")?; @@ -901,6 +923,7 @@ impl AgentOs { args: Vec, env: BTreeMap, cwd: Option, + wasm_backend: Option, ) -> std::result::Result { let ownership = self.vm_scope(); let response = self @@ -916,6 +939,7 @@ impl AgentOs { env: env.into_iter().collect(), cwd, wasm_permission_tier: None, + wasm_backend: wasm_backend.map(Into::into), }), ) .await?; @@ -1050,6 +1074,7 @@ impl AgentOs { args, options.env.clone(), options.cwd.clone(), + options.wasm_backend, ) .await { diff --git a/crates/client/src/shell.rs b/crates/client/src/shell.rs index a2a9f5a41f..985d231877 100644 --- a/crates/client/src/shell.rs +++ b/crates/client/src/shell.rs @@ -289,6 +289,7 @@ impl AgentOs { env: options.env.clone().into_iter().collect(), cwd: options.cwd.clone(), wasm_permission_tier: None, + wasm_backend: None, }; // Background: subscribe to events first (so no output is missed), issue the spawn, fan @@ -440,6 +441,7 @@ impl AgentOs { env: options.env.clone().into_iter().collect(), cwd: options.cwd.clone(), wasm_permission_tier: None, + wasm_backend: None, }; let agent = self.clone(); @@ -596,6 +598,7 @@ impl AgentOs { env: base.env.clone().into_iter().collect(), cwd: base.cwd.clone(), wasm_permission_tier: None, + wasm_backend: None, }; // Subscribe before issuing the spawn so no output is missed. diff --git a/crates/execution/Cargo.toml b/crates/execution/Cargo.toml index 9b700bb757..d9da05b02e 100644 --- a/crates/execution/Cargo.toml +++ b/crates/execution/Cargo.toml @@ -29,11 +29,14 @@ base64 = "0.22" ciborium = "0.2" getrandom = "0.2" flume = "0.11" -nix = { version = "0.29", features = ["fs"] } +nix = { version = "0.29", features = ["fs", "time"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1" +sha2 = "0.10" tokio = { version = "1", features = ["rt", "sync", "time"] } tracing = "0.1" +wasmtime = { workspace = true } +wasmparser = { workspace = true } [dev-dependencies] agentos-wasm-abi-generator = { path = "../wasm-abi-generator" } diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 93a525ed10..99ce792dbb 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -14221,24 +14221,40 @@ if (__agentOSWasiSyscallPhasesEnabled) { } function instantiateWasmModule(targetModule) { - return __agentOSWasmMeasurePhase('WebAssembly.Instance', () => new WebAssembly.Instance(targetModule, { - wasi_snapshot_preview1: wasiImport, - wasi_unstable: wasiImport, - host_system: hostSystemImport, - host_tty: hostTtyImport, - // Read-write commands like DuckDB need fd_dup_min from the patched - // wasi-libc surface, but broader host_process capabilities stay - // reserved for the full tier. - host_process: - permissionTier === 'full' - ? hostProcessImport - : permissionTier === 'isolated' - ? undefined - : limitedHostProcessImport, - host_net: permissionTier === 'full' ? hostNetImport : undefined, - host_user: hostUserImport, - host_fs: hostFsImport, - })); + try { + return __agentOSWasmMeasurePhase('WebAssembly.Instance', () => new WebAssembly.Instance(targetModule, { + wasi_snapshot_preview1: wasiImport, + wasi_unstable: wasiImport, + host_system: hostSystemImport, + host_tty: hostTtyImport, + // Read-write commands like DuckDB need fd_dup_min from the patched + // wasi-libc surface, but broader host_process capabilities stay + // reserved for the full tier. + host_process: + permissionTier === 'full' + ? hostProcessImport + : permissionTier === 'isolated' + ? undefined + : limitedHostProcessImport, + host_net: permissionTier === 'full' ? hostNetImport : undefined, + host_user: hostUserImport, + host_fs: hostFsImport, + })); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const unsupported = /Import #\d+ "([^"]+)" "([^"]+)"/.exec(message); + if (unsupported) { + process.stderr.write( + `ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT: unsupported WebAssembly host import ${unsupported[1]}.${unsupported[2]}\n`, + ); + } else { + process.stderr.write( + 'ERR_AGENTOS_WASM_INSTANTIATION: WebAssembly host imports do not match module requirements\n', + ); + } + process.exit(1); + throw error; + } } let instance = instantiateWasmModule(module); diff --git a/crates/execution/src/backend/event.rs b/crates/execution/src/backend/event.rs index 24085a593d..f5b2cf9c7f 100644 --- a/crates/execution/src/backend/event.rs +++ b/crates/execution/src/backend/event.rs @@ -56,6 +56,7 @@ pub enum ExecutionExit { /// extension and are never consumed by shared host-service implementations. #[derive(Debug, Clone)] #[non_exhaustive] +#[allow(clippy::large_enum_variant)] pub enum ExecutionEvent { Output { stream: OutputStream, diff --git a/crates/execution/src/backend/reply.rs b/crates/execution/src/backend/reply.rs index 72ae61f3a1..bb01d923ec 100644 --- a/crates/execution/src/backend/reply.rs +++ b/crates/execution/src/backend/reply.rs @@ -62,6 +62,24 @@ impl DirectHostReplyTarget for DirectHostReplyWaiterTarget { .send(result) .map_err(|_| HostServiceError::new("EPIPE", "direct host-reply receiver was canceled")) } + + fn dismiss_claimed(&self, call_id: u64) -> Result<(), HostServiceError> { + self.validate_call_id(call_id)?; + let sender = self + .sender + .lock() + .map_err(|_| HostServiceError::new("EIO", "direct host-reply waiter lock is poisoned"))? + .take() + .ok_or_else(|| { + HostServiceError::new("EALREADY", "direct host-reply waiter is already settled") + })?; + sender + .send(Err(HostServiceError::new( + "ERR_AGENTOS_EXEC_REPLACED", + "the kernel committed a replacement process image", + ))) + .map_err(|_| HostServiceError::new("EPIPE", "direct host-reply receiver was canceled")) + } } impl DirectHostReplyWaiterTarget { @@ -503,9 +521,11 @@ mod tests { use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; use std::sync::Mutex; + type RecordedReply = (u64, bool, Result); + #[derive(Default)] struct RecordingTarget { - replies: Mutex)>>, + replies: Mutex>, dismissed: Mutex>, fail_delivery: bool, } @@ -737,6 +757,20 @@ mod tests { assert_eq!(error.code, "EACCES"); } + #[test] + fn dismissed_native_exec_waiter_receives_replacement_outcome() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 26, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + assert!(reply.claim().expect("claim exec")); + reply.dismiss_claimed().expect("dismiss exec"); + let error = poll_direct_receiver(receiver).expect_err("exec replacement"); + assert_eq!(error.code, "ERR_AGENTOS_EXEC_REPLACED"); + } + #[test] fn canceled_native_waiter_prevents_a_claimed_side_effect() { let identity = HostCallIdentity { diff --git a/crates/execution/src/backend/submission.rs b/crates/execution/src/backend/submission.rs index eadf6b2cef..632cb91273 100644 --- a/crates/execution/src/backend/submission.rs +++ b/crates/execution/src/backend/submission.rs @@ -167,9 +167,7 @@ impl ExecutionEventSubmitHandle { let result = self.submit_admitted_identity(event, admission); if let Err(error) = &result { if let Some(reply) = reply { - if let Err(delivery_error) = reply.fail(error.clone()) { - return Err(delivery_error); - } + reply.fail(error.clone())?; } } result @@ -239,9 +237,7 @@ impl ExecutionEventSubmitHandle { }; let queued_retention = match &event { ExecutionEvent::HostCall { reply, .. } => { - if let Err(error) = reply.retain_request(retention) { - return Err(error); - } + reply.retain_request(retention)?; None } _ => Some(retention), diff --git a/crates/execution/src/host/filesystem.rs b/crates/execution/src/host/filesystem.rs index 3bdf14021a..a0a80e13a5 100644 --- a/crates/execution/src/host/filesystem.rs +++ b/crates/execution/src/host/filesystem.rs @@ -128,6 +128,12 @@ pub enum FilesystemOperation { linkable: bool, }, Pipe, + /// Snapshot the process's live kernel fd table. This is read at the point + /// of a spawn/exec decision; executor adapters must not cache it. + Snapshot, + /// Install kernel-owned WASI roots and canonicalize the initial direct + /// guest fd namespace before any guest code executes. + CanonicalPreopens, Preopen { fd: u32, }, diff --git a/crates/execution/src/host/mod.rs b/crates/execution/src/host/mod.rs index c1a315257e..f64a91ba83 100644 --- a/crates/execution/src/host/mod.rs +++ b/crates/execution/src/host/mod.rs @@ -40,6 +40,7 @@ pub struct HostProcessContext { /// Runtime-neutral operation accepted by the shared sidecar host dispatcher. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] +#[allow(clippy::large_enum_variant)] pub enum HostOperation { Filesystem(FilesystemOperation), Network(NetworkOperation), diff --git a/crates/execution/src/host/process.rs b/crates/execution/src/host/process.rs index df7fbcfe19..f4be935085 100644 --- a/crates/execution/src/host/process.rs +++ b/crates/execution/src/host/process.rs @@ -213,6 +213,10 @@ impl BoundedProcessLaunchRequest { /// diverge through separate fd projections. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExecutableImageSource { + /// Client-selected initial image admitted by the trusted sidecar before + /// guest execution starts. This authority is never exposed as a guest + /// import and does not apply to spawn/exec images. + TrustedInitialPath(BoundedString), Path(BoundedString), Descriptor(u32), } diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index a49c6b86f7..39603d75a2 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -38,9 +38,10 @@ pub use python::{ PythonVfsRpcStat, StartPythonExecutionRequest, }; pub use signal::{ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration}; +pub use wasm::wasmtime::TRUSTED_INITIAL_MODULE_PREFIX; pub use wasm::{ - CreateWasmContextRequest, NativeBinaryFormat, StartWasmExecutionRequest, WasmContext, - WasmExecution, WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, + CreateWasmContextRequest, NativeBinaryFormat, StandaloneWasmBackend, StartWasmExecutionRequest, + WasmContext, WasmExecution, WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, WasmExecutionLimits, WasmExecutionResult, WasmPermissionTier, }; diff --git a/crates/execution/src/node_import_cache.rs b/crates/execution/src/node_import_cache.rs index 7879fa6762..2483fc9984 100644 --- a/crates/execution/src/node_import_cache.rs +++ b/crates/execution/src/node_import_cache.rs @@ -2423,7 +2423,7 @@ fn run_node_import_cache_root_cleanup_once(base_dir: &Path, cleanup: impl FnOnce .entry(base_dir.to_path_buf()) .or_insert_with(|| Arc::new(OnceLock::new())) .clone(); - cleanup_cell.get_or_init(|| cleanup()); + cleanup_cell.get_or_init(cleanup); } fn cleanup_stale_node_import_caches(base_dir: &Path) { diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index 39673258af..0b6d8f7a79 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -1,12 +1,13 @@ use crate::backend::{ - DescendantOutputOwnership, DescendantWaitOwnership, ExecutionBackend, ExecutionBackendKind, - ExecutionExit, ExecutionWakeHandle, ExecutionWakeIdentity, HostServiceError, - PublishedSignalCheckpoint, ShutdownOutcome, ShutdownReason, SignalCheckpointOutcome, - SynchronousFdWritePolicy, + DescendantOutputOwnership, DescendantWaitOwnership, DirectHostReplyHandle, ExecutionBackend, + ExecutionBackendKind, ExecutionExit, ExecutionWakeHandle, ExecutionWakeIdentity, + HostServiceError, PublishedSignalCheckpoint, ShutdownOutcome, ShutdownReason, + SignalCheckpointOutcome, SynchronousFdWritePolicy, }; use crate::common::{ encode_json_string, encode_json_string_array, encode_json_string_map, frozen_time_ms, }; +use crate::host::ProcessHostCapabilitySet; use crate::javascript::{ CreateJavascriptContextRequest, GuestRuntimeConfig, HostRpcRequest, JavascriptExecution, JavascriptExecutionEngine, JavascriptExecutionError, JavascriptExecutionEvent, @@ -34,6 +35,11 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::time::{Duration, Instant}; use tokio::sync::Notify; +#[path = "wasm/profile.rs"] +mod profile; +#[path = "wasm/wasmtime/mod.rs"] +pub mod wasmtime; + const WASM_MODULE_PATH_ENV: &str = "AGENTOS_WASM_MODULE_PATH"; const WASM_GUEST_ARGV_ENV: &str = "AGENTOS_GUEST_ARGV"; const WASM_GUEST_ENV_ENV: &str = "AGENTOS_GUEST_ENV"; @@ -174,6 +180,15 @@ pub enum WasmPermissionTier { Isolated, } +/// Sealed standalone-WASM engine choice. JavaScript WebAssembly APIs are not +/// affected by this selector and always remain inside V8. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum StandaloneWasmBackend { + #[default] + V8, + Wasmtime, +} + impl WasmPermissionTier { fn as_env_value(self) -> &'static str { match self { @@ -271,11 +286,18 @@ pub struct StartWasmExecutionRequest { pub guest_runtime: GuestRuntimeConfig, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub enum WasmExecutionEvent { Stdout(Vec), Stderr(Vec), SyncRpcRequest(HostRpcRequest), + /// Native executor request carrying its own generation-bound direct waiter. + /// This is decoded into the same runtime-neutral HostOperation path as the + /// compatibility runner, but has no V8 responder or line protocol. + HostCall { + request: HostRpcRequest, + reply: DirectHostReplyHandle, + }, SignalState { signal: u32, registration: ExecutionSignalHandlerRegistration, @@ -283,6 +305,40 @@ pub enum WasmExecutionEvent { Exited(i32), } +impl PartialEq for WasmExecutionEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Stdout(left), Self::Stdout(right)) + | (Self::Stderr(left), Self::Stderr(right)) => left == right, + (Self::SyncRpcRequest(left), Self::SyncRpcRequest(right)) => left == right, + ( + Self::HostCall { + request: left_request, + reply: left_reply, + }, + Self::HostCall { + request: right_request, + reply: right_reply, + }, + ) => left_request == right_request && left_reply.identity() == right_reply.identity(), + ( + Self::SignalState { + signal: left_signal, + registration: left_registration, + }, + Self::SignalState { + signal: right_signal, + registration: right_registration, + }, + ) => left_signal == right_signal && left_registration == right_registration, + (Self::Exited(left), Self::Exited(right)) => left == right, + _ => false, + } + } +} + +impl Eq for WasmExecutionEvent {} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct WasmExecutionResult { pub execution_id: String, @@ -358,6 +414,7 @@ pub enum WasmExecutionError { limit: usize, observed: usize, }, + Host(HostServiceError), Internal { code: &'static str, message: &'static str, @@ -476,6 +533,7 @@ impl fmt::Display for WasmExecutionError { f, "ERR_AGENTOS_RESOURCE_LIMIT: {limit_name} limit is {limit}, observed {observed}; raise {limit_name} if needed" ), + Self::Host(error) => write!(f, "{}: {}", error.code, error.message), Self::Internal { code, message } => write!(f, "{code}: {message}"), Self::EventChannelClosed => { f.write_str("guest WebAssembly event channel closed unexpectedly") @@ -488,6 +546,17 @@ impl std::error::Error for WasmExecutionError {} #[derive(Debug)] pub struct WasmExecution { + backend: WasmExecutionBackend, +} + +#[derive(Debug)] +enum WasmExecutionBackend { + V8(Box), + Wasmtime(wasmtime::WasmtimeExecution), +} + +#[derive(Debug)] +struct V8WasmExecution { execution_id: String, child_pid: u32, inner: JavascriptExecution, @@ -775,7 +844,8 @@ fn wasm_event_retained_bytes(event: &WasmExecutionEvent) -> usize { WasmExecutionEvent::Stdout(bytes) | WasmExecutionEvent::Stderr(bytes) => { envelope.saturating_add(bytes.len()) } - WasmExecutionEvent::SyncRpcRequest(request) => envelope + WasmExecutionEvent::SyncRpcRequest(request) + | WasmExecutionEvent::HostCall { request, .. } => envelope .saturating_add(request.method.len()) .saturating_add(request.raw_bytes_args.values().map(Vec::len).sum::()) // JSON args arrive through an independently frame-bounded bridge; @@ -799,7 +869,7 @@ struct WasmGuestPathMapping { read_only: bool, } -impl WasmExecution { +impl V8WasmExecution { pub fn sync_rpc_responder(&self) -> JavascriptSyncRpcResponder { self.inner.sync_rpc_responder() } @@ -1044,6 +1114,7 @@ impl WasmExecution { request.method ))); } + WasmExecutionEvent::HostCall { .. } => return Err(no_native_host_consumer()), WasmExecutionEvent::SignalState { .. } => {} WasmExecutionEvent::Exited(exit_code) => { return Ok(WasmExecutionResult { @@ -1386,7 +1457,7 @@ impl WasmExecution { } } -impl ExecutionBackend for WasmExecution { +impl ExecutionBackend for V8WasmExecution { fn kind(&self) -> ExecutionBackendKind { ExecutionBackendKind::WebAssembly } @@ -1404,7 +1475,7 @@ impl ExecutionBackend for WasmExecution { } fn native_process_id(&self) -> Option { - WasmExecution::native_process_id(self) + V8WasmExecution::native_process_id(self) } fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { @@ -1412,11 +1483,11 @@ impl ExecutionBackend for WasmExecution { } fn is_prepared_for_start(&self) -> bool { - WasmExecution::is_prepared_for_start(self) + V8WasmExecution::is_prepared_for_start(self) } fn start_prepared(&mut self) -> Result<(), HostServiceError> { - WasmExecution::start_prepared(self).map_err(|error| { + V8WasmExecution::start_prepared(self).map_err(|error| { HostServiceError::new("ERR_AGENTOS_EXECUTION_START", error.to_string()) }) } @@ -1449,9 +1520,9 @@ impl ExecutionBackend for WasmExecution { fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { let result = if paused { - WasmExecution::pause(self) + V8WasmExecution::pause(self) } else { - WasmExecution::resume(self) + V8WasmExecution::resume(self) }; result.map_err(|error| { HostServiceError::new("ERR_AGENTOS_EXECUTION_CONTROL", error.to_string()) @@ -1464,7 +1535,7 @@ impl ExecutionBackend for WasmExecution { } fn close_stdin(&mut self) -> Result<(), HostServiceError> { - WasmExecution::close_stdin(self).map_err(|error| { + V8WasmExecution::close_stdin(self).map_err(|error| { HostServiceError::new("ERR_AGENTOS_EXECUTION_STDIN", error.to_string()) }) } @@ -1513,6 +1584,406 @@ impl ExecutionBackend for WasmExecution { } } +impl WasmExecution { + pub fn standalone_backend(&self) -> StandaloneWasmBackend { + match &self.backend { + WasmExecutionBackend::V8(_) => StandaloneWasmBackend::V8, + WasmExecutionBackend::Wasmtime(_) => StandaloneWasmBackend::Wasmtime, + } + } + + pub fn sync_rpc_responder(&self) -> Option { + match &self.backend { + WasmExecutionBackend::V8(execution) => Some(execution.sync_rpc_responder()), + WasmExecutionBackend::Wasmtime(_) => None, + } + } + + pub fn execution_id(&self) -> &str { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.execution_id(), + WasmExecutionBackend::Wasmtime(execution) => execution.execution_id(), + } + } + + pub fn native_process_id(&self) -> Option { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.native_process_id(), + WasmExecutionBackend::Wasmtime(_) => None, + } + } + + pub fn v8_session_handle(&self) -> Option { + match &self.backend { + WasmExecutionBackend::V8(execution) => Some(execution.v8_session_handle()), + WasmExecutionBackend::Wasmtime(_) => None, + } + } + + pub fn start_prepared(&mut self) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.start_prepared(), + WasmExecutionBackend::Wasmtime(execution) => execution.start_prepared(), + } + } + + #[doc(hidden)] + pub fn is_prepared_for_start(&self) -> bool { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.is_prepared_for_start(), + WasmExecutionBackend::Wasmtime(execution) => execution.is_prepared_for_start(), + } + } + + pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.write_stdin(chunk), + WasmExecutionBackend::Wasmtime(_) => Ok(()), + } + } + + pub fn write_stdin_kernel_only(&mut self, chunk: &[u8]) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.write_stdin_kernel_only(chunk), + WasmExecutionBackend::Wasmtime(_) => Ok(()), + } + } + + pub fn close_stdin(&mut self) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.close_stdin(), + WasmExecutionBackend::Wasmtime(_) => Ok(()), + } + } + + pub fn send_stream_event( + &self, + event_type: &str, + payload: Value, + ) -> Result<(), WasmExecutionError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.send_stream_event(event_type, payload), + WasmExecutionBackend::Wasmtime(_) => Err(wasmtime_adapter_only_error( + "ENOTSUP", + "native Wasmtime executions do not accept V8 stream events", + )), + } + } + + pub fn terminate(&self) -> Result<(), WasmExecutionError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.terminate(), + WasmExecutionBackend::Wasmtime(execution) => { + execution.terminate(); + Ok(()) + } + } + } + + pub fn pause(&self) -> Result<(), WasmExecutionError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.pause(), + WasmExecutionBackend::Wasmtime(execution) => { + execution.set_paused(true); + Ok(()) + } + } + } + + pub fn resume(&self) -> Result<(), WasmExecutionError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.resume(), + WasmExecutionBackend::Wasmtime(execution) => { + execution.set_paused(false); + Ok(()) + } + } + } + + pub fn respond_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.respond_sync_rpc_success(id, result), + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub fn claim_sync_rpc_response(&mut self, id: u64) -> Result { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.claim_sync_rpc_response(id), + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub fn respond_claimed_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => { + execution.respond_claimed_sync_rpc_success(id, result) + } + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub fn respond_sync_rpc_raw_success( + &mut self, + id: u64, + payload: Vec, + ) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => { + execution.respond_sync_rpc_raw_success(id, payload) + } + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub fn respond_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => { + execution.respond_sync_rpc_error(id, code, message) + } + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub fn respond_claimed_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => { + execution.respond_claimed_sync_rpc_error(id, code, message) + } + WasmExecutionBackend::Wasmtime(_) => Err(no_native_sync_rpc()), + } + } + + pub async fn poll_event( + &mut self, + timeout: Duration, + ) -> Result, WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.poll_event(timeout).await, + WasmExecutionBackend::Wasmtime(execution) => execution.poll_event(timeout).await, + } + } + + pub fn try_poll_event(&mut self) -> Result, WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.try_poll_event(), + WasmExecutionBackend::Wasmtime(execution) => execution.try_poll_event(), + } + } + + pub fn poll_event_blocking( + &mut self, + timeout: Duration, + ) -> Result, WasmExecutionError> { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.poll_event_blocking(timeout), + WasmExecutionBackend::Wasmtime(execution) => execution.poll_event_blocking(timeout), + } + } + + pub fn wait(self) -> Result { + match self.backend { + WasmExecutionBackend::V8(execution) => execution.wait(), + WasmExecutionBackend::Wasmtime(execution) => { + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + loop { + match execution.next_event_blocking()? { + WasmExecutionEvent::Stdout(chunk) => stdout.extend_from_slice(&chunk), + WasmExecutionEvent::Stderr(chunk) => stderr.extend_from_slice(&chunk), + WasmExecutionEvent::Exited(exit_code) => { + return Ok(WasmExecutionResult { + execution_id: execution.execution_id().to_owned(), + exit_code, + stdout, + stderr, + }); + } + WasmExecutionEvent::SyncRpcRequest(_) => { + return Err(no_native_sync_rpc()); + } + WasmExecutionEvent::HostCall { .. } => { + return Err(no_native_host_consumer()); + } + WasmExecutionEvent::SignalState { .. } => {} + } + } + } + } + } +} + +fn no_native_sync_rpc() -> WasmExecutionError { + wasmtime_adapter_only_error( + "ENOTSUP", + "native Wasmtime imports use direct typed host waiters, not V8 sync RPC", + ) +} + +fn no_native_host_consumer() -> WasmExecutionError { + wasmtime_adapter_only_error( + "ENOTCONN", + "native Wasmtime host calls require the sidecar host-event consumer", + ) +} + +fn wasmtime_adapter_only_error(code: &'static str, message: &'static str) -> WasmExecutionError { + WasmExecutionError::Host(HostServiceError::new(code, message)) +} + +impl ExecutionBackend for WasmExecution { + fn kind(&self) -> ExecutionBackendKind { + ExecutionBackendKind::WebAssembly + } + + fn synchronous_fd_write_policy(&self) -> SynchronousFdWritePolicy { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.synchronous_fd_write_policy(), + WasmExecutionBackend::Wasmtime(_) => SynchronousFdWritePolicy::Blocking, + } + } + + fn descendant_wait_ownership(&self) -> DescendantWaitOwnership { + DescendantWaitOwnership::Guest + } + + fn descendant_output_ownership(&self) -> DescendantOutputOwnership { + DescendantOutputOwnership::GuestDescriptors + } + + fn native_process_id(&self) -> Option { + WasmExecution::native_process_id(self) + } + + fn wake_handle(&self, identity: ExecutionWakeIdentity) -> Option { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.wake_handle(identity), + WasmExecutionBackend::Wasmtime(_) => None, + } + } + + fn configure_host_services(&mut self, host: ProcessHostCapabilitySet) { + if let WasmExecutionBackend::Wasmtime(execution) = &self.backend { + execution.configure_host_services(host); + } + } + + fn is_prepared_for_start(&self) -> bool { + WasmExecution::is_prepared_for_start(self) + } + + fn start_prepared(&mut self) -> Result<(), HostServiceError> { + WasmExecution::start_prepared(self).map_err(wasm_execution_host_error) + } + + fn begin_shutdown( + &mut self, + reason: ShutdownReason, + ) -> Result { + match &mut self.backend { + WasmExecutionBackend::V8(execution) => execution.begin_shutdown(reason), + WasmExecutionBackend::Wasmtime(execution) => { + execution.terminate(); + Ok(match reason { + ShutdownReason::Signal(signal) => { + ShutdownOutcome::Exited(ExecutionExit::Signaled { + signal, + core_dumped: false, + }) + } + ShutdownReason::RuntimeFault => { + ShutdownOutcome::Exited(ExecutionExit::Exited(1)) + } + _ => ShutdownOutcome::AwaitExit, + }) + } + } + } + + fn set_paused(&self, paused: bool) -> Result<(), HostServiceError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.set_paused(paused), + WasmExecutionBackend::Wasmtime(execution) => { + execution.set_paused(paused); + Ok(()) + } + } + } + + fn write_stdin(&mut self, _bytes: &[u8]) -> Result<(), HostServiceError> { + Ok(()) + } + + fn close_stdin(&mut self) -> Result<(), HostServiceError> { + WasmExecution::close_stdin(self).map_err(wasm_execution_host_error) + } + + fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result { + match &self.backend { + WasmExecutionBackend::V8(execution) => { + execution.deliver_signal_checkpoint(identity, signal, delivery_token, flags) + } + WasmExecutionBackend::Wasmtime(execution) => { + execution.deliver_signal_checkpoint(identity, signal, delivery_token, flags)?; + Ok(SignalCheckpointOutcome::Published) + } + } + } + + fn take_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.take_signal_checkpoint(identity), + WasmExecutionBackend::Wasmtime(execution) => execution.take_signal_checkpoint(identity), + } + } + + fn discard_signal_checkpoints( + &self, + identity: ExecutionWakeIdentity, + ) -> Result<(), HostServiceError> { + match &self.backend { + WasmExecutionBackend::V8(execution) => execution.discard_signal_checkpoints(identity), + WasmExecutionBackend::Wasmtime(execution) => { + execution.discard_signal_checkpoints(identity) + } + } + } +} + +fn wasm_execution_host_error(error: WasmExecutionError) -> HostServiceError { + match error { + WasmExecutionError::Host(error) => error, + error => HostServiceError::new("ERR_AGENTOS_WASM_EXECUTION", error.to_string()), + } +} + #[derive(Clone, Copy)] enum StreamChannel { Stdout, @@ -1528,6 +1999,7 @@ pub struct WasmExecutionEngine { import_caches: BTreeMap, javascript_context_ids: BTreeMap, javascript_engine: JavascriptExecutionEngine, + event_notify: Option>, } impl Default for WasmExecutionEngine { @@ -1546,6 +2018,7 @@ impl Default for WasmExecutionEngine { import_caches: BTreeMap::new(), javascript_context_ids: BTreeMap::new(), javascript_engine, + event_notify: None, } } } @@ -1563,6 +2036,11 @@ fn default_wasm_test_runtime_context() -> Option { } impl WasmExecutionEngine { + /// Process-wide Wasmtime cache/profile/RSS metrics for operator telemetry. + pub fn wasmtime_metrics(&self) -> Result { + wasmtime::WasmtimeExecutionEngine::metrics() + } + pub fn new(runtime: RuntimeContext) -> Self { Self { runtime: Some(runtime.clone()), @@ -1572,6 +2050,7 @@ impl WasmExecutionEngine { import_caches: BTreeMap::new(), javascript_context_ids: BTreeMap::new(), javascript_engine: JavascriptExecutionEngine::new(runtime), + event_notify: None, } } @@ -1589,7 +2068,8 @@ impl WasmExecutionEngine { } pub fn set_event_notify(&mut self, notify: Option>) { - self.javascript_engine.set_event_notify(notify); + self.javascript_engine.set_event_notify(notify.clone()); + self.event_notify = notify; } pub fn create_context(&mut self, request: CreateWasmContextRequest) -> WasmContext { @@ -1645,6 +2125,15 @@ impl WasmExecutionEngine { self.create_execution_with_runtime(request, runtime, false) } + pub fn start_execution_for_backend( + &mut self, + request: StartWasmExecutionRequest, + backend: StandaloneWasmBackend, + ) -> Result { + let runtime = self.runtime_context()?.clone(); + self.create_execution_for_backend(request, runtime, false, backend) + } + pub fn prepare_execution( &mut self, request: StartWasmExecutionRequest, @@ -1653,6 +2142,24 @@ impl WasmExecutionEngine { self.create_execution_with_runtime(request, runtime, true) } + pub fn prepare_execution_for_backend( + &mut self, + request: StartWasmExecutionRequest, + backend: StandaloneWasmBackend, + ) -> Result { + let runtime = self.runtime_context()?.clone(); + self.create_execution_for_backend(request, runtime, true, backend) + } + + pub fn prepare_execution_with_runtime_for_backend( + &mut self, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + backend: StandaloneWasmBackend, + ) -> Result { + self.create_execution_for_backend(request, runtime, true, backend) + } + pub fn start_execution_with_runtime( &mut self, request: StartWasmExecutionRequest, @@ -1661,6 +2168,67 @@ impl WasmExecutionEngine { self.create_execution_with_runtime(request, runtime, false) } + pub fn start_execution_with_runtime_for_backend( + &mut self, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + backend: StandaloneWasmBackend, + ) -> Result { + self.create_execution_for_backend(request, runtime, false, backend) + } + + fn create_execution_for_backend( + &mut self, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + defer_execute: bool, + backend: StandaloneWasmBackend, + ) -> Result { + match backend { + StandaloneWasmBackend::V8 => { + self.create_execution_with_runtime(request, runtime, defer_execute) + } + StandaloneWasmBackend::Wasmtime => { + self.create_wasmtime_execution(request, runtime, defer_execute) + } + } + } + + fn create_wasmtime_execution( + &mut self, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + defer_execute: bool, + ) -> Result { + let context = self + .contexts + .get(&request.context_id) + .cloned() + .ok_or_else(|| WasmExecutionError::MissingContext(request.context_id.clone()))?; + if context.vm_id != request.vm_id { + return Err(WasmExecutionError::VmMismatch { + expected: context.vm_id, + found: request.vm_id, + }); + } + let module_path = context + .module_path + .ok_or(WasmExecutionError::MissingModulePath)?; + self.next_execution_id += 1; + let execution_id = format!("exec-{}", self.next_execution_id); + let execution = wasmtime::WasmtimeExecution::spawn( + execution_id, + module_path, + request, + runtime, + self.event_notify.clone(), + defer_execute, + )?; + Ok(WasmExecution { + backend: WasmExecutionBackend::Wasmtime(execution), + }) + } + fn create_execution_with_runtime( &mut self, request: StartWasmExecutionRequest, @@ -1684,6 +2252,7 @@ impl WasmExecutionEngine { let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; + validate_module_profile(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; let javascript_context_id = self .javascript_context_ids @@ -1762,6 +2331,7 @@ impl WasmExecutionEngine { let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; + validate_module_profile(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; let javascript_context_id = self .javascript_context_ids @@ -1815,6 +2385,23 @@ impl WasmExecutionEngine { ) } + pub async fn start_execution_with_runtime_async_for_backend( + &mut self, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + backend: StandaloneWasmBackend, + ) -> Result { + match backend { + StandaloneWasmBackend::V8 => { + self.start_execution_with_runtime_async(request, runtime) + .await + } + StandaloneWasmBackend::Wasmtime => { + self.create_wasmtime_execution(request, runtime, false) + } + } + } + #[allow(clippy::too_many_arguments)] fn finish_start_execution( &mut self, @@ -1854,41 +2441,45 @@ impl WasmExecutionEngine { let pending_event_budget = wasm_pending_event_budget(&request.limits)?; Ok(WasmExecution { - execution_id, - child_pid, - inner: javascript_execution, - execution_timeout, - execution_started_at: Instant::now(), - timeout_reported: false, - // Approach-warn (~80%) before the optional WASM elapsed deadline; - // only registered when a wall-clock limit is configured. - wall_clock_gauge: execution_timeout.map(|limit| { - register_limit( - TrackedLimit::WasmWallClockMs, - duration_millis_saturating_usize(limit), - ) - }), - pending_events: WasmEventQueue::new(Arc::clone(&pending_event_budget)), - signal_checkpoints: WasmSignalCheckpointInbox::new(Arc::clone(&pending_event_budget)), - stdout_stream_buffer: Vec::new(), - stderr_stream_buffer: Vec::new(), - max_stack_bytes: request.limits.max_stack_bytes, - pending_v8_stack_overflow: None, - internal_sync_rpc: WasmInternalSyncRpc { - module_guest_paths: wasm_guest_module_paths( - &resolved_module.specifier, - &request.env, - ), - module_host_path: resolved_module.resolved_path.clone(), - guest_cwd: wasm_guest_cwd(&request.env), - host_cwd: request.cwd.clone(), - sandbox_root: sandbox_root.clone(), - guest_path_mappings, - route_fs_through_sidecar: sandbox_root.is_some(), - next_fd: 64, - open_files: BTreeMap::new(), - pending_events: WasmEventQueue::new(pending_event_budget), - }, + backend: WasmExecutionBackend::V8(Box::new(V8WasmExecution { + execution_id, + child_pid, + inner: javascript_execution, + execution_timeout, + execution_started_at: Instant::now(), + timeout_reported: false, + // Approach-warn (~80%) before the optional WASM elapsed deadline; + // only registered when a wall-clock limit is configured. + wall_clock_gauge: execution_timeout.map(|limit| { + register_limit( + TrackedLimit::WasmWallClockMs, + duration_millis_saturating_usize(limit), + ) + }), + pending_events: WasmEventQueue::new(Arc::clone(&pending_event_budget)), + signal_checkpoints: WasmSignalCheckpointInbox::new(Arc::clone( + &pending_event_budget, + )), + stdout_stream_buffer: Vec::new(), + stderr_stream_buffer: Vec::new(), + max_stack_bytes: request.limits.max_stack_bytes, + pending_v8_stack_overflow: None, + internal_sync_rpc: WasmInternalSyncRpc { + module_guest_paths: wasm_guest_module_paths( + &resolved_module.specifier, + &request.env, + ), + module_host_path: resolved_module.resolved_path.clone(), + guest_cwd: wasm_guest_cwd(&request.env), + host_cwd: request.cwd.clone(), + sandbox_root: sandbox_root.clone(), + guest_path_mappings, + route_fs_through_sidecar: sandbox_root.is_some(), + next_fd: 64, + open_files: BTreeMap::new(), + pending_events: WasmEventQueue::new(pending_event_budget), + }, + })), }) } @@ -4583,7 +5174,7 @@ fn module_path( } } -fn guest_visible_wasm_env(env: &BTreeMap) -> BTreeMap { +pub(super) fn guest_visible_wasm_env(env: &BTreeMap) -> BTreeMap { let mut guest_env = env .iter() .filter(|(key, _)| !is_internal_wasm_guest_env_key(key)) @@ -4874,6 +5465,11 @@ fn verify_wasm_module_header( }) } +fn validate_module_profile(resolved_module: &ResolvedWasmModule) -> Result<(), WasmExecutionError> { + let bytes = cached_wasm_module_bytes(&resolved_module.resolved_path)?; + profile::validate_locked_profile(bytes.as_slice()).map_err(WasmExecutionError::Host) +} + fn detect_native_binary_format(header: &[u8]) -> Option { if header.len() >= 4 && &header[..4] == b"\x7fELF" { return Some(NativeBinaryFormat::Elf); diff --git a/crates/execution/src/wasm/profile.rs b/crates/execution/src/wasm/profile.rs new file mode 100644 index 0000000000..df1e4953eb --- /dev/null +++ b/crates/execution/src/wasm/profile.rs @@ -0,0 +1,66 @@ +//! Engine-independent WebAssembly proposal profile. +//! +//! Both standalone engines validate these exact switches before invoking their +//! own compiler. Engine defaults therefore cannot silently widen or narrow the +//! AgentOS guest surface. + +use crate::backend::HostServiceError; +use wasmparser::{Validator, WasmFeatures}; + +pub fn locked_wasm_features() -> WasmFeatures { + let mut features = WasmFeatures::empty(); + // Floating-point operators are part of the MVP profile and are emitted by + // the owned Linux toolchain even for commands whose public behavior is + // integer-only (for example, coreutils `ls`). + features.set(WasmFeatures::FLOATS, true); + features.set(WasmFeatures::MUTABLE_GLOBAL, true); + features.set(WasmFeatures::SATURATING_FLOAT_TO_INT, true); + features.set(WasmFeatures::SIGN_EXTENSION, true); + features.set(WasmFeatures::REFERENCE_TYPES, true); + features.set(WasmFeatures::MULTI_VALUE, true); + features.set(WasmFeatures::BULK_MEMORY, true); + features.set(WasmFeatures::SIMD, true); + features +} + +pub fn validate_locked_profile(bytes: &[u8]) -> Result<(), HostServiceError> { + Validator::new_with_features(locked_wasm_features()) + .validate_all(bytes) + .map(|_| ()) + .map_err(|error| { + eprintln!("ERR_AGENTOS_WASM_PROFILE_VALIDATION: private validator diagnostic: {error}"); + HostServiceError::new( + "ERR_AGENTOS_WASM_INVALID_MODULE", + "WebAssembly module violates the AgentOS feature profile", + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn locked_profile_accepts_simd_and_rejects_threads_and_memory64() { + validate_locked_profile(&wat::parse_str("(module (func (drop (f64.const 1))))").unwrap()) + .expect("MVP floating point is enabled"); + validate_locked_profile( + &wat::parse_str("(module (func (drop (v128.const i32x4 1 2 3 4))))").unwrap(), + ) + .expect("SIMD128 is enabled"); + let threads = wat::parse_str("(module (memory 1 1 shared))").unwrap(); + assert!(validate_locked_profile(&threads).is_err()); + let memory64 = wat::parse_str("(module (memory i64 1))").unwrap(); + assert!(validate_locked_profile(&memory64).is_err()); + } + + #[test] + fn locked_profile_rejects_multi_memory_relaxed_simd_and_tail_calls() { + let multi_memory = wat::parse_str("(module (memory 1) (memory 1))").unwrap(); + assert!(validate_locked_profile(&multi_memory).is_err()); + let tail_call = + wat::parse_str("(module (func $callee) (func (export \"run\") (return_call $callee)))") + .unwrap(); + assert!(validate_locked_profile(&tail_call).is_err()); + } +} diff --git a/crates/execution/src/wasm/wasmtime/cache.rs b/crates/execution/src/wasm/wasmtime/cache.rs new file mode 100644 index 0000000000..b87123f0d5 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/cache.rs @@ -0,0 +1,238 @@ +//! Bounded compiled-module cache. + +use crate::backend::HostServiceError; +use sha2::{Digest, Sha256}; +use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use wasmtime::{Engine, Module}; + +pub const DEFAULT_MODULE_CACHE_ENTRIES: usize = 32; +pub const DEFAULT_MODULE_CACHE_CHARGED_BYTES: usize = 256 * 1024 * 1024; +const MINIMUM_MODULE_CHARGE_BYTES: usize = 1024 * 1024; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct WasmtimeModuleCacheMetrics { + pub entries: usize, + pub hits: u64, + pub misses: u64, + pub evictions: u64, + pub source_bytes: u64, + pub charged_bytes: usize, + pub compile_time: Duration, +} + +#[derive(Debug)] +struct CacheEntry { + module: Arc, + charged_bytes: usize, +} + +#[derive(Debug)] +pub struct WasmtimeModuleCache { + maximum_entries: usize, + maximum_charged_bytes: usize, + entries: HashMap<[u8; 32], CacheEntry>, + lru: VecDeque<[u8; 32]>, + metrics: WasmtimeModuleCacheMetrics, + near_limit_warned: bool, +} + +impl Default for WasmtimeModuleCache { + fn default() -> Self { + Self::new( + DEFAULT_MODULE_CACHE_ENTRIES, + DEFAULT_MODULE_CACHE_CHARGED_BYTES, + ) + } +} + +impl WasmtimeModuleCache { + pub fn new(maximum_entries: usize, maximum_charged_bytes: usize) -> Self { + assert!(maximum_entries > 0); + assert!(maximum_charged_bytes > 0); + Self { + maximum_entries, + maximum_charged_bytes, + entries: HashMap::new(), + lru: VecDeque::new(), + metrics: WasmtimeModuleCacheMetrics::default(), + near_limit_warned: false, + } + } + + pub fn get_or_compile( + &mut self, + engine: &Engine, + bytes: &[u8], + ) -> Result, HostServiceError> { + let key: [u8; 32] = Sha256::digest(bytes).into(); + if let Some(module) = self + .entries + .get(&key) + .map(|entry| Arc::clone(&entry.module)) + { + self.metrics.hits = self.metrics.hits.saturating_add(1); + self.touch(key); + return Ok(module); + } + self.metrics.misses = self.metrics.misses.saturating_add(1); + let charged_bytes = module_charge(bytes.len())?; + if charged_bytes > self.maximum_charged_bytes { + return Err(cache_limit_error( + "limits.wasm.moduleCacheBytes", + self.maximum_charged_bytes, + charged_bytes, + )); + } + while self.entries.len() >= self.maximum_entries + || self.metrics.charged_bytes.saturating_add(charged_bytes) > self.maximum_charged_bytes + { + self.evict_lru()?; + } + let started = Instant::now(); + let module = Arc::new(Module::new(engine, bytes).map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASMTIME_MODULE_COMPILE: private compiler diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_INVALID_MODULE", + "WebAssembly module could not be compiled for the configured feature profile", + ) + })?); + self.metrics.compile_time = self.metrics.compile_time.saturating_add(started.elapsed()); + self.metrics.source_bytes = self + .metrics + .source_bytes + .saturating_add(u64::try_from(bytes.len()).unwrap_or(u64::MAX)); + self.metrics.charged_bytes = self.metrics.charged_bytes.saturating_add(charged_bytes); + self.entries.insert( + key, + CacheEntry { + module: Arc::clone(&module), + charged_bytes, + }, + ); + self.lru.push_back(key); + if self.metrics.charged_bytes >= near_limit_threshold(self.maximum_charged_bytes) + && !self.near_limit_warned + { + self.near_limit_warned = true; + eprintln!( + "WARN_AGENTOS_WASMTIME_MODULE_CACHE_NEAR_LIMIT: chargedBytes={} limit={} config=limits.wasm.moduleCacheBytes", + self.metrics.charged_bytes, self.maximum_charged_bytes + ); + } + Ok(module) + } + + pub fn metrics(&self) -> WasmtimeModuleCacheMetrics { + WasmtimeModuleCacheMetrics { + entries: self.entries.len(), + ..self.metrics + } + } + + fn touch(&mut self, key: [u8; 32]) { + if let Some(index) = self.lru.iter().position(|candidate| *candidate == key) { + self.lru.remove(index); + } + self.lru.push_back(key); + } + + fn evict_lru(&mut self) -> Result<(), HostServiceError> { + let key = self.lru.pop_front().ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_ACCOUNTING", + "module cache cannot free enough charged capacity", + ) + })?; + let entry = self.entries.remove(&key).ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_ACCOUNTING", + "module cache LRU references a missing entry", + ) + })?; + self.metrics.charged_bytes = self + .metrics + .charged_bytes + .saturating_sub(entry.charged_bytes); + self.metrics.evictions = self.metrics.evictions.saturating_add(1); + if self.metrics.charged_bytes < near_limit_threshold(self.maximum_charged_bytes) { + self.near_limit_warned = false; + } + Ok(()) + } +} + +fn module_charge(source_bytes: usize) -> Result { + source_bytes + .checked_mul(8) + .map(|bytes| bytes.max(MINIMUM_MODULE_CHARGE_BYTES)) + .ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_CHARGE_OVERFLOW", + "module cache charge overflows this platform", + ) + }) +} + +fn cache_limit_error(name: &'static str, limit: usize, observed: usize) -> HostServiceError { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_LIMIT", + "compiled Wasmtime Module exceeds the cache admission budget", + ) + .with_details(serde_json::json!({ + "limitName": name, + "limit": limit, + "observed": observed, + })) +} + +fn near_limit_threshold(limit: usize) -> usize { + limit.saturating_sub(limit / 5).max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + use wasmtime::Config; + + fn engine() -> Engine { + let config = Config::new(); + Engine::new(&config).unwrap() + } + + #[test] + fn cache_reuses_modules_and_evicts_at_both_bounds() { + let engine = engine(); + let first = wat::parse_str("(module (func (export \"first\")))").unwrap(); + let second = wat::parse_str("(module (func (export \"second\")))").unwrap(); + let mut cache = WasmtimeModuleCache::new(1, 2 * MINIMUM_MODULE_CHARGE_BYTES); + let first_module = cache.get_or_compile(&engine, &first).unwrap(); + assert!(Arc::ptr_eq( + &first_module, + &cache.get_or_compile(&engine, &first).unwrap() + )); + cache.get_or_compile(&engine, &second).unwrap(); + let metrics = cache.metrics(); + assert_eq!(metrics.hits, 1); + assert_eq!(metrics.misses, 2); + assert_eq!(metrics.evictions, 1); + assert_eq!(metrics.charged_bytes, MINIMUM_MODULE_CHARGE_BYTES); + } + + #[test] + fn single_oversized_module_is_rejected_with_named_limit() { + let engine = engine(); + let mut cache = WasmtimeModuleCache::new(1, MINIMUM_MODULE_CHARGE_BYTES - 1); + let error = cache + .get_or_compile(&engine, &wat::parse_str("(module)").unwrap()) + .unwrap_err(); + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_MODULE_CACHE_LIMIT"); + assert_eq!( + error.details.unwrap()["limitName"], + "limits.wasm.moduleCacheBytes" + ); + } +} diff --git a/crates/execution/src/wasm/wasmtime/engine.rs b/crates/execution/src/wasm/wasmtime/engine.rs new file mode 100644 index 0000000000..0eb0d6cf7d --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/engine.rs @@ -0,0 +1,352 @@ +//! Process-wide Engine profiles and feature configuration. + +use super::cache::{WasmtimeModuleCache, WasmtimeModuleCacheMetrics}; +use crate::backend::HostServiceError; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; +use wasmtime::{Config, Engine, OptLevel, WasmFeatures}; + +pub const DEFAULT_WASM_STACK_BYTES: usize = 512 * 1024; +pub const HOST_CALL_STACK_HEADROOM_BYTES: usize = 1536 * 1024; +pub const DEFAULT_MAX_ENGINE_PROFILES: usize = 8; +pub const ENGINE_PROFILE_LIMIT_CONFIG_PATH: &str = "limits.wasm.maxEngineProfiles"; + +/// Low-cardinality process metrics for operator diagnostics and Phase 3 +/// measurement. Cache counters are aggregated across exact Engine profiles; +/// RSS is process resident memory and is deliberately distinct from Wasmtime's +/// conservative charged-code estimate. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct WasmtimeMetricsSnapshot { + pub engine_profiles: usize, + pub module_entries: usize, + pub module_cache_hits: u64, + pub module_cache_misses: u64, + pub module_cache_evictions: u64, + pub compiled_source_bytes: u64, + pub charged_module_bytes: usize, + pub compile_time: Duration, + pub process_retained_rss_bytes: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WasmtimeFeatureProfile { + /// AgentOS-owned Preview1/POSIX ABI with the proposal switches configured + /// in `build_engine`; changing any switch requires a new keyed variant. + AgentOsOwnedWasiV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WasmtimeEngineProfile { + pub feature_profile: WasmtimeFeatureProfile, + pub wasm_stack_bytes: usize, +} + +impl WasmtimeEngineProfile { + pub fn new(wasm_stack_bytes: Option) -> Result { + let wasm_stack_bytes = wasm_stack_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| invalid_stack("WASM stack limit does not fit this platform"))? + .unwrap_or(DEFAULT_WASM_STACK_BYTES); + if wasm_stack_bytes == 0 { + return Err(invalid_stack("WASM stack limit must be greater than zero")); + } + wasm_stack_bytes + .checked_add(HOST_CALL_STACK_HEADROOM_BYTES) + .ok_or_else(|| invalid_stack("WASM plus host-call stack reservation overflows"))?; + Ok(Self { + feature_profile: WasmtimeFeatureProfile::AgentOsOwnedWasiV1, + wasm_stack_bytes, + }) + } + + pub fn async_stack_bytes(self) -> Result { + self.wasm_stack_bytes + .checked_add(HOST_CALL_STACK_HEADROOM_BYTES) + .ok_or_else(|| invalid_stack("WASM plus host-call stack reservation overflows")) + } +} + +fn invalid_stack(message: &'static str) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_WASMTIME_STACK_CONFIG", message) + .with_details(serde_json::json!({ "configPath": "limits.resources.maxWasmStackBytes" })) +} + +pub struct WasmtimeEngineHandle { + profile: WasmtimeEngineProfile, + engine: Engine, + modules: Mutex, +} + +impl std::fmt::Debug for WasmtimeEngineHandle { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("WasmtimeEngineHandle") + .field("profile", &self.profile) + .finish_non_exhaustive() + } +} + +impl WasmtimeEngineHandle { + pub fn profile(&self) -> WasmtimeEngineProfile { + self.profile + } + + pub fn engine(&self) -> &Engine { + &self.engine + } + + pub(super) fn modules(&self) -> &Mutex { + &self.modules + } +} + +#[derive(Debug, Default)] +struct RegistryState { + engines: HashMap>, + near_limit_warned: bool, +} + +#[derive(Debug)] +pub struct WasmtimeEngineRegistry { + maximum_profiles: usize, + state: Mutex, +} + +impl WasmtimeEngineRegistry { + pub fn process() -> &'static Self { + static PROCESS_REGISTRY: OnceLock = OnceLock::new(); + static EPOCH_TICKER: OnceLock<()> = OnceLock::new(); + let registry = PROCESS_REGISTRY.get_or_init(|| Self::new(DEFAULT_MAX_ENGINE_PROFILES)); + EPOCH_TICKER.get_or_init(|| { + // AGENTOS_THREAD_SITE: process-wasmtime-epoch-ticker + std::thread::Builder::new() + .name(String::from("agentos-wasmtime-epoch")) + .spawn(move || loop { + std::thread::sleep(Duration::from_millis(10)); + let engines = match registry.state.lock() { + Ok(state) => state + .engines + .values() + .map(|handle| handle.engine.clone()) + .collect::>(), + Err(_) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_ENGINE_REGISTRY_POISONED: epoch ticker cannot inspect Engine profiles" + ); + continue; + } + }; + for engine in engines { + engine.increment_epoch(); + } + }) + .expect("process Wasmtime epoch ticker must start"); + }); + registry + } + + pub fn new(maximum_profiles: usize) -> Self { + assert!(maximum_profiles > 0, "engine-profile limit must be nonzero"); + Self { + maximum_profiles, + state: Mutex::new(RegistryState::default()), + } + } + + pub fn get_or_create( + &self, + profile: WasmtimeEngineProfile, + ) -> Result, HostServiceError> { + let mut state = self.state.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENGINE_REGISTRY_POISONED", + "Wasmtime Engine registry lock is poisoned", + ) + })?; + if let Some(engine) = state.engines.get(&profile) { + return Ok(Arc::clone(engine)); + } + let observed = state.engines.len().saturating_add(1); + if observed > self.maximum_profiles { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENGINE_PROFILE_LIMIT", + "Wasmtime Engine profile limit exceeded", + ) + .with_details(serde_json::json!({ + "limitName": ENGINE_PROFILE_LIMIT_CONFIG_PATH, + "limit": self.maximum_profiles, + "observed": observed, + }))); + } + if observed >= near_limit_threshold(self.maximum_profiles) && !state.near_limit_warned { + state.near_limit_warned = true; + eprintln!( + "WARN_AGENTOS_WASMTIME_ENGINE_PROFILES_NEAR_LIMIT: active={} limit={} config={}", + observed, self.maximum_profiles, ENGINE_PROFILE_LIMIT_CONFIG_PATH + ); + } + + let engine = Arc::new(build_engine(profile)?); + state.engines.insert(profile, Arc::clone(&engine)); + Ok(engine) + } + + pub fn profile_count(&self) -> Result { + self.state + .lock() + .map(|state| state.engines.len()) + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENGINE_REGISTRY_POISONED", + "Wasmtime Engine registry lock is poisoned", + ) + }) + } + + pub fn metrics(&self) -> Result { + let state = self.state.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENGINE_REGISTRY_POISONED", + "Wasmtime Engine registry lock is poisoned", + ) + })?; + let mut result = WasmtimeMetricsSnapshot { + engine_profiles: state.engines.len(), + process_retained_rss_bytes: process_retained_rss_bytes(), + ..WasmtimeMetricsSnapshot::default() + }; + for handle in state.engines.values() { + let metrics = handle + .modules + .lock() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_POISONED", + "Wasmtime Module cache lock is poisoned", + ) + })? + .metrics(); + add_cache_metrics(&mut result, metrics); + } + Ok(result) + } +} + +fn add_cache_metrics(result: &mut WasmtimeMetricsSnapshot, metrics: WasmtimeModuleCacheMetrics) { + result.module_entries = result.module_entries.saturating_add(metrics.entries); + result.module_cache_hits = result.module_cache_hits.saturating_add(metrics.hits); + result.module_cache_misses = result.module_cache_misses.saturating_add(metrics.misses); + result.module_cache_evictions = result + .module_cache_evictions + .saturating_add(metrics.evictions); + result.compiled_source_bytes = result + .compiled_source_bytes + .saturating_add(metrics.source_bytes); + result.charged_module_bytes = result + .charged_module_bytes + .saturating_add(metrics.charged_bytes); + result.compile_time = result.compile_time.saturating_add(metrics.compile_time); +} + +#[cfg(target_os = "linux")] +fn process_retained_rss_bytes() -> Option { + let status = std::fs::read_to_string("/proc/self/status").ok()?; + let kibibytes = status.lines().find_map(|line| { + line.strip_prefix("VmRSS:")? + .split_whitespace() + .next()? + .parse::() + .ok() + })?; + kibibytes.checked_mul(1024) +} + +#[cfg(not(target_os = "linux"))] +fn process_retained_rss_bytes() -> Option { + None +} + +fn build_engine(profile: WasmtimeEngineProfile) -> Result { + let mut config = Config::new(); + config + .epoch_interruption(true) + .consume_fuel(true) + .max_wasm_stack(profile.wasm_stack_bytes) + .async_stack_size(profile.async_stack_bytes()?) + .async_stack_zeroing(true) + .cranelift_opt_level(OptLevel::Speed) + .memory_init_cow(true) + .wasm_features(WasmFeatures::TAIL_CALL, false) + .wasm_features(WasmFeatures::CUSTOM_PAGE_SIZES, false) + .wasm_features(WasmFeatures::THREADS, false) + .wasm_features(WasmFeatures::SHARED_EVERYTHING_THREADS, false) + .wasm_features(WasmFeatures::REFERENCE_TYPES, true) + .wasm_features(WasmFeatures::FUNCTION_REFERENCES, false) + .wasm_features(WasmFeatures::GC, false) + .wasm_features(WasmFeatures::SIMD, true) + .wasm_features(WasmFeatures::RELAXED_SIMD, false) + .wasm_features(WasmFeatures::BULK_MEMORY, true) + .wasm_features(WasmFeatures::MULTI_VALUE, true) + .wasm_features(WasmFeatures::MULTI_MEMORY, false) + .wasm_features(WasmFeatures::MEMORY64, false) + .wasm_features(WasmFeatures::EXCEPTIONS, false) + .wasm_features(WasmFeatures::COMPONENT_MODEL, false) + .wasm_features(WasmFeatures::STACK_SWITCHING, false) + .wasm_features(WasmFeatures::WIDE_ARITHMETIC, false); + let engine = Engine::new(&config).map_err(|error| { + eprintln!("ERR_AGENTOS_WASMTIME_ENGINE_CONFIG: private engine diagnostic: {error:#}"); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENGINE_CONFIG", + "failed to construct the configured WebAssembly engine", + ) + })?; + Ok(WasmtimeEngineHandle { + profile, + engine, + modules: Mutex::new(WasmtimeModuleCache::default()), + }) +} + +fn near_limit_threshold(limit: usize) -> usize { + limit.saturating_sub(limit / 5).max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stack_profile_reserves_locked_host_headroom() { + let profile = WasmtimeEngineProfile::new(None).expect("default profile"); + assert_eq!(profile.wasm_stack_bytes, 512 * 1024); + assert_eq!(profile.async_stack_bytes().unwrap(), 2 * 1024 * 1024); + assert!(WasmtimeEngineProfile::new(Some(0)).is_err()); + } + + #[test] + fn registry_is_exact_profile_keyed_and_bounded() { + let registry = WasmtimeEngineRegistry::new(2); + let default = WasmtimeEngineProfile::new(None).unwrap(); + let first = registry.get_or_create(default).unwrap(); + assert!(Arc::ptr_eq( + &first, + ®istry.get_or_create(default).unwrap() + )); + registry + .get_or_create(WasmtimeEngineProfile::new(Some(1024 * 1024)).unwrap()) + .unwrap(); + let error = registry + .get_or_create(WasmtimeEngineProfile::new(Some(2 * 1024 * 1024)).unwrap()) + .unwrap_err(); + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_ENGINE_PROFILE_LIMIT"); + assert_eq!( + error.details.unwrap()["limitName"], + ENGINE_PROFILE_LIMIT_CONFIG_PATH + ); + let metrics = registry.metrics().unwrap(); + assert_eq!(metrics.engine_profiles, 2); + assert_eq!(metrics.module_entries, 0); + } +} diff --git a/crates/execution/src/wasm/wasmtime/error.rs b/crates/execution/src/wasm/wasmtime/error.rs new file mode 100644 index 0000000000..c58a024ce9 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/error.rs @@ -0,0 +1,165 @@ +//! Stable AgentOS outcome classification for private Wasmtime diagnostics. + +use crate::backend::HostServiceError; + +pub(super) fn normalize( + default_code: &'static str, + error: &wasmtime::Error, + cancelled: bool, +) -> HostServiceError { + // Wasmtime wraps limiter/trap causes with instantiation/call context. Use + // the complete private chain for classification, but never expose it as an + // AgentOS API string. + let diagnostic = format!("{error:#}"); + if diagnostic.contains("forcing trap when growing memory") + || diagnostic.contains("forcing a memory growth failure to be a trap") + { + return HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MEMORY_LIMIT", + "WebAssembly linear-memory growth exceeded its configured limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmMemoryBytes", + })); + } + if diagnostic.contains("forcing trap when growing table") + || diagnostic.contains("forcing a table growth failure to be a trap") + { + return HostServiceError::new( + "ERR_AGENTOS_WASMTIME_TABLE_LIMIT", + "WebAssembly table growth exceeded its configured element limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.wasm.maxTableElements", + })); + } + if default_code == "ERR_AGENTOS_WASMTIME_INSTANTIATE" { + if diagnostic.contains("memory minimum size") + && diagnostic.contains("exceeds memory limits") + { + return HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MEMORY_LIMIT", + "WebAssembly initial memory exceeds the configured linear-memory limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmMemoryBytes", + })); + } + if diagnostic.contains("table minimum size") && diagnostic.contains("exceeds table limits") + { + return HostServiceError::new( + "ERR_AGENTOS_WASMTIME_TABLE_LIMIT", + "WebAssembly initial table exceeds the configured element limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.wasm.maxTableElements", + })); + } + return HostServiceError::new( + "ERR_AGENTOS_WASM_INSTANTIATION", + "WebAssembly host imports do not match module requirements", + ); + } + if cancelled || diagnostic.contains("ERR_AGENTOS_WASMTIME_CANCELED") { + return HostServiceError::new("ECANCELED", "Wasmtime execution was canceled"); + } + if diagnostic.contains("ERR_AGENTOS_WASMTIME_ACTIVE_CPU_LIMIT") { + return HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ACTIVE_CPU_LIMIT", + "Wasmtime execution exhausted its active CPU budget", + ); + } + if let Some(trap) = error.downcast_ref::() { + return match trap { + wasmtime::Trap::OutOfFuel => HostServiceError::new( + "ERR_AGENTOS_WASMTIME_FUEL_EXHAUSTED", + "Wasmtime execution exhausted deterministic fuel", + ), + wasmtime::Trap::StackOverflow => HostServiceError::new( + "ERR_AGENTOS_WASMTIME_STACK_EXHAUSTED", + "Wasmtime execution exhausted its configured stack", + ), + wasmtime::Trap::Interrupt => HostServiceError::new( + "ERR_AGENTOS_WASMTIME_INTERRUPTED", + "Wasmtime execution was interrupted", + ), + wasmtime::Trap::MemoryOutOfBounds => stable_guest_trap( + "memory-out-of-bounds", + "WebAssembly accessed memory outside its current bounds", + ), + wasmtime::Trap::HeapMisaligned => stable_guest_trap( + "misaligned-atomic", + "WebAssembly attempted a misaligned atomic memory operation", + ), + wasmtime::Trap::TableOutOfBounds => stable_guest_trap( + "table-out-of-bounds", + "WebAssembly accessed a table outside its current bounds", + ), + wasmtime::Trap::IndirectCallToNull => stable_guest_trap( + "null-indirect-call", + "WebAssembly called an uninitialized table element", + ), + wasmtime::Trap::BadSignature => stable_guest_trap( + "indirect-call-type-mismatch", + "WebAssembly made an indirect call with the wrong function type", + ), + wasmtime::Trap::IntegerOverflow => stable_guest_trap( + "integer-overflow", + "WebAssembly integer arithmetic overflowed", + ), + wasmtime::Trap::IntegerDivisionByZero => stable_guest_trap( + "integer-division-by-zero", + "WebAssembly attempted integer division by zero", + ), + wasmtime::Trap::BadConversionToInteger => stable_guest_trap( + "invalid-float-to-integer", + "WebAssembly attempted an invalid float-to-integer conversion", + ), + wasmtime::Trap::UnreachableCodeReached => stable_guest_trap( + "unreachable", + "WebAssembly executed an unreachable instruction", + ), + wasmtime::Trap::NullReference => stable_guest_trap( + "null-reference", + "WebAssembly dereferenced a null reference", + ), + wasmtime::Trap::AllocationTooLarge => stable_guest_trap( + "allocation-too-large", + "WebAssembly attempted an allocation that is too large", + ), + _ => stable_guest_trap("other", "WebAssembly trapped"), + }; + } + HostServiceError::new(default_code, "WebAssembly validation or execution failed") + .with_details(serde_json::json!({ "engine": "wasmtime" })) +} + +fn stable_guest_trap(kind: &'static str, message: &'static str) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_WASM_TRAP", message) + .with_details(serde_json::json!({ "trapKind": kind })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizes_limit_and_guest_trap_errors_without_engine_strings() { + let memory = wasmtime::format_err!("forcing trap when growing memory to 131072 bytes"); + let error = normalize("ERR_AGENTOS_WASMTIME_TRAP", &memory, false); + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_MEMORY_LIMIT"); + assert_eq!( + error.details.unwrap()["limitName"], + "limits.resources.maxWasmMemoryBytes" + ); + + let trap: wasmtime::Error = wasmtime::Trap::IntegerDivisionByZero.into(); + let error = normalize("ERR_AGENTOS_WASMTIME_TRAP", &trap, false); + assert_eq!(error.code, "ERR_AGENTOS_WASM_TRAP"); + assert_eq!( + error.details.unwrap()["trapKind"], + "integer-division-by-zero" + ); + assert!(!error.message.contains("wasmtime")); + } +} diff --git a/crates/execution/src/wasm/wasmtime/lifecycle.rs b/crates/execution/src/wasm/wasmtime/lifecycle.rs new file mode 100644 index 0000000000..0d38d6b726 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/lifecycle.rs @@ -0,0 +1,1286 @@ +//! Execution lifecycle, cancellation, interruption, and teardown. + +use super::super::{StartWasmExecutionRequest, WasmExecutionError, WasmExecutionEvent}; +use super::engine::{ + WasmtimeEngineHandle, WasmtimeEngineProfile, WasmtimeEngineRegistry, WasmtimeMetricsSnapshot, +}; +use super::linker; +use super::store::{self, WasmtimeHostClient}; +use crate::backend::{ + ExecutionWakeIdentity, HostCallReply, HostServiceError, PayloadLimit, PublishedSignalCheckpoint, +}; +use crate::host::{ + BoundedString, BoundedUsize, ExecutableImageSource, FilesystemOperation, HostOperation, + ProcessHostCapabilitySet, ProcessOperation, +}; +use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger}; +use agentos_runtime::RuntimeContext; +use base64::Engine as _; +use flume::{Receiver, Sender}; +use serde_json::Value; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; +use std::time::Duration; +use tokio::sync::Notify; + +const MODULE_READ_CHUNK_BYTES: usize = 512 * 1024; +const DEFAULT_MAX_MODULE_FILE_BYTES: usize = 256 * 1024 * 1024; + +/// One queued executor event plus the ledger ownership for bytes retained by +/// that queue slot. The reservation is released when the event leaves the +/// Wasmtime queue; downstream output/host-call paths apply their own existing +/// retention accounting after that handoff. +pub(super) struct QueuedWasmtimeEvent { + event: Result, + _retained_bytes: Option, +} + +impl QueuedWasmtimeEvent { + pub(super) fn new( + resources: &Arc, + event: Result, + retained_bytes: usize, + ) -> Result { + let retained_bytes = if retained_bytes == 0 { + None + } else { + Some( + resources + .reserve(ResourceClass::AsyncCompletionBytes, retained_bytes) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_EVENT_BYTES_LIMIT", + error.to_string(), + ) + .with_details(serde_json::json!({ + "limitName": "limits.reactor.maxAsyncCompletionBytes", + "observed": retained_bytes, + })) + })?, + ) + }; + Ok(Self { + event, + _retained_bytes: retained_bytes, + }) + } + + fn into_event(self) -> Result { + self.event + } +} + +#[derive(Debug, Default)] +struct HostLatch { + value: Mutex>, + notify: Notify, +} + +#[derive(Debug)] +struct Control { + cancelled: Arc, + paused: Arc, + pause_notify: Arc, + cancel_notify: Arc, + started: AtomicBool, + start_notify: Notify, + engine: Mutex>>, + signal_checkpoints: Mutex>, + max_signal_checkpoints: usize, + signal_pending: Arc, +} + +impl Control { + fn new(started: bool, max_signal_checkpoints: usize) -> Self { + Self { + cancelled: Arc::new(AtomicBool::new(false)), + paused: Arc::new(AtomicBool::new(false)), + pause_notify: Arc::new(Notify::new()), + cancel_notify: Arc::new(Notify::new()), + started: AtomicBool::new(started), + start_notify: Notify::new(), + engine: Mutex::new(None), + signal_checkpoints: Mutex::new(VecDeque::new()), + max_signal_checkpoints: max_signal_checkpoints.max(1), + signal_pending: Arc::new(AtomicBool::new(false)), + } + } + + fn publish_signal( + &self, + identity: ExecutionWakeIdentity, + delivery: PublishedSignalCheckpoint, + ) -> Result<(), HostServiceError> { + let mut checkpoints = self.signal_checkpoints.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASMTIME_SIGNAL_INBOX_POISONED: signal checkpoint state is poisoned", + ) + })?; + if checkpoints.len() >= self.max_signal_checkpoints { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_SIGNAL_CHECKPOINT_LIMIT", + "Wasmtime signal checkpoint inbox is full", + ) + .with_details(serde_json::json!({ + "limitName": "limits.process.pendingEventCount", + "limit": self.max_signal_checkpoints, + }))); + } + if checkpoints.len().saturating_add(1) * 5 >= self.max_signal_checkpoints * 4 { + eprintln!( + "WARN_AGENTOS_WASMTIME_SIGNAL_CHECKPOINT_LIMIT: signal checkpoint inbox is at {}/{} entries", + checkpoints.len().saturating_add(1), + self.max_signal_checkpoints + ); + } + checkpoints.push_back((identity, delivery)); + self.signal_pending.store(true, Ordering::Release); + Ok(()) + } + + fn take_signal( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + let mut checkpoints = self.signal_checkpoints.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASMTIME_SIGNAL_INBOX_POISONED: signal checkpoint state is poisoned", + ) + })?; + let Some((pending_identity, _)) = checkpoints.front() else { + return Ok(None); + }; + if *pending_identity != identity { + return Err(HostServiceError::new( + "ESTALE", + "published signal delivery identity does not match the active Wasmtime execution", + )); + } + let delivery = checkpoints.pop_front().map(|(_, delivery)| delivery); + self.signal_pending + .store(!checkpoints.is_empty(), Ordering::Release); + Ok(delivery) + } + + fn discard_signals(&self, identity: ExecutionWakeIdentity) -> Result<(), HostServiceError> { + let mut checkpoints = self.signal_checkpoints.lock().map_err(|_| { + HostServiceError::new( + "EIO", + "ERR_AGENTOS_WASMTIME_SIGNAL_INBOX_POISONED: signal checkpoint state is poisoned", + ) + })?; + checkpoints.retain(|(pending_identity, _)| *pending_identity != identity); + self.signal_pending + .store(!checkpoints.is_empty(), Ordering::Release); + Ok(()) + } + + fn interrupt(&self) { + self.cancelled.store(true, Ordering::Release); + self.started.store(true, Ordering::Release); + self.start_notify.notify_waiters(); + self.pause_notify.notify_waiters(); + self.cancel_notify.notify_waiters(); + if let Ok(engine) = self.engine.lock() { + if let Some(engine) = engine.as_ref() { + engine.engine().increment_epoch(); + } + } + } +} + +pub struct WasmtimeExecution { + execution_id: String, + events: Receiver, + host: Arc, + control: Arc, + worker_done: Receiver<()>, + worker: Mutex>>, + teardown_timeout: Duration, + prepared: bool, +} + +impl std::fmt::Debug for WasmtimeExecution { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("WasmtimeExecution") + .field("execution_id", &self.execution_id) + .field("prepared", &self.prepared) + .field("cancelled", &self.control.cancelled.load(Ordering::Acquire)) + .finish_non_exhaustive() + } +} + +impl WasmtimeExecution { + pub fn spawn( + execution_id: String, + module_path: String, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + event_notify: Option>, + defer_execute: bool, + ) -> Result { + let permit = runtime + .vm_executor_admission() + .try_acquire() + .map_err(|error| { + WasmExecutionError::Host( + HostServiceError::new("ERR_AGENTOS_VM_EXECUTOR_LIMIT", error.to_string()) + .with_details(serde_json::json!({ + "limitName": "runtime.maxActiveVmExecutors", + "limit": runtime.max_active_vm_executors(), + })), + ) + })?; + let pending_count = request.limits.pending_event_count.unwrap_or(64).max(1); + let (event_sender, events) = flume::bounded(pending_count); + let (done_sender, worker_done) = flume::bounded(1); + let host = Arc::new(HostLatch::default()); + let control = Arc::new(Control::new( + !defer_execute, + request.limits.pending_event_count.unwrap_or(64), + )); + let worker_host = Arc::clone(&host); + let worker_control = Arc::clone(&control); + let worker_runtime = runtime.clone(); + // AGENTOS_THREAD_SITE: admitted-wasmtime-guest-executor + let worker = std::thread::Builder::new() + .name(format!("agentos-wasmtime-{execution_id}")) + .spawn(move || { + let _permit = permit; + let event_resources = Arc::clone(worker_runtime.resources()); + let result = worker_runtime.handle().block_on(run_execution( + module_path, + request, + worker_runtime.clone(), + Arc::clone(&worker_host), + Arc::clone(&worker_control), + event_sender.clone(), + event_notify.clone(), + )); + publish_worker_result( + &event_sender, + event_notify.as_ref(), + &event_resources, + result, + ); + if done_sender.send(()).is_err() { + eprintln!( + "ERR_AGENTOS_WASMTIME_TEARDOWN_CHANNEL: worker completion receiver was dropped" + ); + } + }) + .map_err(WasmExecutionError::Spawn)?; + Ok(Self { + execution_id, + events, + host, + control, + worker_done, + worker: Mutex::new(Some(worker)), + teardown_timeout: runtime.vm_executor_teardown_timeout(), + prepared: defer_execute, + }) + } + + pub fn execution_id(&self) -> &str { + &self.execution_id + } + + pub fn configure_host_services(&self, host: ProcessHostCapabilitySet) { + match self.host.value.lock() { + Ok(mut slot) if slot.is_none() => { + *slot = Some(host); + self.host.notify.notify_waiters(); + } + Ok(_) => eprintln!( + "ERR_AGENTOS_WASMTIME_HOST_ALREADY_BOUND: ignored duplicate host capability binding" + ), + Err(_) => eprintln!( + "ERR_AGENTOS_WASMTIME_HOST_LATCH_POISONED: failed to bind host capabilities" + ), + } + } + + pub fn is_prepared_for_start(&self) -> bool { + self.prepared && !self.control.started.load(Ordering::Acquire) + } + + pub fn start_prepared(&mut self) -> Result<(), WasmExecutionError> { + if !self.prepared { + return Err(WasmExecutionError::Host(HostServiceError::new( + "ERR_AGENTOS_EXECUTION_NOT_PREPARED", + "Wasmtime execution was not created as a prepared image", + ))); + } + if self.control.started.swap(true, Ordering::AcqRel) { + return Err(WasmExecutionError::Host(HostServiceError::new( + "EALREADY", + "prepared Wasmtime execution has already started", + ))); + } + self.prepared = false; + self.control.start_notify.notify_waiters(); + Ok(()) + } + + pub fn terminate(&self) { + self.control.interrupt(); + self.host.notify.notify_waiters(); + } + + pub fn set_paused(&self, paused: bool) { + self.control.paused.store(paused, Ordering::Release); + if !paused { + self.control.pause_notify.notify_waiters(); + } + if let Ok(engine) = self.control.engine.lock() { + if let Some(engine) = engine.as_ref() { + engine.engine().increment_epoch(); + } + } + } + + pub fn deliver_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + signal: i32, + delivery_token: u64, + flags: u32, + ) -> Result<(), HostServiceError> { + self.control.publish_signal( + identity, + PublishedSignalCheckpoint { + signal, + delivery_token, + flags, + }, + ) + } + + pub fn take_signal_checkpoint( + &self, + identity: ExecutionWakeIdentity, + ) -> Result, HostServiceError> { + self.control.take_signal(identity) + } + + pub fn discard_signal_checkpoints( + &self, + identity: ExecutionWakeIdentity, + ) -> Result<(), HostServiceError> { + self.control.discard_signals(identity) + } + + pub async fn poll_event( + &self, + timeout: Duration, + ) -> Result, WasmExecutionError> { + match tokio::time::timeout(timeout, self.events.recv_async()).await { + Ok(Ok(event)) => event.into_event().map(Some), + Ok(Err(_)) => Err(WasmExecutionError::EventChannelClosed), + Err(_) => Ok(None), + } + } + + pub fn try_poll_event(&self) -> Result, WasmExecutionError> { + match self.events.try_recv() { + Ok(event) => event.into_event().map(Some), + Err(flume::TryRecvError::Empty) => Ok(None), + Err(flume::TryRecvError::Disconnected) => Err(WasmExecutionError::EventChannelClosed), + } + } + + pub fn poll_event_blocking( + &self, + timeout: Duration, + ) -> Result, WasmExecutionError> { + match self.events.recv_timeout(timeout) { + Ok(event) => event.into_event().map(Some), + Err(flume::RecvTimeoutError::Timeout) => Ok(None), + Err(flume::RecvTimeoutError::Disconnected) => { + Err(WasmExecutionError::EventChannelClosed) + } + } + } + + pub fn next_event_blocking(&self) -> Result { + self.events + .recv() + .map_err(|_| WasmExecutionError::EventChannelClosed)? + .into_event() + } + + fn join_worker(&self) { + let handle = match self.worker.lock() { + Ok(mut worker) => worker.take(), + Err(_) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_LOCK_POISONED: unable to join guest executor" + ); + None + } + }; + let Some(handle) = handle else { + return; + }; + match self.worker_done.recv_timeout(self.teardown_timeout) { + Ok(()) => { + if handle.join().is_err() { + eprintln!("ERR_AGENTOS_WASMTIME_WORKER_PANIC: guest executor panicked"); + } + } + Err(error) => eprintln!( + "ERR_AGENTOS_WASMTIME_TEARDOWN_TIMEOUT: guest executor did not stop within {} ms: {error}", + self.teardown_timeout.as_millis() + ), + } + } +} + +impl Drop for WasmtimeExecution { + fn drop(&mut self) { + self.terminate(); + self.join_worker(); + } +} + +pub struct WasmtimeExecutionEngine; + +impl WasmtimeExecutionEngine { + pub fn metrics() -> Result { + WasmtimeEngineRegistry::process().metrics() + } +} + +async fn run_execution( + module_path: String, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + host_latch: Arc, + control: Arc, + event_sender: Sender, + event_notify: Option>, +) -> Result { + wait_until_started(&control).await?; + let host = wait_for_host(&host_latch, &control.cancelled).await?; + let host = WasmtimeHostClient::new( + host, + store::max_host_reply_bytes(&request)?, + Arc::clone(&control.cancelled), + Arc::clone(&control.cancel_notify), + Arc::clone(&control.signal_pending), + Arc::clone(runtime.resources()), + event_sender, + event_notify, + ); + let profile = WasmtimeEngineProfile::new(request.limits.max_stack_bytes)?; + let engine = WasmtimeEngineRegistry::process().get_or_create(profile)?; + control + .engine + .lock() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_CONTROL_POISONED", + "Wasmtime Engine control slot is poisoned", + ) + })? + .replace(Arc::clone(&engine)); + + let future = run_loaded_module( + module_path, + request.clone(), + runtime, + host, + engine, + profile, + Arc::clone(&control.paused), + Arc::clone(&control.pause_notify), + ); + if let Some(limit_ms) = request.limits.wall_clock_limit_ms { + match tokio::time::timeout(Duration::from_millis(limit_ms), future).await { + Ok(result) => result, + Err(_) => { + control.interrupt(); + Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WALL_CLOCK_LIMIT", + "Wasmtime execution exceeded its wall-clock budget", + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmWallClockTimeMs", + "limit": limit_ms, + }))) + } + } + } else { + future.await + } +} + +#[allow(clippy::too_many_arguments)] +async fn run_loaded_module( + module_path: String, + request: StartWasmExecutionRequest, + runtime: RuntimeContext, + host: WasmtimeHostClient, + engine: Arc, + profile: WasmtimeEngineProfile, + paused: Arc, + pause_notify: Arc, +) -> Result { + // The kernel owns the authoritative WASI capability roots and descriptor + // numbers. Initialize them before guest code starts so both executors see + // the same fd namespace without maintaining a Wasmtime-local projection. + host.submit( + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens), + 0, + ) + .await?; + let bytes = load_module(&host, &module_path, &request).await?; + let mut module = super::module::compile_module(&engine, &bytes)?; + let mut request = request; + linker::validate_module_imports(&module, request.permission_tier)?; + let linker = + linker::build_linker(engine.engine(), request.permission_tier).map_err(|error| { + eprintln!("ERR_AGENTOS_WASMTIME_LINKER: private linker diagnostic: {error:#}"); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_LINKER", + "failed to build the AgentOS WebAssembly host linker", + ) + })?; + // One process image may replace itself repeatedly with fexecve. Preserve + // the same active-CPU origin across Stores so exec cannot reset its budget. + let active_cpu_started_ns = store::thread_cpu_time_ns(); + loop { + let mut store = store::create_store( + Arc::clone(&engine), + &runtime, + host.clone(), + &request, + profile, + active_cpu_started_ns, + Arc::clone(&paused), + Arc::clone(&pause_notify), + )?; + let instance = linker + .instantiate_async(&mut store, &module) + .await + .map_err(|error| { + super::error::normalize("ERR_AGENTOS_WASMTIME_INSTANTIATE", &error, false) + })?; + linker::initialize_inherited_signal_mask(&mut store, &instance).await?; + let start = instance + .get_typed_func::<(), ()>(&mut store, "_start") + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASMTIME_ENTRYPOINT: private entrypoint diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ENTRYPOINT", + "WebAssembly module does not export a valid _start function", + ) + })?; + match start.call_async(&mut store, ()).await { + Ok(()) => return Ok(store.data().exit_code.unwrap_or(0)), + Err(error) => { + if let Some(exit_code) = store.data().exit_code { + return Ok(exit_code); + } + if let Some(replacement) = store.data_mut().pending_exec_replacement.take() { + request.argv = replacement.argv; + request.env = replacement.env; + module = replacement.module; + linker::validate_module_imports(&module, request.permission_tier)?; + continue; + } + if store.data().exec_replaced { + return Err(HostServiceError::new( + "ERR_AGENTOS_EXEC_REPLACED", + "the kernel committed a replacement process image", + )); + } + return Err(super::error::normalize( + "ERR_AGENTOS_WASMTIME_TRAP", + &error, + store.data().canceled(), + )); + } + } + } +} + +async fn wait_until_started(control: &Control) -> Result<(), HostServiceError> { + loop { + let notified = control.start_notify.notified(); + if control.cancelled.load(Ordering::Acquire) { + return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled before start", + )); + } + if control.started.load(Ordering::Acquire) { + return Ok(()); + } + notified.await; + } +} + +async fn wait_for_host( + latch: &HostLatch, + cancelled: &AtomicBool, +) -> Result { + loop { + let notified = latch.notify.notified(); + let host = latch + .value + .lock() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_HOST_LATCH_POISONED", + "Wasmtime host capability latch is poisoned", + ) + })? + .clone(); + if let Some(host) = host { + return Ok(host); + } + if cancelled.load(Ordering::Acquire) { + return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled before host binding", + )); + } + notified.await; + } +} + +async fn load_module( + host: &WasmtimeHostClient, + module_path: &str, + request: &StartWasmExecutionRequest, +) -> Result, HostServiceError> { + let path_limit = PayloadLimit::new("runtime.filesystem.maxPathBytes", 4096)?; + let source = if let Some(path) = module_path.strip_prefix(super::TRUSTED_INITIAL_MODULE_PREFIX) + { + ExecutableImageSource::TrustedInitialPath(BoundedString::try_new( + path.to_owned(), + &path_limit, + )?) + } else { + ExecutableImageSource::Path(BoundedString::try_new(module_path.to_owned(), &path_limit)?) + }; + let maximum = request + .limits + .max_module_file_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| HostServiceError::new("EFBIG", "module byte limit does not fit usize"))? + .unwrap_or(DEFAULT_MAX_MODULE_FILE_BYTES); + load_executable_image(host, source, maximum).await +} + +pub(super) async fn load_executable_image( + host: &WasmtimeHostClient, + source: ExecutableImageSource, + maximum: usize, +) -> Result, HostServiceError> { + let retained_request_bytes = match &source { + ExecutableImageSource::TrustedInitialPath(path) | ExecutableImageSource::Path(path) => { + path.as_str().len() + } + ExecutableImageSource::Descriptor(_) => std::mem::size_of::(), + }; + let open = host + .submit( + HostOperation::Process(ProcessOperation::OpenExecutableImage { source }), + retained_request_bytes, + ) + .await?; + let (handle, size) = decode_open_image(open)?; + let result = read_module_image(host, handle, size, maximum).await; + let close = host + .submit( + HostOperation::Process(ProcessOperation::CloseExecutableImage { handle }), + std::mem::size_of::(), + ) + .await; + match (result, close) { + (Ok(bytes), Ok(_)) => Ok(bytes), + (Err(error), Ok(_)) => Err(error), + (Ok(_), Err(error)) => Err(error), + (Err(error), Err(close_error)) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_IMAGE_CLOSE: primary module-load error {}; image-close error {}", + error, close_error + ); + Err(error) + } + } +} + +fn decode_open_image(reply: HostCallReply) -> Result<(u64, usize), HostServiceError> { + let HostCallReply::Json(value) = reply else { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_IMAGE_REPLY", + "executable-image open returned a non-JSON reply", + )); + }; + let handle = value + .get("handle") + .and_then(Value::as_str) + .ok_or_else(|| HostServiceError::new("EIO", "image reply is missing handle"))? + .parse::() + .map_err(|_| HostServiceError::new("EIO", "image handle is not a u64"))?; + let size = value + .get("size") + .and_then(Value::as_u64) + .and_then(|size| usize::try_from(size).ok()) + .ok_or_else(|| HostServiceError::new("EFBIG", "image size does not fit this platform"))?; + Ok((handle, size)) +} + +async fn read_module_image( + host: &WasmtimeHostClient, + handle: u64, + size: usize, + maximum: usize, +) -> Result, HostServiceError> { + if size > maximum { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_FILE_LIMIT", + "WebAssembly module exceeds the configured executable-image limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmModuleFileBytes", + "limit": maximum, + "observed": size, + }))); + } + let read_limit = + PayloadLimit::new("limits.wasm.moduleReadChunkBytes", MODULE_READ_CHUNK_BYTES)?; + let mut bytes = Vec::with_capacity(size); + while bytes.len() < size { + let requested = (size - bytes.len()).min(MODULE_READ_CHUNK_BYTES); + let reply = host + .submit( + HostOperation::Process(ProcessOperation::ReadExecutableImage { + handle, + offset: bytes.len() as u64, + max_bytes: BoundedUsize::try_new(requested, &read_limit)?, + }), + std::mem::size_of::() * 2 + std::mem::size_of::(), + ) + .await?; + let chunk = decode_image_bytes(reply)?; + if chunk.is_empty() { + return Err(HostServiceError::new( + "EIO", + "executable-image read returned EOF before its declared size", + )); + } + if chunk.len() > requested || bytes.len().saturating_add(chunk.len()) > size { + return Err(HostServiceError::new( + "EIO", + "executable-image read exceeded its requested or declared size", + )); + } + bytes.extend_from_slice(&chunk); + } + Ok(bytes) +} + +fn decode_image_bytes(reply: HostCallReply) -> Result, HostServiceError> { + match reply { + HostCallReply::Raw(bytes) => Ok(bytes), + HostCallReply::Json(value) => { + let encoded = value + .get("base64") + .and_then(Value::as_str) + .filter(|_| value.get("__agentOSType").and_then(Value::as_str) == Some("bytes")) + .ok_or_else(|| { + HostServiceError::new("EIO", "image read returned invalid encoded bytes") + })?; + base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|_| HostServiceError::new("EIO", "image read returned invalid base64")) + } + HostCallReply::Empty => Err(HostServiceError::new( + "EIO", + "image read returned an empty reply envelope", + )), + } +} + +fn publish_worker_result( + sender: &Sender, + notify: Option<&Arc>, + resources: &Arc, + result: Result, +) { + match result { + Ok(code) => publish_worker_event( + sender, + notify, + resources, + Ok(WasmExecutionEvent::Exited(code)), + 0, + ), + Err(error) if error.code == "ERR_AGENTOS_EXEC_REPLACED" => {} + Err(error) => { + let message = format!("{}: {}\n", error.code, error.message); + let message_bytes = message.into_bytes(); + let retained_bytes = message_bytes.len(); + publish_worker_event( + sender, + notify, + resources, + Ok(WasmExecutionEvent::Stderr(message_bytes)), + retained_bytes, + ); + publish_worker_event( + sender, + notify, + resources, + Ok(WasmExecutionEvent::Exited(1)), + 0, + ); + } + } +} + +fn publish_worker_event( + sender: &Sender, + notify: Option<&Arc>, + resources: &Arc, + event: Result, + retained_bytes: usize, +) { + let event = match QueuedWasmtimeEvent::new(resources, event, retained_bytes) { + Ok(event) => event, + Err(error) => { + eprintln!("{}: {}", error.code, error.message); + return; + } + }; + if sender.send(event).is_err() { + eprintln!("ERR_AGENTOS_WASMTIME_EVENT_CHANNEL: execution event receiver was dropped"); + } else if let Some(notify) = notify { + notify.notify_one(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{bounded_execution_event_channel, ExecutionEvent}; + use crate::wasm::{GuestRuntimeConfig, WasmExecutionLimits, WasmPermissionTier}; + use agentos_runtime::accounting::{ResourceLedger, ResourceLimit}; + use agentos_runtime::{RuntimeConfig, SidecarRuntime}; + use std::collections::BTreeMap; + use std::path::PathBuf; + + #[test] + fn queued_event_bytes_are_bounded_and_released_on_handoff() { + let resources = Arc::new(ResourceLedger::root( + "wasmtime-event-test", + [( + ResourceClass::AsyncCompletionBytes, + ResourceLimit::new(4, "limits.reactor.maxAsyncCompletionBytes"), + )], + )); + let event = + QueuedWasmtimeEvent::new(&resources, Ok(WasmExecutionEvent::Stderr(vec![0; 4])), 4) + .expect("admit exact-bound event"); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 4); + let error = + QueuedWasmtimeEvent::new(&resources, Ok(WasmExecutionEvent::Stderr(vec![0])), 1) + .err() + .expect("over-bound event must fail"); + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_EVENT_BYTES_LIMIT"); + drop(event); + assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 0); + } + + #[test] + fn executes_kernel_supplied_module_without_v8_or_ambient_wasi() { + let runtime = SidecarRuntime::process(&RuntimeConfig::default()) + .expect("test runtime") + .context(); + let module = wat::parse_str("(module (func (export \"_start\")))").expect("test module"); + let request = StartWasmExecutionRequest { + vm_id: String::from("vm-test"), + context_id: String::from("ctx-test"), + managed_kernel_host: true, + argv: vec![String::from("/test.wasm")], + env: BTreeMap::new(), + cwd: PathBuf::from("/"), + permission_tier: WasmPermissionTier::Full, + limits: WasmExecutionLimits::default(), + guest_runtime: GuestRuntimeConfig::default(), + }; + let process = crate::host::HostProcessContext { + generation: 7, + pid: 42, + }; + let (submission, host_events) = bounded_execution_event_channel( + process, + 8, + PayloadLimit::new("limits.process.pendingEventBytes", 1024 * 1024) + .expect("event byte limit"), + Arc::new(|| {}), + ) + .expect("host event channel"); + let module_for_host = module.clone(); + let host_worker = std::thread::spawn(move || { + let mut completed = 0; + while completed < 5 { + let Some(event) = host_events.try_recv().expect("host event poll") else { + std::thread::yield_now(); + continue; + }; + let ExecutionEvent::HostCall { operation, reply } = event else { + panic!("unexpected non-host event"); + }; + match operation { + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens) => reply + .succeed_json(Value::Null) + .expect("canonical-preopens reply"), + HostOperation::Process(ProcessOperation::OpenExecutableImage { .. }) => reply + .succeed_json(serde_json::json!({ + "handle": "1", + "size": module_for_host.len(), + })) + .expect("open reply"), + HostOperation::Process(ProcessOperation::ReadExecutableImage { + handle, + offset, + max_bytes, + }) => { + assert_eq!(handle, 1); + let start = usize::try_from(offset).expect("test image offset"); + let end = start + .saturating_add(max_bytes.get()) + .min(module_for_host.len()); + reply + .succeed_raw(module_for_host[start..end].to_vec()) + .expect("read reply"); + } + HostOperation::Process(ProcessOperation::CloseExecutableImage { handle }) => { + assert_eq!(handle, 1); + reply.succeed_json(Value::Null).expect("close reply"); + } + HostOperation::Signal(crate::host::SignalOperation::UpdateMask { .. }) => { + reply + .succeed_json(serde_json::json!({ "signals": [] })) + .expect("signal-mask reply"); + } + operation => panic!("unexpected host operation: {operation:?}"), + } + completed += 1; + } + }); + let execution = WasmtimeExecution::spawn( + String::from("exec-test"), + String::from("/test.wasm"), + request, + runtime, + None, + false, + ) + .expect("spawn executor"); + execution + .configure_host_services(ProcessHostCapabilitySet::from_event_submission(submission)); + assert_eq!( + execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("execution event"), + Some(WasmExecutionEvent::Exited(0)) + ); + host_worker.join().expect("host worker"); + } + + #[test] + fn native_preview1_import_uses_owned_direct_waiter_event() { + let runtime = SidecarRuntime::process(&RuntimeConfig::default()) + .expect("test runtime") + .context(); + let module = wat::parse_str( + r#"(module + (import "wasi_snapshot_preview1" "fd_write" + (func $fd_write (param i32 i32 i32 i32) (result i32))) + (memory (export "memory") 1) + (data (i32.const 32) "hello") + (func (export "_start") + (i32.store (i32.const 8) (i32.const 32)) + (i32.store (i32.const 12) (i32.const 5)) + (drop (call $fd_write + (i32.const 1) (i32.const 8) (i32.const 1) (i32.const 16)))))"#, + ) + .expect("test module"); + let request = StartWasmExecutionRequest { + vm_id: String::from("vm-test"), + context_id: String::from("ctx-test"), + managed_kernel_host: true, + argv: vec![String::from("/test.wasm")], + env: BTreeMap::new(), + cwd: PathBuf::from("/"), + permission_tier: WasmPermissionTier::Full, + limits: WasmExecutionLimits::default(), + guest_runtime: GuestRuntimeConfig::default(), + }; + let process = crate::host::HostProcessContext { + generation: 8, + pid: 43, + }; + let (submission, host_events) = bounded_execution_event_channel( + process, + 8, + PayloadLimit::new("limits.process.pendingEventBytes", 1024 * 1024) + .expect("event byte limit"), + Arc::new(|| {}), + ) + .expect("host event channel"); + let module_for_host = module.clone(); + let host_worker = std::thread::spawn(move || { + let mut completed = 0; + while completed < 5 { + let Some(event) = host_events.try_recv().expect("host event poll") else { + std::thread::yield_now(); + continue; + }; + let ExecutionEvent::HostCall { operation, reply } = event else { + panic!("unexpected non-host event"); + }; + match operation { + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens) => reply + .succeed_json(Value::Null) + .expect("canonical-preopens reply"), + HostOperation::Process(ProcessOperation::OpenExecutableImage { .. }) => reply + .succeed_json(serde_json::json!({ + "handle": "1", + "size": module_for_host.len(), + })) + .expect("open reply"), + HostOperation::Process(ProcessOperation::ReadExecutableImage { + offset, + max_bytes, + .. + }) => { + let start = offset as usize; + let end = start + .saturating_add(max_bytes.get()) + .min(module_for_host.len()); + reply + .succeed_raw(module_for_host[start..end].to_vec()) + .expect("read reply"); + } + HostOperation::Process(ProcessOperation::CloseExecutableImage { .. }) => { + reply.succeed_json(Value::Null).expect("close reply"); + } + HostOperation::Signal(crate::host::SignalOperation::UpdateMask { .. }) => { + reply + .succeed_json(serde_json::json!({ "signals": [] })) + .expect("signal-mask reply"); + } + operation => panic!("unexpected host operation: {operation:?}"), + } + completed += 1; + } + }); + let execution = WasmtimeExecution::spawn( + String::from("exec-import-test"), + String::from("/test.wasm"), + request, + runtime, + None, + false, + ) + .expect("spawn executor"); + execution + .configure_host_services(ProcessHostCapabilitySet::from_event_submission(submission)); + let event = execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("host-call event") + .expect("host-call event present"); + let WasmExecutionEvent::HostCall { request, reply } = event else { + panic!("unexpected execution event: {event:?}"); + }; + assert_eq!(request.method, "__kernel_stdio_write"); + assert_eq!(request.raw_bytes_args.get(&1), Some(&b"hello".to_vec())); + reply + .succeed_json(serde_json::json!(5)) + .expect("write reply"); + assert_eq!( + execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("exit event"), + Some(WasmExecutionEvent::Exited(0)) + ); + host_worker.join().expect("host worker"); + } + + #[test] + fn caught_signal_runs_exact_trampoline_at_async_import_boundary() { + let runtime = SidecarRuntime::process(&RuntimeConfig::default()) + .expect("test runtime") + .context(); + let module = wat::parse_str( + r#"(module + (import "wasi_snapshot_preview1" "fd_write" + (func $fd_write (param i32 i32 i32 i32) (result i32))) + (memory (export "memory") 1) + (data (i32.const 32) "h") + (func (export "__wasi_signal_trampoline") (param i32) + (i32.store8 (i32.const 32) (i32.const 115))) + (func (export "_start") + (i32.store (i32.const 8) (i32.const 32)) + (i32.store (i32.const 12) (i32.const 1)) + (drop (call $fd_write + (i32.const 1) (i32.const 8) (i32.const 1) (i32.const 16)))))"#, + ) + .expect("signal test module"); + let request = StartWasmExecutionRequest { + vm_id: String::from("vm-signal"), + context_id: String::from("ctx-signal"), + managed_kernel_host: true, + argv: vec![String::from("/signal.wasm")], + env: BTreeMap::new(), + cwd: PathBuf::from("/"), + permission_tier: WasmPermissionTier::Full, + limits: WasmExecutionLimits::default(), + guest_runtime: GuestRuntimeConfig::default(), + }; + let identity = ExecutionWakeIdentity { + generation: 11, + pid: 51, + }; + let process = crate::host::HostProcessContext { + generation: identity.generation, + pid: identity.pid, + }; + let (submission, host_events) = bounded_execution_event_channel( + process, + 8, + PayloadLimit::new("limits.process.pendingEventBytes", 1024 * 1024) + .expect("event byte limit"), + Arc::new(|| {}), + ) + .expect("host event channel"); + let execution_slot = Arc::new(std::sync::OnceLock::>::new()); + let worker_slot = Arc::clone(&execution_slot); + let module_for_host = module.clone(); + let host_worker = std::thread::spawn(move || { + let mut completed = 0; + while completed < 7 { + let Some(event) = host_events.try_recv().expect("host event poll") else { + std::thread::yield_now(); + continue; + }; + let ExecutionEvent::HostCall { operation, reply } = event else { + panic!("unexpected non-host event"); + }; + match operation { + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens) => reply + .succeed_json(Value::Null) + .expect("canonical-preopens reply"), + HostOperation::Process(ProcessOperation::OpenExecutableImage { .. }) => reply + .succeed_json(serde_json::json!({ + "handle": "1", + "size": module_for_host.len(), + })) + .expect("open reply"), + HostOperation::Process(ProcessOperation::ReadExecutableImage { + offset, + max_bytes, + .. + }) => { + let start = offset as usize; + let end = start + .saturating_add(max_bytes.get()) + .min(module_for_host.len()); + reply + .succeed_raw(module_for_host[start..end].to_vec()) + .expect("read reply"); + } + HostOperation::Process(ProcessOperation::CloseExecutableImage { .. }) => { + reply.succeed_json(Value::Null).expect("close reply"); + } + HostOperation::Signal(crate::host::SignalOperation::UpdateMask { .. }) => { + let execution = loop { + if let Some(execution) = worker_slot.get() { + break execution; + } + std::thread::yield_now(); + }; + execution + .deliver_signal_checkpoint(identity, 10, 99, 0) + .expect("publish signal"); + reply + .succeed_json(serde_json::json!({ "signals": [] })) + .expect("signal-mask reply"); + } + HostOperation::Signal(crate::host::SignalOperation::TakePublishedDelivery) => { + let delivery = worker_slot + .get() + .expect("execution installed") + .take_signal_checkpoint(identity) + .expect("take signal") + .expect("published signal"); + reply + .succeed_json(serde_json::json!({ + "signal": delivery.signal, + "token": delivery.delivery_token, + "flags": delivery.flags, + })) + .expect("take reply"); + } + HostOperation::Signal(crate::host::SignalOperation::EndDelivery { token }) => { + assert_eq!(token, 99); + reply.succeed_json(Value::Null).expect("signal-end reply"); + } + operation => panic!("unexpected host operation: {operation:?}"), + } + completed += 1; + } + }); + let execution = Arc::new( + WasmtimeExecution::spawn( + String::from("exec-signal-test"), + String::from("/signal.wasm"), + request, + runtime, + None, + false, + ) + .expect("spawn executor"), + ); + execution_slot + .set(Arc::clone(&execution)) + .expect("install execution"); + execution + .configure_host_services(ProcessHostCapabilitySet::from_event_submission(submission)); + let event = execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("write event") + .expect("write event present"); + let WasmExecutionEvent::HostCall { request, reply } = event else { + panic!("unexpected execution event: {event:?}"); + }; + assert_eq!(request.method, "__kernel_stdio_write"); + assert_eq!(request.raw_bytes_args.get(&1), Some(&b"s".to_vec())); + reply + .succeed_json(serde_json::json!(1)) + .expect("write reply"); + assert_eq!( + execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("exit event"), + Some(WasmExecutionEvent::Exited(0)) + ); + host_worker.join().expect("host worker"); + } +} diff --git a/crates/execution/src/wasm/wasmtime/limits.rs b/crates/execution/src/wasm/wasmtime/limits.rs new file mode 100644 index 0000000000..3fa9c350ca --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/limits.rs @@ -0,0 +1,117 @@ +//! Store, memory, table, instance, stack, CPU, and cache limits. + +use super::super::WasmExecutionLimits; +use crate::backend::HostServiceError; +use wasmtime::{StoreLimits, StoreLimitsBuilder}; + +pub const DEFAULT_MAX_WASM_MEMORY_BYTES: usize = 128 * 1024 * 1024; +pub const DEFAULT_MAX_TABLE_ELEMENTS: usize = 1_000_000; +pub const DEFAULT_TABLE_ACCOUNTING_BYTES: usize = + DEFAULT_MAX_TABLE_ELEMENTS * std::mem::size_of::(); +pub const DEFAULT_MAX_INSTANCES: usize = 1; +pub const DEFAULT_MAX_TABLES: usize = 1; +pub const DEFAULT_MAX_MEMORIES: usize = 1; + +pub fn store_limits(limits: &WasmExecutionLimits) -> Result { + let memory_bytes = limits + .max_memory_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| limit_overflow("limits.resources.maxWasmMemoryBytes"))? + .unwrap_or(DEFAULT_MAX_WASM_MEMORY_BYTES); + Ok(StoreLimitsBuilder::new() + .memory_size(memory_bytes) + .table_elements(DEFAULT_MAX_TABLE_ELEMENTS) + .instances(DEFAULT_MAX_INSTANCES) + .tables(DEFAULT_MAX_TABLES) + .memories(DEFAULT_MAX_MEMORIES) + .trap_on_grow_failure(true) + .build()) +} + +pub fn max_memory_bytes(limits: &WasmExecutionLimits) -> Result { + limits + .max_memory_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| limit_overflow("limits.resources.maxWasmMemoryBytes")) + .map(|value| value.unwrap_or(DEFAULT_MAX_WASM_MEMORY_BYTES)) +} + +pub fn aggregate_store_memory_bytes( + limits: &WasmExecutionLimits, +) -> Result { + max_memory_bytes(limits)? + .checked_add(DEFAULT_TABLE_ACCOUNTING_BYTES) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes")) +} + +fn limit_overflow(name: &'static str) -> HostServiceError { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_LIMIT_CONFIG", + format!("{name} does not fit this platform"), + ) + .with_details(serde_json::json!({ "limitName": name })) +} + +#[cfg(test)] +mod tests { + use super::*; + use wasmtime::{Engine, Instance, Module, Store}; + + fn instantiate_with(limits: StoreLimits, wat: &str) -> Result<(), wasmtime::Error> { + let engine = Engine::default(); + let bytes = wat::parse_str(wat)?; + let module = Module::new(&engine, bytes)?; + let mut store = Store::new(&engine, limits); + store.limiter(|limits| limits); + Instance::new(&mut store, &module, &[])?; + Ok(()) + } + + #[test] + fn store_limits_accept_exact_memory_and_table_bounds_and_reject_overflow() { + let request = WasmExecutionLimits { + max_memory_bytes: Some(65_536), + ..WasmExecutionLimits::default() + }; + instantiate_with( + store_limits(&request).expect("memory limits"), + "(module (memory 1))", + ) + .expect("exact memory bound"); + assert!(instantiate_with( + store_limits(&request).expect("memory limits"), + "(module (memory 2))", + ) + .is_err()); + + instantiate_with( + store_limits(&WasmExecutionLimits::default()).expect("table limits"), + &format!("(module (table {DEFAULT_MAX_TABLE_ELEMENTS} funcref))"), + ) + .expect("exact table bound"); + assert!(instantiate_with( + store_limits(&WasmExecutionLimits::default()).expect("table limits"), + &format!( + "(module (table {} funcref))", + DEFAULT_MAX_TABLE_ELEMENTS + 1 + ), + ) + .is_err()); + } + + #[test] + fn store_instance_count_is_bounded() { + let engine = Engine::default(); + let module = Module::new( + &engine, + wat::parse_str("(module)").expect("empty module bytes"), + ) + .expect("empty module"); + let mut store = Store::new(&engine, StoreLimitsBuilder::new().instances(1).build()); + store.limiter(|limits| limits); + Instance::new(&mut store, &module, &[]).expect("first instance"); + assert!(Instance::new(&mut store, &module, &[]).is_err()); + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/filesystem.rs b/crates/execution/src/wasm/wasmtime/linker/filesystem.rs new file mode 100644 index 0000000000..598a92e8c5 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/filesystem.rs @@ -0,0 +1,875 @@ +//! AgentOS filesystem extension ABI codecs. + +use super::preview1::{ + call, commit, errno, i64_arg, json_reply, reply_bytes, simple_call, value_u64, ERRNO_2BIG, + ERRNO_FAULT, ERRNO_INVAL, ERRNO_IO, ERRNO_NODATA, ERRNO_RANGE, SUCCESS, +}; +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use wasmtime::{Caller, Val}; + +const XATTR_NAME_MAX: usize = 255; +const XATTR_SIZE_MAX: usize = 64 * 1024; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let name_length_index = match abi.id { + HostFsPathGetxattr | HostFsPathSetxattr | HostFsPathRemovexattr => Some(4), + HostFsFdGetxattr | HostFsFdSetxattr | HostFsFdRemovexattr => Some(2), + _ => None, + }; + if let Some(index) = name_length_index { + if let Ok(length) = i32_arg(params, index) { + let status = super::check_fixed_request_limit( + caller, + "wasm.abi.maxXattrNameBytes", + length as usize, + XATTR_NAME_MAX, + ERRNO_RANGE, + ) + .await; + if status != SUCCESS { + set_i32_result(results, status)?; + return Ok(true); + } + } + } + let value_length_index = match abi.id { + HostFsPathSetxattr => Some(6), + HostFsFdSetxattr => Some(4), + _ => None, + }; + if let Some(index) = value_length_index { + if let Ok(length) = i32_arg(params, index) { + let status = super::check_fixed_request_limit( + caller, + "wasm.abi.maxXattrValueBytes", + length as usize, + XATTR_SIZE_MAX, + ERRNO_2BIG, + ) + .await; + if status != SUCCESS { + set_i32_result(results, status)?; + return Ok(true); + } + } + } + let status = match abi.id { + HostFsSetOpenMode => { + caller.data_mut().pending_open_mode = Some(i32_arg(params, 0)? & 0o7777); + SUCCESS + } + HostFsSetOpenDirect => { + caller.data_mut().pending_open_direct = i32_arg(params, 0)? != 0; + SUCCESS + } + HostFsChmod => path_chmod(caller, params).await, + HostFsFchmod => fd_mode_set(caller, params).await, + HostFsChown | HostFsPathChown => path_chown(caller, params).await, + HostFsFchown | HostFsFdChown => fd_owner_set(caller, params).await, + HostFsFtruncate => { + let (Ok(fd), Ok(length)) = (i32_arg(params, 0), i64_arg(params, 1)) else { + set_i32_result(results, ERRNO_INVAL)?; + return Ok(true); + }; + simple_call( + caller, + "process.fd_truncate", + vec![json!(fd), json!(length.to_string())], + ) + .await + } + HostFsOpenTmpfile => open_tmpfile(caller, params).await, + HostFsFdLink => fd_link(caller, params).await, + HostFsRemount => remount(caller, params).await, + HostFsPathMknod => path_mknod(caller, params).await, + HostFsPathRenameat2 => path_renameat2(caller, params).await, + HostFsPathStatfs => path_statfs(caller, params).await, + HostFsFdFiemap => fd_fiemap(caller, params).await, + HostFsFdPunchHole => range(caller, params, "fs.punchHoleSync", false).await, + HostFsFdZeroRange => range(caller, params, "fs.zeroRangeSync", true).await, + HostFsFdInsertRange => range(caller, params, "fs.insertRangeSync", false).await, + HostFsFdCollapseRange => range(caller, params, "fs.collapseRangeSync", false).await, + HostFsPathOwner => owner_get(caller, params, true).await, + HostFsFdOwner => owner_get(caller, params, false).await, + HostFsPathAccess => path_access(caller, params).await, + HostFsPathGetxattr => xattr_get(caller, params, true).await, + HostFsFdGetxattr => xattr_get(caller, params, false).await, + HostFsPathListxattr => xattr_list(caller, params, true).await, + HostFsFdListxattr => xattr_list(caller, params, false).await, + HostFsPathSetxattr => xattr_set(caller, params, true).await, + HostFsFdSetxattr => xattr_set(caller, params, false).await, + HostFsPathRemovexattr => xattr_remove(caller, params, true).await, + HostFsFdRemovexattr => xattr_remove(caller, params, false).await, + HostFsFdMode => return scalar_stat(caller, params, results, false, "mode", false, 0).await, + HostFsFdSize => { + return scalar_stat(caller, params, results, false, "size", true, u64::MAX).await + } + HostFsFdBlocks => { + return scalar_stat(caller, params, results, false, "blocks", true, u64::MAX).await + } + HostFsPathMode => { + return scalar_stat(caller, params, results, true, "mode", false, 0).await + } + HostFsPathSize => { + return scalar_stat(caller, params, results, true, "size", true, u64::MAX).await + } + HostFsPathBlocks => { + return scalar_stat(caller, params, results, true, "blocks", true, u64::MAX).await + } + HostFsPathRdev => return scalar_stat(caller, params, results, true, "rdev", true, 0).await, + _ => return Ok(false), + }; + set_i32_result(results, status)?; + Ok(true) +} + +fn path( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + pointer: usize, + length: usize, +) -> Result { + let pointer = i32_arg(params, pointer).map_err(|_| ERRNO_FAULT)?; + let length = i32_arg(params, length).map_err(|_| ERRNO_FAULT)? as usize; + memory::read_string(caller, pointer, length).map_err(|_| ERRNO_FAULT) +} + +async fn path_chmod(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(mode)) = (i32_arg(params, 0), i32_arg(params, 3)) else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_chmod_at", + vec![json!(fd), json!(path), json!(mode)], + ) + .await +} + +async fn fd_mode_set(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(mode)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + simple_call(caller, "process.fd_chmod", vec![json!(fd), json!(mode)]).await +} + +async fn path_chown(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(uid), Ok(gid), Ok(follow)) = ( + i32_arg(params, 0), + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + ) else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_chown_at", + vec![ + json!(fd), + json!(path), + json!(uid), + json!(gid), + json!(follow != 0), + ], + ) + .await +} + +async fn fd_owner_set(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(uid), Ok(gid)) = (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.fd_chown", + vec![json!(fd), json!(uid), json!(gid)], + ) + .await +} + +async fn open_tmpfile(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(flags), Ok(mode), Ok(output)) = ( + i32_arg(params, 0), + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + ) else { + return ERRNO_FAULT; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call( + caller, + "process.open_tmpfile_at", + vec![ + json!(fd), + json!(path), + json!(flags), + json!(mode), + json!(flags & (1 << 15) == 0), + ], + HashMap::new(), + ) + .await + { + Ok(reply) => resource_u32(caller, output, reply, "process.fd_close").await, + Err(error) => errno(&error), + } +} + +async fn fd_link(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(dir_fd)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 2, 3) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.fd_link_at", + vec![json!(fd), json!(dir_fd), json!(path)], + ) + .await +} + +async fn remount(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(target), Ok(options)) = (path(caller, params, 0, 1), path(caller, params, 2, 3)) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "fs.remountSync", + vec![json!(target), json!(options)], + ) + .await +} + +async fn path_mknod(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(mode), Ok(device)) = + (i32_arg(params, 0), i32_arg(params, 3), i64_arg(params, 4)) + else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_mknod_at", + vec![json!(fd), json!(path), json!(mode), json!(device)], + ) + .await +} + +async fn path_renameat2(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(old_fd), Ok(new_fd), Ok(flags)) = + (i32_arg(params, 0), i32_arg(params, 3), i32_arg(params, 6)) + else { + return ERRNO_INVAL; + }; + let (Ok(old_path), Ok(new_path)) = (path(caller, params, 1, 2), path(caller, params, 4, 5)) + else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_rename_at2", + vec![ + json!(old_fd), + json!(old_path), + json!(new_fd), + json!(new_path), + json!(flags), + ], + ) + .await +} + +async fn path_statfs(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let outputs = match (3..8) + .map(|index| i32_arg(params, index)) + .collect::, _>>() + { + Ok(value) => value, + Err(_) => return ERRNO_FAULT, + }; + if outputs + .iter() + .any(|output| memory::validate_range(caller, *output, 8).is_err()) + { + return ERRNO_FAULT; + } + match call( + caller, + "process.path_statfs_at", + vec![json!(fd), json!(path)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let fields = [ + "totalBytes", + "usedBytes", + "availableBytes", + "totalInodes", + "freeInodes", + ]; + let Some(values) = fields + .iter() + .map(|field| value.get(*field).and_then(value_u64)) + .collect::>>() + else { + return ERRNO_IO; + }; + if outputs + .iter() + .any(|output| memory::validate_range(caller, *output, 8).is_err()) + { + return ERRNO_FAULT; + } + for (output, value) in outputs.into_iter().zip(values) { + if memory::write_u64(caller, output, value).is_err() { + return ERRNO_FAULT; + } + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +async fn fd_fiemap(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(index), Ok(start), Ok(end), Ok(flags)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, start, 8).is_err() + || memory::validate_range(caller, end, 8).is_err() + || memory::validate_range(caller, flags, 4).is_err() + { + return ERRNO_FAULT; + } + match call( + caller, + "fs.fiemapAtSync", + vec![json!(fd), json!(index)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + if value.is_null() { + return ERRNO_NODATA; + } + let (Some(first), Some(last)) = ( + value.get("start").and_then(value_u64), + value.get("end").and_then(value_u64), + ) else { + return ERRNO_IO; + }; + if memory::write_u64(caller, start, first).is_err() + || memory::write_u64(caller, end, last).is_err() + || memory::write_u32( + caller, + flags, + if value + .get("unwritten") + .and_then(Value::as_bool) + .unwrap_or(false) + { + 0x800 + } else { + 0 + }, + ) + .is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } + } + Err(error) => errno(&error), + } +} + +async fn range( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, + keep_size: bool, +) -> i32 { + let (Ok(fd), Ok(offset), Ok(length)) = + (i32_arg(params, 0), i64_arg(params, 1), i64_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + let mut args = vec![json!(fd), json!(offset), json!(length)]; + if keep_size { + args.push(json!(i32_arg(params, 3).unwrap_or(0))); + } + simple_call(caller, method, args).await +} + +async fn owner_get( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + is_path: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (args, uid_output, gid_output, method) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let (Ok(follow), Ok(uid), Ok(gid)) = + (i32_arg(params, 3), i32_arg(params, 4), i32_arg(params, 5)) + else { + return ERRNO_FAULT; + }; + ( + vec![json!(fd), json!(path), json!(follow != 0)], + uid, + gid, + "process.path_stat_at", + ) + } else { + let (Ok(uid), Ok(gid)) = (i32_arg(params, 1), i32_arg(params, 2)) else { + return ERRNO_FAULT; + }; + (vec![json!(fd)], uid, gid, "process.fd_filestat") + }; + if memory::validate_range(caller, uid_output, 4).is_err() + || memory::validate_range(caller, gid_output, 4).is_err() + { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let (Some(uid), Some(gid)) = ( + value + .get("uid") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()), + value + .get("gid") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()), + ) else { + return ERRNO_IO; + }; + if memory::write_u32(caller, uid_output, uid).is_err() + || memory::write_u32(caller, gid_output, gid).is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } + } + Err(error) => errno(&error), + } +} + +async fn path_access(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(mode), Ok(effective)) = + (i32_arg(params, 0), i32_arg(params, 3), i32_arg(params, 4)) + else { + return ERRNO_INVAL; + }; + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_access_at", + vec![json!(fd), json!(path), json!(mode), json!(effective != 0)], + ) + .await +} + +async fn xattr_get( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + is_path: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (args, value_ptr, capacity, size_ptr, method) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let Ok(name) = bounded_name(caller, params, 3, 4) else { + return ERRNO_RANGE; + }; + let (Ok(value), Ok(capacity), Ok(follow), Ok(size)) = ( + i32_arg(params, 5), + i32_arg(params, 6), + i32_arg(params, 7), + i32_arg(params, 8), + ) else { + return ERRNO_FAULT; + }; + ( + vec![json!(fd), json!(path), json!(name), json!(follow != 0)], + value, + capacity, + size, + "process.path_getxattr_at", + ) + } else { + let Ok(name) = bounded_name(caller, params, 1, 2) else { + return ERRNO_RANGE; + }; + let (Ok(value), Ok(capacity), Ok(size)) = + (i32_arg(params, 3), i32_arg(params, 4), i32_arg(params, 5)) + else { + return ERRNO_FAULT; + }; + ( + vec![json!(fd), json!(name)], + value, + capacity, + size, + "fs.fgetxattrSync", + ) + }; + if memory::validate_range(caller, value_ptr, capacity as usize).is_err() + || memory::validate_range(caller, size_ptr, 4).is_err() + { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => publish_bytes( + caller, + value_ptr, + capacity, + size_ptr, + match reply_bytes(reply) { + Ok(value) => value, + Err(error) => return error, + }, + ), + Err(error) => errno(&error), + } +} + +async fn xattr_list( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + is_path: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (args, output, capacity, size_output, method) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let (Ok(output), Ok(capacity), Ok(follow), Ok(size)) = ( + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + i32_arg(params, 6), + ) else { + return ERRNO_FAULT; + }; + ( + vec![json!(fd), json!(path), json!(follow != 0)], + output, + capacity, + size, + "process.path_listxattr_at", + ) + } else { + let (Ok(output), Ok(capacity), Ok(size)) = + (i32_arg(params, 1), i32_arg(params, 2), i32_arg(params, 3)) + else { + return ERRNO_FAULT; + }; + (vec![json!(fd)], output, capacity, size, "fs.flistxattrSync") + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::validate_range(caller, size_output, 4).is_err() + { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(names) = value.as_array() else { + return ERRNO_IO; + }; + let mut bytes = Vec::new(); + for name in names.iter().filter_map(Value::as_str) { + bytes.extend_from_slice(name.as_bytes()); + bytes.push(0); + } + publish_bytes(caller, output, capacity, size_output, bytes) + } + Err(error) => errno(&error), + } +} + +async fn xattr_set( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + is_path: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (args, raw_index, raw, method) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let Ok(name) = bounded_name(caller, params, 3, 4) else { + return ERRNO_RANGE; + }; + let (Ok(value_ptr), Ok(length), Ok(flags), Ok(follow)) = ( + i32_arg(params, 5), + i32_arg(params, 6), + i32_arg(params, 7), + i32_arg(params, 8), + ) else { + return ERRNO_FAULT; + }; + if length as usize > XATTR_SIZE_MAX { + return ERRNO_2BIG; + } + let Ok(bytes) = memory::read_bytes(caller, value_ptr, length as usize) else { + return ERRNO_FAULT; + }; + ( + vec![ + json!(fd), + json!(path), + json!(name), + Value::Null, + json!(flags), + json!(follow != 0), + ], + 3, + bytes, + "process.path_setxattr_at", + ) + } else { + let Ok(name) = bounded_name(caller, params, 1, 2) else { + return ERRNO_RANGE; + }; + let (Ok(value_ptr), Ok(length), Ok(flags)) = + (i32_arg(params, 3), i32_arg(params, 4), i32_arg(params, 5)) + else { + return ERRNO_FAULT; + }; + if length as usize > XATTR_SIZE_MAX { + return ERRNO_2BIG; + } + let Ok(bytes) = memory::read_bytes(caller, value_ptr, length as usize) else { + return ERRNO_FAULT; + }; + ( + vec![json!(fd), json!(name), Value::Null, json!(flags)], + 2, + bytes, + "fs.fsetxattrSync", + ) + }; + let mut raw_args = HashMap::new(); + raw_args.insert(raw_index, raw); + match call(caller, method, args, raw_args).await { + Ok(_) => SUCCESS, + Err(error) => errno(&error), + } +} + +async fn xattr_remove( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + is_path: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (args, method) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let Ok(name) = bounded_name(caller, params, 3, 4) else { + return ERRNO_RANGE; + }; + let Ok(follow) = i32_arg(params, 5) else { + return ERRNO_INVAL; + }; + ( + vec![json!(fd), json!(path), json!(name), json!(follow != 0)], + "process.path_removexattr_at", + ) + } else { + let Ok(name) = bounded_name(caller, params, 1, 2) else { + return ERRNO_RANGE; + }; + (vec![json!(fd), json!(name)], "fs.fremovexattrSync") + }; + simple_call(caller, method, args).await +} + +fn bounded_name( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + pointer: usize, + length: usize, +) -> Result { + let length_value = i32_arg(params, length).map_err(|_| ERRNO_FAULT)? as usize; + if length_value > XATTR_NAME_MAX { + return Err(ERRNO_RANGE); + } + path(caller, params, pointer, length) +} + +fn publish_bytes( + caller: &mut Caller<'_, WasmtimeStoreState>, + output: u32, + capacity: u32, + size_output: u32, + bytes: Vec, +) -> i32 { + if memory::validate_range(caller, size_output, 4).is_err() + || memory::validate_range(caller, output, capacity as usize).is_err() + { + return ERRNO_FAULT; + } + if memory::write_u32(caller, size_output, bytes.len() as u32).is_err() { + return ERRNO_FAULT; + } + if capacity == 0 { + return SUCCESS; + } + if (capacity as usize) < bytes.len() { + return ERRNO_RANGE; + } + commit(caller, output, &bytes) +} + +async fn scalar_stat( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + results: &mut [Val], + is_path: bool, + field: &str, + i64_result: bool, + failure: u64, +) -> wasmtime::Result { + let Ok(fd) = i32_arg(params, 0) else { + set_scalar(results, i64_result, failure)?; + return Ok(true); + }; + let (method, args) = if is_path { + let Ok(path) = path(caller, params, 1, 2) else { + set_scalar(results, i64_result, failure)?; + return Ok(true); + }; + let follow = i32_arg(params, 3).unwrap_or(0) != 0; + ( + "process.path_stat_at", + vec![json!(fd), json!(path), json!(follow)], + ) + } else { + ("process.fd_filestat", vec![json!(fd)]) + }; + let value = match call(caller, method, args, HashMap::new()).await { + Ok(reply) => json_reply(reply) + .ok() + .and_then(|value| value.get(field).and_then(value_u64)) + .unwrap_or(failure), + Err(_) => failure, + }; + set_scalar(results, i64_result, value)?; + Ok(true) +} + +fn set_scalar(results: &mut [Val], i64_result: bool, value: u64) -> wasmtime::Result<()> { + match (i64_result, results) { + (true, [slot]) => { + *slot = Val::I64(value as i64); + Ok(()) + } + (false, [slot]) => { + *slot = Val::I32(value as i32); + Ok(()) + } + _ => Err(wasmtime::format_err!( + "invalid filesystem scalar result shape" + )), + } +} + +async fn resource_u32( + caller: &mut Caller<'_, WasmtimeStoreState>, + output: u32, + reply: crate::backend::HostCallReply, + rollback_method: &str, +) -> i32 { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(fd) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_IO; + }; + if memory::validate_range(caller, output, 4).is_err() { + if let Err(error) = call(caller, rollback_method, vec![json!(fd)], HashMap::new()).await { + eprintln!( + "ERR_AGENTOS_WASMTIME_RESOURCE_ROLLBACK: method={rollback_method} resource={fd} code={}", + error.code + ); + } + return ERRNO_FAULT; + } + memory::write_u32(caller, output, fd).map_or(ERRNO_FAULT, |_| SUCCESS) +} diff --git a/crates/execution/src/wasm/wasmtime/linker/mod.rs b/crates/execution/src/wasm/wasmtime/linker/mod.rs new file mode 100644 index 0000000000..0819d4c59e --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/mod.rs @@ -0,0 +1,572 @@ +//! Generated AgentOS Preview1 and custom host-import linker. + +use super::super::WasmPermissionTier; +use super::memory; +use super::store::WasmtimeStoreState; +use crate::abi::{ + binding, core_signature, AbiBinding, CoreValueType, ImportId, PermissionTier, Restartability, + ABI_BINDINGS, ALIAS_BINDINGS, +}; +use crate::backend::{HostCallReply, HostServiceError}; +use crate::host::{HostOperation, SignalMaskHow, SignalOperation, SignalSetValue}; +use serde_json::Value; +use wasmtime::{Caller, Engine, FuncType, Linker, Module, Val, ValType}; + +mod filesystem; +mod network; +mod preview1; +mod process; +mod terminal; +mod user; + +const WASI_ERRNO_SUCCESS: i32 = 0; +const WASI_ERRNO_FAULT: i32 = 21; +const WASI_ERRNO_NOSYS: i32 = 52; +const WASI_ERRNO_INTR: i32 = 27; +const SA_RESTART: u32 = 0x1000_0000; +const MAX_SIGNALS_PER_SAFE_POINT: usize = 64; + +pub fn build_linker( + engine: &Engine, + tier: WasmPermissionTier, +) -> wasmtime::Result> { + let mut linker = Linker::new(engine); + for abi in ABI_BINDINGS { + if permitted(*abi, tier) { + link_binding(&mut linker, engine, *abi, abi.module)?; + } + } + for alias in ALIAS_BINDINGS { + let abi = *binding(alias.import); + if permitted(abi, tier) && alias.permission_tiers.contains(permission_tier(tier)) { + link_binding(&mut linker, engine, abi, alias.alias_module)?; + } + } + Ok(linker) +} + +/// Reject unsupported or permission-filtered imports before Wasmtime linker +/// diagnostics are involved. This keeps the public outcome independent of an +/// engine's error-string format while the generated ABI registry remains the +/// sole import allowlist. +pub fn validate_module_imports( + module: &Module, + tier: WasmPermissionTier, +) -> Result<(), HostServiceError> { + for import in module.imports() { + if import_permitted(import.module(), import.name(), tier) { + continue; + } + return Err(HostServiceError::new( + "ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT", + format!( + "unsupported WebAssembly host import {}.{}", + import.module(), + import.name() + ), + ) + .with_details(serde_json::json!({ + "module": import.module(), + "name": import.name(), + }))); + } + Ok(()) +} + +fn import_permitted(module: &str, name: &str, tier: WasmPermissionTier) -> bool { + ABI_BINDINGS + .iter() + .any(|abi| abi.module == module && abi.name == name && permitted(*abi, tier)) + || ALIAS_BINDINGS.iter().any(|alias| { + let abi = *binding(alias.import); + alias.alias_module == module + && abi.name == name + && permitted(abi, tier) + && alias.permission_tiers.contains(permission_tier(tier)) + }) +} + +fn link_binding( + linker: &mut Linker, + engine: &Engine, + abi: AbiBinding, + module: &'static str, +) -> wasmtime::Result<()> { + let signature = core_signature(abi.signature); + let ty = FuncType::new( + engine, + signature.params.iter().copied().map(value_type), + signature.results.iter().copied().map(value_type), + ); + linker.func_new_async(module, abi.name, ty, move |mut caller, params, results| { + Box::new(async move { dispatch(&mut caller, abi, params, results).await }) + })?; + Ok(()) +} + +fn value_type(value: CoreValueType) -> ValType { + match value { + CoreValueType::I32 => ValType::I32, + CoreValueType::I64 => ValType::I64, + } +} + +fn permitted(abi: AbiBinding, tier: WasmPermissionTier) -> bool { + abi.permission_tiers.contains(permission_tier(tier)) +} + +fn permission_tier(tier: WasmPermissionTier) -> PermissionTier { + match tier { + WasmPermissionTier::Isolated => PermissionTier::Isolated, + WasmPermissionTier::ReadOnly => PermissionTier::ReadOnly, + WasmPermissionTier::ReadWrite => PermissionTier::ReadWrite, + WasmPermissionTier::Full => PermissionTier::Full, + } +} + +async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result<()> { + if caller.data().canceled() { + return Err(wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_CANCELED: execution canceled" + )); + } + drain_signal_checkpoints(caller).await?; + loop { + dispatch_once(caller, abi, params, results).await?; + let signals = drain_signal_checkpoints(caller).await?; + let interrupted = matches!(results, [Val::I32(value)] if *value == WASI_ERRNO_INTR); + if interrupted + && abi.restartability == Restartability::SignalRestartable + && signals.delivered + && signals.all_restart + { + continue; + } + return Ok(()); + } +} + +async fn dispatch_once( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result<()> { + if preview1::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + if user::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + if terminal::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + if filesystem::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + if network::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + if process::dispatch(caller, abi, params, results).await? { + return Ok(()); + } + match abi.id { + ImportId::WasiSnapshotPreview1ArgsSizesGet => { + let count = caller.data().argv.len(); + let bytes = table_bytes(&caller.data().argv)?; + let count = + u32::try_from(count).map_err(|_| wasmtime::format_err!("argv count overflow"))?; + let bytes = + u32::try_from(bytes).map_err(|_| wasmtime::format_err!("argv bytes overflow"))?; + let status = if memory::validate_range(caller, i32_arg(params, 0)?, 4).is_err() + || memory::validate_range(caller, i32_arg(params, 1)?, 4).is_err() + { + WASI_ERRNO_FAULT + } else { + memory::write_u32(caller, i32_arg(params, 0)?, count).expect("prevalidated argc"); + memory::write_u32(caller, i32_arg(params, 1)?, bytes) + .expect("prevalidated argv bytes"); + WASI_ERRNO_SUCCESS + }; + set_i32_result(results, status)?; + } + ImportId::WasiSnapshotPreview1ArgsGet => { + let values = caller.data().argv.clone(); + let status = if memory::write_string_table( + caller, + i32_arg(params, 0)?, + i32_arg(params, 1)?, + &values, + ) + .is_ok() + { + WASI_ERRNO_SUCCESS + } else { + WASI_ERRNO_FAULT + }; + set_i32_result(results, status)?; + } + ImportId::WasiSnapshotPreview1EnvironSizesGet => { + let count = caller.data().env.len(); + let bytes = table_bytes(&caller.data().env)?; + let count = + u32::try_from(count).map_err(|_| wasmtime::format_err!("env count overflow"))?; + let bytes = + u32::try_from(bytes).map_err(|_| wasmtime::format_err!("env bytes overflow"))?; + let status = if memory::validate_range(caller, i32_arg(params, 0)?, 4).is_err() + || memory::validate_range(caller, i32_arg(params, 1)?, 4).is_err() + { + WASI_ERRNO_FAULT + } else { + memory::write_u32(caller, i32_arg(params, 0)?, count) + .expect("prevalidated env count"); + memory::write_u32(caller, i32_arg(params, 1)?, bytes) + .expect("prevalidated env bytes"); + WASI_ERRNO_SUCCESS + }; + set_i32_result(results, status)?; + } + ImportId::WasiSnapshotPreview1EnvironGet => { + let values = caller.data().env.clone(); + let status = if memory::write_string_table( + caller, + i32_arg(params, 0)?, + i32_arg(params, 1)?, + &values, + ) + .is_ok() + { + WASI_ERRNO_SUCCESS + } else { + WASI_ERRNO_FAULT + }; + set_i32_result(results, status)?; + } + ImportId::WasiSnapshotPreview1SchedYield => set_i32_result(results, WASI_ERRNO_SUCCESS)?, + ImportId::WasiSnapshotPreview1ProcExit => { + let code = i32_arg(params, 0)? as i32; + caller.data_mut().exit_code = Some(code); + return Err(wasmtime::format_err!("agentos:wasi-exit:{code}")); + } + _ => set_default_result(results)?, + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct SignalDispatch { + delivered: bool, + all_restart: bool, +} + +async fn drain_signal_checkpoints( + caller: &mut Caller<'_, WasmtimeStoreState>, +) -> wasmtime::Result { + let mut outcome = SignalDispatch { + delivered: false, + all_restart: true, + }; + for _ in 0..MAX_SIGNALS_PER_SAFE_POINT { + let host = caller.data().host.clone(); + if !host.signal_pending() { + return Ok(outcome); + } + let reply = host + .submit( + HostOperation::Signal(SignalOperation::TakePublishedDelivery), + 0, + ) + .await + .map_err(wasmtime_host_error)?; + let value = host_json(reply, "process.take_signal")?; + if value.is_null() { + return Ok(outcome); + } + let signal = value + .get("signal") + .and_then(Value::as_i64) + .and_then(|value| i32::try_from(value).ok()) + .ok_or_else(|| wasmtime::format_err!("process.take_signal omitted a valid signal"))?; + let token = value + .get("token") + .and_then(Value::as_u64) + .ok_or_else(|| wasmtime::format_err!("process.take_signal omitted a delivery token"))?; + let flags = value + .get("flags") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| wasmtime::format_err!("process.take_signal omitted valid flags"))?; + outcome.delivered = true; + outcome.all_restart &= flags & SA_RESTART != 0; + + let trampoline = caller + .get_export("__wasi_signal_trampoline") + .and_then(|export| export.into_func()) + .ok_or_else(|| { + wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_SIGNAL_TRAMPOLINE: caught signal has no trampoline" + ) + })? + .typed::(&mut *caller) + .map_err(|error| { + wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_SIGNAL_TRAMPOLINE: invalid trampoline type: {error}" + ) + })?; + let handler_result = trampoline.call_async(&mut *caller, signal).await; + let end_result = caller + .data() + .host + .clone() + .submit( + HostOperation::Signal(SignalOperation::EndDelivery { token }), + std::mem::size_of::(), + ) + .await; + if let Err(error) = handler_result { + if let Err(end_error) = end_result { + eprintln!( + "ERR_AGENTOS_WASMTIME_SIGNAL_SETTLEMENT: handler failed with {error}; token settlement also failed with {end_error}" + ); + } + return Err(error); + } + end_result.map_err(wasmtime_host_error)?; + } + Err(wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_SIGNAL_DRAIN_LIMIT: more than {MAX_SIGNALS_PER_SAFE_POINT} signals were delivered at one safe point" + )) +} + +pub async fn initialize_inherited_signal_mask( + store: &mut wasmtime::Store, + instance: &wasmtime::Instance, +) -> Result<(), HostServiceError> { + let reply = store + .data() + .host + .clone() + .submit( + HostOperation::Signal(SignalOperation::UpdateMask { + how: SignalMaskHow::Block, + set: SignalSetValue::default(), + }), + std::mem::size_of::(), + ) + .await?; + let value = match reply { + HostCallReply::Json(value) => value, + _ => { + return Err(HostServiceError::new( + "EIO", + "process.signal_mask returned a non-JSON reply", + )); + } + }; + let signals = value + .get("signals") + .and_then(Value::as_array) + .ok_or_else(|| HostServiceError::new("EIO", "signal-mask query omitted signals"))?; + let mut low = 0u32; + let mut high = 0u32; + for signal in signals { + let signal = signal + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .filter(|value| (1..=64).contains(value)) + .ok_or_else(|| { + HostServiceError::new("EIO", "signal-mask query returned an invalid signal") + })?; + if signal <= 32 { + low |= 1 << (signal - 1); + } else { + high |= 1 << (signal - 33); + } + } + if low == 0 && high == 0 { + return Ok(()); + } + let setter = instance + .get_typed_func::<(i32, i32), i32>(&mut *store, "__agentos_set_initial_sigmask") + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASMTIME_INITIAL_SIGNAL_MASK: private export diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_INITIAL_SIGNAL_MASK", + "WebAssembly module cannot initialize its inherited signal mask", + ) + })?; + let status = setter + .call_async(&mut *store, (low as i32, high as i32)) + .await + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASMTIME_INITIAL_SIGNAL_MASK: private initialization diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_INITIAL_SIGNAL_MASK", + "inherited signal-mask initialization trapped", + ) + })?; + if status != 0 { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_INITIAL_SIGNAL_MASK", + format!("inherited signal-mask initialization failed with errno {status}"), + )); + } + Ok(()) +} + +fn host_json(reply: HostCallReply, method: &str) -> wasmtime::Result { + match reply { + HostCallReply::Json(value) => Ok(value), + _ => Err(wasmtime::format_err!("{method} returned a non-JSON reply")), + } +} + +fn wasmtime_host_error(error: HostServiceError) -> wasmtime::Error { + wasmtime::format_err!("{}: {}", error.code, error.message) +} + +fn table_bytes(values: &[Vec]) -> wasmtime::Result { + values.iter().try_fold(0usize, |total, value| { + total + .checked_add(value.len()) + .ok_or_else(|| wasmtime::format_err!("string table byte count overflow")) + }) +} + +fn i32_arg(params: &[Val], index: usize) -> wasmtime::Result { + match params.get(index) { + Some(Val::I32(value)) => Ok(*value as u32), + _ => Err(wasmtime::format_err!( + "invalid i32 ABI argument at index {index}" + )), + } +} + +fn set_i32_result(results: &mut [Val], value: i32) -> wasmtime::Result<()> { + match results { + [slot] => { + *slot = Val::I32(value); + Ok(()) + } + _ => Err(wasmtime::format_err!("invalid i32 ABI result shape")), + } +} + +fn set_default_result(results: &mut [Val]) -> wasmtime::Result<()> { + match results { + [] => Ok(()), + [slot @ Val::I32(_)] => { + *slot = Val::I32(WASI_ERRNO_NOSYS); + Ok(()) + } + [slot @ Val::I64(_)] => { + *slot = Val::I64(-1); + Ok(()) + } + _ => Err(wasmtime::format_err!( + "unsupported AgentOS ABI result shape" + )), + } +} + +async fn check_fixed_request_limit( + caller: &mut Caller<'_, WasmtimeStoreState>, + limit_name: &'static str, + observed: usize, + maximum: usize, + errno: i32, +) -> i32 { + let warning_at = maximum.saturating_sub(maximum / 5).max(1); + let publish = + observed >= warning_at && caller.data_mut().warned_fixed_limits.insert(limit_name); + if publish { + let warning = format!( + "[agentos] WASM request is near {limit_name} ({observed}/{maximum}); split the request if needed\n" + ); + let host = caller.data().host.clone(); + if let Err(error) = host.publish_stderr(warning.into_bytes()).await { + eprintln!( + "ERR_AGENTOS_WASMTIME_LIMIT_WARNING: failed to publish {limit_name} warning: {error}" + ); + } + } + if observed > maximum { + errno + } else { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::abi::PermissionTier; + + #[test] + fn linker_permission_filter_matches_generated_registry() { + for tier in [ + WasmPermissionTier::Isolated, + WasmPermissionTier::ReadOnly, + WasmPermissionTier::ReadWrite, + WasmPermissionTier::Full, + ] { + assert_eq!( + ABI_BINDINGS + .iter() + .filter(|abi| permitted(**abi, tier)) + .count(), + ABI_BINDINGS + .iter() + .filter(|abi| abi.permission_tiers.contains(permission_tier(tier))) + .count() + ); + } + assert_eq!( + permission_tier(WasmPermissionTier::Full), + PermissionTier::Full + ); + } + + #[test] + fn module_import_validation_uses_generated_registry_not_engine_strings() { + let engine = Engine::default(); + let allowed = ABI_BINDINGS + .iter() + .find(|abi| permitted(**abi, WasmPermissionTier::Isolated)) + .expect("isolated import"); + let module = Module::new( + &engine, + wat::parse_str(format!( + "(module (import {:?} {:?} (func)))", + allowed.module, allowed.name + )) + .expect("allowed import module"), + ) + .expect("compile allowed import module"); + validate_module_imports(&module, WasmPermissionTier::Isolated) + .expect("generated registry permits import"); + + let hostile = Module::new( + &engine, + wat::parse_str("(module (import \"ambient_host\" \"escape\" (func)))") + .expect("hostile import module"), + ) + .expect("compile hostile import module"); + let error = validate_module_imports(&hostile, WasmPermissionTier::Full) + .expect_err("unknown import must fail before linker diagnostics"); + assert_eq!(error.code, "ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT"); + assert_eq!( + error.details.expect("typed import details")["module"], + "ambient_host" + ); + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/network.rs b/crates/execution/src/wasm/wasmtime/linker/network.rs new file mode 100644 index 0000000000..888d550787 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/network.rs @@ -0,0 +1,1067 @@ +//! AgentOS host-network ABI codecs. +//! +//! The sidecar kernel owns socket descriptions and guest fd allocation. These +//! codecs translate only the owned libc wire format; they do not project or +//! synchronize a second executor-local socket table. + +use super::preview1::{ + call, commit, errno, json_reply, reply_bytes, simple_call, value_u64, ERRNO_2BIG, ERRNO_FAULT, + ERRNO_INVAL, ERRNO_IO, SUCCESS, +}; +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::backend::HostCallReply; +use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; +use base64::Engine as _; +use serde_json::{json, Value}; +use std::collections::HashMap; +use wasmtime::{Caller, Val}; + +const ERRNO_AGAIN: i32 = 6; +const ERRNO_BADF: i32 = 8; +const ERRNO_NOBUFS: i32 = 42; +const ERRNO_NOTSUP: i32 = 58; +const ERRNO_TIMEDOUT: i32 = 73; +const MAX_POLL_FDS: usize = 1024; +const MAX_DNS_RECORDS: usize = 4096; +const MAX_DNS_PAYLOAD: usize = 64 * 1024; +const SOCK_TYPE_MASK: u32 = 0xf; +const SOCK_DGRAM: u32 = 5; +const SOCK_STREAM: u32 = 6; +const SOCK_CLOEXEC: u32 = 0x2000; +const SOCK_NONBLOCK: u32 = 0x4000; +const KERNEL_O_NONBLOCK: u32 = 0x800; +const MSG_TRUNC: u32 = 0x20; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let status = match abi.id { + HostNetNetSocket => socket(caller, params).await, + HostNetNetSetNonblock => set_nonblock(caller, params).await, + HostNetNetConnect => address_call(caller, params, "process.hostnet_connect").await, + HostNetNetBind => address_call(caller, params, "process.hostnet_bind").await, + HostNetNetGetaddrinfo => getaddrinfo(caller, params).await, + HostNetNetDnsQueryRrV1 => dns_query(caller, params).await, + HostNetNetListen => listen(caller, params).await, + HostNetNetAccept => accept(caller, params).await, + HostNetNetValidateSocket => validate(caller, params, false).await, + HostNetNetValidateAccept => validate(caller, params, true).await, + HostNetNetGetsockname => address_output(caller, params, false).await, + HostNetNetGetpeername => address_output(caller, params, true).await, + HostNetNetSend => send(caller, params, false).await, + HostNetNetSendto => send(caller, params, true).await, + HostNetNetRecv => receive(caller, params, false).await, + HostNetNetRecvfrom => receive(caller, params, true).await, + HostNetNetSetsockopt => set_option(caller, params).await, + HostNetNetGetsockopt => get_option(caller, params).await, + HostNetNetClose => close(caller, params).await, + HostNetNetTlsConnect => tls_connect(caller, params).await, + HostNetNetPoll => poll(caller, params).await, + _ => return Ok(false), + }; + set_i32_result(results, status)?; + Ok(true) +} + +async fn socket(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(domain), Ok(socket_type), Ok(_protocol), Ok(output)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_INVAL; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + let kind = socket_type & SOCK_TYPE_MASK; + if !matches!(kind, SOCK_DGRAM | SOCK_STREAM) { + return ERRNO_NOTSUP; + } + if domain == 3 && kind != SOCK_STREAM { + return ERRNO_NOTSUP; + } + match call( + caller, + "process.hostnet_fd_open", + vec![ + json!(domain), + json!(kind), + json!(socket_type & SOCK_NONBLOCK != 0), + json!(socket_type & SOCK_CLOEXEC != 0), + ], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(fd) = value + .get("fd") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + if commit(caller, output, &fd.to_le_bytes()) == SUCCESS { + SUCCESS + } else { + log_fd_rollback(caller, fd, "socket output commit").await; + ERRNO_FAULT + } + } + Err(error) => errno(&error), + } +} + +async fn set_nonblock(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(enable)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let flags = match call(caller, "process.fd_stat", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => match json_reply(reply) + .ok() + .and_then(|value| value.get("flags").and_then(value_u64)) + .and_then(|value| u32::try_from(value).ok()) + { + Some(value) => value, + None => return ERRNO_IO, + }, + Err(error) => return errno(&error), + }; + let flags = if enable != 0 { + flags | KERNEL_O_NONBLOCK + } else { + flags & !KERNEL_O_NONBLOCK + }; + simple_call( + caller, + "process.fd_set_flags", + vec![json!(fd), json!(flags)], + ) + .await +} + +async fn address_call( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, +) -> i32 { + let (Ok(fd), Ok(pointer), Ok(length)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + let Ok(mut text) = memory::read_string(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + if let Some(index) = text.find('\0') { + text.truncate(index); + } + let Ok(address) = decode_address(&text) else { + return ERRNO_INVAL; + }; + let args = if method.ends_with("connect") { + vec![json!(fd), address, Value::Null] + } else { + vec![json!(fd), address] + }; + simple_call(caller, method, args).await +} + +fn decode_address(text: &str) -> Result { + if text == "unix-autobind" { + return Ok(json!({"type": "unix-autobind"})); + } + if let Some(hex) = text.strip_prefix("unix-abstract:") { + if hex.len().is_multiple_of(2) && hex.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Ok(json!({"type": "unix-abstract", "hex": hex.to_ascii_lowercase()})); + } + return Err(()); + } + if let Some(hex) = text.strip_prefix("unix-path-hex:") { + let bytes = decode_hex(hex)?; + let path = String::from_utf8(bytes).map_err(|_| ())?; + return Ok(json!({"type": "unix-path", "path": path})); + } + if let Some(path) = text.strip_prefix("unix:") { + return Ok(json!({"type": "unix-path", "path": path})); + } + let (host, port) = if let Some(rest) = text.strip_prefix('[') { + let (host, port) = rest.split_once("]:").ok_or(())?; + (host, port) + } else { + text.rsplit_once(':').ok_or(())? + }; + if host.is_empty() { + return Err(()); + } + let port = port.parse::().map_err(|_| ())?; + Ok(json!({"type": "inet", "host": host, "port": port})) +} + +fn decode_hex(text: &str) -> Result, ()> { + if !text.len().is_multiple_of(2) { + return Err(()); + } + text.as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).map_err(|_| ())?; + u8::from_str_radix(pair, 16).map_err(|_| ()) + }) + .collect() +} + +async fn getaddrinfo(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(host_pointer), Ok(host_length), Ok(family), Ok(output), Ok(length_output)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 4), + i32_arg(params, 5), + i32_arg(params, 6), + ) else { + return ERRNO_FAULT; + }; + let Ok(hostname) = memory::read_string(caller, host_pointer, host_length as usize) else { + return ERRNO_FAULT; + }; + let Ok(capacity) = memory::read_u32(caller, length_output) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + { + return ERRNO_FAULT; + } + if !matches!(family, 0 | 4 | 6) { + return ERRNO_INVAL; + } + let mut options = serde_json::Map::from_iter([ + ("hostname".to_owned(), json!(hostname)), + ("all".to_owned(), json!(true)), + ]); + if family != 0 { + options.insert("family".to_owned(), json!(family)); + } + match call( + caller, + "dns.lookup", + vec![Value::Object(options)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(records) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(records) = records.as_array() else { + return ERRNO_IO; + }; + let mut normalized = Vec::with_capacity(records.len()); + for record in records { + let Some(family) = record.get("family").and_then(value_u64) else { + return ERRNO_IO; + }; + let Some(address) = record.get("address").and_then(Value::as_str) else { + return ERRNO_IO; + }; + if !matches!(family, 4 | 6) { + return ERRNO_IO; + } + normalized.push(json!({"addr": address, "family": family})); + } + let Ok(bytes) = serde_json::to_vec(&normalized) else { + return ERRNO_IO; + }; + publish_bytes(caller, output, capacity, length_output, &bytes, false) + } + Err(error) => errno(&error), + } +} + +async fn dns_query(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let ( + Ok(name_pointer), + Ok(name_length), + Ok(record_type), + Ok(output), + Ok(capacity), + Ok(length_output), + Ok(ttl_output), + Ok(flags_output), + ) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + i32_arg(params, 6), + i32_arg(params, 7), + ) + else { + return ERRNO_FAULT; + }; + let Ok(name) = memory::read_string(caller, name_pointer, name_length as usize) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || [length_output, ttl_output, flags_output] + .into_iter() + .any(|pointer| memory::validate_range(caller, pointer, 4).is_err()) + { + return ERRNO_FAULT; + } + let requested = match record_type { + 12 => "PTR", + 44 => "SSHFP", + _ => return ERRNO_NOTSUP, + }; + match call( + caller, + "dns.resolveRawRr", + vec![json!({"hostname": name, "rrtype": requested})], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(status) = value.get("status").and_then(Value::as_str) else { + return ERRNO_IO; + }; + if !matches!(status, "ok" | "nxdomain" | "nodata") { + return ERRNO_IO; + } + let Some(records) = value.get("records").and_then(Value::as_array) else { + return ERRNO_IO; + }; + if records.len() > MAX_DNS_RECORDS { + return ERRNO_NOBUFS; + } + let mut payload = Vec::from((records.len() as u32).to_le_bytes()); + let mut ttl: Option = None; + for record in records { + let Some(encoded) = record.get("data").and_then(Value::as_str) else { + return ERRNO_IO; + }; + let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) else { + return ERRNO_IO; + }; + if (requested == "PTR" && bytes.is_empty()) + || (requested == "SSHFP" && bytes.len() < 2) + { + return ERRNO_IO; + } + let Some(record_ttl) = record + .get("ttl") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + ttl = Some(ttl.map_or(record_ttl, |prior| prior.min(record_ttl))); + let Ok(length) = u32::try_from(bytes.len()) else { + return ERRNO_NOBUFS; + }; + payload.extend_from_slice(&length.to_le_bytes()); + payload.extend_from_slice(&bytes); + if payload.len() > MAX_DNS_PAYLOAD { + return ERRNO_NOBUFS; + } + } + if commit(caller, length_output, &(payload.len() as u32).to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + if payload.len() > capacity as usize { + return ERRNO_NOBUFS; + } + if commit(caller, output, &payload) != SUCCESS + || commit(caller, ttl_output, &ttl.unwrap_or(0).to_le_bytes()) != SUCCESS + || commit( + caller, + flags_output, + &(if status == "nxdomain" { + 2u32 + } else if status == "nodata" { + 4 + } else { + 0 + }) + .to_le_bytes(), + ) != SUCCESS + { + ERRNO_FAULT + } else { + SUCCESS + } + } + Err(error) => errno(&error), + } +} + +async fn listen(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(backlog)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.hostnet_listen", + vec![json!(fd), json!(backlog)], + ) + .await +} + +async fn validate( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + listening: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + simple_call( + caller, + "process.hostnet_validate", + vec![json!(fd), json!(listening)], + ) + .await +} + +async fn accept(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(fd_output), Ok(address_output), Ok(length_output)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_FAULT; + }; + let Ok(capacity) = memory::read_u32(caller, length_output) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, fd_output, 4).is_err() + || memory::validate_range(caller, address_output, capacity as usize).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + { + return ERRNO_FAULT; + } + match call( + caller, + "process.hostnet_accept", + vec![json!(fd), json!(false), json!(false), Value::Null], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + if value.is_null() || value.get("kind").and_then(Value::as_str) == Some("wouldBlock") { + return ERRNO_AGAIN; + } + let Some(accepted_fd) = value + .get("fd") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + let address = encode_address(value.get("info").unwrap_or(&value), true); + let copied = if commit(caller, fd_output, &accepted_fd.to_le_bytes()) == SUCCESS { + publish_bytes( + caller, + address_output, + capacity, + length_output, + address.as_bytes(), + false, + ) + } else { + ERRNO_FAULT + }; + if copied == SUCCESS { + SUCCESS + } else { + log_fd_rollback(caller, accepted_fd, "accept output commit").await; + copied + } + } + Err(error) => errno(&error), + } +} + +async fn log_fd_rollback( + caller: &mut Caller<'_, WasmtimeStoreState>, + fd: u32, + context: &'static str, +) { + let error = simple_call(caller, "process.fd_close", vec![json!(fd)]).await; + if error != SUCCESS { + eprintln!("ERR_AGENTOS_WASMTIME_FD_ROLLBACK: context={context} fd={fd} errno={error}"); + } +} + +async fn address_output( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + peer: bool, +) -> i32 { + let (Ok(fd), Ok(output), Ok(length_output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let Ok(capacity) = memory::read_u32(caller, length_output) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + { + return ERRNO_FAULT; + } + let method = if peer { + "process.hostnet_peer_address" + } else { + "process.hostnet_local_address" + }; + match call(caller, method, vec![json!(fd)], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let text = encode_address(&value, false); + publish_bytes( + caller, + output, + capacity, + length_output, + text.as_bytes(), + false, + ) + } + Err(error) => errno(&error), + } +} + +fn encode_address(value: &Value, peer_fields: bool) -> String { + let prefix = if peer_fields { "remote" } else { "" }; + let field = |plain: &str, prefixed: &str| { + value + .get(if peer_fields { prefixed } else { plain }) + .or_else(|| value.get(plain)) + }; + if let (Some(address), Some(port)) = ( + field("address", "remoteAddress").and_then(Value::as_str), + field("port", "remotePort").and_then(value_u64), + ) { + let _ = prefix; + return if address.contains(':') { + format!("[{address}]:{port}") + } else { + format!("{address}:{port}") + }; + } + if let Some(hex) = field("abstractPathHex", "remoteAbstractPathHex").and_then(Value::as_str) { + return format!("unix-abstract:{hex}"); + } + if let Some(path) = field("path", "remotePath").and_then(Value::as_str) { + return format!("unix:{path}"); + } + "unix-unnamed".to_owned() +} + +async fn send(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], to: bool) -> i32 { + let (Ok(fd), Ok(pointer), Ok(length), Ok(flags)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_FAULT; + }; + let output_index = if to { 6 } else { 4 }; + let Ok(output) = i32_arg(params, output_index) else { + return ERRNO_FAULT; + }; + let Ok(bytes) = memory::read_bytes(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + let address = if to { + let (Ok(pointer), Ok(length)) = (i32_arg(params, 4), i32_arg(params, 5)) else { + return ERRNO_FAULT; + }; + let Ok(text) = memory::read_string(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + match decode_address(&text) { + Ok(value) => value, + Err(_) => return ERRNO_INVAL, + } + } else { + Value::Null + }; + let mut raw = HashMap::new(); + raw.insert(1, bytes); + match call( + caller, + "process.hostnet_send", + vec![json!(fd), Value::Null, json!(flags), address, Value::Null], + raw, + ) + .await + { + Ok(reply) => { + let written = match reply { + HostCallReply::Json(value) => { + value_u64(&value).or_else(|| value.get("bytes").and_then(value_u64)) + } + _ => None, + }; + let Some(written) = written.and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_IO; + }; + commit(caller, output, &written.to_le_bytes()) + } + Err(error) => errno(&error), + } +} + +async fn receive(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], from: bool) -> i32 { + let (Ok(fd), Ok(output), Ok(capacity), Ok(flags)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_FAULT; + }; + let Ok(length_output) = i32_arg(params, 4) else { + return ERRNO_FAULT; + }; + let address_outputs = if from { + let (Ok(address), Ok(length)) = (i32_arg(params, 5), i32_arg(params, 6)) else { + return ERRNO_FAULT; + }; + let Ok(address_capacity) = memory::read_u32(caller, length) else { + return ERRNO_FAULT; + }; + Some((address, address_capacity, length)) + } else { + None + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + || address_outputs.is_some_and(|(pointer, capacity, length)| { + memory::validate_range(caller, pointer, capacity as usize).is_err() + || memory::validate_range(caller, length, 4).is_err() + }) + { + return ERRNO_FAULT; + } + match call( + caller, + "process.hostnet_recv", + vec![json!(fd), json!(capacity), json!(flags), Value::Null], + HashMap::new(), + ) + .await + { + Ok(reply) => { + if matches!(&reply, HostCallReply::Json(Value::Null)) { + return commit(caller, length_output, &0u32.to_le_bytes()); + } + let (bytes, address) = match reply { + HostCallReply::Json(value) + if value.get("kind").and_then(Value::as_str) == Some("wouldBlock") => + { + return ERRNO_AGAIN; + } + HostCallReply::Json(value) + if value.get("type").and_then(Value::as_str) == Some("message") => + { + let bytes = value + .get("data") + .cloned() + .map(HostCallReply::Json) + .and_then(|value| reply_bytes(value).ok()) + .ok_or(ERRNO_IO); + let address = encode_address(&value, true); + match bytes { + Ok(bytes) => (bytes, Some(address)), + Err(error) => return error, + } + } + reply => match reply_bytes(reply) { + Ok(bytes) => (bytes, None), + Err(error) => return error, + }, + }; + let written = bytes.len().min(capacity as usize); + if commit(caller, output, &bytes[..written]) != SUCCESS { + return ERRNO_FAULT; + } + let reported = if flags & MSG_TRUNC != 0 { + bytes.len() + } else { + written + }; + let Ok(reported) = u32::try_from(reported) else { + return ERRNO_2BIG; + }; + if commit(caller, length_output, &reported.to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + if let Some((address_output, address_capacity, address_length_output)) = address_outputs + { + let Some(address) = address else { + return ERRNO_IO; + }; + return publish_bytes( + caller, + address_output, + address_capacity, + address_length_output, + address.as_bytes(), + false, + ); + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +fn timeval_ms(bytes: &[u8]) -> Result, ()> { + if bytes.len() != 16 { + return Err(()); + } + let seconds = i64::from_le_bytes(bytes[0..8].try_into().map_err(|_| ())?); + let micros = i64::from_le_bytes(bytes[8..16].try_into().map_err(|_| ())?); + if seconds < 0 || !(0..1_000_000).contains(µs) { + return Err(()); + } + if seconds == 0 && micros == 0 { + return Ok(None); + } + Ok(Some( + (seconds as u64) + .saturating_mul(1000) + .saturating_add((micros as u64).div_ceil(1000)), + )) +} + +fn option_kind(level: u32, name: u32, length: u32) -> Option<&'static str> { + let socket_level = matches!(level, 1 | 0x7fff_ffff); + if socket_level && matches!(name, 20 | 66) && length == 16 { + Some("receive-timeout") + } else if (socket_level && name == 9) + || (level == 6 && name == 1) + || (level == 0 && name == 1) + || (level == 41 && name == 67) + { + Some("ignore") + } else { + None + } +} + +async fn set_option(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(level), Ok(name), Ok(pointer), Ok(length)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_INVAL; + }; + let Some(kind) = option_kind(level, name, length) else { + return ERRNO_INVAL; + }; + if kind == "ignore" { + return SUCCESS; + } + let Ok(bytes) = memory::read_bytes(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + let Ok(duration) = timeval_ms(&bytes) else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.hostnet_set_option", + vec![json!(fd), json!(kind), json!({"durationMs": duration})], + ) + .await +} + +async fn get_option(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(level), Ok(name), Ok(output), Ok(length_output)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_FAULT; + }; + let Ok(capacity) = memory::read_u32(caller, length_output) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + { + return ERRNO_FAULT; + } + if !matches!(level, 1 | 0x7fff_ffff) || name != 4 || capacity < 4 { + return ERRNO_INVAL; + } + match call( + caller, + "process.hostnet_get_option", + vec![json!(fd), json!("error")], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(value) = value.as_i64().and_then(|value| i32::try_from(value).ok()) else { + return ERRNO_IO; + }; + if commit(caller, output, &value.to_le_bytes()) != SUCCESS { + ERRNO_FAULT + } else { + commit(caller, length_output, &4u32.to_le_bytes()) + } + } + Err(error) => errno(&error), + } +} + +async fn close(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + simple_call(caller, "process.fd_close", vec![json!(fd)]).await +} + +async fn tls_connect(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(pointer), Ok(length)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let Ok(hostname) = memory::read_string(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.hostnet_tls_connect", + vec![json!(fd), json!(hostname), json!([]), Value::Null], + ) + .await +} + +async fn poll(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(pointer), Ok(count), Ok(timeout), Ok(ready_output)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_FAULT; + }; + let count = count as usize; + let limit = super::check_fixed_request_limit( + caller, + "wasm.abi.maxPollFds", + count, + MAX_POLL_FDS, + ERRNO_INVAL, + ) + .await; + if limit != SUCCESS { + return limit; + } + if memory::validate_range(caller, pointer, count.saturating_mul(8)).is_err() + || memory::validate_range(caller, ready_output, 4).is_err() + { + return ERRNO_FAULT; + } + let mut entries = Vec::with_capacity(count); + for index in 0..count { + let base = pointer + (index * 8) as u32; + let Ok(fd) = memory::read_u32(caller, base) else { + return ERRNO_FAULT; + }; + let Ok(events) = memory::read_bytes(caller, base + 4, 2) else { + return ERRNO_FAULT; + }; + entries.push(json!({"fd": fd, "events": u16::from_le_bytes([events[0], events[1]])})); + } + let requested_timeout = timeout as i32; + let blocking_limit_ms = caller.data().max_blocking_read_ms; + let safeguard_applies = requested_timeout < 0 + || u64::try_from(requested_timeout).is_ok_and(|timeout| timeout > blocking_limit_ms); + let first_wait_ms = if safeguard_applies { + blocking_limit_ms.saturating_mul(4) / 5 + } else { + u64::try_from(requested_timeout).unwrap_or_default() + }; + let mut reply = call( + caller, + "process.posix_poll", + vec![ + Value::Array(entries.clone()), + json!(first_wait_ms), + Value::Null, + ], + HashMap::new(), + ) + .await; + if safeguard_applies + && reply + .as_ref() + .ok() + .and_then(|reply| json_reply(reply.clone()).ok()) + .and_then(|value| value.get("readyCount").and_then(value_u64)) + .unwrap_or_default() + == 0 + { + let warning = format!( + "[agentos] blocking poll is nearing limits.resources.maxBlockingReadMs ({blocking_limit_ms} ms)\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { + return ERRNO_IO; + } + reply = call( + caller, + "process.posix_poll", + vec![ + Value::Array(entries), + json!(blocking_limit_ms.saturating_sub(first_wait_ms)), + Value::Null, + ], + HashMap::new(), + ) + .await; + if reply + .as_ref() + .ok() + .and_then(|reply| json_reply(reply.clone()).ok()) + .and_then(|value| value.get("readyCount").and_then(value_u64)) + .unwrap_or_default() + == 0 + { + let warning = format!( + "[agentos] blocking poll exceeded limits.resources.maxBlockingReadMs ({blocking_limit_ms} ms); raise limits.resources.maxBlockingReadMs if needed\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { + return ERRNO_IO; + } + return if commit(caller, ready_output, &0u32.to_le_bytes()) == SUCCESS { + ERRNO_TIMEDOUT + } else { + ERRNO_FAULT + }; + } + } + match reply { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(fds) = value.get("fds").and_then(Value::as_array) else { + return ERRNO_IO; + }; + for entry in fds { + let (Some(fd), Some(revents)) = ( + entry.get("fd").and_then(value_u64), + entry.get("revents").and_then(value_u64), + ) else { + return ERRNO_IO; + }; + if let Some(index) = (0..count).find(|index| { + memory::read_u32(caller, pointer + (*index * 8) as u32).ok() == Some(fd as u32) + }) { + if commit( + caller, + pointer + (index * 8 + 6) as u32, + &(revents as u16).to_le_bytes(), + ) != SUCCESS + { + return ERRNO_FAULT; + } + } + } + let ready = value + .get("readyCount") + .and_then(value_u64) + .unwrap_or_else(|| { + fds.iter() + .filter(|entry| entry.get("revents").and_then(value_u64).unwrap_or(0) != 0) + .count() as u64 + }); + let Ok(ready) = u32::try_from(ready) else { + return ERRNO_2BIG; + }; + commit(caller, ready_output, &ready.to_le_bytes()) + } + Err(error) => errno(&error), + } +} + +fn publish_bytes( + caller: &mut Caller<'_, WasmtimeStoreState>, + output: u32, + capacity: u32, + length_output: u32, + bytes: &[u8], + require_capacity: bool, +) -> i32 { + let written = bytes.len().min(capacity as usize); + if require_capacity && written != bytes.len() { + return ERRNO_NOBUFS; + } + if memory::validate_range(caller, output, written).is_err() + || memory::validate_range(caller, length_output, 4).is_err() + || memory::write_bytes(caller, output, &bytes[..written]).is_err() + || memory::write_u32(caller, length_output, written as u32).is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/preview1.rs b/crates/execution/src/wasm/wasmtime/linker/preview1.rs new file mode 100644 index 0000000000..3f6b92fdc6 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/preview1.rs @@ -0,0 +1,1356 @@ +//! AgentOS-owned Preview1 codecs. +//! +//! These codecs deliberately use kernel descriptor numbers directly. Unlike +//! the V8 compatibility runner there is no ambient Node-WASI descriptor table +//! to project or synchronize: the sidecar kernel remains the sole source of +//! truth for fd identity, offsets, flags, rights, and lifecycle. + +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::backend::{HostCallReply, HostServiceError}; +use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; +use base64::Engine as _; +use serde_json::{json, Value}; +use std::collections::HashMap; +use wasmtime::{Caller, Val}; + +pub(super) const SUCCESS: i32 = 0; +pub(super) const ERRNO_2BIG: i32 = 1; +const ERRNO_ACCES: i32 = 2; +const ERRNO_ADDRINUSE: i32 = 3; +const ERRNO_ADDRNOTAVAIL: i32 = 4; +const ERRNO_AFNOSUPPORT: i32 = 5; +const ERRNO_AGAIN: i32 = 6; +const ERRNO_ALREADY: i32 = 7; +const ERRNO_BADF: i32 = 8; +const ERRNO_BUSY: i32 = 10; +const ERRNO_CHILD: i32 = 12; +const ERRNO_CONNREFUSED: i32 = 14; +const ERRNO_CONNRESET: i32 = 15; +const ERRNO_DEADLK: i32 = 16; +const ERRNO_DESTADDRREQ: i32 = 17; +const ERRNO_EXIST: i32 = 20; +pub(super) const ERRNO_FAULT: i32 = 21; +const ERRNO_FBIG: i32 = 22; +const ERRNO_HOSTUNREACH: i32 = 23; +const ERRNO_ILSEQ: i32 = 25; +const ERRNO_INPROGRESS: i32 = 26; +const ERRNO_INTR: i32 = 27; +pub(super) const ERRNO_INVAL: i32 = 28; +pub(super) const ERRNO_IO: i32 = 29; +const ERRNO_ISCONN: i32 = 30; +const ERRNO_ISDIR: i32 = 31; +const ERRNO_LOOP: i32 = 32; +const ERRNO_MFILE: i32 = 33; +const ERRNO_MSGSIZE: i32 = 35; +pub(super) const ERRNO_NAMETOOLONG: i32 = 37; +const ERRNO_NETUNREACH: i32 = 40; +const ERRNO_NFILE: i32 = 41; +const ERRNO_NOBUFS: i32 = 42; +const ERRNO_NOENT: i32 = 44; +const ERRNO_NOEXEC: i32 = 45; +const ERRNO_NOSPC: i32 = 51; +const ERRNO_NOSYS: i32 = 52; +const ERRNO_NOTCONN: i32 = 53; +const ERRNO_NOTDIR: i32 = 54; +const ERRNO_NOTEMPTY: i32 = 55; +const ERRNO_NOTSOCK: i32 = 57; +const ERRNO_NOTSUP: i32 = 58; +const ERRNO_NXIO: i32 = 60; +const ERRNO_OVERFLOW: i32 = 61; +const ERRNO_PERM: i32 = 63; +const ERRNO_PIPE: i32 = 64; +const ERRNO_PROTONOSUPPORT: i32 = 66; +pub(super) const ERRNO_RANGE: i32 = 68; +const ERRNO_ROFS: i32 = 69; +const ERRNO_SPIPE: i32 = 70; +const ERRNO_SRCH: i32 = 71; +const ERRNO_TIMEDOUT: i32 = 73; +const ERRNO_XDEV: i32 = 75; +pub(super) const ERRNO_NODATA: i32 = 78; + +const MAX_IOVECS: usize = 1024; +const MAX_POLL_SUBSCRIPTIONS: usize = 1024; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let status = match abi.id { + WasiSnapshotPreview1ArgsGet + | WasiSnapshotPreview1ArgsSizesGet + | WasiSnapshotPreview1EnvironGet + | WasiSnapshotPreview1EnvironSizesGet + | WasiSnapshotPreview1ProcExit + | WasiSnapshotPreview1SchedYield => return Ok(false), + WasiSnapshotPreview1ClockTimeGet => clock_value(caller, params, false).await, + WasiSnapshotPreview1ClockResGet => clock_value(caller, params, true).await, + WasiSnapshotPreview1RandomGet => random_get(caller, params).await, + WasiSnapshotPreview1FdAllocate => fd_allocate(caller, params).await, + WasiSnapshotPreview1FdClose => { + simple_call(caller, "process.fd_close", vec![u32v(params, 0)?]).await + } + WasiSnapshotPreview1FdDatasync => { + simple_call(caller, "process.fd_datasync", vec![u32v(params, 0)?]).await + } + WasiSnapshotPreview1FdSync => { + simple_call(caller, "process.fd_sync", vec![u32v(params, 0)?]).await + } + WasiSnapshotPreview1FdFdstatGet => fd_fdstat_get(caller, params).await, + WasiSnapshotPreview1FdFdstatSetFlags => fd_fdstat_set_flags(caller, params).await, + WasiSnapshotPreview1FdFilestatGet => fd_filestat_get(caller, params).await, + WasiSnapshotPreview1FdFilestatSetSize => { + simple_call( + caller, + "process.fd_truncate", + vec![u32v(params, 0)?, json!(i64_arg(params, 1)?.to_string())], + ) + .await + } + WasiSnapshotPreview1FdFilestatSetTimes => fd_filestat_set_times(caller, params).await, + WasiSnapshotPreview1FdPread => fd_read(caller, params, true).await, + WasiSnapshotPreview1FdRead => fd_read(caller, params, false).await, + WasiSnapshotPreview1FdPwrite => fd_write(caller, params, true).await, + WasiSnapshotPreview1FdWrite => fd_write(caller, params, false).await, + WasiSnapshotPreview1FdPrestatGet => fd_prestat_get(caller, params).await, + WasiSnapshotPreview1FdPrestatDirName => fd_prestat_dir_name(caller, params).await, + WasiSnapshotPreview1FdReaddir => fd_readdir(caller, params).await, + WasiSnapshotPreview1FdRenumber => fd_renumber(caller, params).await, + WasiSnapshotPreview1FdSeek => fd_seek(caller, params, false).await, + WasiSnapshotPreview1FdTell => fd_seek(caller, params, true).await, + WasiSnapshotPreview1PathCreateDirectory => path_create_directory(caller, params).await, + WasiSnapshotPreview1PathFilestatGet => path_filestat_get(caller, params).await, + WasiSnapshotPreview1PathFilestatSetTimes => path_filestat_set_times(caller, params).await, + WasiSnapshotPreview1PathLink => path_link(caller, params).await, + WasiSnapshotPreview1PathOpen => path_open(caller, params).await, + WasiSnapshotPreview1PathReadlink => path_readlink(caller, params).await, + WasiSnapshotPreview1PathRemoveDirectory => { + path_one(caller, params, "process.path_remove_dir_at").await + } + WasiSnapshotPreview1PathRename => path_rename(caller, params).await, + WasiSnapshotPreview1PathSymlink => path_symlink(caller, params).await, + WasiSnapshotPreview1PathUnlinkFile => { + path_one(caller, params, "process.path_unlink_at").await + } + WasiSnapshotPreview1PollOneoff => poll_oneoff(caller, params).await, + WasiSnapshotPreview1SockShutdown => sock_shutdown(caller, params).await, + _ => return Ok(false), + }; + set_i32_result(results, status)?; + Ok(true) +} + +pub(super) async fn call( + caller: &mut Caller<'_, WasmtimeStoreState>, + method: &str, + args: Vec, + raw: HashMap>, +) -> Result { + // Clone the capability before awaiting. No Caller, Store borrow, Memory, + // slice, or pointer-derived reference crosses this suspension point. + let host = caller.data().host.clone(); + host.submit_adapter_call(method.to_owned(), args, raw).await +} + +pub(super) async fn simple_call( + caller: &mut Caller<'_, WasmtimeStoreState>, + method: &str, + args: Vec, +) -> i32 { + match call(caller, method, args, HashMap::new()).await { + Ok(_) => SUCCESS, + Err(error) => errno(&error), + } +} + +fn u32v(params: &[Val], index: usize) -> wasmtime::Result { + Ok(json!(i32_arg(params, index)?)) +} + +pub(super) fn i64_arg(params: &[Val], index: usize) -> wasmtime::Result { + match params.get(index) { + Some(Val::I64(value)) => Ok(*value as u64), + _ => Err(wasmtime::format_err!( + "invalid i64 ABI argument at index {index}" + )), + } +} + +pub(super) fn errno(error: &HostServiceError) -> i32 { + match error.code.as_str() { + "E2BIG" => ERRNO_2BIG, + "EACCES" => ERRNO_ACCES, + "EADDRINUSE" => ERRNO_ADDRINUSE, + "EADDRNOTAVAIL" => ERRNO_ADDRNOTAVAIL, + "EAFNOSUPPORT" => ERRNO_AFNOSUPPORT, + "EAGAIN" | "EWOULDBLOCK" => ERRNO_AGAIN, + "EALREADY" => ERRNO_ALREADY, + "EBADF" => ERRNO_BADF, + "EBUSY" => ERRNO_BUSY, + "ECHILD" => ERRNO_CHILD, + "ECONNREFUSED" => ERRNO_CONNREFUSED, + "ECONNRESET" => ERRNO_CONNRESET, + "EDEADLK" => ERRNO_DEADLK, + "EDESTADDRREQ" => ERRNO_DESTADDRREQ, + "EEXIST" => ERRNO_EXIST, + "EFAULT" => ERRNO_FAULT, + "EFBIG" => ERRNO_FBIG, + "EHOSTUNREACH" => ERRNO_HOSTUNREACH, + "EILSEQ" => ERRNO_ILSEQ, + "EINPROGRESS" => ERRNO_INPROGRESS, + "EINTR" => ERRNO_INTR, + "EINVAL" => ERRNO_INVAL, + "EIO" => ERRNO_IO, + "EISCONN" => ERRNO_ISCONN, + "EISDIR" => ERRNO_ISDIR, + "ELOOP" => ERRNO_LOOP, + "EMFILE" => ERRNO_MFILE, + "EMSGSIZE" => ERRNO_MSGSIZE, + "ENAMETOOLONG" => ERRNO_NAMETOOLONG, + "ENETUNREACH" => ERRNO_NETUNREACH, + "ENFILE" => ERRNO_NFILE, + "ENOBUFS" => ERRNO_NOBUFS, + "ENODATA" => ERRNO_NODATA, + "ENOENT" => ERRNO_NOENT, + "ENOEXEC" => ERRNO_NOEXEC, + "ENOSPC" => ERRNO_NOSPC, + "ENOSYS" => ERRNO_NOSYS, + "ENOTCONN" => ERRNO_NOTCONN, + "ENOTDIR" => ERRNO_NOTDIR, + "ENOTEMPTY" => ERRNO_NOTEMPTY, + "ENOTSOCK" => ERRNO_NOTSOCK, + "ENOTSUP" | "EOPNOTSUPP" => ERRNO_NOTSUP, + "ENXIO" => ERRNO_NXIO, + "EOVERFLOW" => ERRNO_OVERFLOW, + "EPERM" => ERRNO_PERM, + "EPIPE" => ERRNO_PIPE, + "EPROTONOSUPPORT" => ERRNO_PROTONOSUPPORT, + "ERANGE" => ERRNO_RANGE, + "EROFS" => ERRNO_ROFS, + "ESPIPE" => ERRNO_SPIPE, + "ESRCH" => ERRNO_SRCH, + "ETIMEDOUT" => ERRNO_TIMEDOUT, + "EXDEV" => ERRNO_XDEV, + _ => ERRNO_IO, + } +} + +pub(super) fn json_reply(reply: HostCallReply) -> Result { + match reply { + HostCallReply::Json(value) => Ok(value), + _ => Err(ERRNO_IO), + } +} + +pub(super) fn reply_bytes(reply: HostCallReply) -> Result, i32> { + match reply { + HostCallReply::Raw(bytes) => Ok(bytes), + HostCallReply::Json(Value::String(value)) => Ok(value.into_bytes()), + HostCallReply::Json(value) => { + let encoded = value + .get("base64") + .and_then(Value::as_str) + .ok_or(ERRNO_IO)?; + base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|_| ERRNO_IO) + } + HostCallReply::Empty => Err(ERRNO_IO), + } +} + +pub(super) fn value_u64(value: &Value) -> Option { + value + .as_u64() + .or_else(|| value.as_str().and_then(|value| value.parse().ok())) +} + +async fn clock_value( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + resolution: bool, +) -> i32 { + let output = if resolution { + i32_arg(params, 1) + } else { + i32_arg(params, 2) + }; + let Ok(output) = output else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 8).is_err() { + return ERRNO_FAULT; + } + let Ok(clock) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + let (method, args) = if resolution { + ("process.clock_resolution", vec![json!(clock)]) + } else { + let Ok(precision) = i64_arg(params, 1) else { + return ERRNO_INVAL; + }; + ( + "process.clock_time", + vec![json!(clock), json!(precision.to_string()), Value::Null], + ) + }; + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(value) = value_u64(&value) else { + return ERRNO_OVERFLOW; + }; + if memory::validate_range(caller, output, 8).is_err() { + return ERRNO_FAULT; + } + memory::write_u64(caller, output, value).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn random_get(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(pointer) = i32_arg(params, 0) else { + return ERRNO_FAULT; + }; + let Ok(length) = i32_arg(params, 1).map(|value| value as usize) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, pointer, length).is_err() { + return ERRNO_FAULT; + } + let mut output = Vec::with_capacity(length); + while output.len() < length { + let requested = (length - output.len()).min(64 * 1024); + match call( + caller, + "process.random_get", + vec![json!(requested)], + HashMap::new(), + ) + .await + { + Ok(reply) => match reply_bytes(reply) { + Ok(bytes) if bytes.len() == requested => output.extend(bytes), + _ => return ERRNO_IO, + }, + Err(error) => return errno(&error), + } + } + if memory::validate_range(caller, pointer, length).is_err() { + return ERRNO_FAULT; + } + memory::write_bytes(caller, pointer, &output).map_or(ERRNO_FAULT, |_| SUCCESS) +} + +async fn fd_allocate(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(offset), Ok(length)) = + (i32_arg(params, 0), i64_arg(params, 1), i64_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "fs.fallocateSync", + vec![json!(fd), json!(offset), json!(length)], + ) + .await +} + +async fn fd_fdstat_get(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 24).is_err() { + return ERRNO_FAULT; + } + match call(caller, "process.fd_stat", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => { + let Ok(stat) = json_reply(reply) else { + return ERRNO_IO; + }; + let filetype = stat.get("filetype").and_then(value_u64).unwrap_or(0) as u8; + let kernel_flags = stat.get("flags").and_then(value_u64).unwrap_or(0) as u32; + let flags = (if kernel_flags & 0x400 != 0 { 1 } else { 0 }) + | (if kernel_flags & 0x800 != 0 { 4 } else { 0 }); + let rights_base = stat.get("rightsBase").and_then(value_u64).unwrap_or(0); + let rights_inheriting = stat + .get("rightsInheriting") + .and_then(value_u64) + .unwrap_or(0); + let mut bytes = [0u8; 24]; + bytes[0] = filetype; + bytes[2..4].copy_from_slice(&(flags as u16).to_le_bytes()); + bytes[8..16].copy_from_slice(&rights_base.to_le_bytes()); + bytes[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); + commit(caller, output, &bytes) + } + Err(error) => errno(&error), + } +} + +async fn fd_fdstat_set_flags(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(flags)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let kernel = + (if flags & 1 != 0 { 0x400 } else { 0 }) | (if flags & 4 != 0 { 0x800 } else { 0 }); + simple_call( + caller, + "process.fd_set_flags", + vec![json!(fd), json!(kernel)], + ) + .await +} + +async fn fd_filestat_get(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + filestat_call(caller, "process.fd_filestat", vec![json!(fd)], output).await +} + +async fn fd_filestat_set_times(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(atime), Ok(mtime), Ok(flags)) = ( + i32_arg(params, 0), + i64_arg(params, 1), + i64_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.fd_utimes", + vec![ + json!(fd), + json!(atime.to_string()), + json!(mtime.to_string()), + json!(flags), + ], + ) + .await +} + +#[derive(Clone, Copy)] +struct Iovec { + pointer: u32, + length: usize, +} + +async fn read_iovecs( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + count: u32, + writable: bool, +) -> Result, i32> { + let count = usize::try_from(count).map_err(|_| ERRNO_2BIG)?; + let limit = super::check_fixed_request_limit( + caller, + "wasm.abi.maxIovecs", + count, + MAX_IOVECS, + ERRNO_INVAL, + ) + .await; + if limit != SUCCESS { + return Err(limit); + } + let descriptor_bytes = count.checked_mul(8).ok_or(ERRNO_2BIG)?; + memory::validate_range(caller, pointer, descriptor_bytes).map_err(|_| ERRNO_FAULT)?; + let mut iovecs = Vec::with_capacity(count); + let mut total = 0usize; + for index in 0..count { + let slot = pointer + .checked_add(u32::try_from(index * 8).map_err(|_| ERRNO_FAULT)?) + .ok_or(ERRNO_FAULT)?; + let data = memory::read_u32(caller, slot).map_err(|_| ERRNO_FAULT)?; + let length = usize::try_from(memory::read_u32(caller, slot + 4).map_err(|_| ERRNO_FAULT)?) + .map_err(|_| ERRNO_2BIG)?; + memory::validate_range(caller, data, length).map_err(|_| ERRNO_FAULT)?; + total = total.checked_add(length).ok_or(ERRNO_2BIG)?; + iovecs.push(Iovec { + pointer: data, + length, + }); + } + let _ = writable; + Ok(iovecs) +} + +fn iovec_total(iovecs: &[Iovec]) -> Result { + iovecs.iter().try_fold(0usize, |total, iov| { + total.checked_add(iov.length).ok_or(ERRNO_2BIG) + }) +} + +fn collect_iovecs( + caller: &mut Caller<'_, WasmtimeStoreState>, + iovecs: &[Iovec], +) -> Result, i32> { + let mut bytes = Vec::with_capacity(iovec_total(iovecs)?); + for iov in iovecs { + bytes.extend(memory::read_bytes(caller, iov.pointer, iov.length).map_err(|_| ERRNO_FAULT)?); + } + Ok(bytes) +} + +fn scatter_iovecs( + caller: &mut Caller<'_, WasmtimeStoreState>, + iovecs: &[Iovec], + bytes: &[u8], +) -> Result { + let mut offset = 0usize; + for iov in iovecs { + if offset == bytes.len() { + break; + } + let length = iov.length.min(bytes.len() - offset); + memory::write_bytes(caller, iov.pointer, &bytes[offset..offset + length]) + .map_err(|_| ERRNO_FAULT)?; + offset += length; + } + Ok(offset) +} + +async fn fd_read( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + positioned: bool, +) -> i32 { + let (Ok(fd), Ok(iovs_ptr), Ok(iovs_len)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let result_index = if positioned { 4 } else { 3 }; + let Ok(result_ptr) = i32_arg(params, result_index) else { + return ERRNO_FAULT; + }; + let iovecs = match read_iovecs(caller, iovs_ptr, iovs_len, true).await { + Ok(value) => value, + Err(error) => return error, + }; + if memory::validate_range(caller, result_ptr, 4).is_err() { + return ERRNO_FAULT; + } + let Ok(total) = iovec_total(&iovecs) else { + return ERRNO_2BIG; + }; + let mut args = vec![json!(fd), json!(total)]; + let method = if positioned { + let Ok(offset) = i64_arg(params, 3) else { + return ERRNO_INVAL; + }; + args.push(json!(offset.to_string())); + "process.fd_pread" + } else { + args.push(Value::Null); + "process.fd_read" + }; + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(bytes) = reply_bytes(reply) else { + return ERRNO_IO; + }; + if bytes.len() > total { + return ERRNO_IO; + } + if read_iovecs(caller, iovs_ptr, iovs_len, true).await.is_err() + || memory::validate_range(caller, result_ptr, 4).is_err() + { + return ERRNO_FAULT; + } + let Ok(written) = scatter_iovecs(caller, &iovecs, &bytes) else { + return ERRNO_FAULT; + }; + memory::write_u32(caller, result_ptr, written as u32).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn fd_write( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + positioned: bool, +) -> i32 { + let (Ok(fd), Ok(iovs_ptr), Ok(iovs_len)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let result_index = if positioned { 4 } else { 3 }; + let Ok(result_ptr) = i32_arg(params, result_index) else { + return ERRNO_FAULT; + }; + let iovecs = match read_iovecs(caller, iovs_ptr, iovs_len, false).await { + Ok(value) => value, + Err(error) => return error, + }; + if memory::validate_range(caller, result_ptr, 4).is_err() { + return ERRNO_FAULT; + } + let bytes = match collect_iovecs(caller, &iovecs) { + Ok(value) => value, + Err(error) => return error, + }; + let mut args = vec![json!(fd), Value::Null]; + let method = if positioned { + let Ok(offset) = i64_arg(params, 3) else { + return ERRNO_INVAL; + }; + args.push(json!(offset.to_string())); + "process.fd_pwrite" + } else { + "process.fd_write" + }; + let mut raw = HashMap::new(); + raw.insert(1, bytes.clone()); + // Stdio aliases must use the shared ordered-output operation so host + // capture, PTY cooking, and 1>&2/2>&1 routing match the V8 adapter. The + // typed operation rejects ordinary descriptors with EINVAL without a + // write side effect; those continue through the regular fd path. + let reply = if positioned { + call(caller, method, args, raw).await + } else { + match call(caller, "__kernel_stdio_write", args.clone(), raw.clone()).await { + Err(error) if error.code == "EINVAL" => call(caller, method, args, raw).await, + result => result, + } + }; + match reply { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(written) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + if written as usize > bytes.len() { + return ERRNO_IO; + } + if memory::validate_range(caller, result_ptr, 4).is_err() { + return ERRNO_FAULT; + } + memory::write_u32(caller, result_ptr, written).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn fd_prestat_get(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 8).is_err() { + return ERRNO_FAULT; + } + match preopen(caller, fd).await { + Ok(path) => { + let mut bytes = [0u8; 8]; + bytes[4..8].copy_from_slice(&(path.len() as u32).to_le_bytes()); + commit(caller, output, &bytes) + } + Err(error) => error, + } +} + +async fn fd_prestat_dir_name(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output), Ok(length)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, length as usize).is_err() { + return ERRNO_FAULT; + } + match preopen(caller, fd).await { + Ok(path) if path.len() <= length as usize => commit(caller, output, path.as_bytes()), + Ok(_) => ERRNO_NAMETOOLONG, + Err(error) => error, + } +} + +async fn preopen(caller: &mut Caller<'_, WasmtimeStoreState>, fd: u32) -> Result { + match call( + caller, + "process.fd_preopen", + vec![json!(fd)], + HashMap::new(), + ) + .await + { + Ok(reply) => json_reply(reply)? + .get("guestPath") + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or(ERRNO_BADF), + Err(error) => Err(errno(&error)), + } +} + +async fn fd_readdir(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output), Ok(length), Ok(cookie), Ok(used_ptr)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i64_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, length as usize).is_err() + || memory::validate_range(caller, used_ptr, 4).is_err() + { + return ERRNO_FAULT; + } + let max_entries = ((length as usize / 24) + 1).clamp(1, 4096); + match call( + caller, + "process.fd_readdir", + vec![json!(fd), json!(cookie.to_string()), json!(max_entries)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(entries) = value.as_array() else { + return ERRNO_IO; + }; + let mut bytes = Vec::with_capacity(length as usize); + for entry in entries { + let name = entry + .get("name") + .and_then(Value::as_str) + .unwrap_or("") + .as_bytes(); + let mut record = vec![0u8; 24 + name.len()]; + record[0..8].copy_from_slice( + &entry + .get("next") + .and_then(value_u64) + .unwrap_or(0) + .to_le_bytes(), + ); + record[8..16].copy_from_slice( + &entry + .get("ino") + .and_then(value_u64) + .unwrap_or(0) + .to_le_bytes(), + ); + record[16..20].copy_from_slice(&(name.len() as u32).to_le_bytes()); + record[20] = entry.get("filetype").and_then(value_u64).unwrap_or(0) as u8; + record[24..].copy_from_slice(name); + let remaining = length as usize - bytes.len(); + bytes.extend_from_slice(&record[..record.len().min(remaining)]); + if bytes.len() == length as usize { + break; + } + } + if memory::validate_range(caller, output, length as usize).is_err() + || memory::validate_range(caller, used_ptr, 4).is_err() + { + return ERRNO_FAULT; + } + if memory::write_bytes(caller, output, &bytes).is_err() { + return ERRNO_FAULT; + } + memory::write_u32(caller, used_ptr, bytes.len() as u32).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn fd_renumber(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(from), Ok(to)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_BADF; + }; + if from == to { + return SUCCESS; + } + match call( + caller, + "process.fd_move", + vec![json!(from), json!(to)], + HashMap::new(), + ) + .await + { + Ok(reply) => match json_reply(reply).ok().as_ref().and_then(value_u64) { + Some(value) if value == u64::from(to) => SUCCESS, + Some(_) => ERRNO_IO, + None => ERRNO_IO, + }, + Err(error) => errno(&error), + } +} + +async fn fd_seek(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], tell: bool) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + let (offset, whence, output) = if tell { + let Ok(output) = i32_arg(params, 1) else { + return ERRNO_FAULT; + }; + (0i64, 1u32, output) + } else { + let (Ok(raw_offset), Ok(whence), Ok(output)) = + (i64_arg(params, 1), i32_arg(params, 2), i32_arg(params, 3)) + else { + return ERRNO_INVAL; + }; + (raw_offset as i64, whence, output) + }; + if memory::validate_range(caller, output, 8).is_err() { + return ERRNO_FAULT; + } + match call( + caller, + "process.fd_seek", + vec![json!(fd), json!(offset.to_string()), json!(whence)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(next) = value_u64(&value) else { + return ERRNO_OVERFLOW; + }; + if memory::validate_range(caller, output, 8).is_err() { + return ERRNO_FAULT; + } + memory::write_u64(caller, output, next).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +fn guest_path( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + pointer_index: usize, + length_index: usize, +) -> Result { + let pointer = i32_arg(params, pointer_index).map_err(|_| ERRNO_FAULT)?; + let length = i32_arg(params, length_index).map_err(|_| ERRNO_FAULT)? as usize; + memory::read_string(caller, pointer, length).map_err(|_| ERRNO_FAULT) +} + +async fn path_create_directory(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + path_one(caller, params, "process.path_mkdir_at").await +} + +async fn path_one( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + let Ok(path) = guest_path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + simple_call(caller, method, vec![json!(fd), json!(path)]).await +} + +async fn path_filestat_get(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(flags), Ok(output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 4)) + else { + return ERRNO_FAULT; + }; + let Ok(path) = guest_path(caller, params, 2, 3) else { + return ERRNO_FAULT; + }; + filestat_call( + caller, + "process.path_stat_at", + vec![json!(fd), json!(path), json!(flags & 1 != 0)], + output, + ) + .await +} + +async fn filestat_call( + caller: &mut Caller<'_, WasmtimeStoreState>, + method: &str, + args: Vec, + output: u32, +) -> i32 { + if memory::validate_range(caller, output, 64).is_err() { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(stat) = json_reply(reply) else { + return ERRNO_IO; + }; + let mut bytes = [0u8; 64]; + bytes[8..16].copy_from_slice( + &stat + .get("ino") + .and_then(value_u64) + .unwrap_or(0) + .to_le_bytes(), + ); + bytes[16] = stat.get("filetype").and_then(value_u64).unwrap_or(0) as u8; + bytes[24..32].copy_from_slice( + &stat + .get("nlink") + .and_then(value_u64) + .unwrap_or(1) + .to_le_bytes(), + ); + bytes[32..40].copy_from_slice( + &stat + .get("size") + .and_then(value_u64) + .unwrap_or(0) + .to_le_bytes(), + ); + for (offset, name) in [(40, "atimeMs"), (48, "mtimeMs"), (56, "ctimeMs")] { + let ns = stat + .get(name) + .and_then(Value::as_f64) + .map(|value| (value * 1_000_000.0) as u64) + .or_else(|| { + stat.get(name) + .and_then(value_u64) + .map(|value| value.saturating_mul(1_000_000)) + }) + .unwrap_or(0); + bytes[offset..offset + 8].copy_from_slice(&ns.to_le_bytes()); + } + commit(caller, output, &bytes) + } + Err(error) => errno(&error), + } +} + +async fn path_filestat_set_times( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], +) -> i32 { + let (Ok(fd), Ok(lookup), Ok(atime), Ok(mtime), Ok(flags)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i64_arg(params, 4), + i64_arg(params, 5), + i32_arg(params, 6), + ) else { + return ERRNO_INVAL; + }; + let Ok(path) = guest_path(caller, params, 2, 3) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_utimes_at", + vec![ + json!(fd), + json!(path), + json!(lookup & 1 != 0), + json!(atime.to_string()), + json!(mtime.to_string()), + json!(flags), + ], + ) + .await +} + +async fn path_link(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(old_fd), Ok(flags), Ok(new_fd)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 4)) + else { + return ERRNO_BADF; + }; + let Ok(old_path) = guest_path(caller, params, 2, 3) else { + return ERRNO_FAULT; + }; + let Ok(new_path) = guest_path(caller, params, 5, 6) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_link_at", + vec![ + json!(old_fd), + json!(old_path), + json!(new_fd), + json!(new_path), + json!(flags & 1 != 0), + ], + ) + .await +} + +async fn path_open(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let ( + Ok(fd), + Ok(lookup), + Ok(oflags), + Ok(rights_base), + Ok(rights_inheriting), + Ok(fdflags), + Ok(output), + ) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 4), + i64_arg(params, 5), + i64_arg(params, 6), + i32_arg(params, 7), + i32_arg(params, 8), + ) + else { + return ERRNO_INVAL; + }; + let Ok(path) = guest_path(caller, params, 2, 3) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + let create_mode = caller.data_mut().pending_open_mode.take().unwrap_or(0o666); + let direct = std::mem::take(&mut caller.data_mut().pending_open_direct); + const RIGHT_READ: u64 = 1 << 1; + const RIGHT_WRITE: u64 = 1 << 6; + let mut flags = if rights_base & RIGHT_WRITE != 0 { + if rights_base & RIGHT_READ != 0 { + 2 + } else { + 1 + } + } else { + 0 + }; + if oflags & 1 != 0 { + flags |= 0x40; + } + if oflags & 2 != 0 { + flags |= 0x10000; + } + if oflags & 4 != 0 { + flags |= 0x80; + } + if oflags & 8 != 0 { + flags |= 0x200; + } + if fdflags & 1 != 0 { + flags |= 0x400; + } + if fdflags & 4 != 0 { + flags |= 0x800; + } + if direct { + flags |= 0x4000; + } + if lookup & 1 == 0 { + flags |= 0x20000; + } + match call( + caller, + "process.path_open_at", + vec![ + json!(fd), + json!(path), + json!(flags), + json!(create_mode), + json!(rights_base.to_string()), + json!(rights_inheriting.to_string()), + ], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(opened) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_IO; + }; + if memory::validate_range(caller, output, 4).is_err() { + if let Err(error) = call( + caller, + "process.fd_close", + vec![json!(opened)], + HashMap::new(), + ) + .await + { + eprintln!( + "ERR_AGENTOS_WASMTIME_FD_ROLLBACK: context=path_open output commit fd={opened} code={}", + error.code + ); + } + return ERRNO_FAULT; + } + memory::write_u32(caller, output, opened).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn path_readlink(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output), Ok(length), Ok(used)) = ( + i32_arg(params, 0), + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + ) else { + return ERRNO_FAULT; + }; + let Ok(path) = guest_path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, length as usize).is_err() + || memory::validate_range(caller, used, 4).is_err() + { + return ERRNO_FAULT; + } + match call( + caller, + "process.path_readlink_at", + vec![json!(fd), json!(path)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(target) = value.as_str() else { + return ERRNO_IO; + }; + let bytes = target.as_bytes(); + let written = bytes.len().min(length as usize); + if memory::write_bytes(caller, output, &bytes[..written]).is_err() + || memory::write_u32(caller, used, written as u32).is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } + } + Err(error) => errno(&error), + } +} + +async fn path_rename(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(old_fd), Ok(new_fd)) = (i32_arg(params, 0), i32_arg(params, 3)) else { + return ERRNO_BADF; + }; + let Ok(old_path) = guest_path(caller, params, 1, 2) else { + return ERRNO_FAULT; + }; + let Ok(new_path) = guest_path(caller, params, 4, 5) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_rename_at", + vec![ + json!(old_fd), + json!(old_path), + json!(new_fd), + json!(new_path), + ], + ) + .await +} + +async fn path_symlink(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(old_path) = guest_path(caller, params, 0, 1) else { + return ERRNO_FAULT; + }; + let Ok(fd) = i32_arg(params, 2) else { + return ERRNO_BADF; + }; + let Ok(new_path) = guest_path(caller, params, 3, 4) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "process.path_symlink_at", + vec![json!(old_path), json!(fd), json!(new_path)], + ) + .await +} + +async fn sock_shutdown(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(how)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let mode = match how { + 1 => 0, + 2 => 1, + 3 => 2, + _ => return ERRNO_INVAL, + }; + simple_call( + caller, + "process.fd_socket_shutdown", + vec![json!(fd), json!(mode)], + ) + .await +} + +async fn poll_oneoff(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(input), Ok(output), Ok(count), Ok(event_count)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + ) else { + return ERRNO_FAULT; + }; + let count = count as usize; + let limit = super::check_fixed_request_limit( + caller, + "wasm.abi.maxPollSubscriptions", + count, + MAX_POLL_SUBSCRIPTIONS, + ERRNO_INVAL, + ) + .await; + if limit != SUCCESS { + return limit; + } + if memory::validate_range(caller, input, count.saturating_mul(48)).is_err() + || memory::validate_range(caller, output, count.saturating_mul(32)).is_err() + || memory::validate_range(caller, event_count, 4).is_err() + { + return ERRNO_FAULT; + } + let mut interests = Vec::new(); + let mut subscriptions = Vec::with_capacity(count); + let mut timeout_ms: Option = None; + for index in 0..count { + let base = input + (index * 48) as u32; + let userdata = memory::read_u64(caller, base).unwrap_or(0); + let tag = memory::read_bytes(caller, base + 8, 1) + .ok() + .and_then(|bytes| bytes.first().copied()) + .unwrap_or(255); + if tag == 0 { + let timeout_ns = memory::read_u64(caller, base + 24).unwrap_or(0); + timeout_ms = Some(timeout_ms.map_or(timeout_ns / 1_000_000, |prior| { + prior.min(timeout_ns / 1_000_000) + })); + subscriptions.push((userdata, tag, 0u32)); + } else if tag == 1 || tag == 2 { + let fd = memory::read_u32(caller, base + 16).unwrap_or(u32::MAX); + interests.push(json!({ "fd": fd, "events": if tag == 1 { 1 } else { 4 } })); + subscriptions.push((userdata, tag, fd)); + } else { + subscriptions.push((userdata, tag, 0)); + } + } + let reply = if interests.is_empty() { + if let Some(delay) = timeout_ms { + simple_call(caller, "process.sleep", vec![json!(delay)]).await; + } + json!({"fds": []}) + } else { + match call( + caller, + "__kernel_poll", + vec![ + Value::Array(interests), + timeout_ms.map(Value::from).unwrap_or(Value::Null), + ], + HashMap::new(), + ) + .await + { + Ok(reply) => match json_reply(reply) { + Ok(value) => value, + Err(error) => return error, + }, + Err(error) => return errno(&error), + } + }; + let fds = reply + .get("fds") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + let mut encoded = Vec::new(); + for (userdata, tag, fd) in subscriptions { + let ready = if tag == 0 { + true + } else { + fds.iter().any(|entry| { + entry.get("fd").and_then(value_u64) == Some(u64::from(fd)) + && entry.get("revents").and_then(value_u64).unwrap_or(0) != 0 + }) + }; + if !ready { + continue; + } + let mut event = [0u8; 32]; + event[0..8].copy_from_slice(&userdata.to_le_bytes()); + event[10] = tag; + if tag == 1 { + event[16..24].copy_from_slice(&1u64.to_le_bytes()); + } + if tag == 2 { + event[16..24].copy_from_slice(&65536u64.to_le_bytes()); + } + encoded.extend_from_slice(&event); + } + if memory::validate_range(caller, output, count.saturating_mul(32)).is_err() + || memory::validate_range(caller, event_count, 4).is_err() + { + return ERRNO_FAULT; + } + if memory::write_bytes(caller, output, &encoded).is_err() + || memory::write_u32(caller, event_count, (encoded.len() / 32) as u32).is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } +} + +pub(super) fn commit( + caller: &mut Caller<'_, WasmtimeStoreState>, + output: u32, + bytes: &[u8], +) -> i32 { + if memory::validate_range(caller, output, bytes.len()).is_err() { + return ERRNO_FAULT; + } + memory::write_bytes(caller, output, bytes).map_or(ERRNO_FAULT, |_| SUCCESS) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stable_errno_mapping_matches_preview1() { + assert_eq!(errno(&HostServiceError::new("EBADF", "bad fd")), 8); + assert_eq!(errno(&HostServiceError::new("EWOULDBLOCK", "wait")), 6); + assert_eq!(errno(&HostServiceError::new("unknown", "fault")), 29); + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/process.rs b/crates/execution/src/wasm/wasmtime/linker/process.rs new file mode 100644 index 0000000000..647595c906 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/process.rs @@ -0,0 +1,1722 @@ +//! AgentOS process, descriptor-control, and signal-registration ABI codecs. +//! +//! Process and descriptor state remains sidecar/kernel-owned. The adapter +//! snapshots live fds only while constructing one spawn request and never +//! retains a child table, signal mask, or fd projection in the Store. + +use super::preview1::{ + call, commit, errno, i64_arg, json_reply, reply_bytes, simple_call, value_u64, ERRNO_2BIG, + ERRNO_FAULT, ERRNO_INVAL, ERRNO_IO, SUCCESS, +}; +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::backend::HostCallReply; +use crate::host::ExecutableImageSource; +use crate::wasm::wasmtime::{ + lifecycle, memory, module, + store::{PendingExecReplacement, WasmtimeStoreState}, +}; +use serde_json::{json, Value}; +use std::collections::{BTreeMap, HashMap}; +use wasmtime::{Caller, Extern, Val, ValType}; + +const ERRNO_BADF: i32 = 8; +const ERRNO_NOENT: i32 = 44; +const ERRNO_NOEXEC: i32 = 45; +const ERRNO_NOTSUP: i32 = 58; +const ERRNO_PERM: i32 = 63; +const ERRNO_SRCH: i32 = 71; +const MAX_FDS: usize = 1 << 20; +const MAX_RIGHTS: usize = 253; +const SUPPORTED_SPAWN_FLAGS: u32 = 0xff; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let status = match abi.id { + HostProcessProcSpawn => spawn(caller, params, SpawnVersion::Legacy).await, + HostProcessProcSpawnV2 => spawn(caller, params, SpawnVersion::V2).await, + HostProcessProcSpawnV3 => spawn(caller, params, SpawnVersion::V3).await, + HostProcessProcSpawnV4 => spawn(caller, params, SpawnVersion::V4).await, + HostProcessProcExec => exec(caller, params, false).await, + HostProcessProcFexec => exec(caller, params, true).await, + HostProcessProcWaitpid => wait(caller, params, WaitVersion::Legacy).await, + HostProcessProcWaitpidV2 => wait(caller, params, WaitVersion::V2).await, + HostProcessProcWaitpidV3 => wait(caller, params, WaitVersion::V3).await, + HostProcessProcKill => kill(caller, params).await, + HostProcessProcGetpid => local_pid(caller, params, false), + HostProcessProcGetppid => local_pid(caller, params, true), + HostProcessProcGetrlimit => getrlimit(caller, params).await, + HostProcessProcSetrlimit => setrlimit(caller, params).await, + HostProcessProcUmask => umask(caller, params, true).await, + HostProcessUmask => umask(caller, params, false).await, + HostProcessProcItimerReal => itimer(caller, params).await, + HostProcessProcGetpgid => getpgid(caller, params).await, + HostProcessProcSetpgid => setpgid(caller, params).await, + HostProcessFdPipe => pair(caller, params, PairKind::Pipe).await, + HostProcessFdSocketpair => pair(caller, params, PairKind::Socket).await, + HostProcessPtyOpen => pair(caller, params, PairKind::Pty).await, + HostProcessFdDup => duplicate(caller, params, DuplicateKind::Any).await, + HostProcessFdDupMin => duplicate(caller, params, DuplicateKind::Minimum).await, + HostProcessFdDup2 => duplicate_to(caller, params).await, + HostProcessFdGetfd => descriptor_flags(caller, params, false).await, + HostProcessFdSetfd => descriptor_flags(caller, params, true).await, + HostProcessFdFlock => flock(caller, params).await, + HostProcessFdRecordLock => record_lock(caller, params).await, + HostProcessProcClosefrom => closefrom(caller, params).await, + HostProcessFdSendmsgRights => send_rights(caller, params).await, + HostProcessFdRecvmsgRights => receive_rights(caller, params).await, + HostProcessSleepMs => sleep(caller, params).await, + HostProcessProcSigaction => sigaction(caller, params).await, + HostProcessProcSignalMaskV2 => signal_mask(caller, params).await, + HostProcessProcPpollV1 => ppoll(caller, params).await, + _ => return Ok(false), + }; + if caller.data().exec_replaced { + return Err(wasmtime::format_err!("agentos:exec-replaced")); + } + set_i32_result(results, status)?; + Ok(true) +} + +#[derive(Clone, Copy)] +enum SpawnVersion { + Legacy, + V2, + V3, + V4, +} + +struct SpawnInput { + command: String, + argv: Vec, + env: BTreeMap, + cwd: Option, + actions: Vec, + attr_flags: u32, + exact_path: bool, + search_path: Option, + sched_policy: Option, + sched_priority: Option, + pgroup: Option, + signal_defaults: Vec, + signal_mask: Vec, + output: u32, +} + +async fn spawn( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + version: SpawnVersion, +) -> i32 { + let output_index = match version { + SpawnVersion::Legacy => 9, + SpawnVersion::V2 => 11, + SpawnVersion::V3 => 16, + SpawnVersion::V4 => 20, + }; + let Ok(output) = i32_arg(params, output_index) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + if matches!(version, SpawnVersion::V3 | SpawnVersion::V4) { + let Ok(action_bytes) = arg(params, 7) + .map(usize::try_from) + .and_then(|value| value.map_err(|_| ERRNO_2BIG)) + else { + return ERRNO_2BIG; + }; + let limit = caller.data().max_spawn_file_action_bytes; + if action_bytes > limit { + let message = format!( + "[agentos] posix_spawn file-action payload is {action_bytes} bytes, exceeding limits.process.maxSpawnFileActionBytes ({limit}); raise limits.process.maxSpawnFileActionBytes if needed\n" + ); + if caller + .data() + .host + .publish_stderr(message.into_bytes()) + .await + .is_err() + { + return ERRNO_IO; + } + return ERRNO_2BIG; + } + } + let mut input = match decode_spawn(caller, params, version, output) { + Ok(input) => input, + Err(error) => return error, + }; + if input.command.is_empty() { + return ERRNO_NOENT; + } + let snapshot = match fd_snapshot(caller).await { + Ok(value) => value, + Err(error) => return error, + }; + let live_guest_fds = snapshot + .iter() + .filter_map(|entry| { + entry + .get("fd") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + }) + .collect::>(); + for action in &mut input.actions { + if action.get("command").and_then(value_u64) != Some(6) { + continue; + } + let Some(minimum) = action + .get("guestFd") + .and_then(Value::as_i64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_BADF; + }; + action["closeFromGuestFds"] = Value::Array( + live_guest_fds + .iter() + .copied() + .filter(|fd| *fd >= minimum) + .map(Value::from) + .collect(), + ); + } + let (mappings, host_net) = spawn_fd_state(&snapshot); + let argv0 = input + .argv + .first() + .cloned() + .unwrap_or_else(|| input.command.clone()); + let request = json!({ + "command": input.command, + "args": input.argv.into_iter().skip(1).collect::>(), + "options": { + "argv0": argv0, + "cwd": input.cwd, + "env": input.env, + "internalBootstrapEnv": {}, + "spawnAttrFlags": input.attr_flags, + "spawnExactPath": input.exact_path, + "spawnSearchPath": input.search_path, + "spawnSchedPolicy": input.sched_policy, + "spawnSchedPriority": input.sched_priority, + "spawnPgroup": input.pgroup, + "spawnSignalDefaults": input.signal_defaults, + "spawnSignalMask": input.signal_mask, + "spawnFileActions": input.actions, + "spawnFdMappings": mappings, + "spawnHostNetFds": host_net, + "shell": false, + "stdio": ["inherit", "inherit", "inherit"], + } + }); + match call(caller, "child_process.spawn", vec![request], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(pid) = value + .get("pid") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + commit(caller, input.output, &pid.to_le_bytes()) + } + Err(error) => errno(&error), + } +} + +fn decode_spawn( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + version: SpawnVersion, + output: u32, +) -> Result { + let (command_pointer, command_length, argv_pointer, argv_length, env_pointer, env_length) = + match version { + SpawnVersion::Legacy => { + let pointer = arg(params, 0)?; + let length = arg(params, 1)?; + let bytes = memory::read_bytes(caller, pointer, length as usize) + .map_err(|_| ERRNO_FAULT)?; + let command_length = bytes + .iter() + .position(|byte| *byte == 0) + .ok_or(ERRNO_FAULT)?; + if command_length == 0 { + return Err(ERRNO_FAULT); + } + ( + pointer, + command_length as u32, + pointer, + length, + arg(params, 2)?, + arg(params, 3)?, + ) + } + _ => ( + arg(params, 0)?, + arg(params, 1)?, + arg(params, 2)?, + arg(params, 3)?, + arg(params, 4)?, + arg(params, 5)?, + ), + }; + let command = memory::read_string(caller, command_pointer, command_length as usize) + .map_err(|_| ERRNO_FAULT)?; + let argv = nul_strings( + memory::read_bytes(caller, argv_pointer, argv_length as usize).map_err(|_| ERRNO_FAULT)?, + )?; + let env = serialized_env( + memory::read_bytes(caller, env_pointer, env_length as usize).map_err(|_| ERRNO_FAULT)?, + )?; + let mut actions = Vec::new(); + let mut attr_flags = 0; + let mut exact_path = false; + let mut search_path = None; + let mut sched_policy = None; + let mut sched_priority = None; + let mut pgroup = None; + let mut signal_defaults = Vec::new(); + let mut signal_mask = Vec::new(); + let cwd = match version { + SpawnVersion::Legacy => cwd(caller, params, 7, 8)?, + SpawnVersion::V2 => { + actions.extend(stdio_actions([ + arg(params, 6)?, + arg(params, 7)?, + arg(params, 8)?, + ])); + cwd(caller, params, 9, 10)? + } + SpawnVersion::V3 | SpawnVersion::V4 => { + actions = decode_actions( + caller, + arg(params, 6)?, + arg(params, 7)?, + caller.data().max_spawn_file_action_bytes, + caller.data().max_spawn_file_actions, + )?; + let value = cwd(caller, params, 8, 9)?; + let base = if matches!(version, SpawnVersion::V4) { + 12 + } else { + 10 + }; + attr_flags = arg(params, base)?; + if attr_flags & !SUPPORTED_SPAWN_FLAGS != 0 { + return Err(ERRNO_NOTSUP); + } + signal_defaults = if attr_flags & 4 != 0 { + signal_set(arg(params, base + 1)?, arg(params, base + 2)?) + } else { + Vec::new() + }; + signal_mask = signal_set(arg(params, base + 3)?, arg(params, base + 4)?) + .into_iter() + .filter(|signal| !matches!(signal, 9 | 19)) + .collect(); + pgroup = Some(arg(params, base + 5)? as i32); + if matches!(version, SpawnVersion::V4) { + if arg(params, 10)? != 0 { + search_path = Some( + memory::read_string(caller, arg(params, 10)?, arg(params, 11)? as usize) + .map_err(|_| ERRNO_FAULT)?, + ); + } else { + exact_path = true; + } + sched_policy = Some(arg(params, 18)? as i32); + sched_priority = Some(arg(params, 19)? as i32); + if attr_flags & 128 != 0 && attr_flags & 2 != 0 { + return Err(ERRNO_PERM); + } + if attr_flags & (16 | 32) != 0 && sched_priority != Some(0) { + return Err(ERRNO_INVAL); + } + if attr_flags & 32 != 0 && sched_policy != Some(0) { + return Err(ERRNO_PERM); + } + } + value + } + }; + if matches!(version, SpawnVersion::Legacy) { + actions.extend(stdio_actions([ + arg(params, 4)?, + arg(params, 5)?, + arg(params, 6)?, + ])); + } + Ok(SpawnInput { + command, + argv, + env, + cwd, + actions, + attr_flags, + exact_path, + search_path, + sched_policy, + sched_priority, + pgroup, + signal_defaults, + signal_mask, + output, + }) +} + +fn arg(params: &[Val], index: usize) -> Result { + i32_arg(params, index).map_err(|_| ERRNO_INVAL) +} + +fn cwd( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + pointer_index: usize, + length_index: usize, +) -> Result, i32> { + let pointer = arg(params, pointer_index)?; + let length = arg(params, length_index)? as usize; + if length == 0 { + Ok(None) + } else { + memory::read_string(caller, pointer, length) + .map(Some) + .map_err(|_| ERRNO_FAULT) + } +} + +fn nul_strings(bytes: Vec) -> Result, i32> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let mut values = bytes.split(|byte| *byte == 0).collect::>(); + if values.last().is_some_and(|value| value.is_empty()) { + values.pop(); + } + values + .into_iter() + .map(|value| String::from_utf8(value.to_vec()).map_err(|_| ERRNO_INVAL)) + .collect() +} + +fn serialized_env(bytes: Vec) -> Result, i32> { + let mut env = BTreeMap::new(); + for value in nul_strings(bytes)? { + let Some((key, value)) = value.split_once('=') else { + continue; + }; + if !key.is_empty() { + env.insert(key.to_owned(), value.to_owned()); + } + } + Ok(env) +} + +fn stdio_actions(fds: [u32; 3]) -> Vec { + fds.into_iter() + .enumerate() + .filter_map(|(target, source)| { + if source == target as u32 { + None + } else if source == u32::MAX { + Some(action(1, target as i32, -1, 0, 0, "", Vec::new())) + } else { + Some(action( + 2, + target as i32, + source as i32, + 0, + 0, + "", + Vec::new(), + )) + } + }) + .collect() +} + +fn decode_actions( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + length: u32, + max_bytes: usize, + max_actions: usize, +) -> Result, i32> { + let length = length as usize; + if length > max_bytes { + return Err(ERRNO_2BIG); + } + let bytes = memory::read_bytes(caller, pointer, length).map_err(|_| ERRNO_FAULT)?; + let mut offset = 0usize; + let mut actions = Vec::new(); + while offset < bytes.len() { + if bytes.len() - offset < 24 || actions.len() >= max_actions { + return Err(if actions.len() >= max_actions { + ERRNO_2BIG + } else { + ERRNO_INVAL + }); + } + let command = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let fd = i32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + let source = i32::from_le_bytes(bytes[offset + 8..offset + 12].try_into().unwrap()); + let flags = i32::from_le_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()); + let mode = u32::from_le_bytes(bytes[offset + 16..offset + 20].try_into().unwrap()); + let path_length = + u32::from_le_bytes(bytes[offset + 20..offset + 24].try_into().unwrap()) as usize; + offset += 24; + let end = offset.checked_add(path_length).ok_or(ERRNO_INVAL)?; + let path = String::from_utf8(bytes.get(offset..end).ok_or(ERRNO_INVAL)?.to_vec()) + .map_err(|_| ERRNO_INVAL)?; + offset = end; + if !matches!(command, 1..=6) { + return Err(ERRNO_INVAL); + } + if fd < 0 && matches!(command, 1 | 2 | 3 | 5 | 6) { + return Err(ERRNO_BADF); + } + actions.push(action(command, fd, source, flags, mode, &path, Vec::new())); + } + Ok(actions) +} + +fn action( + command: u32, + fd: i32, + source: i32, + flags: i32, + mode: u32, + path: &str, + close_from: Vec, +) -> Value { + json!({ + "command": command, + "guestFd": fd, + "fd": fd, + "guestSourceFd": source, + "sourceFd": source, + "oflag": flags, + "mode": mode, + "path": path, + "closeFromGuestFds": close_from, + }) +} + +fn signal_set(low: u32, high: u32) -> Vec { + (1..=64) + .filter(|signal| { + let bit = signal - 1; + if bit < 32 { + low & (1 << bit) != 0 + } else { + high & (1 << (bit - 32)) != 0 + } + }) + .collect() +} + +async fn fd_snapshot(caller: &mut Caller<'_, WasmtimeStoreState>) -> Result, i32> { + match call(caller, "process.fd_snapshot", vec![], HashMap::new()).await { + Ok(reply) => json_reply(reply)?.as_array().cloned().ok_or(ERRNO_IO), + Err(error) => Err(errno(&error)), + } +} + +fn spawn_fd_state(snapshot: &[Value]) -> (Vec, Vec) { + let mut mappings = Vec::new(); + let mut host_net = Vec::new(); + for entry in snapshot { + let Some(fd) = entry + .get("fd") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + continue; + }; + mappings.push(json!([fd, fd])); + if entry.get("kind").and_then(Value::as_str) == Some("socket") { + host_net.push(json!({ + "guestFd": fd, + "descriptionId": entry.get("descriptionId").cloned().unwrap_or(Value::Null), + "closeOnExec": entry.get("fdFlags").and_then(value_u64).unwrap_or(0) & 1 != 0, + "metadata": {}, + })); + } + } + (mappings, host_net) +} + +async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd: bool) -> i32 { + let (command, argv_start, env_start, close_start) = if by_fd { + (None, 1, 3, 5) + } else { + let (Ok(pointer), Ok(length)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + let Ok(command) = memory::read_string(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + if command.is_empty() { + return ERRNO_NOENT; + } + (Some(command), 2, 4, 6) + }; + let ( + Ok(argv_pointer), + Ok(argv_length), + Ok(env_pointer), + Ok(env_length), + Ok(close_pointer), + Ok(close_count), + ) = ( + i32_arg(params, argv_start), + i32_arg(params, argv_start + 1), + i32_arg(params, env_start), + i32_arg(params, env_start + 1), + i32_arg(params, close_start), + i32_arg(params, close_start + 1), + ) + else { + return ERRNO_FAULT; + }; + let Ok(argv) = memory::read_bytes(caller, argv_pointer, argv_length as usize) + .map_err(|_| ERRNO_FAULT) + .and_then(nul_strings) + else { + return ERRNO_FAULT; + }; + let Ok(env) = memory::read_bytes(caller, env_pointer, env_length as usize) + .map_err(|_| ERRNO_FAULT) + .and_then(serialized_env) + else { + return ERRNO_FAULT; + }; + if close_count as usize > MAX_FDS { + return ERRNO_2BIG; + } + let Ok(close_bytes) = memory::read_bytes(caller, close_pointer, close_count as usize * 4) + else { + return ERRNO_FAULT; + }; + let close_fds = close_bytes + .chunks_exact(4) + .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) + .collect::>(); + let executable_fd = if by_fd { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + Some(fd) + } else { + None + }; + let prepared_replacement = if let Some(fd) = executable_fd { + let host = caller.data().host.clone(); + let engine = caller.data().engine.clone(); + let maximum = caller.data().max_module_file_bytes; + let bytes = match lifecycle::load_executable_image( + &host, + ExecutableImageSource::Descriptor(fd), + maximum, + ) + .await + { + Ok(bytes) => bytes, + Err(error) => return errno(&error), + }; + let compiled = match module::compile_module(&engine, &bytes) { + Ok(module) => module, + Err(error) if error.code == "ERR_AGENTOS_WASM_INVALID_MODULE" => { + return ERRNO_NOEXEC; + } + Err(error) => return errno(&error), + }; + Some(compiled) + } else { + None + }; + let command = command.unwrap_or_else(|| format!("/proc/self/fd/{}", executable_fd.unwrap())); + let request = json!({ + "command": command, + "args": argv.iter().skip(1).cloned().collect::>(), + "options": { + "argv0": argv.first().cloned().unwrap_or_else(|| command.clone()), + "env": env, + "shell": false, + "cloexecFds": close_fds, + "localReplacement": by_fd, + "executableFd": executable_fd, + "internalBootstrapEnv": {}, + } + }); + let method = if by_fd { + "process.exec_fd_image_commit" + } else { + "process.exec" + }; + match call(caller, method, vec![request], HashMap::new()).await { + Ok(_) if by_fd => { + caller.data_mut().pending_exec_replacement = Some(PendingExecReplacement { + module: prepared_replacement.expect("fexec replacement was precompiled"), + argv, + env, + }); + caller.data_mut().exec_replaced = true; + ERRNO_IO + } + Ok(_) => ERRNO_IO, + Err(error) if error.code == "ERR_AGENTOS_EXEC_REPLACED" => { + caller.data_mut().exec_replaced = true; + ERRNO_IO + } + Err(error) => errno(&error), + } +} + +#[derive(Clone, Copy)] +enum WaitVersion { + Legacy, + V2, + V3, +} + +async fn wait( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + version: WaitVersion, +) -> i32 { + let (Ok(raw_pid), Ok(options)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let pointers = match version { + WaitVersion::Legacy | WaitVersion::V3 => vec![arg(params, 2), arg(params, 3)], + WaitVersion::V2 => vec![ + arg(params, 2), + arg(params, 3), + arg(params, 4), + arg(params, 5), + ], + }; + let pointers = match pointers.into_iter().collect::, _>>() { + Ok(value) => value, + Err(error) => return error, + }; + if pointers + .iter() + .any(|pointer| memory::validate_range(caller, *pointer, 4).is_err()) + { + return ERRNO_FAULT; + } + let supported = if matches!(version, WaitVersion::V3) { + 1 | 2 | 8 + } else { + 1 + }; + if options & !supported != 0 { + return ERRNO_INVAL; + } + match call( + caller, + "process.waitpid", + vec![json!(raw_pid as i32), json!(options)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + if value.is_null() { + for pointer in pointers { + if commit(caller, pointer, &0u32.to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + } + return SUCCESS; + } + let field = |name: &str| { + value + .get(name) + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + }; + let values = match version { + WaitVersion::Legacy => vec![field("status"), field("pid")], + WaitVersion::V3 => vec![field("rawStatus"), field("pid")], + WaitVersion::V2 => vec![ + field("exitCode"), + field("signal"), + field("pid"), + value + .get("coreDumped") + .and_then(Value::as_bool) + .map(u32::from), + ], + }; + if values.iter().any(Option::is_none) { + return ERRNO_IO; + } + for (pointer, value) in pointers.into_iter().zip(values.into_iter().flatten()) { + if commit(caller, pointer, &value.to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +fn signal_name(signal: u32) -> Option<&'static str> { + const NAMES: [&str; 32] = [ + "0", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGKILL", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGPIPE", + "SIGALRM", + "SIGTERM", + "SIGSTKFLT", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGTSTP", + "SIGTTIN", + "SIGTTOU", + "SIGURG", + "SIGXCPU", + "SIGXFSZ", + "SIGVTALRM", + "SIGPROF", + "SIGWINCH", + "SIGIO", + "SIGPWR", + "SIGSYS", + ]; + NAMES.get(signal as usize).copied() +} + +async fn kill(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(pid), Ok(signal)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + let Some(name) = signal_name(signal) else { + return ERRNO_INVAL; + }; + simple_call(caller, "process.kill", vec![json!(pid as i32), json!(name)]).await +} + +fn local_pid(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], parent: bool) -> i32 { + let Ok(output) = i32_arg(params, 0) else { + return ERRNO_FAULT; + }; + let value = if parent { + caller.data().virtual_ppid + } else { + caller.data().virtual_pid + }; + commit(caller, output, &value.to_le_bytes()) +} + +async fn getrlimit(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(resource), Ok(soft_output), Ok(hard_output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + if resource > 9 { + return ERRNO_INVAL; + } + if memory::validate_range(caller, soft_output, 8).is_err() + || memory::validate_range(caller, hard_output, 8).is_err() + { + return ERRNO_FAULT; + } + match call( + caller, + "process.getrlimit", + vec![json!(resource)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let (Some(soft), Some(hard)) = ( + value.get("soft").and_then(value_u64), + value.get("hard").and_then(value_u64), + ) else { + return ERRNO_IO; + }; + if commit(caller, soft_output, &soft.to_le_bytes()) != SUCCESS { + ERRNO_FAULT + } else { + commit(caller, hard_output, &hard.to_le_bytes()) + } + } + Err(error) => errno(&error), + } +} + +async fn setrlimit(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(resource), Ok(soft), Ok(hard)) = + (i32_arg(params, 0), i64_arg(params, 1), i64_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + if resource > 9 { + return ERRNO_INVAL; + } + simple_call( + caller, + "process.setrlimit", + vec![ + json!(resource), + json!(soft.to_string()), + json!(hard.to_string()), + ], + ) + .await +} + +async fn umask( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + always_set: bool, +) -> i32 { + let (mask, output) = if always_set { + let (Ok(mask), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + (Some(mask & 0o777), output) + } else { + let (Ok(mask), Ok(set), Ok(output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + ((set != 0).then_some(mask & 0o777), output) + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call( + caller, + "process.umask", + mask.map(|value| vec![json!(value)]).unwrap_or_default(), + HashMap::new(), + ) + .await + { + Ok(reply) => match json_reply(reply) + .ok() + .as_ref() + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + { + Some(value) => commit(caller, output, &value.to_le_bytes()), + None => ERRNO_IO, + }, + Err(error) => errno(&error), + } +} + +async fn itimer(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(operation), Ok(value), Ok(interval), Ok(remaining_output), Ok(interval_output)) = ( + i32_arg(params, 0), + i64_arg(params, 1), + i64_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_FAULT; + }; + if operation > 1 { + return ERRNO_INVAL; + } + if memory::validate_range(caller, remaining_output, 8).is_err() + || memory::validate_range(caller, interval_output, 8).is_err() + { + return ERRNO_FAULT; + } + let args = if operation == 0 { + vec![json!(0)] + } else { + vec![json!(1), json!(value), json!(interval)] + }; + match call(caller, "process.itimer_real", args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let (Some(remaining), Some(interval)) = ( + value.get("remainingUs").and_then(value_u64), + value.get("intervalUs").and_then(value_u64), + ) else { + return ERRNO_IO; + }; + if commit(caller, remaining_output, &remaining.to_le_bytes()) != SUCCESS { + ERRNO_FAULT + } else { + commit(caller, interval_output, &interval.to_le_bytes()) + } + } + Err(error) => errno(&error), + } +} + +async fn getpgid(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(pid), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if (pid as i32) < 0 { + return ERRNO_SRCH; + } + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call(caller, "process.getpgid", vec![json!(pid)], HashMap::new()).await { + Ok(reply) => match json_reply(reply) + .ok() + .as_ref() + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + { + Some(value) => commit(caller, output, &value.to_le_bytes()), + None => ERRNO_IO, + }, + Err(error) => errno(&error), + } +} + +async fn setpgid(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(pid), Ok(pgid)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + if (pid as i32) < 0 || (pgid as i32) < 0 { + return ERRNO_INVAL; + } + simple_call(caller, "process.setpgid", vec![json!(pid), json!(pgid)]).await +} + +#[derive(Clone, Copy)] +enum PairKind { + Pipe, + Socket, + Pty, +} + +async fn pair(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], kind: PairKind) -> i32 { + let (first_output_index, second_output_index, method, args) = match kind { + PairKind::Pipe => (0, 1, "process.fd_pipe", vec![]), + PairKind::Pty => (0, 1, "process.pty_open", vec![]), + PairKind::Socket => { + let (Ok(socket_kind), Ok(nonblocking), Ok(close_on_exec)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + ( + 3, + 4, + "process.fd_socketpair", + vec![ + json!(socket_kind), + json!(nonblocking != 0), + json!(close_on_exec != 0), + ], + ) + } + }; + let (Ok(first_output), Ok(second_output)) = ( + i32_arg(params, first_output_index), + i32_arg(params, second_output_index), + ) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, first_output, 4).is_err() + || memory::validate_range(caller, second_output, 4).is_err() + { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let names = match kind { + PairKind::Pipe => ("readFd", "writeFd"), + PairKind::Socket => ("firstFd", "secondFd"), + PairKind::Pty => ("masterFd", "slaveFd"), + }; + let (Some(first), Some(second)) = ( + value + .get(names.0) + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()), + value + .get(names.1) + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()), + ) else { + return ERRNO_IO; + }; + let status = if commit(caller, first_output, &first.to_le_bytes()) == SUCCESS + && commit(caller, second_output, &second.to_le_bytes()) == SUCCESS + { + SUCCESS + } else { + ERRNO_FAULT + }; + if status != SUCCESS { + log_fd_rollback(caller, first, "pair first output commit").await; + log_fd_rollback(caller, second, "pair second output commit").await; + } + status + } + Err(error) => errno(&error), + } +} + +#[derive(Clone, Copy)] +enum DuplicateKind { + Any, + Minimum, +} + +async fn duplicate( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + kind: DuplicateKind, +) -> i32 { + let (Ok(fd), Ok(output)) = ( + i32_arg(params, 0), + i32_arg( + params, + if matches!(kind, DuplicateKind::Any) { + 1 + } else { + 2 + }, + ), + ) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + let (method, args) = match kind { + DuplicateKind::Any => ("process.fd_dup", vec![json!(fd)]), + DuplicateKind::Minimum => { + let Ok(minimum) = i32_arg(params, 1) else { + return ERRNO_INVAL; + }; + ("process.fd_dup_min", vec![json!(fd), json!(minimum)]) + } + }; + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let value = match json_reply(reply) { + Ok(value) => value, + Err(error) => return error, + }; + let Some(new_fd) = value_u64(&value) + .or_else(|| value.get("fd").and_then(value_u64)) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + if commit(caller, output, &new_fd.to_le_bytes()) == SUCCESS { + SUCCESS + } else { + log_fd_rollback(caller, new_fd, "duplicate output commit").await; + ERRNO_FAULT + } + } + Err(error) => errno(&error), + } +} + +async fn duplicate_to(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(target)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_BADF; + }; + simple_call(caller, "process.fd_dup2", vec![json!(fd), json!(target)]).await +} + +async fn descriptor_flags( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + set: bool, +) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { + return ERRNO_BADF; + }; + if set { + let Ok(flags) = i32_arg(params, 1) else { + return ERRNO_INVAL; + }; + return simple_call(caller, "process.fd_setfd", vec![json!(fd), json!(flags)]).await; + } + let Ok(output) = i32_arg(params, 1) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call(caller, "process.fd_getfd", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => match json_reply(reply) + .ok() + .as_ref() + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + { + Some(value) => commit(caller, output, &value.to_le_bytes()), + None => ERRNO_IO, + }, + Err(error) => errno(&error), + } +} + +async fn flock(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(operation)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.fd_flock", + vec![json!(fd), json!(operation)], + ) + .await +} + +async fn record_lock(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(command), Ok(kind), Ok(start), Ok(length)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i64_arg(params, 3), + i64_arg(params, 4), + ) else { + return ERRNO_INVAL; + }; + if command == 12 { + for index in [5usize, 6, 7, 8] { + let Ok(pointer) = (if index < 7 { + i32_arg(params, index).map(|value| (value, 4)) + } else { + i32_arg(params, index).map(|value| (value, 8)) + }) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, pointer.0, pointer.1).is_err() { + return ERRNO_FAULT; + } + } + } + match call( + caller, + "process.fd_record_lock", + vec![ + json!(fd), + json!(command), + json!(kind), + json!(start.to_string()), + json!(length.to_string()), + ], + HashMap::new(), + ) + .await + { + Ok(_reply) if command != 12 => SUCCESS, + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let values = [ + value.get("type").and_then(value_u64), + value.get("pid").and_then(value_u64), + value.get("start").and_then(value_u64), + value.get("length").and_then(value_u64), + ]; + if values.iter().any(Option::is_none) { + return ERRNO_IO; + } + for (index, value) in values.into_iter().flatten().enumerate() { + let pointer = i32_arg(params, index + 5).unwrap(); + let bytes = if index < 2 { + (value as u32).to_le_bytes().to_vec() + } else { + value.to_le_bytes().to_vec() + }; + if commit(caller, pointer, &bytes) != SUCCESS { + return ERRNO_FAULT; + } + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +async fn closefrom(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(minimum) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + simple_call( + caller, + "process.fd_closefrom", + vec![json!(minimum), Value::Null], + ) + .await +} + +async fn send_rights(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let ( + Ok(fd), + Ok(data_pointer), + Ok(data_length), + Ok(rights_pointer), + Ok(rights_count), + Ok(flags), + Ok(output), + ) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + i32_arg(params, 6), + ) + else { + return ERRNO_FAULT; + }; + if rights_count as usize > MAX_RIGHTS { + return ERRNO_INVAL; + } + let Ok(bytes) = memory::read_bytes(caller, data_pointer, data_length as usize) else { + return ERRNO_FAULT; + }; + let Ok(rights_bytes) = memory::read_bytes(caller, rights_pointer, rights_count as usize * 4) + else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + let rights = rights_bytes + .chunks_exact(4) + .map(|bytes| json!(u32::from_le_bytes(bytes.try_into().unwrap()))) + .collect::>(); + let mut raw = HashMap::new(); + raw.insert(1, bytes); + match call( + caller, + "process.fd_sendmsg_rights", + vec![json!(fd), Value::Null, Value::Array(rights), json!(flags)], + raw, + ) + .await + { + Ok(reply) => { + let value = match json_reply(reply) { + Ok(value) => value, + Err(error) => return error, + }; + let Some(sent) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_IO; + }; + commit(caller, output, &sent.to_le_bytes()) + } + Err(error) => errno(&error), + } +} + +async fn receive_rights(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let values = (0..9) + .map(|index| i32_arg(params, index).map_err(|_| ERRNO_FAULT)) + .collect::, _>>(); + let Ok(values) = values else { + return ERRNO_FAULT; + }; + let [fd, data_pointer, data_capacity, rights_pointer, rights_capacity, flags, received_output, rights_count_output, message_flags_output] = + values.as_slice() + else { + return ERRNO_FAULT; + }; + if *rights_capacity as usize > MAX_RIGHTS + || memory::validate_range(caller, *data_pointer, *data_capacity as usize).is_err() + || memory::validate_range(caller, *rights_pointer, *rights_capacity as usize * 4).is_err() + || [ + *received_output, + *rights_count_output, + *message_flags_output, + ] + .into_iter() + .any(|pointer| memory::validate_range(caller, pointer, 4).is_err()) + { + return ERRNO_FAULT; + } + let close_on_exec = *flags & 0x4000_0000 != 0; + match call( + caller, + "process.fd_recvmsg_rights", + vec![ + json!(fd), + json!(data_capacity), + json!(rights_capacity), + json!(close_on_exec), + json!(*flags & 0x2 != 0), + json!(*flags & 0x40 != 0), + json!(*flags & 0x100 != 0), + ], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let data = value + .get("data") + .cloned() + .map(HostCallReply::Json) + .and_then(|value| reply_bytes(value).ok()) + .unwrap_or_default(); + let Some(rights) = value.get("rights").and_then(Value::as_array) else { + return ERRNO_IO; + }; + let mut installed = Vec::new(); + for right in rights { + let fd = right + .get("fd") + .and_then(value_u64) + .or_else(|| value_u64(right)) + .and_then(|value| u32::try_from(value).ok()); + let Some(fd) = fd else { + rollback_fds(caller, &installed).await; + return ERRNO_IO; + }; + installed.push(fd); + } + if data.len() > *data_capacity as usize || installed.len() > *rights_capacity as usize { + rollback_fds(caller, &installed).await; + return ERRNO_IO; + } + if commit(caller, *data_pointer, &data) != SUCCESS { + rollback_fds(caller, &installed).await; + return ERRNO_FAULT; + } + for (index, fd) in installed.iter().enumerate() { + if commit( + caller, + *rights_pointer + (index * 4) as u32, + &fd.to_le_bytes(), + ) != SUCCESS + { + rollback_fds(caller, &installed).await; + return ERRNO_FAULT; + } + } + let full_length = value + .get("fullLength") + .and_then(value_u64) + .unwrap_or(data.len() as u64); + let message_flags = u32::from( + value + .get("payloadTruncated") + .and_then(Value::as_bool) + .unwrap_or(false), + ) | (u32::from( + value + .get("controlTruncated") + .and_then(Value::as_bool) + .unwrap_or(false), + ) << 1) + | (u32::try_from(full_length).unwrap_or(u32::MAX) << 2); + let outputs = [ + (*received_output, data.len() as u32), + (*rights_count_output, installed.len() as u32), + (*message_flags_output, message_flags), + ]; + for (pointer, value) in outputs { + if commit(caller, pointer, &value.to_le_bytes()) != SUCCESS { + rollback_fds(caller, &installed).await; + return ERRNO_FAULT; + } + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +async fn rollback_fds(caller: &mut Caller<'_, WasmtimeStoreState>, fds: &[u32]) { + for fd in fds { + log_fd_rollback(caller, *fd, "received-rights output commit").await; + } +} + +async fn log_fd_rollback( + caller: &mut Caller<'_, WasmtimeStoreState>, + fd: u32, + context: &'static str, +) { + let error = simple_call(caller, "process.fd_close", vec![json!(fd)]).await; + if error != SUCCESS { + eprintln!("ERR_AGENTOS_WASMTIME_FD_ROLLBACK: context={context} fd={fd} errno={error}"); + } +} + +async fn sleep(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(milliseconds) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + simple_call(caller, "process.sleep", vec![json!(milliseconds)]).await +} + +fn has_signal_trampoline(caller: &mut Caller<'_, WasmtimeStoreState>) -> bool { + let Some(Extern::Func(function)) = caller.get_export("__wasi_signal_trampoline") else { + return false; + }; + let ty = function.ty(&mut *caller); + let mut params = ty.params(); + matches!(params.next(), Some(ValType::I32)) + && params.next().is_none() + && ty.results().next().is_none() +} + +async fn sigaction(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(signal), Ok(action), Ok(low), Ok(high), Ok(flags)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_INVAL; + }; + if !(1..=64).contains(&signal) || action > 2 { + return ERRNO_INVAL; + } + if action == 2 && !has_signal_trampoline(caller) { + return ERRNO_NOTSUP; + } + let action_name = match action { + 0 => "default", + 1 => "ignore", + _ => "user", + }; + let mask = signal_set(low, high); + simple_call( + caller, + "process.signal_state", + vec![ + json!(signal), + json!(action_name), + json!(serde_json::to_string(&mask).unwrap()), + json!(flags), + ], + ) + .await +} + +async fn signal_mask(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(how), Ok(low), Ok(high), Ok(old_low), Ok(old_high)) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i32_arg(params, 2), + i32_arg(params, 3), + i32_arg(params, 4), + ) else { + return ERRNO_FAULT; + }; + if how > 3 { + return ERRNO_INVAL; + } + if memory::validate_range(caller, old_low, 4).is_err() + || memory::validate_range(caller, old_high, 4).is_err() + { + return ERRNO_FAULT; + } + let requested = signal_set(low, high) + .into_iter() + .filter(|signal| !matches!(signal, 9 | 19)) + .collect::>(); + match call( + caller, + "process.signal_mask", + vec![json!(how), json!(requested)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(signals) = value.get("signals").and_then(Value::as_array) else { + return ERRNO_IO; + }; + let mut low = 0u32; + let mut high = 0u32; + for signal in signals.iter().filter_map(value_u64) { + if (1..=32).contains(&signal) { + low |= 1 << (signal - 1); + } else if (33..=64).contains(&signal) { + high |= 1 << (signal - 33); + } + } + if commit(caller, old_low, &low.to_le_bytes()) != SUCCESS { + ERRNO_FAULT + } else { + commit(caller, old_high, &high.to_le_bytes()) + } + } + Err(error) => errno(&error), + } +} + +async fn ppoll(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let ( + Ok(pointer), + Ok(count), + Ok(seconds), + Ok(nanoseconds), + Ok(low), + Ok(high), + Ok(has_mask), + Ok(ready_output), + ) = ( + i32_arg(params, 0), + i32_arg(params, 1), + i64_arg(params, 2), + i64_arg(params, 3), + i32_arg(params, 4), + i32_arg(params, 5), + i32_arg(params, 6), + i32_arg(params, 7), + ) + else { + return ERRNO_FAULT; + }; + let seconds = seconds as i64; + let nanoseconds = nanoseconds as i64; + let timeout = if seconds < 0 && nanoseconds < 0 { + Value::Null + } else { + if seconds < 0 || !(0..1_000_000_000).contains(&nanoseconds) { + return ERRNO_INVAL; + } + let milliseconds = (seconds as u128) + .saturating_mul(1000) + .saturating_add((nanoseconds as u128).div_ceil(1_000_000)) + .min(i32::MAX as u128) as u64; + json!(milliseconds) + }; + let count = count as usize; + if count > 1024 { + return ERRNO_INVAL; + } + if memory::validate_range(caller, pointer, count.saturating_mul(8)).is_err() + || memory::validate_range(caller, ready_output, 4).is_err() + { + return ERRNO_FAULT; + } + let mut entries = Vec::with_capacity(count); + let mut fds = Vec::with_capacity(count); + for index in 0..count { + let base = pointer + (index * 8) as u32; + let Ok(fd) = memory::read_u32(caller, base) else { + return ERRNO_FAULT; + }; + let Ok(events) = memory::read_bytes(caller, base + 4, 2) else { + return ERRNO_FAULT; + }; + fds.push(fd); + entries.push(json!({"fd": fd, "events": u16::from_le_bytes([events[0], events[1]])})); + } + let mask = if has_mask != 0 { + json!(signal_set(low, high) + .into_iter() + .filter(|signal| !matches!(signal, 9 | 19)) + .collect::>()) + } else { + Value::Null + }; + match call( + caller, + "process.posix_poll", + vec![Value::Array(entries), timeout, mask], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(ready_fds) = value.get("fds").and_then(Value::as_array) else { + return ERRNO_IO; + }; + for (index, fd) in fds.iter().copied().enumerate().take(count) { + let revents = ready_fds + .iter() + .find(|entry| entry.get("fd").and_then(value_u64) == Some(fd as u64)) + .and_then(|entry| entry.get("revents")) + .and_then(value_u64) + .unwrap_or(0) as u16; + if commit( + caller, + pointer + (index * 8 + 6) as u32, + &revents.to_le_bytes(), + ) != SUCCESS + { + return ERRNO_FAULT; + } + } + let ready = value + .get("readyCount") + .and_then(value_u64) + .unwrap_or_else(|| { + ready_fds + .iter() + .filter(|entry| entry.get("revents").and_then(value_u64).unwrap_or(0) != 0) + .count() as u64 + }); + let Ok(ready) = u32::try_from(ready) else { + return ERRNO_2BIG; + }; + commit(caller, ready_output, &ready.to_le_bytes()) + } + Err(error) => errno(&error), + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/terminal.rs b/crates/execution/src/wasm/wasmtime/linker/terminal.rs new file mode 100644 index 0000000000..59d19c1c95 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/terminal.rs @@ -0,0 +1,318 @@ +//! System identity and terminal ABI codecs. + +use super::preview1::{ + call, commit, errno, json_reply, simple_call, value_u64, ERRNO_FAULT, ERRNO_INVAL, ERRNO_IO, + ERRNO_NAMETOOLONG, SUCCESS, +}; +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; +use base64::Engine as _; +use serde_json::{json, Value}; +use std::collections::HashMap; +use wasmtime::{Caller, Val}; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let value = match abi.id { + HostSystemGetIdentity => identity(caller, params).await, + HostTtyRead => tty_read(caller, params).await, + HostTtyIsatty => tty_isatty(caller, params).await, + HostTtyGetSize => tty_size(caller, params).await, + HostTtySetSize => tty_set_size(caller, params).await, + HostTtyGetAttr => tty_get_attr(caller, params).await, + HostTtySetAttr => tty_set_attr(caller, params).await, + HostTtyGetPgrp => tty_scalar_output(caller, params, "__kernel_tcgetpgrp").await, + HostTtyGetSid => tty_scalar_output(caller, params, "__kernel_tcgetsid").await, + HostTtySetPgrp => { + let (Ok(fd), Ok(pgid)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + set_i32_result(results, ERRNO_INVAL)?; + return Ok(true); + }; + simple_call(caller, "__kernel_tcsetpgrp", vec![json!(fd), json!(pgid)]).await + } + HostTtySetRawMode => { + let Ok(enabled) = i32_arg(params, 0) else { + set_i32_result(results, ERRNO_INVAL)?; + return Ok(true); + }; + simple_call(caller, "__pty_set_raw_mode", vec![json!(enabled != 0)]).await + } + _ => return Ok(false), + }; + set_i32_result(results, value)?; + Ok(true) +} + +async fn identity(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(field), Ok(output), Ok(capacity)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let fields = [ + "hostname", + "type", + "release", + "version", + "machine", + "domainName", + ]; + let Some(field) = fields.get(field as usize) else { + return ERRNO_INVAL; + }; + if capacity == 0 { + return ERRNO_NAMETOOLONG; + } + if memory::validate_range(caller, output, capacity as usize).is_err() { + return ERRNO_FAULT; + } + match call(caller, "process.system_identity", vec![], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(value) = value.get(*field).and_then(Value::as_str) else { + return ERRNO_IO; + }; + if value.len().saturating_add(1) > capacity as usize { + return ERRNO_NAMETOOLONG; + } + let mut bytes = value.as_bytes().to_vec(); + bytes.push(0); + commit(caller, output, &bytes) + } + Err(error) => errno(&error), + } +} + +async fn tty_read(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(output), Ok(capacity), Ok(timeout)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return 0; + }; + if capacity == 0 || memory::validate_range(caller, output, capacity as usize).is_err() { + return 0; + } + let timeout = if timeout == u32::MAX { + Value::Null + } else { + json!(timeout) + }; + match call( + caller, + "__kernel_stdin_read", + vec![json!(capacity), timeout], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let bytes = match reply { + crate::backend::HostCallReply::Raw(bytes) => bytes, + crate::backend::HostCallReply::Json(value) => value + .get("dataBase64") + .and_then(Value::as_str) + .and_then(|encoded| { + base64::engine::general_purpose::STANDARD + .decode(encoded) + .ok() + }) + .unwrap_or_default(), + crate::backend::HostCallReply::Empty => Vec::new(), + }; + let length = bytes.len().min(capacity as usize); + if memory::validate_range(caller, output, capacity as usize).is_err() + || memory::write_bytes(caller, output, &bytes[..length]).is_err() + { + 0 + } else { + length as i32 + } + } + Err(_) => 0, + } +} + +async fn tty_isatty(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let Ok(fd) = i32_arg(params, 0) else { return 0 }; + match call(caller, "__kernel_isatty", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => json_reply(reply) + .ok() + .and_then(|value| value.as_bool()) + .map(i32::from) + .unwrap_or(0), + Err(_) => 0, + } +} + +async fn tty_size(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(columns), Ok(rows)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, columns, 2).is_err() + || memory::validate_range(caller, rows, 2).is_err() + { + return ERRNO_FAULT; + } + match call(caller, "__kernel_tty_size", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(cols) = value + .get("cols") + .and_then(value_u64) + .and_then(|value| u16::try_from(value).ok()) + else { + return 25; + }; + let Some(row_count) = value + .get("rows") + .and_then(value_u64) + .and_then(|value| u16::try_from(value).ok()) + else { + return 25; + }; + if commit(caller, columns, &cols.to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + commit(caller, rows, &row_count.to_le_bytes()) + } + Err(error) if error.code == "EBADF" => 9, + Err(error) if error.code == "ENOTTY" => 25, + Err(_) => 5, + } +} + +async fn tty_set_size(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(columns), Ok(rows)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_INVAL; + }; + if columns > u16::MAX.into() || rows > u16::MAX.into() { + return ERRNO_INVAL; + } + simple_call( + caller, + "__kernel_tty_set_size", + vec![json!(fd), json!(columns), json!(rows)], + ) + .await +} + +async fn tty_get_attr(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(flags_output), Ok(cc_output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, flags_output, 4).is_err() + || memory::validate_range(caller, cc_output, 7).is_err() + { + return ERRNO_FAULT; + } + match call( + caller, + "__kernel_tcgetattr", + vec![json!(fd)], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(flags) = value + .get("flags") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + let Some(cc) = value + .get("cc") + .and_then(Value::as_array) + .filter(|cc| cc.len() == 7) + else { + return ERRNO_IO; + }; + let Some(cc) = cc + .iter() + .map(value_u64) + .map(|value| value.and_then(|value| u8::try_from(value).ok())) + .collect::>>() + else { + return ERRNO_IO; + }; + if memory::validate_range(caller, flags_output, 4).is_err() + || memory::validate_range(caller, cc_output, 7).is_err() + { + return ERRNO_FAULT; + } + if memory::write_u32(caller, flags_output, flags).is_err() + || memory::write_bytes(caller, cc_output, &cc).is_err() + { + ERRNO_FAULT + } else { + SUCCESS + } + } + Err(error) => errno(&error), + } +} + +async fn tty_set_attr(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(flags), Ok(cc_pointer)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let Ok(cc) = memory::read_bytes(caller, cc_pointer, 7) else { + return ERRNO_FAULT; + }; + simple_call( + caller, + "__kernel_tcsetattr", + vec![json!(fd), json!(flags), json!(cc)], + ) + .await +} + +async fn tty_scalar_output( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, +) -> i32 { + let (Ok(fd), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call(caller, method, vec![json!(fd)], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(value) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_IO; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + memory::write_u32(caller, output, value).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} diff --git a/crates/execution/src/wasm/wasmtime/linker/user.rs b/crates/execution/src/wasm/wasmtime/linker/user.rs new file mode 100644 index 0000000000..96b87be32a --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/linker/user.rs @@ -0,0 +1,346 @@ +//! Identity and account database ABI codecs. + +use super::preview1::{ + call, commit, errno, json_reply, simple_call, value_u64, ERRNO_2BIG, ERRNO_FAULT, ERRNO_INVAL, + ERRNO_IO, ERRNO_NAMETOOLONG, ERRNO_RANGE, SUCCESS, +}; +use super::{i32_arg, set_i32_result}; +use crate::abi::{AbiBinding, ImportId}; +use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use wasmtime::{Caller, Val}; + +const MAX_GROUPS: usize = 64; +const MAX_ACCOUNT_NAME_BYTES: usize = 4096; + +pub async fn dispatch( + caller: &mut Caller<'_, WasmtimeStoreState>, + abi: AbiBinding, + params: &[Val], + results: &mut [Val], +) -> wasmtime::Result { + use ImportId::*; + let fixed_limit = match abi.id { + HostUserGetgroups | HostUserSetgroups => i32_arg(params, 0).ok().map(|count| { + ( + "wasm.abi.maxSupplementaryGroups", + count as usize, + MAX_GROUPS, + ERRNO_INVAL, + ) + }), + HostUserGetpwnam | HostUserGetgrnam => i32_arg(params, 1).ok().map(|length| { + ( + "wasm.abi.maxAccountNameBytes", + length as usize, + MAX_ACCOUNT_NAME_BYTES, + ERRNO_NAMETOOLONG, + ) + }), + _ => None, + }; + if let Some((name, observed, maximum, error)) = fixed_limit { + let status = super::check_fixed_request_limit(caller, name, observed, maximum, error).await; + if status != SUCCESS { + set_i32_result(results, status)?; + return Ok(true); + } + } + let status = match abi.id { + HostUserGetuid => scalar(caller, params, "process.getuid").await, + HostUserGetgid => scalar(caller, params, "process.getgid").await, + HostUserGeteuid => scalar(caller, params, "process.geteuid").await, + HostUserGetegid => scalar(caller, params, "process.getegid").await, + HostUserGetresuid => triple(caller, params, "process.getresuid").await, + HostUserGetresgid => triple(caller, params, "process.getresgid").await, + HostUserSetuid => set_one(caller, params, "process.setuid").await, + HostUserSeteuid => set_one(caller, params, "process.seteuid").await, + HostUserSetgid => set_one(caller, params, "process.setgid").await, + HostUserSetegid => set_one(caller, params, "process.setegid").await, + HostUserSetreuid => set_optional(caller, params, "process.setreuid", 2).await, + HostUserSetregid => set_optional(caller, params, "process.setregid", 2).await, + HostUserSetresuid => set_optional(caller, params, "process.setresuid", 3).await, + HostUserSetresgid => set_optional(caller, params, "process.setresgid", 3).await, + HostUserGetgroups => getgroups(caller, params).await, + HostUserSetgroups => setgroups(caller, params).await, + HostUserIsatty => isatty(caller, params).await, + HostUserGetpwuid => account(caller, params, "process.getpwuid", AccountKey::Scalar).await, + HostUserGetpwent => account(caller, params, "process.getpwent", AccountKey::Scalar).await, + HostUserGetgrgid => account(caller, params, "process.getgrgid", AccountKey::Scalar).await, + HostUserGetgrent => account(caller, params, "process.getgrent", AccountKey::Scalar).await, + HostUserGetpwnam => account(caller, params, "process.getpwnam", AccountKey::Name).await, + HostUserGetgrnam => account(caller, params, "process.getgrnam", AccountKey::Name).await, + _ => return Ok(false), + }; + set_i32_result(results, status)?; + Ok(true) +} + +async fn scalar(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], method: &str) -> i32 { + let Ok(output) = i32_arg(params, 0) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call(caller, method, vec![], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(value) = value_u64(&value).and_then(|value| u32::try_from(value).ok()) else { + return ERRNO_INVAL; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + memory::write_u32(caller, output, value).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn triple(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], method: &str) -> i32 { + let pointers = match (0..3) + .map(|index| i32_arg(params, index)) + .collect::, _>>() + { + Ok(value) => value, + Err(_) => return ERRNO_FAULT, + }; + if pointers + .iter() + .any(|pointer| memory::validate_range(caller, *pointer, 4).is_err()) + { + return ERRNO_FAULT; + } + match call(caller, method, vec![], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(values) = value.as_array().filter(|values| values.len() == 3) else { + return ERRNO_IO; + }; + let decoded = values + .iter() + .map(value_u64) + .map(|value| value.and_then(|value| u32::try_from(value).ok())) + .collect::>>(); + let Some(decoded) = decoded else { + return ERRNO_IO; + }; + if pointers + .iter() + .any(|pointer| memory::validate_range(caller, *pointer, 4).is_err()) + { + return ERRNO_FAULT; + } + for (pointer, value) in pointers.into_iter().zip(decoded) { + if memory::write_u32(caller, pointer, value).is_err() { + return ERRNO_FAULT; + } + } + SUCCESS + } + Err(error) => errno(&error), + } +} + +async fn set_one(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], method: &str) -> i32 { + let Ok(value) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + simple_call(caller, method, vec![json!(value)]).await +} + +fn optional_id(value: u32) -> Value { + if value == u32::MAX { + Value::Null + } else { + json!(value) + } +} + +async fn set_optional( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, + count: usize, +) -> i32 { + let mut args = Vec::with_capacity(count); + for index in 0..count { + let Ok(value) = i32_arg(params, index) else { + return ERRNO_INVAL; + }; + args.push(optional_id(value)); + } + simple_call(caller, method, args).await +} + +async fn getgroups(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(capacity), Ok(groups), Ok(count_output)) = + (i32_arg(params, 0), i32_arg(params, 1), i32_arg(params, 2)) + else { + return ERRNO_FAULT; + }; + let capacity = capacity as usize; + if capacity > MAX_GROUPS { + return ERRNO_2BIG; + } + if memory::validate_range(caller, count_output, 4).is_err() + || (capacity != 0 + && memory::validate_range(caller, groups, capacity.saturating_mul(4)).is_err()) + { + return ERRNO_FAULT; + } + match call(caller, "process.getgroups", vec![], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(values) = value.as_array().filter(|values| values.len() <= MAX_GROUPS) else { + return ERRNO_INVAL; + }; + if capacity != 0 && capacity < values.len() { + return ERRNO_INVAL; + } + let decoded = values + .iter() + .map(value_u64) + .map(|value| value.and_then(|value| u32::try_from(value).ok())) + .collect::>>(); + let Some(decoded) = decoded else { + return ERRNO_INVAL; + }; + if memory::validate_range(caller, count_output, 4).is_err() + || (capacity != 0 + && memory::validate_range(caller, groups, decoded.len().saturating_mul(4)) + .is_err()) + { + return ERRNO_FAULT; + } + if capacity != 0 { + for (index, value) in decoded.iter().enumerate() { + if memory::write_u32(caller, groups + (index * 4) as u32, *value).is_err() { + return ERRNO_FAULT; + } + } + } + memory::write_u32(caller, count_output, decoded.len() as u32) + .map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +async fn setgroups(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(count), Ok(pointer)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + let count = count as usize; + if count > MAX_GROUPS { + return ERRNO_2BIG; + } + if memory::validate_range(caller, pointer, count.saturating_mul(4)).is_err() { + return ERRNO_FAULT; + } + let mut values = Vec::with_capacity(count); + for index in 0..count { + let Ok(value) = memory::read_u32(caller, pointer + (index * 4) as u32) else { + return ERRNO_FAULT; + }; + values.push(json!(value)); + } + simple_call(caller, "process.setgroups", vec![Value::Array(values)]).await +} + +async fn isatty(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { + let (Ok(fd), Ok(output)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, output, 4).is_err() { + return ERRNO_FAULT; + } + match call(caller, "__kernel_isatty", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let value = u32::from(value.as_bool().unwrap_or(false)); + memory::write_u32(caller, output, value).map_or(ERRNO_FAULT, |_| SUCCESS) + } + Err(error) => errno(&error), + } +} + +enum AccountKey { + Scalar, + Name, +} + +async fn account( + caller: &mut Caller<'_, WasmtimeStoreState>, + params: &[Val], + method: &str, + key: AccountKey, +) -> i32 { + let (args, buffer_index) = match key { + AccountKey::Scalar => { + let Ok(value) = i32_arg(params, 0) else { + return ERRNO_INVAL; + }; + (vec![json!(value)], 1) + } + AccountKey::Name => { + let (Ok(pointer), Ok(length)) = (i32_arg(params, 0), i32_arg(params, 1)) else { + return ERRNO_FAULT; + }; + if length as usize > MAX_ACCOUNT_NAME_BYTES { + return ERRNO_NAMETOOLONG; + } + let Ok(name) = memory::read_string(caller, pointer, length as usize) else { + return ERRNO_FAULT; + }; + (vec![json!(name)], 2) + } + }; + let (Ok(buffer), Ok(capacity), Ok(required_output)) = ( + i32_arg(params, buffer_index), + i32_arg(params, buffer_index + 1), + i32_arg(params, buffer_index + 2), + ) else { + return ERRNO_FAULT; + }; + if memory::validate_range(caller, required_output, 4).is_err() + || memory::validate_range(caller, buffer, capacity as usize).is_err() + { + return ERRNO_FAULT; + } + match call(caller, method, args, HashMap::new()).await { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + let Some(record) = value.as_str() else { + return ERRNO_IO; + }; + let bytes = record.as_bytes(); + if memory::validate_range(caller, required_output, 4).is_err() + || (capacity as usize >= bytes.len() + && memory::validate_range(caller, buffer, bytes.len()).is_err()) + { + return ERRNO_FAULT; + } + if memory::write_u32(caller, required_output, bytes.len() as u32).is_err() { + return ERRNO_FAULT; + } + if (capacity as usize) < bytes.len() { + return ERRNO_RANGE; + } + commit(caller, buffer, bytes) + } + Err(error) => errno(&error), + } +} diff --git a/crates/execution/src/wasm/wasmtime/memory.rs b/crates/execution/src/wasm/wasmtime/memory.rs new file mode 100644 index 0000000000..ef0ab5722b --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/memory.rs @@ -0,0 +1,159 @@ +//! Checked guest-memory codecs. + +use super::store::WasmtimeStoreState; +use std::ops::Range; +use wasmtime::{Caller, Extern, Memory}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestMemoryError { + MissingMemory, + AddressOverflow, + OutOfBounds, + InvalidUtf8, +} + +impl std::fmt::Display for GuestMemoryError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::MissingMemory => "guest module does not export linear memory as `memory`", + Self::AddressOverflow => "guest memory range overflows the host address space", + Self::OutOfBounds => "guest memory range is out of bounds", + Self::InvalidUtf8 => "guest string is not valid UTF-8", + }) + } +} + +impl std::error::Error for GuestMemoryError {} + +pub fn exported_memory( + caller: &mut Caller<'_, WasmtimeStoreState>, +) -> Result { + match caller.get_export("memory") { + Some(Extern::Memory(memory)) => Ok(memory), + _ => Err(GuestMemoryError::MissingMemory), + } +} + +pub fn validate_range( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + length: usize, +) -> Result<(Memory, Range), GuestMemoryError> { + let memory = exported_memory(caller)?; + let start = usize::try_from(pointer).map_err(|_| GuestMemoryError::AddressOverflow)?; + let end = start + .checked_add(length) + .ok_or(GuestMemoryError::AddressOverflow)?; + if end > memory.data_size(&mut *caller) { + return Err(GuestMemoryError::OutOfBounds); + } + Ok((memory, start..end)) +} + +pub fn read_bytes( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + length: usize, +) -> Result, GuestMemoryError> { + let (memory, range) = validate_range(caller, pointer, length)?; + Ok(memory.data(&mut *caller)[range].to_vec()) +} + +pub fn read_string( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + length: usize, +) -> Result { + String::from_utf8(read_bytes(caller, pointer, length)?) + .map_err(|_| GuestMemoryError::InvalidUtf8) +} + +pub fn write_bytes( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + bytes: &[u8], +) -> Result<(), GuestMemoryError> { + let (memory, range) = validate_range(caller, pointer, bytes.len())?; + memory.data_mut(&mut *caller)[range].copy_from_slice(bytes); + Ok(()) +} + +pub fn write_u32( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + value: u32, +) -> Result<(), GuestMemoryError> { + write_bytes(caller, pointer, &value.to_le_bytes()) +} + +pub fn write_u64( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, + value: u64, +) -> Result<(), GuestMemoryError> { + write_bytes(caller, pointer, &value.to_le_bytes()) +} + +pub fn read_u32( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, +) -> Result { + let bytes = read_bytes(caller, pointer, 4)?; + Ok(u32::from_le_bytes(bytes.try_into().expect("four bytes"))) +} + +pub fn read_u64( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointer: u32, +) -> Result { + let bytes = read_bytes(caller, pointer, 8)?; + Ok(u64::from_le_bytes(bytes.try_into().expect("eight bytes"))) +} + +pub fn validate_string_table_outputs( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointers: u32, + buffer: u32, + strings: &[Vec], +) -> Result<(), GuestMemoryError> { + let pointer_bytes = strings + .len() + .checked_mul(4) + .ok_or(GuestMemoryError::AddressOverflow)?; + let buffer_bytes = strings.iter().try_fold(0usize, |total, value| { + total + .checked_add(value.len()) + .ok_or(GuestMemoryError::AddressOverflow) + })?; + validate_range(caller, pointers, pointer_bytes)?; + validate_range(caller, buffer, buffer_bytes)?; + Ok(()) +} + +pub fn write_string_table( + caller: &mut Caller<'_, WasmtimeStoreState>, + pointers: u32, + buffer: u32, + strings: &[Vec], +) -> Result<(), GuestMemoryError> { + validate_string_table_outputs(caller, pointers, buffer, strings)?; + let mut offset = 0usize; + for (index, value) in strings.iter().enumerate() { + let value_pointer = usize::try_from(buffer) + .ok() + .and_then(|base| base.checked_add(offset)) + .and_then(|pointer| u32::try_from(pointer).ok()) + .ok_or(GuestMemoryError::AddressOverflow)?; + let pointer_slot = usize::try_from(pointers) + .ok() + .and_then(|base| base.checked_add(index.saturating_mul(4))) + .and_then(|pointer| u32::try_from(pointer).ok()) + .ok_or(GuestMemoryError::AddressOverflow)?; + write_u32(caller, pointer_slot, value_pointer)?; + write_bytes(caller, value_pointer, value)?; + offset = offset + .checked_add(value.len()) + .ok_or(GuestMemoryError::AddressOverflow)?; + } + Ok(()) +} diff --git a/crates/execution/src/wasm/wasmtime/mod.rs b/crates/execution/src/wasm/wasmtime/mod.rs new file mode 100644 index 0000000000..0489783298 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/mod.rs @@ -0,0 +1,26 @@ +//! Wasmtime standalone-WebAssembly backend. +//! +//! This module is an ABI adapter over AgentOS host capabilities. It never owns +//! filesystem, descriptor, socket, process, terminal, signal, identity, or +//! permission semantics, and it deliberately does not construct a +//! `wasmtime-wasi` context. + +mod cache; +mod engine; +mod error; +mod lifecycle; +mod limits; +mod linker; +mod memory; +mod module; +mod store; + +pub use engine::{ + WasmtimeEngineHandle, WasmtimeEngineProfile, WasmtimeEngineRegistry, WasmtimeFeatureProfile, + WasmtimeMetricsSnapshot, DEFAULT_WASM_STACK_BYTES, HOST_CALL_STACK_HEADROOM_BYTES, +}; +pub use lifecycle::{WasmtimeExecution, WasmtimeExecutionEngine}; +pub use limits::DEFAULT_TABLE_ACCOUNTING_BYTES; + +pub const PINNED_WASMTIME_VERSION: &str = "46.0.0"; +pub const TRUSTED_INITIAL_MODULE_PREFIX: &str = "agentos-trusted-initial:"; diff --git a/crates/execution/src/wasm/wasmtime/module.rs b/crates/execution/src/wasm/wasmtime/module.rs new file mode 100644 index 0000000000..a7adbc872f --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/module.rs @@ -0,0 +1,24 @@ +//! Shared-profile module compilation. + +use super::super::profile::validate_locked_profile; +use super::engine::WasmtimeEngineHandle; +use crate::backend::HostServiceError; +use std::sync::Arc; +use wasmtime::Module; + +pub fn compile_module( + engine: &WasmtimeEngineHandle, + bytes: &[u8], +) -> Result, HostServiceError> { + validate_locked_profile(bytes)?; + engine + .modules() + .lock() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_POISONED", + "compiled Module cache lock is poisoned", + ) + })? + .get_or_compile(engine.engine(), bytes) +} diff --git a/crates/execution/src/wasm/wasmtime/store.rs b/crates/execution/src/wasm/wasmtime/store.rs new file mode 100644 index 0000000000..ced6241ea3 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/store.rs @@ -0,0 +1,635 @@ +//! Per-execution Store state backed by AgentOS host capabilities. + +use super::super::{guest_visible_wasm_env, StartWasmExecutionRequest, WasmExecutionEvent}; +use super::engine::{WasmtimeEngineHandle, WasmtimeEngineProfile}; +use super::lifecycle::QueuedWasmtimeEvent; +use super::limits; +use crate::backend::{ + direct_host_reply_channel, HostCallIdentity, HostCallReply, HostServiceError, +}; +use crate::host::{HostOperation, HostProcessContext, ProcessHostCapabilitySet}; +use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger}; +use agentos_runtime::RuntimeContext; +use flume::Sender; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use tokio::sync::Notify; +use wasmtime::{Store, StoreLimits, UpdateDeadline}; + +pub const DEFAULT_MAX_HOST_REPLY_BYTES: usize = 16 * 1024 * 1024; + +pub struct PendingExecReplacement { + pub module: Arc, + pub argv: Vec, + pub env: BTreeMap, +} + +/// One generation-bound direct waiter namespace shared by module loading and +/// every import issued by a Store. It owns no sidecar or kernel state. +#[derive(Clone)] +pub struct WasmtimeHostClient { + host: ProcessHostCapabilitySet, + next_call_id: Arc, + max_host_reply_bytes: usize, + cancelled: Arc, + cancel_notify: Arc, + signal_pending: Arc, + resources: Arc, + events: Sender, + event_notify: Option>, +} + +impl WasmtimeHostClient { + #[allow(clippy::too_many_arguments)] + pub fn new( + host: ProcessHostCapabilitySet, + max_host_reply_bytes: usize, + cancelled: Arc, + cancel_notify: Arc, + signal_pending: Arc, + resources: Arc, + events: Sender, + event_notify: Option>, + ) -> Self { + Self { + host, + next_call_id: Arc::new(AtomicU64::new(1)), + max_host_reply_bytes, + cancelled, + cancel_notify, + signal_pending, + resources, + events, + event_notify, + } + } + + pub fn process(&self) -> HostProcessContext { + self.host.process() + } + + pub fn canceled(&self) -> bool { + self.cancelled.load(Ordering::Acquire) + } + + pub fn signal_pending(&self) -> bool { + self.signal_pending.load(Ordering::Acquire) + } + + pub async fn submit( + &self, + operation: HostOperation, + retained_request_bytes: usize, + ) -> Result { + if self.canceled() { + return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled before host-call admission", + )); + } + let call_id = self + .next_call_id + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { + current.checked_add(1) + }) + .map_err(|_| { + HostServiceError::new( + "EOVERFLOW", + "Wasmtime host-call identity space is exhausted", + ) + })?; + let process = self.host.process(); + let identity = HostCallIdentity { + generation: process.generation, + pid: process.pid, + call_id, + }; + let admission = self.host.admit_request(retained_request_bytes)?; + let (reply, receiver) = direct_host_reply_channel(identity, self.max_host_reply_bytes)?; + self.host.submit(operation, reply, admission)?; + tokio::select! { + reply = receiver => reply, + () = self.cancel_notify.notified() => Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled while awaiting a host operation", + )), + } + } + + /// Route an owned native ABI request through the shared compatibility + /// decoder and then the common typed host-operation dispatcher. The reply + /// capability is direct and call-specific; this path never uses V8's + /// synchronous line protocol or scans an event stream for a response. + pub async fn submit_adapter_call( + &self, + method: String, + args: Vec, + raw_bytes_args: HashMap>, + ) -> Result { + if self.canceled() { + return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled before adapter-call admission", + )); + } + let call_id = self + .next_call_id + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { + current.checked_add(1) + }) + .map_err(|_| { + HostServiceError::new( + "EOVERFLOW", + "Wasmtime host-call identity space is exhausted", + ) + })?; + let process = self.host.process(); + let identity = HostCallIdentity { + generation: process.generation, + pid: process.pid, + call_id, + }; + let (reply, receiver) = direct_host_reply_channel(identity, self.max_host_reply_bytes)?; + let request = crate::javascript::HostRpcRequest { + id: call_id, + method, + args, + raw_bytes_args, + }; + let retained_event_bytes = request + .method + .len() + .checked_add( + serde_json::to_vec(&request.args) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_HOST_CALL_ENCODING", + error.to_string(), + ) + })? + .len(), + ) + .and_then(|total| { + request + .raw_bytes_args + .values() + .try_fold(total, |total, bytes| total.checked_add(bytes.len())) + }) + .ok_or_else(|| { + HostServiceError::new( + "EOVERFLOW", + "Wasmtime host-call retained byte accounting overflowed", + ) + })?; + let event = QueuedWasmtimeEvent::new( + &self.resources, + Ok(WasmExecutionEvent::HostCall { request, reply }), + retained_event_bytes, + )?; + tokio::select! { + result = self.events.send_async(event) => result.map_err(|_| HostServiceError::new( + "EPIPE", + "Wasmtime host-call event receiver was dropped", + ))?, + () = self.cancel_notify.notified() => return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled while admitting an adapter call", + )), + } + if let Some(notify) = self.event_notify.as_ref() { + notify.notify_one(); + } + tokio::select! { + reply = receiver => reply, + () = self.cancel_notify.notified() => Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled while awaiting an adapter call", + )), + } + } + + pub async fn publish_stderr(&self, bytes: Vec) -> Result<(), HostServiceError> { + let retained_bytes = bytes.len(); + let event = QueuedWasmtimeEvent::new( + &self.resources, + Ok(WasmExecutionEvent::Stderr(bytes)), + retained_bytes, + )?; + tokio::select! { + result = self.events.send_async(event) => { + result.map_err(|_| HostServiceError::new( + "EPIPE", + "Wasmtime stderr event receiver was dropped", + ))?; + } + () = self.cancel_notify.notified() => return Err(HostServiceError::new( + "ECANCELED", + "Wasmtime execution was canceled while publishing stderr", + )), + } + if let Some(notify) = self.event_notify.as_ref() { + notify.notify_one(); + } + Ok(()) + } +} + +pub struct WasmtimeStoreState { + pub host: WasmtimeHostClient, + pub engine: Arc, + pub argv: Vec>, + pub env: Vec>, + pub virtual_pid: u32, + pub virtual_ppid: u32, + pub limits: StoreLimits, + pub exit_code: Option, + pub exec_replaced: bool, + pub pending_exec_replacement: Option, + pub max_module_file_bytes: usize, + pub max_blocking_read_ms: u64, + pub max_spawn_file_actions: usize, + pub max_spawn_file_action_bytes: usize, + pub warned_fixed_limits: HashSet<&'static str>, + pub pending_open_mode: Option, + pub pending_open_direct: bool, + active_cpu_limit_ns: Option, + active_cpu_started_ns: u64, + paused: Arc, + pause_notify: Arc, + _async_stack_reservation: Reservation, + _guest_memory_reservation: Reservation, +} + +impl std::fmt::Debug for WasmtimeStoreState { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("WasmtimeStoreState") + .field("process", &self.host.process()) + .field("argv_count", &self.argv.len()) + .field("env_count", &self.env.len()) + .field("exit_code", &self.exit_code) + .finish_non_exhaustive() + } +} + +impl WasmtimeStoreState { + #[allow(clippy::too_many_arguments)] + pub fn new( + runtime: &RuntimeContext, + host: WasmtimeHostClient, + engine: Arc, + request: &StartWasmExecutionRequest, + profile: WasmtimeEngineProfile, + active_cpu_started_ns: u64, + paused: Arc, + pause_notify: Arc, + ) -> Result { + let async_stack_bytes = profile.async_stack_bytes()?; + let async_stack_reservation = runtime + .resources() + .reserve(ResourceClass::WasmMemoryBytes, async_stack_bytes) + .map_err(|error| { + HostServiceError::new("ERR_AGENTOS_WASMTIME_ASYNC_STACK_LIMIT", error.to_string()) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmStackBytes", + "observed": async_stack_bytes, + })) + })?; + let linear_memory_bytes = limits::max_memory_bytes(&request.limits)?; + let aggregate_memory_bytes = limits::aggregate_store_memory_bytes(&request.limits)?; + let guest_memory_reservation = runtime + .resources() + .reserve(ResourceClass::WasmMemoryBytes, aggregate_memory_bytes) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_AGGREGATE_MEMORY_LIMIT", + error.to_string(), + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmMemoryBytes", + "observed": aggregate_memory_bytes, + "linearMemoryBytes": linear_memory_bytes, + "tableAccountingBytes": limits::DEFAULT_TABLE_ACCOUNTING_BYTES, + "resource": "wasmtimeGuestMemory", + })) + })?; + let argv = nul_terminated_strings(request.argv.iter().map(String::as_str), "argv")?; + let guest_env = guest_visible_wasm_env(&request.env); + let env = nul_terminated_strings( + guest_env + .iter() + .map(|(key, value)| format!("{key}={value}")), + "environment", + )?; + let virtual_pid = request + .guest_runtime + .virtual_pid + .and_then(|value| u32::try_from(value).ok()) + .unwrap_or_else(|| host.process().pid); + let virtual_ppid = request + .guest_runtime + .virtual_ppid + .and_then(|value| u32::try_from(value).ok()) + .unwrap_or_default(); + Ok(Self { + host, + engine, + argv, + env, + virtual_pid, + virtual_ppid, + limits: limits::store_limits(&request.limits)?, + exit_code: None, + exec_replaced: false, + pending_exec_replacement: None, + max_module_file_bytes: request + .limits + .max_module_file_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| { + HostServiceError::new("EFBIG", "module byte limit does not fit this platform") + })? + .unwrap_or(256 * 1024 * 1024), + max_blocking_read_ms: request.limits.max_blocking_read_ms.unwrap_or(30_000), + max_spawn_file_actions: request + .limits + .max_spawn_file_actions + .map(usize::try_from) + .transpose() + .map_err(|_| { + HostServiceError::new( + "E2BIG", + "spawn file-action count limit does not fit this platform", + ) + })? + .unwrap_or(4096), + max_spawn_file_action_bytes: request + .limits + .max_spawn_file_action_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| { + HostServiceError::new( + "E2BIG", + "spawn file-action byte limit does not fit this platform", + ) + })? + .unwrap_or(1024 * 1024), + warned_fixed_limits: HashSet::new(), + pending_open_mode: None, + pending_open_direct: false, + active_cpu_limit_ns: request + .limits + .active_cpu_time_limit_ms + .map(|value| u64::from(value).saturating_mul(1_000_000)), + active_cpu_started_ns, + paused, + pause_notify, + _async_stack_reservation: async_stack_reservation, + _guest_memory_reservation: guest_memory_reservation, + }) + } + + pub fn canceled(&self) -> bool { + self.host.canceled() + } + + fn active_cpu_exhausted(&self) -> bool { + self.active_cpu_limit_ns.is_some_and(|limit| { + thread_cpu_time_ns().saturating_sub(self.active_cpu_started_ns) >= limit + }) + } + + fn pause_waiter(&self) -> Option<(Arc, Arc)> { + self.paused + .load(Ordering::Acquire) + .then(|| (Arc::clone(&self.paused), Arc::clone(&self.pause_notify))) + } +} + +pub fn max_host_reply_bytes( + request: &StartWasmExecutionRequest, +) -> Result { + request + .limits + .max_sync_rpc_response_line_bytes + .map(usize::try_from) + .transpose() + .map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_LIMIT_CONFIG", + "limits.reactor.maxBridgeResponseBytes does not fit this platform", + ) + }) + .map(|value| value.unwrap_or(DEFAULT_MAX_HOST_REPLY_BYTES)) +} + +#[allow(clippy::too_many_arguments)] +pub fn create_store( + engine: Arc, + runtime: &RuntimeContext, + host: WasmtimeHostClient, + request: &StartWasmExecutionRequest, + profile: WasmtimeEngineProfile, + active_cpu_started_ns: u64, + paused: Arc, + pause_notify: Arc, +) -> Result, HostServiceError> { + let deterministic_fuel = request.limits.deterministic_fuel; + let mut store = Store::new( + engine.engine(), + WasmtimeStoreState::new( + runtime, + host, + Arc::clone(&engine), + request, + profile, + active_cpu_started_ns, + paused, + pause_notify, + )?, + ); + store.limiter(|state| &mut state.limits); + // Fuel instrumentation is enabled on every Engine so the optional policy + // is Store-local. A Store with no requested deterministic budget receives + // effectively unbounded fuel while active CPU time remains epoch-managed. + store + .set_fuel(deterministic_fuel.unwrap_or(u64::MAX)) + .map_err(|error| { + eprintln!("ERR_AGENTOS_WASMTIME_FUEL_CONFIG: private Store diagnostic: {error:#}"); + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_FUEL_CONFIG", + "failed to configure the deterministic execution budget", + ) + })?; + store.set_epoch_deadline(1); + store.epoch_deadline_callback(|context| { + if context.data().canceled() { + return Err(wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_CANCELED: execution canceled" + )); + } + if context.data().active_cpu_exhausted() { + return Err(wasmtime::format_err!( + "ERR_AGENTOS_WASMTIME_ACTIVE_CPU_LIMIT: active CPU budget exhausted" + )); + } + if let Some((paused, notify)) = context.data().pause_waiter() { + return Ok(UpdateDeadline::YieldCustom( + 1, + Box::pin(async move { + loop { + let notified = notify.notified(); + if !paused.load(Ordering::Acquire) { + break; + } + notified.await; + } + }), + )); + } + Ok(UpdateDeadline::Yield(1)) + }); + Ok(store) +} + +#[cfg(unix)] +pub(super) fn thread_cpu_time_ns() -> u64 { + let value = match nix::time::clock_gettime(nix::time::ClockId::CLOCK_THREAD_CPUTIME_ID) { + Ok(value) => value, + Err(error) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_CPU_CLOCK: failed to read guest executor CPU clock: {error}" + ); + return 0; + } + }; + u64::try_from(value.tv_sec()) + .unwrap_or_default() + .saturating_mul(1_000_000_000) + .saturating_add(u64::try_from(value.tv_nsec()).unwrap_or_default()) +} + +#[cfg(not(unix))] +pub(super) fn thread_cpu_time_ns() -> u64 { + 0 +} + +fn nul_terminated_strings( + values: I, + field: &'static str, +) -> Result>, HostServiceError> +where + I: IntoIterator, + S: AsRef, +{ + values + .into_iter() + .map(|value| { + let value = value.as_ref(); + if value.as_bytes().contains(&0) { + return Err(HostServiceError::new( + "EINVAL", + format!("WebAssembly {field} value contains an interior NUL"), + )); + } + let mut bytes = Vec::with_capacity(value.len().saturating_add(1)); + bytes.extend_from_slice(value.as_bytes()); + bytes.push(0); + Ok(bytes) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{bounded_execution_event_channel, PayloadLimit}; + use crate::wasm::{GuestRuntimeConfig, WasmExecutionLimits, WasmPermissionTier}; + use agentos_runtime::accounting::{ResourceLedger, ResourceLimit}; + use agentos_runtime::{RuntimeConfig, SidecarRuntime}; + use std::path::PathBuf; + + #[test] + fn store_reservations_are_released_on_teardown() { + let runtime = SidecarRuntime::process(&RuntimeConfig::default()).expect("test runtime"); + let process = crate::host::HostProcessContext { + generation: 97, + pid: 41, + }; + let resources = Arc::new(ResourceLedger::child( + "wasmtime-store-teardown-test", + [( + ResourceClass::WasmMemoryBytes, + ResourceLimit::new(1024 * 1024 * 1024, "runtime.resources.maxWasmMemoryBytes"), + )], + Arc::clone(runtime.context().resources()), + )); + let scoped = runtime + .context() + .scoped_for_vm(Arc::clone(&resources), process.generation); + let (submission, _host_events) = bounded_execution_event_channel( + process, + 4, + PayloadLimit::new("limits.process.pendingEventBytes", 1024).expect("event byte limit"), + Arc::new(|| {}), + ) + .expect("host event channel"); + let (events, _event_receiver) = flume::bounded(4); + let host = WasmtimeHostClient::new( + ProcessHostCapabilitySet::from_event_submission(submission), + DEFAULT_MAX_HOST_REPLY_BYTES, + Arc::new(AtomicBool::new(false)), + Arc::new(Notify::new()), + Arc::new(AtomicBool::new(false)), + Arc::clone(&resources), + events, + None, + ); + let request = StartWasmExecutionRequest { + vm_id: String::from("vm-store-teardown"), + context_id: String::from("ctx-store-teardown"), + managed_kernel_host: true, + argv: vec![String::from("/test.wasm")], + env: BTreeMap::new(), + cwd: PathBuf::from("/"), + permission_tier: WasmPermissionTier::Full, + limits: WasmExecutionLimits::default(), + guest_runtime: GuestRuntimeConfig::default(), + }; + let profile = + WasmtimeEngineProfile::new(request.limits.max_stack_bytes).expect("engine profile"); + let engine = super::super::engine::WasmtimeEngineRegistry::new(1) + .get_or_create(profile) + .expect("engine"); + let expected = profile + .async_stack_bytes() + .expect("async stack") + .saturating_add( + limits::aggregate_store_memory_bytes(&request.limits) + .expect("aggregate store bytes"), + ); + + let state = WasmtimeStoreState::new( + &scoped, + host, + engine, + &request, + profile, + thread_cpu_time_ns(), + Arc::new(AtomicBool::new(false)), + Arc::new(Notify::new()), + ) + .expect("store state"); + assert_eq!( + resources.usage(ResourceClass::WasmMemoryBytes).used, + expected + ); + drop(state); + assert_eq!(resources.usage(ResourceClass::WasmMemoryBytes).used, 0); + assert!(resources.integrity_ok()); + } +} diff --git a/crates/execution/tests/wasm.rs b/crates/execution/tests/wasm.rs index 0952b1d2a5..96393a69ef 100644 --- a/crates/execution/tests/wasm.rs +++ b/crates/execution/tests/wasm.rs @@ -1640,6 +1640,9 @@ fn wasm_execution_streams_exit_event_after_signal_mask_commit() { .expect("commit the initial standalone signal mask"); saw_initial_signal_mask = true; } + Some(WasmExecutionEvent::HostCall { .. }) => { + panic!("V8 compatibility test received a native Wasmtime host call") + } Some(WasmExecutionEvent::SignalState { .. }) => {} None => panic!("timed out waiting for wasm execution event"), } @@ -1889,6 +1892,9 @@ fn wasm_execution_waits_for_kernel_signal_state_commit_before_continuing() { Some(WasmExecutionEvent::SignalState { .. }) => { panic!("managed signal state must use the reply-bearing host-call path") } + Some(WasmExecutionEvent::HostCall { .. }) => { + panic!("V8 compatibility test received a native Wasmtime host call") + } None => panic!("timed out waiting for wasm execution event"), } } @@ -1959,6 +1965,9 @@ fn wasm_execution_preserves_stdout_when_signal_state_marker_shares_stdout_chunk( Some(WasmExecutionEvent::SyncRpcRequest(request)) => { panic!("unexpected sync RPC request: {request:?}") } + Some(WasmExecutionEvent::HostCall { .. }) => { + panic!("V8 compatibility test received a native Wasmtime host call") + } None => panic!("timed out waiting for wasm execution event"), } } @@ -2026,6 +2035,9 @@ fn wasm_execution_reassembles_split_signal_state_marker_across_stdout_chunks() { Some(WasmExecutionEvent::SyncRpcRequest(request)) => { panic!("unexpected sync RPC request: {request:?}") } + Some(WasmExecutionEvent::HostCall { .. }) => { + panic!("V8 compatibility test received a native Wasmtime host call") + } None => panic!("timed out waiting for wasm execution event"), } } @@ -2462,6 +2474,9 @@ fn wasm_execution_poll_path_times_out_at_wall_clock_limit() { Some(WasmExecutionEvent::SyncRpcRequest(request)) => { panic!("unexpected sync RPC request: {request:?}") } + Some(WasmExecutionEvent::HostCall { .. }) => { + panic!("V8 compatibility test received a native Wasmtime host call") + } Some(WasmExecutionEvent::Stdout(_)) | Some(WasmExecutionEvent::SignalState { .. }) | None => {} diff --git a/crates/kernel/src/fd_table.rs b/crates/kernel/src/fd_table.rs index 1bf0139315..e8f67ea81e 100644 --- a/crates/kernel/src/fd_table.rs +++ b/crates/kernel/src/fd_table.rs @@ -8,6 +8,12 @@ use crate::vfs::VirtualStat; pub const MAX_FDS_PER_PROCESS: usize = 1024; +// The owned libc uses this private descriptor namespace for immutable WASI +// capability roots. A Linux program may close or replace the visible preopen +// fd while absolute-path libc operations must retain their kernel-owned root. +pub const WASI_HIDDEN_PREOPEN_FD_TAG: u32 = 0x4000_0000; +pub const WASI_HIDDEN_PREOPEN_FD_MASK: u32 = 0x3fff_ffff; + pub const O_RDONLY: u32 = 0; pub const O_WRONLY: u32 = 1; pub const O_RDWR: u32 = 2; @@ -1044,20 +1050,24 @@ impl FlockOperation { #[derive(Debug, Clone)] pub struct ProcessFdTable { entries: BTreeMap, + hidden_wasi_preopens: BTreeMap, next_fd: u32, alloc_desc: DescriptionFactory, max_fds: usize, wasi_preopens_initialized: bool, + canonical_wasi_layout_initialized: bool, } impl ProcessFdTable { fn new(alloc_desc: DescriptionFactory, max_fds: usize) -> Self { Self { entries: BTreeMap::new(), + hidden_wasi_preopens: BTreeMap::new(), next_fd: 3, alloc_desc, max_fds, wasi_preopens_initialized: false, + canonical_wasi_layout_initialized: false, } } @@ -1297,8 +1307,7 @@ impl ProcessFdTable { pub fn transfer(&self, fd: u32) -> FdResult { let entry = self - .entries - .get(&fd) + .get(fd) .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; entry.description.increment_ref_count(); Ok(TransferredFd { @@ -1424,6 +1433,9 @@ impl ProcessFdTable { } pub fn get(&self, fd: u32) -> Option<&FdEntry> { + if fd & WASI_HIDDEN_PREOPEN_FD_TAG != 0 { + return self.hidden_wasi_preopens.get(&fd); + } self.entries.get(&fd) } @@ -1431,6 +1443,86 @@ impl ProcessFdTable { self.entries.values() } + /// Canonicalize the initial direct-executor descriptor namespace. + /// + /// WASI capability roots occupy the stable range beginning at fd 3. All + /// other inherited descriptors retain their sorted order immediately + /// after that range. This is the same layout the V8 compatibility adapter + /// projects, but it is committed once in the kernel so a direct executor + /// never needs a shadow fd table. This operation is only valid before + /// guest code starts. + pub fn canonicalize_initial_wasi_layout(&mut self) -> FdResult<()> { + if self.canonical_wasi_layout_initialized { + return Ok(()); + } + let preopen_fds = self + .entries + .values() + .filter(|entry| entry.wasi_preopen_path.is_some()) + .map(|entry| entry.fd) + .collect::>(); + if preopen_fds.is_empty() { + self.canonical_wasi_layout_initialized = true; + return Ok(()); + } + let inherited_fds = self + .entries + .values() + .filter(|entry| entry.fd > 2 && entry.wasi_preopen_path.is_none()) + .map(|entry| entry.fd) + .collect::>(); + let required = 3usize + .checked_add(preopen_fds.len()) + .and_then(|value| value.checked_add(inherited_fds.len())) + .ok_or_else(FdTableError::too_many_open_files)?; + if required > self.max_fds { + return Err(FdTableError::too_many_open_files()); + } + + let mut targets = BTreeMap::new(); + for fd in self.entries.keys().copied().filter(|fd| *fd <= 2) { + targets.insert(fd, fd); + } + for (index, fd) in preopen_fds.into_iter().enumerate() { + targets.insert(fd, 3 + index as u32); + } + let inherited_base = 3 + targets.values().filter(|fd| **fd > 2).count() as u32; + for (index, fd) in inherited_fds.into_iter().enumerate() { + targets.insert(fd, inherited_base + index as u32); + } + + let entries = std::mem::take(&mut self.entries); + for (source, mut entry) in entries { + let target = targets.get(&source).copied().ok_or_else(|| { + FdTableError::invalid_argument(format!( + "initial WASI layout omitted descriptor {source}" + )) + })?; + entry.fd = target; + if self.entries.insert(target, entry).is_some() { + return Err(FdTableError::invalid_argument(format!( + "initial WASI layout assigned descriptor {target} twice" + ))); + } + } + self.next_fd = (3..self.max_fds as u32) + .find(|fd| !self.entries.contains_key(fd)) + .unwrap_or_default(); + for entry in self + .entries + .values() + .filter(|entry| entry.wasi_preopen_path.is_some()) + { + let hidden_fd = WASI_HIDDEN_PREOPEN_FD_TAG | entry.fd; + entry.description.increment_ref_count(); + let mut hidden = entry.clone(); + hidden.fd = hidden_fd; + self.hidden_wasi_preopens.insert(hidden_fd, hidden); + } + self.canonical_wasi_layout_initialized = true; + Ok(()) + } + /// Mark the one-time installation of WASI capability roots for this /// process image. Forked processes inherit this state so spawn file /// actions that deliberately close a preopen cannot cause it to be @@ -1498,6 +1590,9 @@ impl ProcessFdTable { let closed = self.close(fd); debug_assert!(closed); } + for (_, entry) in std::mem::take(&mut self.hidden_wasi_preopens) { + entry.description.decrement_ref_count(); + } return; } let base = rights_base.expect("checked above"); @@ -1509,6 +1604,10 @@ impl ProcessFdTable { entry.rights &= base; entry.rights_inheriting &= inheriting; } + for entry in self.hidden_wasi_preopens.values_mut() { + entry.rights &= base; + entry.rights_inheriting &= inheriting; + } } pub fn close(&mut self, fd: u32) -> bool { @@ -1578,8 +1677,7 @@ impl ProcessFdTable { pub fn stat(&self, fd: u32) -> FdResult { let entry = self - .entries - .get(&fd) + .get(fd) .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; Ok(FdStat { filetype: entry.filetype, @@ -1659,6 +1757,7 @@ impl ProcessFdTable { let mut child = Self::new(self.alloc_desc.clone(), self.max_fds); child.next_fd = self.next_fd; child.wasi_preopens_initialized = self.wasi_preopens_initialized; + child.canonical_wasi_layout_initialized = self.canonical_wasi_layout_initialized; for (fd, entry) in &self.entries { // Kernel process creation is spawn (fork + exec combined), so @@ -1685,6 +1784,10 @@ impl ProcessFdTable { }, ); } + for (fd, entry) in &self.hidden_wasi_preopens { + entry.description.increment_ref_count(); + child.hidden_wasi_preopens.insert(*fd, entry.clone()); + } child } @@ -1701,6 +1804,9 @@ impl ProcessFdTable { for fd in fds { self.close(fd); } + for (_, entry) in std::mem::take(&mut self.hidden_wasi_preopens) { + entry.description.decrement_ref_count(); + } } pub fn len(&self) -> usize { @@ -2467,6 +2573,78 @@ mod wasi_preopen_tests { assert!(table.stat(fd).is_err()); } + #[test] + fn canonical_layout_resolves_collisions_and_protects_hidden_preopen_aliases() { + let mut manager = FdTableManager::new(); + let table = manager.create(8); + let inherited_three = table.open("pipe:three", O_RDWR).expect("open fd 3"); + let inherited_four = table.open("pipe:four", O_RDWR).expect("open fd 4"); + assert_eq!((inherited_three, inherited_four), (3, 4)); + let inherited_ids = ( + table.get(3).expect("fd 3").description.id(), + table.get(4).expect("fd 4").description.id(), + ); + let preopen = table + .open_with_details("/", O_DIRECTORY, FILETYPE_DIRECTORY, None) + .expect("open preopen directory"); + table + .mark_wasi_preopen( + preopen, + String::from("/"), + WASI_PREOPEN_WRITE_RIGHTS_BASE, + WASI_PREOPEN_WRITE_RIGHTS_INHERITING, + ) + .expect("mark preopen"); + let preopen_id = table.get(preopen).expect("preopen").description.id(); + + table + .canonicalize_initial_wasi_layout() + .expect("canonicalize initial descriptors"); + assert_eq!( + table.get(3).expect("visible preopen").description.id(), + preopen_id + ); + assert_eq!( + table.get(4).expect("first inherited").description.id(), + inherited_ids.0 + ); + assert_eq!( + table.get(5).expect("second inherited").description.id(), + inherited_ids.1 + ); + + let hidden = WASI_HIDDEN_PREOPEN_FD_TAG | 3; + assert_eq!( + table.get(hidden).expect("hidden preopen").description.id(), + preopen_id + ); + assert!(table.close(3)); + assert!(table.stat(3).is_err()); + assert_eq!( + table + .stat(hidden) + .expect("hidden preopen survives visible close") + .wasi_preopen_path + .as_deref(), + Some("/") + ); + + // Executor initialization is idempotent and must not recreate a guest + // fd that the process deliberately closed. + table + .canonicalize_initial_wasi_layout() + .expect("repeat canonicalization"); + assert!(table.stat(3).is_err()); + + let mut child = table.fork(); + assert_eq!( + child.stat(hidden).expect("forked hidden preopen").rights, + WASI_PREOPEN_WRITE_RIGHTS_BASE + ); + child.restrict_wasi_preopens(None, None); + assert!(child.stat(hidden).is_err()); + } + #[test] fn descriptor_rights_survive_dup_fork_and_transfer_exactly() { let mut manager = FdTableManager::new(); diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 2694658120..240196e3c5 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -4683,6 +4683,26 @@ impl KernelVm { self.wasi_preopens(requester_driver, pid) } + /// Install WASI capability roots and commit the direct executor's initial + /// guest-visible descriptor layout into the authoritative kernel table. + /// This must run before guest code starts and replaces executor-local fd + /// projections for runtimes that can call the kernel ABI directly. + pub fn initialize_canonical_wasi_preopens( + &mut self, + requester_driver: &str, + pid: u32, + ) -> KernelResult> { + self.initialize_wasi_preopens(requester_driver, pid)?; + { + let mut tables = lock_or_recover(&self.fd_tables); + tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .canonicalize_initial_wasi_layout()?; + } + self.wasi_preopens(requester_driver, pid) + } + pub fn wasi_preopens( &self, requester_driver: &str, @@ -6462,6 +6482,7 @@ impl KernelVm { /// Open a descriptor under the kernel-owned Preview1 capability model. /// Parent/tier/access validation is complete before `fd_open` can create /// or truncate a path. `None` requests Linux-style synthesized rights. + #[allow(clippy::too_many_arguments)] pub fn fd_open_with_rights( &mut self, requester_driver: &str, @@ -8839,6 +8860,7 @@ impl KernelVm { Ok(None) } + #[allow(clippy::type_complexity)] fn prepare_detached_directory_backing( &mut self, path: &str, diff --git a/crates/kernel/src/process_table.rs b/crates/kernel/src/process_table.rs index 07b122b6ec..f4d477a572 100644 --- a/crates/kernel/src/process_table.rs +++ b/crates/kernel/src/process_table.rs @@ -855,6 +855,7 @@ impl ProcessTable { /// signals, and the runtime endpoint used to report the eventual exit. /// Caught dispositions reset to default while ignored dispositions survive, /// matching execve(2). + #[allow(clippy::too_many_arguments)] pub fn exec( &self, pid: u32, diff --git a/crates/native-sidecar-core/src/limits.rs b/crates/native-sidecar-core/src/limits.rs index e1d2703485..a79f64722c 100644 --- a/crates/native-sidecar-core/src/limits.rs +++ b/crates/native-sidecar-core/src/limits.rs @@ -1909,7 +1909,8 @@ mod tests { #[test] fn process_queue_limits_reject_zero_before_runtime_construction() { - let cases: [(&str, fn(&mut ProcessLimitsConfig)); 3] = [ + type ProcessLimitMutation = fn(&mut ProcessLimitsConfig); + let cases: [(&str, ProcessLimitMutation); 3] = [ ("limits.process.pending_stdin_bytes", |process| { process.pending_stdin_bytes = Some(0) }), diff --git a/crates/native-sidecar-core/src/root_fs.rs b/crates/native-sidecar-core/src/root_fs.rs index 96b6b676e8..137a9de898 100644 --- a/crates/native-sidecar-core/src/root_fs.rs +++ b/crates/native-sidecar-core/src/root_fs.rs @@ -194,8 +194,8 @@ pub fn build_root_filesystem_with_loaded_snapshot( for lower in &mut descriptor.lowers { lower.entries.retain(|entry| { let path = normalize_path(&entry.path); - !(bootstrap_replaces_bundle && path == crate::ca::CA_CERTIFICATES_GUEST_PATH) - && !(bootstrap_replaces_cert_pem && path == crate::ca::CA_CERTIFICATES_SYMLINK_PATH) + !(bootstrap_replaces_bundle && path == crate::ca::CA_CERTIFICATES_GUEST_PATH + || bootstrap_replaces_cert_pem && path == crate::ca::CA_CERTIFICATES_SYMLINK_PATH) }); } descriptor.lowers.push(default_services_snapshot()); diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index 1283cc82f1..730e86c66a 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -3872,6 +3872,7 @@ where Ok(parent.child_processes.keys().cloned().collect()) } + #[allow(clippy::too_many_arguments)] fn rollback_registered_child_process_sync( &mut self, vm_id: &str, @@ -4361,6 +4362,16 @@ where _ => None, } } else { + if parent.runtime == GuestRuntimeKind::WebAssembly + && parent.standalone_wasm_backend == ExecutionStandaloneWasmBackend::Wasmtime + { + // Native Wasmtime children publish output through their + // inherited kernel descriptors and settle wait(2) through + // the guest-owned kernel process table. The proactive + // sidecar pump may still retire the child here, but there + // is no V8 stream session to notify. + return Ok(true); + } let payload = match event_type { "stdout" => json!({ "sessionId": child_process_id, @@ -5574,7 +5585,13 @@ where let total_start = Instant::now(); let process_event_capacity = self.config.runtime.protocol.max_process_events; let phase_start = Instant::now(); - let (parent_env, parent_guest_cwd, parent_host_cwd, parent_kernel_pid) = { + let ( + parent_env, + parent_guest_cwd, + parent_host_cwd, + parent_kernel_pid, + standalone_wasm_backend, + ) = { let vm = self .vms .get_mut(vm_id) @@ -5588,6 +5605,7 @@ where parent.guest_cwd.clone(), parent.host_cwd.clone(), parent.kernel_pid, + parent.standalone_wasm_backend, ) }; let mut resolved = @@ -6031,9 +6049,16 @@ where Some(u64::from(kernel_pid)), Some(u64::from(parent_kernel_pid)), ); + let module_path = match standalone_wasm_backend { + ExecutionStandaloneWasmBackend::Wasmtime => execution_env + .get("AGENTOS_GUEST_ENTRYPOINT") + .cloned() + .unwrap_or_else(|| resolved.entrypoint.clone()), + ExecutionStandaloneWasmBackend::V8 => resolved.entrypoint.clone(), + }; let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.to_owned(), - module_path: Some(resolved.entrypoint.clone()), + module_path: Some(module_path), }); let context_id = context.context_id; let runtime_control = attach_child_runtime_control!( @@ -6041,7 +6066,7 @@ where ); let execution_result = self .wasm_engine - .start_execution_with_runtime_async( + .start_execution_with_runtime_async_for_backend( StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), @@ -6058,6 +6083,7 @@ where guest_runtime: wasm_guest_runtime, }, vm.runtime_context.clone(), + standalone_wasm_backend, ) .await; self.wasm_engine.dispose_context(&context_id); @@ -6290,6 +6316,7 @@ where Arc::clone(&self.process_event_notify), ) .with_adapter_policy(resolved.adapter_policy) + .with_standalone_wasm_backend(standalone_wasm_backend) .with_process_event_limits(&process_event_limits) .with_vm_pending_byte_budgets( Arc::clone(&vm_pending_stdin_bytes_budget), @@ -6560,7 +6587,14 @@ where )); } - let (guest_cwd, host_cwd, kernel_pid, parent_kernel_pid, current_adapter_policy) = { + let ( + guest_cwd, + host_cwd, + kernel_pid, + parent_kernel_pid, + current_adapter_policy, + standalone_wasm_backend, + ) = { let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; let root = vm .active_processes @@ -6584,6 +6618,7 @@ where process.kernel_pid, parent_kernel_pid, process.adapter_policy, + process.standalone_wasm_backend, ) }; @@ -6848,14 +6883,21 @@ where )); execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); execution_env.insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); + let module_path = match standalone_wasm_backend { + ExecutionStandaloneWasmBackend::Wasmtime => execution_env + .get("AGENTOS_GUEST_ENTRYPOINT") + .cloned() + .unwrap_or_else(|| resolved.entrypoint.clone()), + ExecutionStandaloneWasmBackend::V8 => resolved.entrypoint.clone(), + }; let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.to_owned(), - module_path: Some(resolved.entrypoint.clone()), + module_path: Some(module_path), }); let context_id = context.context_id; let replacement_result = - self.wasm_engine - .prepare_execution(StartWasmExecutionRequest { + self.wasm_engine.prepare_execution_with_runtime_for_backend( + StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), managed_kernel_host: true, @@ -6877,7 +6919,10 @@ where Some(u64::from(kernel_pid)), Some(u64::from(parent_kernel_pid)), ), - }); + }, + vm.runtime_context.clone(), + standalone_wasm_backend, + ); self.wasm_engine.dispose_context(&context_id); ActiveExecution::Wasm(Box::new(replacement_result.map_err(wasm_error)?)) } @@ -7301,7 +7346,13 @@ where let total_start = Instant::now(); let process_event_capacity = self.config.runtime.protocol.max_process_events; let phase_start = Instant::now(); - let (parent_env, parent_guest_cwd, parent_host_cwd, parent_kernel_pid) = { + let ( + parent_env, + parent_guest_cwd, + parent_host_cwd, + parent_kernel_pid, + standalone_wasm_backend, + ) = { let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; let root = vm .active_processes @@ -7318,6 +7369,7 @@ where parent.guest_cwd.clone(), parent.host_cwd.clone(), parent.kernel_pid, + parent.standalone_wasm_backend, ) }; let mut resolved = @@ -7732,9 +7784,16 @@ where Some(u64::from(kernel_pid)), Some(u64::from(parent_kernel_pid)), ); + let module_path = match standalone_wasm_backend { + ExecutionStandaloneWasmBackend::Wasmtime => execution_env + .get("AGENTOS_GUEST_ENTRYPOINT") + .cloned() + .unwrap_or_else(|| resolved.entrypoint.clone()), + ExecutionStandaloneWasmBackend::V8 => resolved.entrypoint.clone(), + }; let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.to_owned(), - module_path: Some(resolved.entrypoint.clone()), + module_path: Some(module_path), }); let context_id = context.context_id; let runtime_control = ActiveProcess::attach_runtime_control_before_start( @@ -7743,7 +7802,7 @@ where )?; let execution_result = self .wasm_engine - .start_execution_with_runtime_async( + .start_execution_with_runtime_async_for_backend( StartWasmExecutionRequest { vm_id: vm_id.to_owned(), context_id: context_id.clone(), @@ -7763,6 +7822,7 @@ where guest_runtime: wasm_guest_runtime, }, vm.runtime_context.clone(), + standalone_wasm_backend, ) .await; self.wasm_engine.dispose_context(&context_id); @@ -8024,6 +8084,7 @@ where Arc::clone(&self.process_event_notify), ) .with_adapter_policy(resolved.adapter_policy) + .with_standalone_wasm_backend(standalone_wasm_backend) .with_process_event_limits(&process_event_limits) .with_vm_pending_byte_budgets( Arc::clone(&vm_pending_stdin_bytes_budget), @@ -8320,6 +8381,7 @@ where .await } + #[allow(clippy::too_many_arguments)] fn settle_descendant_managed_network_response( &self, vm_id: &str, @@ -10047,13 +10109,13 @@ where offset: None, deadline_ms: Some(timeout_ms), }, - ) => Some((Some(*fd), max_bytes.clone(), *timeout_ms)), + ) => Some((Some(*fd), *max_bytes, *timeout_ms)), HostOperation::Filesystem( agentos_execution::host::FilesystemOperation::StdinRead { max_bytes, timeout_ms, }, - ) => Some((None, max_bytes.clone(), *timeout_ms)), + ) => Some((None, *max_bytes, *timeout_ms)), _ => None, }; if let Some((fd, max_bytes, timeout_ms)) = deferred_kernel_read { @@ -11325,7 +11387,7 @@ where format!("unknown process pid {target_pid}"), )); }; - let action = if signal == 0 { + if signal == 0 { SignalDispositionAction::Default } else { protocol_signal_registration( @@ -11335,8 +11397,7 @@ where .map_err(kernel_error)?, ) .action - }; - action + } }; let Some(root) = vm.active_processes.get_mut(process_id) else { diff --git a/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs index b4b1eee45a..7bd81268d2 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs @@ -451,11 +451,30 @@ pub(super) fn decode( }, linkable: javascript_sync_rpc_option_bool(&request.args, 3, "linkable").unwrap_or(true), }, + "process.open_tmpfile_at" => FilesystemOperation::OpenTmpfileAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "unnamed-file dir fd")?, + path: path(1, "unnamed-file directory")?, + options: GuestOpenSpec { + flags: javascript_sync_rpc_arg_u32(&request.args, 2, "unnamed-file flags")?, + mode: Some(javascript_sync_rpc_arg_u32( + &request.args, + 3, + "unnamed-file mode", + )?), + rights: GuestOpenRights::Synthesized, + }, + linkable: javascript_sync_rpc_option_bool(&request.args, 4, "linkable").unwrap_or(true), + }, "fs.linkFdSync" => FilesystemOperation::LinkDescriptorAt { fd: javascript_sync_rpc_arg_u32(&request.args, 0, "unnamed-file fd")?, dir_fd: NODE_CWD_FD, path: path(1, "unnamed-file link destination")?, }, + "process.fd_link_at" => FilesystemOperation::LinkDescriptorAt { + fd: javascript_sync_rpc_arg_u32(&request.args, 0, "unnamed-file fd")?, + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 1, "unnamed-file link dir fd")?, + path: path(2, "unnamed-file link destination")?, + }, "fs.punchHoleSync" => FilesystemOperation::Range { fd: javascript_sync_rpc_arg_u32(&request.args, 0, "punch-hole fd")?, operation: FileRangeOperation::PunchHole, @@ -491,6 +510,10 @@ pub(super) fn decode( dir_fd: NODE_CWD_FD, path: path(0, "filesystem statfs path")?, }, + "process.path_statfs_at" => FilesystemOperation::FilesystemStatsAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "statfs dir fd")?, + path: path(1, "filesystem statfs path")?, + }, "fs.statSync" => FilesystemOperation::NodeStatAt { dir_fd: NODE_CWD_FD, path: path(0, "filesystem stat path")?, @@ -501,6 +524,12 @@ pub(super) fn decode( mode: javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem mknod mode")?, device: javascript_sync_rpc_arg_u64(&request.args, 2, "filesystem mknod device")?, }, + "process.path_mknod_at" => FilesystemOperation::MakeNodeAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "mknod dir fd")?, + path: path(1, "filesystem mknod path")?, + mode: javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem mknod mode")?, + device: javascript_sync_rpc_arg_u64(&request.args, 3, "filesystem mknod device")?, + }, "fs.remountSync" => FilesystemOperation::Remount { path: path(0, "filesystem remount path")?, options: BoundedString::try_new( @@ -517,6 +546,19 @@ pub(super) fn decode( new_path: path(1, "filesystem renameat2 destination")?, flags: javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem renameat2 flags")?, }, + "process.path_rename_at2" => FilesystemOperation::RenameAt { + old_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "renameat2 old dir fd")?, + old_path: path(1, "filesystem renameat2 source")?, + new_dir_fd: javascript_sync_rpc_arg_u32(&request.args, 2, "renameat2 new dir fd")?, + new_path: path(3, "filesystem renameat2 destination")?, + flags: javascript_sync_rpc_arg_u32(&request.args, 4, "filesystem renameat2 flags")?, + }, + "process.path_access_at" => FilesystemOperation::AccessAt { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "access dir fd")?, + path: path(1, "filesystem access path")?, + mode: javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem access mode")?, + effective_ids: javascript_sync_rpc_arg_bool(&request.args, 3, "effective IDs")?, + }, "fs.getxattrSync" | "fs.listxattrSync" | "fs.setxattrSync" | "fs.removexattrSync" => { let (operation, name_value, value, follow_index) = match request.method.as_str() { "fs.getxattrSync" => (XattrOperation::Get, Some(name(1, "xattr name")?), None, 2), @@ -554,6 +596,47 @@ pub(super) fn decode( .map_err(SidecarError::Host)?, } } + "process.path_getxattr_at" + | "process.path_listxattr_at" + | "process.path_setxattr_at" + | "process.path_removexattr_at" => { + let (operation, name_value, value, follow_index) = match request.method.as_str() { + "process.path_getxattr_at" => { + (XattrOperation::Get, Some(name(2, "xattr name")?), None, 3) + } + "process.path_listxattr_at" => (XattrOperation::List, None, None, 2), + "process.path_setxattr_at" => ( + XattrOperation::Set { + flags: javascript_sync_rpc_arg_u32(&request.args, 4, "xattr flags")?, + }, + Some(name(2, "xattr name")?), + Some(bytes(3, "xattr value")?), + 5, + ), + _ => ( + XattrOperation::Remove, + Some(name(2, "xattr name")?), + None, + 3, + ), + }; + FilesystemOperation::Xattr { + target: MetadataTarget::Path { + dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "xattr dir fd")?, + follow_symlinks: javascript_sync_rpc_arg_bool( + &request.args, + follow_index, + "follow symlinks", + )?, + }, + path: Some(path(1, "xattr path")?), + name: name_value, + value, + operation, + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + .map_err(SidecarError::Host)?, + } + } "fs.fgetxattrSync" | "fs.flistxattrSync" | "fs.fsetxattrSync" | "fs.fremovexattrSync" => { let (operation, name_value, value) = match request.method.as_str() { "fs.fgetxattrSync" => (XattrOperation::Get, Some(name(1, "xattr name")?), None), @@ -582,6 +665,7 @@ pub(super) fn decode( } } "process.fd_pipe" => FilesystemOperation::Pipe, + "process.fd_snapshot" => FilesystemOperation::Snapshot, "process.fd_open" => FilesystemOperation::OpenAt { dir_fd: NODE_CWD_FD, path: path(0, "fd_open path")?, @@ -1003,6 +1087,8 @@ impl SidecarHostCapability for FilesystemCapability { | FilesystemOperation::OpenAt { .. } | FilesystemOperation::OpenTmpfileAt { .. } | FilesystemOperation::Pipe + | FilesystemOperation::Snapshot + | FilesystemOperation::CanonicalPreopens | FilesystemOperation::Preopen { .. } | FilesystemOperation::Preopens | FilesystemOperation::Close { .. } @@ -1155,6 +1241,33 @@ impl SidecarHostCapability for FilesystemCapability { .map_err(kernel_host_error)?; json!({ "readFd": read_fd, "writeFd": write_fd }) } + FilesystemOperation::Snapshot => Value::Array( + kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)? + .into_iter() + .map(|entry| { + json!({ + "fd": entry.fd, + "descriptionId": entry.description_id.to_string(), + "fdFlags": entry.fd_flags, + "statusFlags": entry.status_flags, + "filetype": entry.filetype, + "rightsBase": entry.rights_base.to_string(), + "rightsInheriting": entry.rights_inheriting.to_string(), + "kind": if entry.is_socket { + "socket" + } else if entry.is_pipe { + "pipe" + } else if entry.is_pty { + "pty" + } else { + "file" + }, + }) + }) + .collect(), + ), FilesystemOperation::Preopen { fd } => kernel .wasi_preopen(EXECUTION_DRIVER_NAME, pid, fd) .map_err(kernel_host_error)? @@ -1166,6 +1279,12 @@ impl SidecarHostCapability for FilesystemCapability { .map_err(kernel_host_error)?; Value::Array(preopens.into_iter().map(preopen_value).collect()) } + FilesystemOperation::CanonicalPreopens => { + let preopens = kernel + .initialize_canonical_wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_host_error)?; + Value::Array(preopens.into_iter().map(preopen_value).collect()) + } FilesystemOperation::Close { fd } => { kernel .fd_close(EXECUTION_DRIVER_NAME, pid, fd) @@ -2968,7 +3087,7 @@ mod tests { &mut process, Some(( stdin_reader_fd, - maximum.clone(), + maximum, DeferredKernelReadResponse::KernelStdin, Instant::now(), direct_reply(Arc::clone(&target), identity.generation, pid, call_id), @@ -3080,7 +3199,7 @@ mod tests { &mut parent, Some(( read_fd, - maximum.clone(), + maximum, Instant::now() + Duration::from_secs(1), direct_reply(Arc::clone(&target), identity.generation, parent_pid, 1), )), @@ -3120,7 +3239,7 @@ mod tests { panic!("read reply must be JSON") }; assert_eq!( - javascript_sync_rpc_bytes_arg(&[payload.clone()], 0, "read reply") + javascript_sync_rpc_bytes_arg(std::slice::from_ref(payload), 0, "read reply") .expect("decode read reply"), b"child-payload" ); @@ -3148,7 +3267,7 @@ mod tests { panic!("EOF reply must be JSON") }; assert!( - javascript_sync_rpc_bytes_arg(&[payload.clone()], 0, "EOF reply") + javascript_sync_rpc_bytes_arg(std::slice::from_ref(payload), 0, "EOF reply") .expect("decode EOF reply") .is_empty() ); diff --git a/crates/native-sidecar/src/execution/host_dispatch/mod.rs b/crates/native-sidecar/src/execution/host_dispatch/mod.rs index 557da2e3be..c7628fa0b6 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/mod.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/mod.rs @@ -658,7 +658,11 @@ pub(super) fn authorize_host_operation( ) => Ok(()), HostOperation::Network(operation) => Err(tier_denied("EACCES", tier, "network", operation)), HostOperation::Process( - ProcessOperation::GetImage { .. } | ProcessOperation::SystemIdentity, + ProcessOperation::GetImage { .. } + | ProcessOperation::SystemIdentity + | ProcessOperation::OpenExecutableImage { .. } + | ProcessOperation::ReadExecutableImage { .. } + | ProcessOperation::CloseExecutableImage { .. }, ) => Ok(()), HostOperation::Process(ProcessOperation::GetResourceLimit { .. }) => Ok(()), HostOperation::Process( diff --git a/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs index af9f6dc2e1..570125208e 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs @@ -308,6 +308,7 @@ fn fail_deferred_posix_poll( } #[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] pub(in crate::execution) fn service_deferred_posix_poll( generation: u64, runtime: &agentos_runtime::RuntimeContext, @@ -496,7 +497,7 @@ pub(in crate::execution) fn service_deferred_posix_poll( return std::task::Poll::Pending; } for waiter in &native_listener_waiters { - if let std::task::Poll::Ready(_) = waiter.poll_read_ready(cx) { + if waiter.poll_read_ready(cx).is_ready() { return std::task::Poll::Ready(()); } } @@ -718,9 +719,9 @@ where } .map_err(kernel_error); let prune_result = prune_managed_descriptions_after_fd_mutation( - &bridge, + bridge, vm_id, - &socket_paths, + socket_paths, vm, kernel_pid, candidate_descriptions, @@ -1505,6 +1506,7 @@ fn decode_unix_address( #[allow(dead_code)] pub(super) struct NetworkCapability; +#[allow(clippy::too_many_arguments)] fn dispatch_context_stream_read( sidecar: &mut NativeSidecar, vm_id: &str, @@ -1545,6 +1547,7 @@ where ) } +#[allow(clippy::too_many_arguments)] pub(in crate::execution) fn dispatch_descendant_context_stream_read( sidecar: &mut NativeSidecar, vm_id: &str, @@ -2643,7 +2646,7 @@ where "UDP socket requires INET address", )); }; - let response = service_managed_udp_operation( + service_managed_udp_operation( ManagedUdpServiceRequest { bridge: context.bridge, kernel: &mut *context.kernel, @@ -2659,8 +2662,7 @@ where host: Some(host.clone()), port: *port, }, - )?; - response + )? } _ => return Err(SidecarError::host("EOPNOTSUPP", "unsupported socket bind")), }; diff --git a/crates/native-sidecar/src/execution/host_dispatch/process.rs b/crates/native-sidecar/src/execution/host_dispatch/process.rs index fceec43821..68ae876ae9 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/process.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/process.rs @@ -43,6 +43,11 @@ impl SidecarHostCapability for ProcessCapability { )); } let image = match source { + ExecutableImageSource::TrustedInitialPath(path) => kernel + .load_trusted_initial_runtime_image( + path.as_str(), + process.limits.wasm.max_module_file_bytes, + ), ExecutableImageSource::Path(path) => { let path = if path.as_str().starts_with('/') { normalize_path(path.as_str()) diff --git a/crates/native-sidecar/src/execution/javascript/rpc.rs b/crates/native-sidecar/src/execution/javascript/rpc.rs index 3b7d75518d..be20148ba0 100644 --- a/crates/native-sidecar/src/execution/javascript/rpc.rs +++ b/crates/native-sidecar/src/execution/javascript/rpc.rs @@ -532,6 +532,7 @@ pub(in crate::execution) fn descriptor_rights_compat_request( }) } +#[allow(clippy::too_many_arguments)] pub(in crate::execution) async fn service_descriptor_rights_compat_operation( bridge: &SharedBridge, vm_id: &str, @@ -3076,7 +3077,7 @@ where .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) .map_err(kernel_error)?; process.apply_runtime_controls()?; - Ok(Value::Null.into()) + Ok(Value::Null) } "process.umask" => { let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?; diff --git a/crates/native-sidecar/src/execution/launch.rs b/crates/native-sidecar/src/execution/launch.rs index 1bbad50bea..48c45c2e7e 100644 --- a/crates/native-sidecar/src/execution/launch.rs +++ b/crates/native-sidecar/src/execution/launch.rs @@ -3835,15 +3835,13 @@ mod bounded_host_launch_source_tests { .write_all(b"d") .expect("grow launch source after metadata admission"); let stale = read_open_host_launch_source(opened, 3) - .err() - .expect("growth after admission metadata must be rejected"); + .expect_err("growth after admission metadata must be rejected"); assert_eq!(stale.code(), Some("ESTALE")); let oversize_path = directory.join("oversize.wasm"); fs::write(&oversize_path, b"abcde").expect("write oversized launch source"); let oversize = open_host_launch_source(oversize_path, 4) - .err() - .expect("oversized source must fail from handle metadata before reading"); + .expect_err("oversized source must fail from handle metadata before reading"); assert_eq!(oversize.code(), Some("E2BIG")); fs::remove_dir_all(directory).expect("remove launch source test directory"); @@ -5049,14 +5047,21 @@ where .map_err(kernel_error), "top-level compatibility-WASM permission lookup" ); + let module_path = match payload.wasm_backend { + Some(StandaloneWasmBackend::Wasmtime) => env + .get("AGENTOS_GUEST_ENTRYPOINT") + .map(|path| format!("{TRUSTED_INITIAL_MODULE_PREFIX}{path}")) + .unwrap_or_else(|| resolved.entrypoint.clone()), + Some(StandaloneWasmBackend::V8) | None => resolved.entrypoint.clone(), + }; let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.clone(), - module_path: Some(resolved.entrypoint.clone()), + module_path: Some(module_path), }); let context_id = context.context_id; let execution = match self .wasm_engine - .start_execution_with_runtime_async( + .start_execution_with_runtime_async_for_backend( StartWasmExecutionRequest { vm_id: vm_id.clone(), context_id: context_id.clone(), @@ -5069,6 +5074,14 @@ where guest_runtime: wasm_guest_runtime, }, vm.runtime_context.clone(), + match payload.wasm_backend { + Some(StandaloneWasmBackend::Wasmtime) => { + ExecutionStandaloneWasmBackend::Wasmtime + } + Some(StandaloneWasmBackend::V8) | None => { + ExecutionStandaloneWasmBackend::V8 + } + }, ) .await .map_err(wasm_error) diff --git a/crates/native-sidecar/src/execution/mod.rs b/crates/native-sidecar/src/execution/mod.rs index 380aeebb09..21181ee229 100644 --- a/crates/native-sidecar/src/execution/mod.rs +++ b/crates/native-sidecar/src/execution/mod.rs @@ -92,7 +92,8 @@ use crate::protocol::{ ProcessSnapshotEntry, ProcessSnapshotStatus, PtyResizedResponse, QueueSnapshotEntry, RequestFrame, ResizePtyRequest, ResourceSnapshotResponse, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction, SignalHandlerRegistration, SocketStateEntry, - StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier, WriteStdinRequest, + StandaloneWasmBackend, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier, + WriteStdinRequest, }; use crate::service::{ audit_fields, dirname, emit_security_audit_event, emit_structured_event_or_stderr, @@ -185,8 +186,10 @@ use agentos_execution::{ ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration, GuestRuntimeConfig, HostRpcRequest, JavascriptExecutionEvent, JavascriptExecutionLimits, JavascriptSyncRpcResponder, PythonExecutionEvent, PythonExecutionLimits, PythonVfsRpcResponder, - StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest, - WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier, + StandaloneWasmBackend as ExecutionStandaloneWasmBackend, StartJavascriptExecutionRequest, + StartPythonExecutionRequest, StartWasmExecutionRequest, WasmExecutionEvent, + WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier, + TRUSTED_INITIAL_MODULE_PREFIX, }; use agentos_kernel::dns::{ DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource, @@ -537,7 +540,7 @@ mod configured_socket_capacity_tests { .await .expect_err("operation must still expire at the hard deadline"); - let warnings = captured.lock().expect("deadline warnings"); + let warnings = captured.lock().expect("deadline warnings").clone(); assert_eq!(warnings.len(), 2); for warning in warnings.iter() { assert_eq!(warning.limit_name, "limits.reactor.operationDeadlineMs"); @@ -547,7 +550,6 @@ mod configured_socket_capacity_tests { "warning must precede the hard deadline: {warning:?}" ); } - drop(warnings); // The synchronous TCP/Unix write path uses the same warning state // machine even though its readiness wait is `poll(2)`, not a Future. @@ -574,7 +576,10 @@ mod configured_socket_capacity_tests { tokio::time::sleep(reparked.remaining_until_deadline()).await; assert!(reparked.expired()); - let warnings = captured.lock().expect("deadline warnings after sync paths"); + let warnings = captured + .lock() + .expect("deadline warnings after sync paths") + .clone(); assert_eq!(warnings.len(), 4); assert_eq!( warnings diff --git a/crates/native-sidecar/src/execution/network/tcp.rs b/crates/native-sidecar/src/execution/network/tcp.rs index ca18d4366c..5d26be75a2 100644 --- a/crates/native-sidecar/src/execution/network/tcp.rs +++ b/crates/native-sidecar/src/execution/network/tcp.rs @@ -474,13 +474,9 @@ impl ActiveTcpSocket { )>, owner_notify: Arc, ) { - let (Some(session), Some((capability_id, capability_generation))) = (session, identity) - else { - return; - }; self.readiness_registration.register( - Some(session), - Some((capability_id, capability_generation)), + session, + identity, owner_notify, agentos_runtime::readiness::ReadyFlags::READABLE, ); diff --git a/crates/native-sidecar/src/execution/network/unix.rs b/crates/native-sidecar/src/execution/network/unix.rs index e17e7c26f9..5a63132f4d 100644 --- a/crates/native-sidecar/src/execution/network/unix.rs +++ b/crates/native-sidecar/src/execution/network/unix.rs @@ -704,13 +704,9 @@ impl ActiveUnixSocket { )>, owner_notify: Arc, ) { - let (Some(session), Some((capability_id, capability_generation))) = (session, identity) - else { - return; - }; self.readiness_registration.register( - Some(session), - Some((capability_id, capability_generation)), + session, + identity, owner_notify, agentos_runtime::readiness::ReadyFlags::READABLE, ); @@ -1575,13 +1571,9 @@ impl ActiveUnixListener { )>, owner_notify: Arc, ) { - let (Some(session), Some((capability_id, capability_generation))) = (session, identity) - else { - return; - }; self.readiness_registration.register( - Some(session), - Some((capability_id, capability_generation)), + session, + identity, owner_notify, agentos_runtime::readiness::ReadyFlags::ACCEPT, ); diff --git a/crates/native-sidecar/src/execution/process.rs b/crates/native-sidecar/src/execution/process.rs index 437e0fe66e..f0e015deaf 100644 --- a/crates/native-sidecar/src/execution/process.rs +++ b/crates/native-sidecar/src/execution/process.rs @@ -241,6 +241,7 @@ impl ActiveProcess { ExecutionBackend::configure_host_services(&mut execution, host_capabilities.clone()); let control_notify = Arc::clone(&process_event_notify); runtime_control.set_wake(Arc::new(move || control_notify.notify_one())); + let standalone_wasm_backend = execution.standalone_wasm_backend(); Self { kernel_pid, kernel_handle, @@ -261,6 +262,7 @@ impl ActiveProcess { ), tty_master_fd: None, runtime, + standalone_wasm_backend, adapter_policy: ExecutionAdapterPolicy::BINDING, detached: false, execution, @@ -941,8 +943,7 @@ impl ActiveProcess { timeout, &host_capabilities, ) - .await - .map_err(|error| SidecarError::Execution(error.to_string()))?; + .await?; if let Some(event) = event { return Ok(Some(PolledExecutionEvent::unreserved(event))); } @@ -955,6 +956,7 @@ impl ActiveProcess { } #[cfg(test)] + #[allow(dead_code)] pub(crate) async fn poll_execution_event_for_test( &mut self, timeout: Duration, @@ -984,14 +986,11 @@ impl ActiveProcess { ))); } let host_capabilities = self.host_capabilities.clone(); - let event = self - .execution - .try_poll_event_with_host( - self.kernel_handle.runtime_identity(), - self.limits.reactor.max_bridge_response_bytes, - &host_capabilities, - ) - .map_err(|error| SidecarError::Execution(error.to_string()))?; + let event = self.execution.try_poll_event_with_host( + self.kernel_handle.runtime_identity(), + self.limits.reactor.max_bridge_response_bytes, + &host_capabilities, + )?; if let Some(event) = event { return Ok(Some(PolledExecutionEvent::unreserved(event))); } @@ -1095,6 +1094,14 @@ impl ActiveProcess { self } + pub(crate) fn with_standalone_wasm_backend( + mut self, + backend: ExecutionStandaloneWasmBackend, + ) -> Self { + self.standalone_wasm_backend = backend; + self + } + pub(crate) fn allocate_child_process_id(&mut self) -> String { self.next_child_process_id += 1; format!("child-{}", self.next_child_process_id) @@ -2746,6 +2753,19 @@ impl ExecutionBackend for BindingExecution { } impl ActiveExecution { + /// Engine affinity is adapter construction metadata used only when this + /// process later replaces or spawns its standalone-WASM image. Keep the + /// storage-enum match behind the adapter boundary so common process + /// lifecycle code never switches on an executor variant. + fn standalone_wasm_backend(&self) -> ExecutionStandaloneWasmBackend { + match self { + Self::Wasm(execution) => execution.standalone_backend(), + Self::Javascript(_) | Self::Python(_) | Self::Binding(_) => { + ExecutionStandaloneWasmBackend::V8 + } + } + } + fn backend(&self) -> &dyn ExecutionBackend { match self { Self::Javascript(execution) => execution, @@ -3453,7 +3473,7 @@ fn map_python_execution_event_with_host( fn map_wasm_execution_event_with_host( event: WasmExecutionEvent, - responder: JavascriptSyncRpcResponder, + responder: Option, identity: ProcessRuntimeIdentity, max_reply_bytes: usize, host: Option<&ProcessHostCapabilitySet>, @@ -3462,6 +3482,12 @@ fn map_wasm_execution_event_with_host( WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), WasmExecutionEvent::SyncRpcRequest(request) => { + let responder = responder.ok_or_else(|| { + SidecarError::host( + "ERR_AGENTOS_WASMTIME_SYNC_RPC", + "native Wasmtime emitted an impossible V8 sync RPC event", + ) + })?; return route_compatibility_host_call( execution_host_call(request, responder, identity, max_reply_bytes)?, true, @@ -3469,6 +3495,14 @@ fn map_wasm_execution_event_with_host( host, ); } + WasmExecutionEvent::HostCall { request, reply } => { + return route_compatibility_host_call( + ExecutionHostCall { request, reply }, + true, + max_reply_bytes, + host, + ); + } WasmExecutionEvent::SignalState { signal, registration, @@ -3493,7 +3527,8 @@ fn route_compatibility_host_call( args: &'a [Value], } - let admission = host + let reply = call.reply.clone(); + let admission = match host .map(|host| { let raw_bytes = call .request @@ -3515,8 +3550,22 @@ fn route_compatibility_host_call( ) }) .transpose() - .map_err(SidecarError::from)?; - let event = decode_compatibility_host_call(call, full_filesystem, max_reply_bytes)?; + { + Ok(admission) => admission, + Err(error) => { + reply.fail(error).map_err(SidecarError::from)?; + return Ok(None); + } + }; + let event = match decode_compatibility_host_call(call, full_filesystem, max_reply_bytes) { + Ok(event) => event, + Err(error) => { + reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from)?; + return Ok(None); + } + }; let Some(host) = host else { return Ok(Some(event)); }; diff --git a/crates/native-sidecar/src/execution/process_events.rs b/crates/native-sidecar/src/execution/process_events.rs index e85224039f..ac31f1f1fd 100644 --- a/crates/native-sidecar/src/execution/process_events.rs +++ b/crates/native-sidecar/src/execution/process_events.rs @@ -455,14 +455,13 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) eprintln!( "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding failure output; queue limit state retains the typed failure" ); - } else if output_enqueued { - if !enqueue(ActiveExecutionEvent::Exited(1)) - && !cancelled.load(Ordering::Acquire) - { - eprintln!( - "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding failure exit event; queue limit state retains the typed failure" - ); - } + } else if output_enqueued + && !enqueue(ActiveExecutionEvent::Exited(1)) + && !cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding failure exit event; queue limit state retains the typed failure" + ); } } BindingCommandResolution::Invoke { request, timeout } => { @@ -519,14 +518,13 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) eprintln!( "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding result output; queue limit state retains the typed failure" ); - } else if output_enqueued { - if !enqueue(ActiveExecutionEvent::Exited(exit_code)) - && !cancelled.load(Ordering::Acquire) - { - eprintln!( - "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding exit event; queue limit state retains the typed failure" - ); - } + } else if output_enqueued + && !enqueue(ActiveExecutionEvent::Exited(exit_code)) + && !cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue binding exit event; queue limit state retains the typed failure" + ); } } } @@ -552,14 +550,13 @@ pub(super) fn spawn_binding_process_events(request: BindingProcessEventRequest) eprintln!( "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue blocking-admission failure output; queue limit state retains the typed failure" ); - } else if output_enqueued { - if !enqueue_failure(ActiveExecutionEvent::Exited(1)) - && !failure_cancelled.load(Ordering::Acquire) - { - eprintln!( - "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue blocking-admission exit event; queue limit state retains the typed failure" - ); - } + } else if output_enqueued + && !enqueue_failure(ActiveExecutionEvent::Exited(1)) + && !failure_cancelled.load(Ordering::Acquire) + { + eprintln!( + "ERR_AGENTOS_BINDING_EVENT_DELIVERY: failed to enqueue blocking-admission exit event; queue limit state retains the typed failure" + ); } } } diff --git a/crates/native-sidecar/src/filesystem.rs b/crates/native-sidecar/src/filesystem.rs index ad8b7b5707..d1ca6fad56 100644 --- a/crates/native-sidecar/src/filesystem.rs +++ b/crates/native-sidecar/src/filesystem.rs @@ -200,9 +200,8 @@ where )); } }; - let response = core_guest_filesystem_call(&mut vm.kernel, payload) - .map_err(native_guest_filesystem_core_error)?; - response + core_guest_filesystem_call(&mut vm.kernel, payload) + .map_err(native_guest_filesystem_core_error)? }; Ok(DispatchResult { diff --git a/crates/native-sidecar/src/service.rs b/crates/native-sidecar/src/service.rs index 31f5712b10..6c8d1c777e 100644 --- a/crates/native-sidecar/src/service.rs +++ b/crates/native-sidecar/src/service.rs @@ -2377,7 +2377,7 @@ where .active_processes .get(process_id) .expect("process existence checked above"); - deferred_kernel_wait_request_for_process(&request, &vm.kernel, process)? + deferred_kernel_wait_request_for_process(request, &vm.kernel, process)? .filter(|request| request.method == "process.fd_write") }; @@ -2721,7 +2721,7 @@ where process.tty_master_owner.expect("guarded by match arm"), ) }; - self.service_shared_tty_stdio_write(vm_id, writer_kernel_pid, owner, &request) + self.service_shared_tty_stdio_write(vm_id, writer_kernel_pid, owner, request) .map(Into::into) } "__kernel_stdin_read" | "__kernel_poll" => { @@ -2764,7 +2764,7 @@ where kernel: &mut vm.kernel, kernel_readiness, process, - sync_request: &request, + sync_request: request, capabilities, managed_descriptions: Some(managed_descriptions), }) @@ -2857,12 +2857,12 @@ where other => other, }; - if response.is_ok() && javascript_sync_rpc_may_make_fd_readable(&request) { + if response.is_ok() && javascript_sync_rpc_may_make_fd_readable(request) { if let Some(vm) = self.vms.get_mut(vm_id) { Self::wake_ready_deferred_fd_reads(vm)?; } } - if response.is_ok() && javascript_sync_rpc_may_make_fd_writable(&request) { + if response.is_ok() && javascript_sync_rpc_may_make_fd_writable(request) { if let Some(vm) = self.vms.get_mut(vm_id) { Self::wake_ready_deferred_fd_writes(vm)?; } @@ -3231,6 +3231,7 @@ where | ResourceClass::UdpBytes | ResourceClass::TlsBytes | ResourceClass::ExecutorBytes + | ResourceClass::WasmMemoryBytes | ResourceClass::Http2BufferedBytes | ResourceClass::Http2HeaderBytes | ResourceClass::Http2DataBytes diff --git a/crates/native-sidecar/src/state.rs b/crates/native-sidecar/src/state.rs index 30230ecd48..e390c8f004 100644 --- a/crates/native-sidecar/src/state.rs +++ b/crates/native-sidecar/src/state.rs @@ -22,7 +22,7 @@ use agentos_execution::{ BoundedUsize, BoundedVec, KernelPollInterest, SocketAddress, SocketDomain as HostSocketDomain, SocketKind as HostSocketKind, WaitTarget, }, - HostRpcRequest, JavascriptExecution, PythonExecution, WasmExecution, + HostRpcRequest, JavascriptExecution, PythonExecution, StandaloneWasmBackend, WasmExecution, }; use agentos_kernel::fd_table::TransferredFd; use agentos_kernel::kernel::{KernelProcessHandle, KernelVm}; @@ -1527,6 +1527,10 @@ pub(crate) struct ActiveProcess { /// host (instead of the raw guest stdout/stderr execution events). pub(crate) tty_master_fd: Option, pub(crate) runtime: GuestRuntimeKind, + /// Standalone-WASM engine affinity inherited across spawn and exec. This + /// is independent of the current image so a Wasmtime process that execs + /// JavaScript and later spawns WASM retains its selected backend. + pub(crate) standalone_wasm_backend: StandaloneWasmBackend, /// Executor-selected transport facts consumed by common POSIX paths. /// This is intentionally independent of `runtime`: compatibility WASM and /// Wasmtime share a guest kind but may use different host-call transports. @@ -2040,7 +2044,7 @@ pub(crate) fn tcp_socket_event_retained_bytes(event: &TcpSocketEvent) -> usize { #[derive(Clone, Debug)] pub(crate) struct SocketEventPusher { - pub(crate) session: ExecutionWakeHandle, + pub(crate) session: Option, pub(crate) capability_id: agentos_runtime::capability::CapabilityId, pub(crate) capability_generation: agentos_runtime::capability::CapabilityGeneration, live: Arc, @@ -2062,8 +2066,9 @@ impl SocketEventPusher { return Ok(false); } self.owner_notify.notify_one(); - self.session - .publish_readiness(self.capability_id, self.capability_generation, flags)?; + if let Some(session) = &self.session { + session.publish_readiness(self.capability_id, self.capability_generation, flags)?; + } Ok(true) } } @@ -2179,14 +2184,15 @@ impl SocketReadinessSubscribers { if !target.is_live() { return Ok(false); } - target - .session - .set_application_read_interest( - target.capability_id, - target.capability_generation, - enabled, - ) - .map_err(|error| SidecarError::Execution(error.to_string()))?; + if let Some(session) = &target.session { + session + .set_application_read_interest( + target.capability_id, + target.capability_generation, + enabled, + ) + .map_err(|error| SidecarError::Execution(error.to_string()))?; + } self.set_application_read_interest_state(identity, enabled) } @@ -2265,8 +2271,7 @@ impl SocketReadinessRegistration { owner_notify: Arc, replay_flags: agentos_runtime::readiness::ReadyFlags, ) { - let (Some(session), Some((capability_id, capability_generation))) = (session, identity) - else { + let Some((capability_id, capability_generation)) = identity else { return; }; let live = Arc::new(AtomicBool::new(true)); @@ -3093,6 +3098,7 @@ impl BindingExecution { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub(crate) enum ActiveExecutionEvent { Common(ExecutionEvent), Stdout(Vec), @@ -3735,6 +3741,44 @@ mod socket_readiness_registry_tests { assert!(subscribers.targets().is_empty()); } + #[test] + fn socket_subscription_without_executor_wake_notifies_posix_poll_owner() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .expect("build notify test runtime"); + let take_notify_permit = |notify: &Notify| { + runtime.block_on(async { + tokio::time::timeout(Duration::from_millis(10), notify.notified()) + .await + .is_ok() + }) + }; + let resources = ResourceLedger::root( + "runtime-neutral-socket-wake-test", + [( + ResourceClass::Capabilities, + ResourceLimit::new(1, "limits.reactor.maxCapabilities"), + )], + ); + let subscribers = SocketReadinessSubscribers::new(&resources); + let registration = SocketReadinessRegistration::new(Arc::clone(&subscribers), None, None); + let owner_notify = Arc::new(Notify::new()); + registration.register( + None, + Some((1, 1)), + Arc::clone(&owner_notify), + agentos_runtime::readiness::ReadyFlags::READABLE, + ); + + assert!(take_notify_permit(&owner_notify)); + let target = subscribers.targets().pop().expect("registered target"); + assert!(target + .publish_readiness(agentos_runtime::readiness::ReadyFlags::READABLE) + .expect("runtime-neutral readiness publish")); + assert!(take_notify_permit(&owner_notify)); + } + #[test] fn native_alias_registration_is_raii_and_read_interest_is_aggregate_or() { let process_runtime = diff --git a/crates/native-sidecar/src/vm.rs b/crates/native-sidecar/src/vm.rs index 692a6ed62a..03e6b162e6 100644 --- a/crates/native-sidecar/src/vm.rs +++ b/crates/native-sidecar/src/vm.rs @@ -1820,6 +1820,9 @@ fn vm_resource_ledger( ResourceClass::Tasks => "runtime.resources.maxTasks", ResourceClass::ExecutorSlots => "runtime.blocking.maxJobs", ResourceClass::ExecutorBytes => "runtime.blocking.maxQueuedBytes", + ResourceClass::WasmMemoryBytes => { + "runtime.resources.maxWasmMemoryBytes" + } ResourceClass::Http2Connections => "limits.http2.maxConnections", ResourceClass::Http2Streams => "limits.http2.maxStreams", ResourceClass::Http2BufferedBytes => "limits.http2.maxBufferedBytes", @@ -2651,13 +2654,12 @@ fn initialize_vm_runtime_scratch_root(root: PathBuf) -> Result = Ok(root); match initialized { Ok(root) => Ok(root), diff --git a/crates/native-sidecar/tests/architecture_guards.rs b/crates/native-sidecar/tests/architecture_guards.rs index b26652db23..4dbcae0438 100644 --- a/crates/native-sidecar/tests/architecture_guards.rs +++ b/crates/native-sidecar/tests/architecture_guards.rs @@ -284,15 +284,58 @@ fn sidecar_publishes_only_one_signal_delivery_scope_at_a_time() { } #[test] -fn phase_one_contains_no_wasmtime_implementation_or_dependency() { +fn phase_two_keeps_wasmtime_scoped_to_the_standalone_wasm_adapter() { let root = repo_root(); + let workspace = std::fs::read_to_string(root.join("Cargo.toml")).expect("read Cargo.toml"); let lock = std::fs::read_to_string(root.join("Cargo.lock")).expect("read Cargo.lock"); assert!( - !lock.contains("name = \"wasmtime") && !lock.contains("name = \"wasmtime-"), - "Wasmtime dependencies belong to the later executor revision, not the prerequisite refactor" + workspace.contains("wasmtime = { version = \"=46.0.0\", default-features = false") + && workspace.contains("wasmparser = \"=0.251.0\"") + && lock.contains("name = \"wasmtime\"\nversion = \"46.0.0\"") + && !lock.contains("name = \"wasmtime-wasi\""), + "Phase 2 must pin reviewed Wasmtime without installing ambient wasmtime-wasi" ); - let mut manifests = vec![root.join("Cargo.toml")]; + let wasm_adapter = std::fs::read_to_string(root.join("crates/execution/src/wasm.rs")) + .expect("read standalone WASM adapter"); + let wasmtime_module = + std::fs::read_to_string(root.join("crates/execution/src/wasm/wasmtime/module.rs")) + .expect("read Wasmtime module compiler"); + assert!( + wasm_adapter + .matches("validate_module_profile(&resolved_module)?;") + .count() + >= 2 + && wasmtime_module.contains("profile::validate_locked_profile"), + "both V8-WASM start paths and Wasmtime compilation must use the shared wasmparser profile" + ); + + let publish = std::fs::read_to_string(root.join(".github/workflows/publish.yaml")) + .expect("read publish workflow"); + let linux = std::fs::read_to_string(root.join("docker/build/linux-gnu.Dockerfile")) + .expect("read Linux release build"); + let darwin = std::fs::read_to_string(root.join("docker/build/darwin.Dockerfile")) + .expect("read Darwin release build"); + for (name, source) in [ + ("publish workflow", publish.as_str()), + ("Linux release build", linux.as_str()), + ("Darwin release build", darwin.as_str()), + ] { + assert!( + source.contains("1.94.0"), + "{name} must use Wasmtime 46's reviewed Rust MSRV" + ); + } + assert!( + linux.contains("cargo test -p agentos-execution wasmtime --lib --target \"$TARGET\"") + && publish.contains("runner: macos-15-intel") + && publish.contains("runner: macos-15") + && publish.contains("cargo test -p agentos-execution wasmtime --lib") + && publish.contains("needs.wasmtime-darwin-smoke.result == 'success'"), + "all four release platforms must compile and natively smoke the reviewed Wasmtime embedding" + ); + + let mut manifests = Vec::new(); for entry in std::fs::read_dir(root.join("crates")).expect("read workspace crates") { let path = entry .expect("read workspace crate entry") @@ -305,18 +348,17 @@ fn phase_one_contains_no_wasmtime_implementation_or_dependency() { for manifest in manifests { let source = std::fs::read_to_string(&manifest) .unwrap_or_else(|error| panic!("read {}: {error}", manifest.display())); - for line in source.lines().map(strip_line_comment) { + if source.lines().map(strip_line_comment).any(|line| { let compact = line .chars() .filter(|ch| !ch.is_whitespace()) .collect::(); - assert!( - !compact.starts_with("wasmtime=") - && !compact.starts_with("wasmtime-") - && !compact.contains("package=\"wasmtime"), - "Wasmtime dependency appeared in {}: {}", - manifest.display(), - line.trim() + compact.starts_with("wasmtime=") || compact.starts_with("wasmtime-") + }) { + assert_eq!( + manifest, + root.join("crates/execution/Cargo.toml"), + "only the execution crate may depend on Wasmtime" ); } } @@ -325,11 +367,13 @@ fn phase_one_contains_no_wasmtime_implementation_or_dependency() { let source = std::fs::read_to_string(root.join(&relative)) .unwrap_or_else(|error| panic!("read {}: {error}", relative.display())); let production = production_source_text(&source); - assert!( - !production.contains("wasmtime::") && !production.contains("extern crate wasmtime"), - "Wasmtime implementation code appeared before Phase 1 closed in {}", - relative.display() - ); + if production.contains("use wasmtime::") || production.contains("extern crate wasmtime") { + assert!( + relative.starts_with("crates/execution/src/wasm/wasmtime"), + "external Wasmtime API use escaped the standalone adapter: {}", + relative.display() + ); + } } } @@ -506,14 +550,15 @@ fn production_backends_route_typed_host_calls_through_family_capabilities() { ) .expect("read shared host dispatcher"); - // JavaScript, Python's embedded-JavaScript bridge, and compatibility WASM - // all use the same decoder before a sidecar semantic operation is chosen. + // JavaScript, Python's embedded-JavaScript bridge, compatibility WASM, and + // native Wasmtime host calls all enter the same typed capability router + // before a sidecar semantic operation is chosen. assert_eq!( process .matches("return route_compatibility_host_call(") .count(), - 3, - "every production V8-backed adapter must use the bound common host-call submission path" + 4, + "every production executor host-call lane must use the bound common submission path" ); assert!( process.contains("ExecutionBackend::configure_host_services") @@ -1198,6 +1243,9 @@ const FS_ALLOW: &[&str] = &[ "crates/execution/src/javascript.rs", "crates/execution/src/node_import_cache.rs", "crates/execution/src/runtime_support.rs", + // Process RSS is sampled only for operator-visible Wasmtime diagnostics; + // it is not guest filesystem access or an ambient WASI capability. + "crates/execution/src/wasm/wasmtime/engine.rs", // Host-side V8 diagnostics: module-trace and sync-RPC latency profilers // write to an operator-provided file path, and snapshot bootstrap reads the // userland bundle from PI_SNAPSHOT_BUNDLE_PATH. Host-only, not guest-reachable. @@ -1506,19 +1554,38 @@ fn production_execution_lifecycle_is_runtime_neutral_and_delegated() { ); let wasm = std::fs::read_to_string(root.join("crates/execution/src/wasm.rs")) - .expect("read compatibility WASM adapter"); + .expect("read standalone WASM adapters"); + let v8_backend_start = wasm + .find("impl ExecutionBackend for V8WasmExecution") + .expect("find V8-WASM backend adapter"); + let v8_backend_end = wasm[v8_backend_start..] + .find("impl WasmExecution") + .map(|offset| v8_backend_start + offset) + .expect("find end of V8-WASM backend adapter"); + let v8_backend: String = wasm[v8_backend_start..v8_backend_end] + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + assert!( + v8_backend.contains("fndeliver_signal_checkpoint(") + && v8_backend.contains("self.wake_handle(identity)") + && v8_backend.contains("wake.publish_signal(signal,delivery_token)"), + "compatibility WASM checkpoints must publish through the runtime-neutral kernel wake capability" + ); let wasm_backend_start = wasm .find("impl ExecutionBackend for WasmExecution") - .expect("find compatibility WASM backend adapter"); + .expect("find standalone WASM backend adapter"); let wasm_backend: String = wasm[wasm_backend_start..] .chars() .filter(|character| !character.is_whitespace()) .collect(); assert!( - wasm_backend.contains("fndeliver_signal_checkpoint(") - && wasm_backend.contains("self.wake_handle(identity)") - && wasm_backend.contains("wake.publish_signal(signal,delivery_token)"), - "compatibility WASM checkpoints must publish through the runtime-neutral kernel wake capability" + wasm_backend + .matches("execution.deliver_signal_checkpoint(identity,signal,delivery_token,flags)") + .count() + >= 2 + && wasm_backend.contains("WasmExecutionBackend::Wasmtime(execution)"), + "both standalone WASM engines must consume the common signal-checkpoint contract" ); } @@ -2772,6 +2839,9 @@ fn nested_child_start_never_blocks_the_shared_runtime_worker() { source .matches(".start_execution_with_runtime_async(") .count() + + source + .matches(".start_execution_with_runtime_async_for_backend(") + .count() >= 6, "top-level plus root/descendant Python and WASM startup must use async runtime adapters" ); @@ -3018,6 +3088,14 @@ fn production_threads_match_the_reviewed_topology_manifest() { "serialized-v8-maintenance", "crates/execution/src/v8_host.rs", ), + ( + "process-wasmtime-epoch-ticker", + "crates/execution/src/wasm/wasmtime/engine.rs", + ), + ( + "admitted-wasmtime-guest-executor", + "crates/execution/src/wasm/wasmtime/lifecycle.rs", + ), ( "constant-stdio-writer", "crates/native-sidecar/src/stdio.rs", diff --git a/crates/native-sidecar/tests/builtin_conformance.rs b/crates/native-sidecar/tests/builtin_conformance.rs index 79140607c4..a83671a582 100644 --- a/crates/native-sidecar/tests/builtin_conformance.rs +++ b/crates/native-sidecar/tests/builtin_conformance.rs @@ -1532,6 +1532,7 @@ process.stdin.once("data", (canonical) => { env: HashMap::from([(String::from("AGENTOS_EXEC_TTY"), String::from("1"))]), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start TTY stdin discipline probe"); diff --git a/crates/native-sidecar/tests/extension.rs b/crates/native-sidecar/tests/extension.rs index 9881739471..09ec9d8ea7 100644 --- a/crates/native-sidecar/tests/extension.rs +++ b/crates/native-sidecar/tests/extension.rs @@ -106,6 +106,7 @@ impl Extension for EchoExtension { env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }) .await?; assert_eq!(started.process_id, process_id); @@ -129,6 +130,7 @@ impl Extension for EchoExtension { env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }) .await?; assert_eq!(lifecycle_started.process_id, lifecycle_process_id); diff --git a/crates/native-sidecar/tests/filesystem.rs b/crates/native-sidecar/tests/filesystem.rs index 8f5b0548ac..968a4b7401 100644 --- a/crates/native-sidecar/tests/filesystem.rs +++ b/crates/native-sidecar/tests/filesystem.rs @@ -640,6 +640,7 @@ mod kernel_authority { dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); } + #[allow(clippy::too_many_arguments)] fn execute_command( sidecar: &mut agentos_native_sidecar::NativeSidecar, connection_id: &str, @@ -663,6 +664,7 @@ mod kernel_authority { env: HashMap::new(), cwd: Some(String::from("/workspace")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch execute"); @@ -699,6 +701,7 @@ mod kernel_authority { env: HashMap::new(), cwd: Some(String::from("/workspace")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch execute"); diff --git a/crates/native-sidecar/tests/fixtures/limits-inventory.json b/crates/native-sidecar/tests/fixtures/limits-inventory.json index 062cd5c607..71b479d8f9 100644 --- a/crates/native-sidecar/tests/fixtures/limits-inventory.json +++ b/crates/native-sidecar/tests/fixtures/limits-inventory.json @@ -2456,5 +2456,166 @@ "path": "crates/vfs/src/posix/tar_fs.rs", "class": "invariant", "rationale": "Process-local memoization cap; clearing the cache changes performance only, not guest behavior." + }, + { + "name": "DEFAULT_MODULE_CACHE_CHARGED_BYTES", + "path": "crates/execution/src/wasm/wasmtime/cache.rs", + "class": "policy-deferred", + "rationale": "Process-wide compiled-module cache byte budget; expose through runtime configuration when cache tuning is supported." + }, + { + "name": "DEFAULT_MODULE_CACHE_ENTRIES", + "path": "crates/execution/src/wasm/wasmtime/cache.rs", + "class": "policy-deferred", + "rationale": "Process-wide compiled-module cache entry budget; expose through runtime configuration when cache tuning is supported." + }, + { + "name": "DEFAULT_MAX_ENGINE_PROFILES", + "path": "crates/execution/src/wasm/wasmtime/engine.rs", + "class": "policy-deferred", + "rationale": "Process-wide Wasmtime Engine profile registry bound; expose through runtime configuration when profile tuning is supported." + }, + { + "name": "DEFAULT_WASM_STACK_BYTES", + "path": "crates/execution/src/wasm/wasmtime/engine.rs", + "class": "policy", + "rationale": "Fallback for the typed per-execution Wasmtime guest stack limit.", + "wired": "WasmExecutionLimits.max_stack_bytes" + }, + { + "name": "ENGINE_PROFILE_LIMIT_CONFIG_PATH", + "path": "crates/execution/src/wasm/wasmtime/engine.rs", + "class": "invariant", + "rationale": "Stable typed limit identifier, not the limit value." + }, + { + "name": "DEFAULT_MAX_INSTANCES", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "invariant", + "rationale": "The standalone executor instantiates exactly one guest module per Store." + }, + { + "name": "DEFAULT_MAX_MEMORIES", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "policy-deferred", + "rationale": "Bounded memories per standalone Store; expose with the Wasm feature-policy surface if multi-memory workloads require it." + }, + { + "name": "DEFAULT_MAX_TABLES", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "policy-deferred", + "rationale": "Bounded tables per standalone Store; expose with the Wasm feature-policy surface if multi-table workloads require it." + }, + { + "name": "DEFAULT_MAX_TABLE_ELEMENTS", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "policy-deferred", + "rationale": "Bounded guest table elements per Store; expose through Wasm resource configuration if operators need to tune it." + }, + { + "name": "DEFAULT_MAX_WASM_MEMORY_BYTES", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "policy", + "rationale": "Fallback for the typed per-execution Wasmtime linear-memory limit.", + "wired": "WasmExecutionLimits.max_memory_bytes" + }, + { + "name": "DEFAULT_TABLE_ACCOUNTING_BYTES", + "path": "crates/execution/src/wasm/wasmtime/limits.rs", + "class": "invariant", + "rationale": "Conservative aggregate-ledger charge derived from the fixed table-element representation ceiling." + }, + { + "name": "DEFAULT_MAX_MODULE_FILE_BYTES", + "path": "crates/execution/src/wasm/wasmtime/lifecycle.rs", + "class": "policy", + "rationale": "Fallback for the typed standalone-WASM executable image byte limit.", + "wired": "WasmExecutionLimits.max_module_file_bytes" + }, + { + "name": "MAX_ACCOUNT_NAME_BYTES", + "path": "crates/execution/src/wasm/wasmtime/linker/user.rs", + "class": "invariant", + "rationale": "Owned account lookup ABI input ceiling shared with the sidecar host dispatch." + }, + { + "name": "MAX_GROUPS", + "path": "crates/execution/src/wasm/wasmtime/linker/user.rs", + "class": "invariant", + "rationale": "Owned Linux supplementary-group ABI ceiling." + }, + { + "name": "MAX_DNS_PAYLOAD", + "path": "crates/execution/src/wasm/wasmtime/linker/network.rs", + "class": "invariant", + "rationale": "Owned DNS raw-record ABI payload ceiling kept identical to the V8-WASM adapter." + }, + { + "name": "MAX_DNS_RECORDS", + "path": "crates/execution/src/wasm/wasmtime/linker/network.rs", + "class": "invariant", + "rationale": "Owned DNS raw-record ABI count ceiling kept identical to the V8-WASM adapter." + }, + { + "name": "MAX_POLL_FDS", + "path": "crates/execution/src/wasm/wasmtime/linker/network.rs", + "class": "invariant", + "rationale": "Linux IOV_MAX-compatible poll batch ceiling kept identical to the V8-WASM adapter." + }, + { + "name": "MAX_FDS", + "path": "crates/execution/src/wasm/wasmtime/linker/process.rs", + "class": "invariant", + "rationale": "Absolute Linux descriptor-number ceiling for validating exec close lists; the configured RLIMIT_NOFILE is tighter." + }, + { + "name": "MAX_RIGHTS", + "path": "crates/execution/src/wasm/wasmtime/linker/process.rs", + "class": "invariant", + "rationale": "Generated host-permission registry width, not a tunable runtime bound." + }, + { + "name": "MAX_IOVECS", + "path": "crates/execution/src/wasm/wasmtime/linker/preview1.rs", + "class": "invariant", + "rationale": "Linux IOV_MAX-compatible vectored-I/O ceiling kept identical to the V8-WASM adapter." + }, + { + "name": "MAX_POLL_SUBSCRIPTIONS", + "path": "crates/execution/src/wasm/wasmtime/linker/preview1.rs", + "class": "invariant", + "rationale": "Owned Preview1 poll batch ceiling kept identical to the V8-WASM adapter." + }, + { + "name": "MAX_SIGNALS_PER_SAFE_POINT", + "path": "crates/execution/src/wasm/wasmtime/linker/mod.rs", + "class": "invariant", + "rationale": "Bounded signal-delivery work quantum per guest safe point to preserve scheduler fairness." + }, + { + "name": "XATTR_NAME_MAX", + "path": "crates/execution/src/wasm/wasmtime/linker/filesystem.rs", + "class": "invariant", + "rationale": "Linux XATTR_NAME_MAX compatibility constant." + }, + { + "name": "XATTR_SIZE_MAX", + "path": "crates/execution/src/wasm/wasmtime/linker/filesystem.rs", + "class": "invariant", + "rationale": "Linux xattr value-size compatibility ceiling." + }, + { + "name": "DEFAULT_MAX_HOST_REPLY_BYTES", + "path": "crates/execution/src/wasm/wasmtime/store.rs", + "class": "policy", + "rationale": "Fallback for the typed Wasmtime host-reply transport byte limit.", + "wired": "WasmExecutionLimits.max_sync_rpc_response_line_bytes" + }, + { + "name": "DEFAULT_MAX_PROCESS_WASM_MEMORY_BYTES", + "path": "crates/runtime/src/lib.rs", + "class": "policy", + "rationale": "Default aggregate linear-memory envelope for active standalone-WASM Stores.", + "wired": "RuntimeConfig.resources.max_wasm_memory_bytes" } ] diff --git a/crates/native-sidecar/tests/guest_identity.rs b/crates/native-sidecar/tests/guest_identity.rs index 3dc852256f..db7d7feb7a 100644 --- a/crates/native-sidecar/tests/guest_identity.rs +++ b/crates/native-sidecar/tests/guest_identity.rs @@ -416,7 +416,7 @@ fn wasm_guest_identity_commands_use_kernel_owned_defaults() { &vm_id, "proc-wasm-identity", GuestRuntimeKind::WebAssembly, - &wasm_path, + wasm_path, Vec::new(), ); @@ -630,7 +630,7 @@ fn wasm_guest_created_pty_uses_live_bounded_kernel_state() { &vm_id, "proc-wasm-guest-pty", GuestRuntimeKind::WebAssembly, - &wasm_path, + wasm_path, Vec::new(), ); let (stdout, stderr, exit_code) = collect_guest_identity_process_output( @@ -772,7 +772,7 @@ fn wasm_guest_env_filters_internal_control_vars_and_uses_kernel_defaults() { &vm_id, "proc-wasm-env", GuestRuntimeKind::WebAssembly, - &wasm_path, + wasm_path, Vec::new(), ); @@ -930,7 +930,7 @@ fn wasm_preopens_and_rights_are_kernel_authoritative() { &vm_id, "proc-wasm-preopen", GuestRuntimeKind::WebAssembly, - &wasm_path, + wasm_path, Vec::new(), ); let (stdout, stderr, exit_code) = collect_guest_identity_process_output( @@ -979,18 +979,30 @@ fn guest_identity_cases() { let current_exe = std::env::current_exe().expect("current test binary path"); for case_name in GUEST_IDENTITY_CASES { - let status = Command::new(¤t_exe) - .arg("--exact") - .arg("__guest_identity_case_runner") - .arg("--nocapture") - .env("AGENTOS_GUEST_IDENTITY_CASE", case_name) - .status() - .unwrap_or_else(|error| panic!("spawn guest_identity runner for {case_name}: {error}")); - - assert!( - status.success(), - "guest_identity case {case_name} failed with status {status}" - ); + let backends: &[Option<&str>] = if case_name.starts_with("wasm_") { + &[Some("v8"), Some("wasmtime")] + } else { + &[None] + }; + for backend in backends { + let mut command = Command::new(¤t_exe); + command + .arg("--exact") + .arg("__guest_identity_case_runner") + .arg("--nocapture") + .env("AGENTOS_GUEST_IDENTITY_CASE", case_name); + if let Some(backend) = backend { + command.env("AGENTOS_TEST_WASM_BACKEND", backend); + } + let status = command.status().unwrap_or_else(|error| { + panic!("spawn guest_identity runner for {case_name}/{backend:?}: {error}") + }); + + assert!( + status.success(), + "guest_identity case {case_name}/{backend:?} failed with status {status}" + ); + } } } diff --git a/crates/native-sidecar/tests/posix_compliance.rs b/crates/native-sidecar/tests/posix_compliance.rs index 981de30bbe..023343c716 100644 --- a/crates/native-sidecar/tests/posix_compliance.rs +++ b/crates/native-sidecar/tests/posix_compliance.rs @@ -123,6 +123,7 @@ fn wait_for_process_output( #[derive(Default)] struct MockProcessState { kills: Vec, + #[allow(dead_code)] exit_code: Option, binding: Option<(ProcessTable, u32)>, } @@ -152,6 +153,7 @@ impl MockDriverProcess { .binding = Some((table.clone(), pid)); } + #[allow(dead_code)] fn exit(&self, exit_code: i32) { let binding = { let mut state = self.state.lock().expect("mock process lock poisoned"); diff --git a/crates/native-sidecar/tests/python.rs b/crates/native-sidecar/tests/python.rs index 691a003d80..c84f12eaaa 100644 --- a/crates/native-sidecar/tests/python.rs +++ b/crates/native-sidecar/tests/python.rs @@ -414,6 +414,7 @@ fn execute_python_entrypoint_with_env( env, cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start python execution through wire"); @@ -451,6 +452,7 @@ fn execute_javascript_with_env( env, cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start JavaScript execution through wire"); @@ -3282,6 +3284,7 @@ fn execute_python_cli( env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start python CLI execution through wire"); @@ -3319,6 +3322,7 @@ fn execute_python_cli_with_env( env, cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start python CLI execution through wire"); diff --git a/crates/native-sidecar/tests/security_hardening.rs b/crates/native-sidecar/tests/security_hardening.rs index a2379e12f0..3f0da04516 100644 --- a/crates/native-sidecar/tests/security_hardening.rs +++ b/crates/native-sidecar/tests/security_hardening.rs @@ -382,6 +382,7 @@ fn vm_resource_limits_cap_active_processes_without_poisoning_followup_execs() { env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch second execute"); @@ -455,6 +456,7 @@ fn execute_rejects_cwd_outside_vm_sandbox_root() { env: HashMap::new(), cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch execute request"); @@ -528,6 +530,7 @@ fn execute_rejects_host_only_absolute_command_path() { env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch host-only command execute"); @@ -597,6 +600,7 @@ fn execute_ignores_host_node_binary_override_for_javascript_runtime() { env: HashMap::new(), cwd: Some(nested_cwd.to_string_lossy().into_owned()), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch execute request"); diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index 32fecc8423..abfadea74e 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -2558,6 +2558,7 @@ ykAheWCsAteSEWVc0w==\n\ env: env.into_iter().collect(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch guest command"); @@ -2973,7 +2974,7 @@ console.log(JSON.stringify({ status: "ok", summary })); outputs } - fn ledger_usage_snapshot(resources: &ResourceLedger) -> [usize; 29] { + fn ledger_usage_snapshot(resources: &ResourceLedger) -> [usize; 30] { ResourceClass::ALL.map(|class| resources.usage(class).used) } @@ -11436,6 +11437,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch lifecycle publication failure"); @@ -11505,6 +11507,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch python execute"); @@ -11618,6 +11621,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch wasm command execute"); @@ -11722,6 +11726,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch wasm command execute"); @@ -11784,6 +11789,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch wasm execute"); @@ -11867,6 +11873,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ]), cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch managed WASM pipe probe"); @@ -11900,7 +11907,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .collect::>(); assert!( - snapshot.pipes >= baseline.pipes + 1, + snapshot.pipes > baseline.pipes, "fd_pipe must allocate in the authoritative kernel pipe table" ); assert_eq!( @@ -12161,6 +12168,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch wasm execute"); @@ -12216,6 +12224,7 @@ console.log(JSON.stringify({ status: "ok", summary })); env: std::collections::HashMap::new(), cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch wasm execute"); @@ -14837,6 +14846,7 @@ process.stdout.write(`${JSON.stringify({ env: std::collections::HashMap::new(), cwd: Some(String::from("/workspace")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch javascript command execute"); @@ -15073,6 +15083,7 @@ if (child.status !== 0) { env: std::collections::HashMap::new(), cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch agentos package execute"); @@ -15143,6 +15154,7 @@ if (child.status !== 0) { env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch node eval execute"); @@ -15185,6 +15197,7 @@ if (child.status !== 0) { env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch missing command execute"); @@ -16926,6 +16939,7 @@ console.log(seen.join("\n")); env: std::collections::HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("dispatch import fresh execute"); diff --git a/crates/native-sidecar/tests/signal.rs b/crates/native-sidecar/tests/signal.rs index ba429e8b0a..ac828f9189 100644 --- a/crates/native-sidecar/tests/signal.rs +++ b/crates/native-sidecar/tests/signal.rs @@ -821,6 +821,7 @@ fn pty_resize_delivers_sigwinch_to_nested_foreground_runtime() { env: HashMap::from([(String::from("AGENTOS_EXEC_TTY"), String::from("1"))]), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("start PTY parent"); diff --git a/crates/native-sidecar/tests/stdio_binary.rs b/crates/native-sidecar/tests/stdio_binary.rs index a93e18d093..a44627a4ef 100644 --- a/crates/native-sidecar/tests/stdio_binary.rs +++ b/crates/native-sidecar/tests/stdio_binary.rs @@ -775,6 +775,7 @@ fn native_sidecar_binary_runs_the_framed_protocol_over_stdio() { env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend: None, }), ), ); diff --git a/crates/native-sidecar/tests/support/mod.rs b/crates/native-sidecar/tests/support/mod.rs index e389b6f271..d7071f422b 100644 --- a/crates/native-sidecar/tests/support/mod.rs +++ b/crates/native-sidecar/tests/support/mod.rs @@ -346,6 +346,15 @@ pub fn execute_wire( entrypoint: &Path, args: Vec, ) { + let wasm_backend = if runtime == agentos_native_sidecar::wire::GuestRuntimeKind::WebAssembly { + match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { + Ok("wasmtime") => Some(agentos_native_sidecar::wire::StandaloneWasmBackend::Wasmtime), + Ok("v8") | Err(_) => Some(agentos_native_sidecar::wire::StandaloneWasmBackend::V8), + Ok(value) => panic!("unknown AGENTOS_TEST_WASM_BACKEND value {value:?}"), + } + } else { + None + }; let result = sidecar .dispatch_wire_blocking(wire_request( request_id, @@ -360,6 +369,7 @@ pub fn execute_wire( env: HashMap::new(), cwd: None, wasm_permission_tier: None, + wasm_backend, }, ), )) diff --git a/crates/native-sidecar/tests/wasm_raw_abi.rs b/crates/native-sidecar/tests/wasm_raw_abi.rs index 95e4096e0e..0ec8bea074 100644 --- a/crates/native-sidecar/tests/wasm_raw_abi.rs +++ b/crates/native-sidecar/tests/wasm_raw_abi.rs @@ -1,7 +1,8 @@ mod support; use agentos_native_sidecar::wire::{ - ExecuteRequest, GuestRuntimeKind, RequestPayload, ResponsePayload, WasmPermissionTier, + ExecuteRequest, GuestRuntimeKind, RequestPayload, ResponsePayload, StandaloneWasmBackend, + WasmPermissionTier, }; use agentos_wasm_abi_generator::{ imports_module, raw_call_assertion_module, single_import_module, AbiImport, AbiManifest, @@ -376,6 +377,34 @@ fn run_raw_module_with_metadata( module: &[u8], tier: WasmPermissionTier, metadata: HashMap, +) -> (String, String, i32) { + let v8 = run_raw_module_for_backend( + &format!("{name}-v8"), + module, + tier, + metadata.clone(), + StandaloneWasmBackend::V8, + ); + let wasmtime = run_raw_module_for_backend( + &format!("{name}-wasmtime"), + module, + tier, + metadata, + StandaloneWasmBackend::Wasmtime, + ); + assert_eq!( + wasmtime, v8, + "Wasmtime and V8-WASM raw ABI outcomes diverged for {name}" + ); + wasmtime +} + +fn run_raw_module_for_backend( + name: &str, + module: &[u8], + tier: WasmPermissionTier, + metadata: HashMap, + backend: StandaloneWasmBackend, ) -> (String, String, i32) { let mut sidecar = new_sidecar(name); let cwd = temp_dir(&format!("{name}-cwd")); @@ -406,6 +435,7 @@ fn run_raw_module_with_metadata( env: HashMap::new(), cwd: None, wasm_permission_tier: Some(tier), + wasm_backend: Some(backend), }), )) .expect("start generated raw-ABI fixture through sidecar"); diff --git a/crates/native-sidecar/tests/wasm_software_parity.rs b/crates/native-sidecar/tests/wasm_software_parity.rs new file mode 100644 index 0000000000..c6b6b83f8e --- /dev/null +++ b/crates/native-sidecar/tests/wasm_software_parity.rs @@ -0,0 +1,422 @@ +mod support; + +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::{Duration, Instant}; + +use agentos_native_sidecar::wire::{ + ExecuteRequest, GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, + RequestPayload, ResponsePayload, RootFilesystemEntryEncoding, StandaloneWasmBackend, +}; +use base64::Engine as _; + +use support::{ + authenticate_wire, collect_process_output_wire_with_timeout, new_sidecar, open_session_wire, + temp_dir, wire_request, wire_vm, write_fixture, +}; + +fn command_artifact(name: &str) -> PathBuf { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let staged = root.join("packages/runtime-core/commands").join(name); + if staged.is_file() { + return staged; + } + root.join("toolchain/target/wasm32-wasip1/release/commands") + .join(name) +} + +fn run_command( + test_name: &str, + command: &str, + args: &[&str], + backend: StandaloneWasmBackend, +) -> (String, String, i32) { + run_command_with_files(test_name, command, args, &[], backend) +} + +fn run_command_with_files( + test_name: &str, + command: &str, + args: &[&str], + extra_commands: &[&str], + backend: StandaloneWasmBackend, +) -> (String, String, i32) { + run_command_with_files_and_metadata( + test_name, + command, + args, + extra_commands, + backend, + HashMap::new(), + ) +} + +fn run_command_with_files_and_metadata( + test_name: &str, + command: &str, + args: &[&str], + extra_commands: &[&str], + backend: StandaloneWasmBackend, + metadata: HashMap, +) -> (String, String, i32) { + let artifact = command_artifact(command); + let module = std::fs::read(&artifact).unwrap_or_else(|error| { + panic!( + "generated command artifact {} is required: {error}", + artifact.display() + ) + }); + let mut sidecar = new_sidecar(test_name); + let cwd = temp_dir(&format!("{test_name}-cwd")); + let entrypoint = cwd.join(command); + write_fixture(&entrypoint, &module); + make_executable(&entrypoint); + for extra in extra_commands { + let path = cwd.join(extra); + write_fixture( + &path, + std::fs::read(command_artifact(extra)) + .unwrap_or_else(|error| panic!("generated {extra} command is required: {error}")), + ); + make_executable(&path); + } + + let connection_id = authenticate_wire(&mut sidecar, "conn-software-parity"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + metadata, + ); + support::write_guest_file_wire( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "/workspace/fixture.txt", + "fixture\n", + ); + for (index, extra) in extra_commands.iter().enumerate() { + write_guest_binary( + &mut sidecar, + 10 + index as i64, + &connection_id, + &session_id, + &vm_id, + &format!("/workspace/{extra}"), + &std::fs::read(command_artifact(extra)).expect("read generated child command"), + ); + } + let process_id = format!("process-{test_name}"); + let started = sidecar + .dispatch_wire_blocking(wire_request( + 100, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.clone(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: args.iter().map(|arg| (*arg).to_owned()).collect(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: None, + wasm_backend: Some(backend), + }), + )) + .expect("start generated software command through sidecar"); + assert!( + matches!( + started.response.payload, + ResponsePayload::ProcessStartedResponse(_) + ), + "unexpected software start response: {:?}", + started.response.payload + ); + + collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &process_id, + Duration::from_secs(60), + ) +} + +fn write_guest_binary( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + request_id: i64, + connection_id: &str, + session_id: &str, + vm_id: &str, + path: &str, + contents: &[u8], +) { + let result = sidecar + .dispatch_wire_blocking(wire_request( + request_id, + wire_vm(connection_id, session_id, vm_id), + RequestPayload::GuestFilesystemCallRequest(GuestFilesystemCallRequest { + operation: GuestFilesystemOperation::WriteFile, + path: path.to_owned(), + destination_path: None, + target: None, + content: Some(base64::engine::general_purpose::STANDARD.encode(contents)), + encoding: Some(RootFilesystemEntryEncoding::Base64), + recursive: false, + max_depth: None, + mode: Some(0o755), + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + }), + )) + .expect("write generated child command into guest VFS"); + assert!( + matches!( + result.response.payload, + ResponsePayload::GuestFilesystemResultResponse(_) + ), + "unexpected guest binary write response: {:?}", + result.response.payload + ); + let chmod = sidecar + .dispatch_wire_blocking(wire_request( + request_id + 1_000, + wire_vm(connection_id, session_id, vm_id), + RequestPayload::GuestFilesystemCallRequest(GuestFilesystemCallRequest { + operation: GuestFilesystemOperation::Chmod, + path: path.to_owned(), + destination_path: None, + target: None, + content: None, + encoding: None, + recursive: false, + max_depth: None, + mode: Some(0o755), + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + }), + )) + .expect("mark generated child command executable in guest VFS"); + assert!( + matches!( + chmod.response.payload, + ResponsePayload::GuestFilesystemResultResponse(_) + ), + "unexpected guest chmod response: {:?}", + chmod.response.payload + ); +} + +#[cfg(unix)] +fn make_executable(path: &Path) { + use std::os::unix::fs::PermissionsExt; + let mut permissions = std::fs::metadata(path) + .expect("command metadata") + .permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(path, permissions).expect("mark command executable"); +} + +#[cfg(not(unix))] +fn make_executable(_path: &Path) {} + +fn assert_command_parity(command: &str, args: &[&str]) -> (String, String, i32) { + let v8 = run_command( + &format!("software-{command}-v8"), + command, + args, + StandaloneWasmBackend::V8, + ); + let wasmtime = run_command( + &format!("software-{command}-wasmtime"), + command, + args, + StandaloneWasmBackend::Wasmtime, + ); + assert_eq!(wasmtime, v8, "software command {command} diverged"); + wasmtime +} + +fn assert_command_parity_with_files( + command: &str, + args: &[&str], + extra_commands: &[&str], +) -> (String, String, i32) { + let v8 = run_command_with_files( + &format!("software-{command}-v8"), + command, + args, + extra_commands, + StandaloneWasmBackend::V8, + ); + let wasmtime = run_command_with_files( + &format!("software-{command}-wasmtime"), + command, + args, + extra_commands, + StandaloneWasmBackend::Wasmtime, + ); + assert_eq!(wasmtime, v8, "software command {command} diverged"); + wasmtime +} + +fn run_curl_http_backend(backend: StandaloneWasmBackend) -> (String, String, i32) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind parity HTTP listener"); + listener + .set_nonblocking(true) + .expect("make parity HTTP listener deadline-aware"); + let port = listener.local_addr().expect("listener address").port(); + let server = thread::spawn(move || { + let deadline = Instant::now() + Duration::from_secs(60); + let (mut stream, _) = loop { + match listener.accept() { + Ok(accepted) => break accepted, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + assert!( + Instant::now() < deadline, + "curl did not reach the parity HTTP listener before its deadline" + ); + thread::sleep(Duration::from_millis(10)); + } + Err(error) => panic!("accept parity HTTP request: {error}"), + } + }; + stream + .set_read_timeout(Some(Duration::from_secs(10))) + .expect("set request deadline"); + let mut request = Vec::new(); + let mut chunk = [0u8; 1024]; + while !request.windows(4).any(|window| window == b"\r\n\r\n") { + let read = stream.read(&mut chunk).expect("read parity HTTP request"); + assert_ne!(read, 0, "curl closed before sending request headers"); + request.extend_from_slice(&chunk[..read]); + assert!( + request.len() <= 16 * 1024, + "curl request headers are bounded" + ); + } + assert!( + request.starts_with(b"GET /wasmtime-parity HTTP/1."), + "unexpected curl request: {:?}", + String::from_utf8_lossy(&request) + ); + stream + .write_all( + b"HTTP/1.1 200 OK\r\nContent-Length: 21\r\nConnection: close\r\n\r\nagentos-network-path\n", + ) + .expect("write parity HTTP response"); + }); + let result = run_command_with_files_and_metadata( + &format!("software-curl-http-{backend:?}"), + "curl", + &["-fsS", &format!("http://127.0.0.1:{port}/wasmtime-parity")], + &[], + backend, + HashMap::from([( + String::from("env.AGENTOS_LOOPBACK_EXEMPT_PORTS"), + format!("[{port}]"), + )]), + ); + server.join().expect("parity HTTP server"); + result +} + +#[test] +#[ignore = "requires the generated packages/runtime-core/commands corpus"] +fn coreutils_ls_matches_v8_wasm() { + let (stdout, stderr, exit_code) = assert_command_parity("ls", &["-1", "/workspace"]); + assert_eq!(exit_code, 0, "stderr: {stderr}"); + assert!(stdout.contains("fixture.txt"), "stdout: {stdout}"); +} + +#[test] +#[ignore = "requires the generated packages/runtime-core/commands corpus"] +fn direct_software_corpus_matches_v8_wasm() { + for (command, args, expected) in [ + ("grep", vec!["fixture", "/workspace/fixture.txt"], "fixture"), + ("sqlite3", vec![":memory:", "select 1;"], "1"), + ("git", vec!["--version"], "git version"), + ( + "tar", + vec![ + "-cf", + "/workspace/fixture.tar", + "-C", + "/workspace", + "fixture.txt", + ], + "", + ), + ("gzip", vec!["-c", "/workspace/fixture.txt"], ""), + ("curl", vec!["--version"], "curl"), + ("stat", vec!["-c", "%s", "/workspace/fixture.txt"], "8"), + ] { + let (stdout, stderr, exit_code) = assert_command_parity(command, &args); + assert_eq!(exit_code, 0, "{command} stderr: {stderr}"); + assert!( + stdout.contains(expected), + "{command} output omitted {expected:?}: {stdout:?}" + ); + } +} + +#[test] +#[ignore = "requires the generated packages/runtime-core/commands corpus"] +fn shell_pipeline_and_child_backend_affinity_match_v8_wasm() { + let script = "printf 'alpha\\nbeta\\n' | /workspace/grep beta"; + let (stdout, stderr, exit_code) = + assert_command_parity_with_files("sh", &["-c", script], &["grep"]); + assert_eq!(exit_code, 0, "stderr: {stderr}"); + assert_eq!(stdout, "beta\n"); +} + +#[test] +#[ignore = "requires the focused generated vim command artifact"] +fn vim_batch_edit_matches_v8_wasm() { + let (stdout, stderr, exit_code) = assert_command_parity( + "vim", + &[ + "-Nu", + "NONE", + "-n", + "-e", + "-c", + "%s/fixture/edited/", + "-c", + "%print", + "-c", + "q!", + "/workspace/fixture.txt", + ], + ); + assert_eq!(exit_code, 0, "stderr: {stderr}"); + assert!(stdout.contains("edited"), "stdout: {stdout:?}"); +} + +#[test] +#[ignore = "requires the generated curl command artifact"] +fn curl_network_path_matches_v8_wasm() { + let v8 = run_curl_http_backend(StandaloneWasmBackend::V8); + let wasmtime = run_curl_http_backend(StandaloneWasmBackend::Wasmtime); + assert_eq!(wasmtime, v8, "curl HTTP behavior diverged"); + assert_eq!(wasmtime.2, 0, "stderr: {}", wasmtime.1); + assert_eq!(wasmtime.0, "agentos-network-path\n"); +} diff --git a/crates/native-sidecar/tests/wasmtime_safety.rs b/crates/native-sidecar/tests/wasmtime_safety.rs new file mode 100644 index 0000000000..80b1a16105 --- /dev/null +++ b/crates/native-sidecar/tests/wasmtime_safety.rs @@ -0,0 +1,305 @@ +mod support; + +use std::collections::HashMap; +use std::time::Duration; + +use agentos_native_sidecar::wire::{ + ExecuteRequest, GuestRuntimeKind, KillProcessRequest, RequestPayload, ResponsePayload, + StandaloneWasmBackend, WasmPermissionTier, +}; +use support::{ + authenticate_wire, collect_process_output_wire_with_timeout, new_sidecar, open_session_wire, + temp_dir, wire_request, wire_vm, write_fixture, +}; + +fn run_wasmtime( + name: &str, + module: &[u8], + metadata: HashMap, +) -> (String, String, i32) { + run_wasmtime_with_tier(name, module, metadata, WasmPermissionTier::Isolated) +} + +fn run_wasmtime_with_tier( + name: &str, + module: &[u8], + metadata: HashMap, + permission_tier: WasmPermissionTier, +) -> (String, String, i32) { + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("fixture.wasm"); + write_fixture(&entrypoint, module); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-safety"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + metadata, + ); + let process_id = format!("process-{name}"); + let started = sidecar + .dispatch_wire_blocking(wire_request( + 4, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.clone(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: Some(permission_tier), + wasm_backend: Some(StandaloneWasmBackend::Wasmtime), + }), + )) + .expect("start Wasmtime safety fixture"); + assert!(matches!( + started.response.payload, + ResponsePayload::ProcessStartedResponse(_) + )); + collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &process_id, + Duration::from_secs(10), + ) +} + +#[test] +fn permission_filter_denies_ungranted_host_families_without_ambient_fallback() { + let denied = wat::parse_str( + r#"(module + (import "host_process" "sleep_ms" (func $sleep (param i32) (result i32))) + (func (export "_start") (drop (call $sleep (i32.const 1)))))"#, + ) + .expect("denied import fixture"); + let (_, stderr, exit_code) = run_wasmtime("wasmtime-denied-import", &denied, HashMap::new()); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT"), + "stderr: {stderr}" + ); + assert!(stderr.contains("host_process.sleep_ms"), "stderr: {stderr}"); +} + +#[test] +fn active_cpu_budget_pauses_during_async_kernel_waits() { + let waiting = wat::parse_str( + r#"(module + (import "host_process" "sleep_ms" (func $sleep (param i32) (result i32))) + (func (export "_start") + (if (i32.ne (call $sleep (i32.const 100)) (i32.const 0)) + (then unreachable))))"#, + ) + .expect("async wait fixture"); + let (stdout, stderr, exit_code) = run_wasmtime_with_tier( + "wasmtime-active-cpu-paused-wait", + &waiting, + HashMap::from([( + String::from("limits.wasm.active_cpu_time_limit_ms"), + String::from("20"), + )]), + WasmPermissionTier::Full, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +fn infinite_loop_module() -> Vec { + wat::parse_str( + r#"(module + (func (export "_start") + (loop $forever (br $forever))))"#, + ) + .expect("infinite-loop fixture") +} + +#[test] +fn malformed_and_hostile_imports_fail_with_stable_typed_errors() { + let (_, stderr, exit_code) = + run_wasmtime("wasmtime-malformed", b"\0asm\x01\0\0", HashMap::new()); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASM_INVALID_MODULE"), + "stderr: {stderr}" + ); + + let hostile = wat::parse_str( + r#"(module + (import "env" "ambient_host_escape" (func $escape)) + (func (export "_start") (call $escape)))"#, + ) + .expect("hostile import fixture"); + let (_, stderr, exit_code) = run_wasmtime("wasmtime-hostile-import", &hostile, HashMap::new()); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT"), + "stderr: {stderr}" + ); + assert!(!stderr.contains("unknown import"), "stderr: {stderr}"); +} + +#[test] +fn memory_table_stack_fuel_cpu_and_wall_limits_fail_closed() { + let exact_memory = + wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#) + .expect("exact-memory fixture"); + let (stdout, stderr, exit_code) = run_wasmtime( + "wasmtime-memory-at-limit", + &exact_memory, + HashMap::from([( + String::from("resource.max_wasm_memory_bytes"), + String::from("65536"), + )]), + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); + + let oversized_memory = + wat::parse_str(r#"(module (memory (export "memory") 2) (func (export "_start")))"#) + .expect("memory-limit fixture"); + let (_, stderr, exit_code) = run_wasmtime( + "wasmtime-memory-limit", + &oversized_memory, + HashMap::from([( + String::from("resource.max_wasm_memory_bytes"), + String::from("65536"), + )]), + ); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASMTIME_MEMORY_LIMIT"), + "stderr: {stderr}" + ); + + let oversized_table = + wat::parse_str(r#"(module (table 1000001 funcref) (func (export "_start")))"#) + .expect("table-limit fixture"); + let (_, stderr, exit_code) = + run_wasmtime("wasmtime-table-limit", &oversized_table, HashMap::new()); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASMTIME_TABLE_LIMIT"), + "stderr: {stderr}" + ); + + let recursive = wat::parse_str( + r#"(module + (func $recurse (call $recurse)) + (func (export "_start") (call $recurse)))"#, + ) + .expect("stack-limit fixture"); + let (_, stderr, exit_code) = run_wasmtime( + "wasmtime-stack-limit", + &recursive, + HashMap::from([( + String::from("resource.max_wasm_stack_bytes"), + String::from("65536"), + )]), + ); + assert_eq!(exit_code, 1); + assert!( + stderr.contains("ERR_AGENTOS_WASMTIME_STACK_EXHAUSTED"), + "stderr: {stderr}" + ); + + for (name, metadata_key, value, expected) in [ + ( + "fuel", + "limits.wasm.deterministic_fuel", + "1000", + "ERR_AGENTOS_WASMTIME_FUEL_EXHAUSTED", + ), + ( + "active-cpu", + "limits.wasm.active_cpu_time_limit_ms", + "20", + "ERR_AGENTOS_WASMTIME_ACTIVE_CPU_LIMIT", + ), + ( + "wall-clock", + "limits.wasm.wall_clock_limit_ms", + "20", + "ERR_AGENTOS_WASMTIME_WALL_CLOCK_LIMIT", + ), + ] { + let (_, stderr, exit_code) = run_wasmtime( + &format!("wasmtime-{name}-limit"), + &infinite_loop_module(), + HashMap::from([(metadata_key.to_owned(), value.to_owned())]), + ); + assert_eq!(exit_code, 1, "{name} stderr: {stderr}"); + assert!(stderr.contains(expected), "{name} stderr: {stderr}"); + } +} + +#[test] +fn terminal_signal_interrupts_and_reaps_pure_guest_compute() { + let name = "wasmtime-terminal-signal"; + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("loop.wasm"); + write_fixture(&entrypoint, infinite_loop_module()); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-kill"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + ); + let process_id = String::from("process-wasmtime-loop"); + let started = sidecar + .dispatch_wire_blocking(wire_request( + 4, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.clone(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: Some(WasmPermissionTier::Isolated), + wasm_backend: Some(StandaloneWasmBackend::Wasmtime), + }), + )) + .expect("start pure-compute Wasmtime process"); + assert!(matches!( + started.response.payload, + ResponsePayload::ProcessStartedResponse(_) + )); + + let killed = sidecar + .dispatch_wire_blocking(wire_request( + 5, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::KillProcessRequest(KillProcessRequest { + process_id: process_id.clone(), + signal: String::from("SIGKILL"), + }), + )) + .expect("kill pure-compute Wasmtime process"); + assert!(matches!( + killed.response.payload, + ResponsePayload::ProcessKilledResponse(_) + )); + let (_, _, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &process_id, + Duration::from_secs(5), + ); + assert_eq!(exit_code, 137); +} diff --git a/crates/native-sidecar/tests/xfstests_correctness.rs b/crates/native-sidecar/tests/xfstests_correctness.rs index 75f18f730e..d17fa080f3 100644 --- a/crates/native-sidecar/tests/xfstests_correctness.rs +++ b/crates/native-sidecar/tests/xfstests_correctness.rs @@ -662,6 +662,7 @@ fn try_execute_command_with_env( env, cwd: Some(String::from("/")), wasm_permission_tier: None, + wasm_backend: None, }), )) .expect("execute verification command"); diff --git a/crates/resource/src/lib.rs b/crates/resource/src/lib.rs index 4778d642a8..9c6f4b1bb6 100644 --- a/crates/resource/src/lib.rs +++ b/crates/resource/src/lib.rs @@ -35,6 +35,7 @@ pub enum ResourceClass { Tasks, ExecutorSlots, ExecutorBytes, + WasmMemoryBytes, Http2Connections, Http2Streams, Http2BufferedBytes, @@ -47,7 +48,7 @@ pub enum ResourceClass { } impl ResourceClass { - pub const ALL: [Self; 29] = [ + pub const ALL: [Self; 30] = [ Self::Capabilities, Self::ReadyHandles, Self::Sockets, @@ -68,6 +69,7 @@ impl ResourceClass { Self::Tasks, Self::ExecutorSlots, Self::ExecutorBytes, + Self::WasmMemoryBytes, Self::Http2Connections, Self::Http2Streams, Self::Http2BufferedBytes, @@ -101,6 +103,7 @@ impl ResourceClass { Self::Tasks => "tasks", Self::ExecutorSlots => "executorSlots", Self::ExecutorBytes => "executorBytes", + Self::WasmMemoryBytes => "wasmMemoryBytes", Self::Http2Connections => "http2Connections", Self::Http2Streams => "http2Streams", Self::Http2BufferedBytes => "http2BufferedBytes", diff --git a/crates/runtime/src/accounting.rs b/crates/runtime/src/accounting.rs index d09af1dd47..de3446dc85 100644 --- a/crates/runtime/src/accounting.rs +++ b/crates/runtime/src/accounting.rs @@ -47,6 +47,9 @@ impl ResourceUsageObserver for RuntimeMetrics { ResourceClass::Tasks => self.observe_resource(ResourceMetricClass::Tasks, used), ResourceClass::ExecutorSlots => {} ResourceClass::ExecutorBytes => self.observe_buffer(BufferMetricClass::Executor, used), + ResourceClass::WasmMemoryBytes => { + self.observe_buffer(BufferMetricClass::Executor, used) + } ResourceClass::Http2BufferedBytes => { self.observe_buffer(BufferMetricClass::Http2, used) } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 84752e7ba8..8b850e72da 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -62,6 +62,7 @@ const DEFAULT_MAX_PROCESS_ASYNC_COMPLETION_BYTES: usize = 256 * 1024 * 1024; const DEFAULT_MAX_PROCESS_UDP_DATAGRAMS: usize = 65_536; const DEFAULT_MAX_PROCESS_UDP_BYTES: usize = 256 * 1024 * 1024; const DEFAULT_MAX_PROCESS_TLS_BYTES: usize = 256 * 1024 * 1024; +const DEFAULT_MAX_PROCESS_WASM_MEMORY_BYTES: usize = 8 * 1024 * 1024 * 1024; const DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS: usize = 4_096; const DEFAULT_MAX_PROCESS_HTTP2_STREAMS: usize = 65_536; const DEFAULT_MAX_PROCESS_HTTP2_BYTES: usize = 512 * 1024 * 1024; @@ -217,6 +218,9 @@ pub struct RuntimeResourceConfig { pub max_udp_datagrams: usize, pub max_udp_bytes: usize, pub max_tls_bytes: usize, + /// Aggregate admitted linear-memory envelopes for active standalone WASM + /// Stores. This must not share the blocking-work byte ledger. + pub max_wasm_memory_bytes: usize, pub max_http2_connections: usize, pub max_http2_streams: usize, pub max_http2_buffered_bytes: usize, @@ -249,6 +253,7 @@ impl Default for RuntimeResourceConfig { max_udp_datagrams: DEFAULT_MAX_PROCESS_UDP_DATAGRAMS, max_udp_bytes: DEFAULT_MAX_PROCESS_UDP_BYTES, max_tls_bytes: DEFAULT_MAX_PROCESS_TLS_BYTES, + max_wasm_memory_bytes: DEFAULT_MAX_PROCESS_WASM_MEMORY_BYTES, max_http2_connections: DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS, max_http2_streams: DEFAULT_MAX_PROCESS_HTTP2_STREAMS, max_http2_buffered_bytes: DEFAULT_MAX_PROCESS_HTTP2_BYTES, @@ -358,6 +363,13 @@ impl RuntimeResourceConfig { ResourceClass::TlsBytes, ResourceLimit::new(self.max_tls_bytes, "runtime.resources.maxTlsBytes"), ), + ( + ResourceClass::WasmMemoryBytes, + ResourceLimit::new( + self.max_wasm_memory_bytes, + "runtime.resources.maxWasmMemoryBytes", + ), + ), ( ResourceClass::Http2Connections, ResourceLimit::new( @@ -591,6 +603,10 @@ impl RuntimeConfig { "runtime.resources.maxTlsBytes", self.resources.max_tls_bytes, ), + ( + "runtime.resources.maxWasmMemoryBytes", + self.resources.max_wasm_memory_bytes, + ), ( "runtime.resources.maxHttp2Connections", self.resources.max_http2_connections, diff --git a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare index 5b15f04036..3ad407c148 100644 --- a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare +++ b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare @@ -216,6 +216,11 @@ type WasmPermissionTier enum { ISOLATED } +type StandaloneWasmBackend enum { + V8 + WASMTIME +} + # agentOS package descriptor. `path` is the trusted host path of the package: # normally the packed `.aospkg` file (header + vbare manifest + mount index + # mount tar; see crates/vfs/package-format/v1.bare). The sidecar reads the @@ -382,6 +387,7 @@ type ExecuteRequest struct { env: map cwd: optional wasmPermissionTier: optional + wasmBackend: optional } type WriteStdinRequest struct { diff --git a/crates/sidecar-protocol/src/protocol.rs b/crates/sidecar-protocol/src/protocol.rs index 07594a59bd..38c6323639 100644 --- a/crates/sidecar-protocol/src/protocol.rs +++ b/crates/sidecar-protocol/src/protocol.rs @@ -1463,6 +1463,7 @@ pub type SoftwareDescriptor = crate::wire::SoftwareDescriptor; pub type ProjectedModuleDescriptor = crate::wire::ProjectedModuleDescriptor; pub type WasmPermissionTier = crate::wire::WasmPermissionTier; +pub type StandaloneWasmBackend = crate::wire::StandaloneWasmBackend; pub type ExecuteRequest = crate::wire::ExecuteRequest; diff --git a/crates/sidecar-protocol/src/wire.rs b/crates/sidecar-protocol/src/wire.rs index b93ef6e3b5..08805011bd 100644 --- a/crates/sidecar-protocol/src/wire.rs +++ b/crates/sidecar-protocol/src/wire.rs @@ -17,6 +17,7 @@ pub use crate::generated_protocol::v1::*; impl Copy for crate::generated_protocol::v1::GuestFilesystemOperation {} impl Copy for crate::generated_protocol::v1::RootFilesystemMode {} impl Copy for crate::generated_protocol::v1::WasmPermissionTier {} +impl Copy for crate::generated_protocol::v1::StandaloneWasmBackend {} // `derive(Default)` cannot be added: these are foreign generated types, so the // `Default` impl must be written by hand here (orphan rule). diff --git a/crates/vm-config/src/lib.rs b/crates/vm-config/src/lib.rs index 4052e23893..4d17820061 100644 --- a/crates/vm-config/src/lib.rs +++ b/crates/vm-config/src/lib.rs @@ -413,7 +413,7 @@ fn validate_account_record_size( syntax_bytes: usize, ) -> Result<(), VmConfigError> { let record_bytes = field_bytes.try_fold(syntax_bytes, usize::checked_add); - if !record_bytes.is_some_and(|bytes| bytes <= MAX_ACCOUNT_RECORD_BYTES) { + if record_bytes.is_none_or(|bytes| bytes > MAX_ACCOUNT_RECORD_BYTES) { return Err(VmConfigError::new(format!( "{label} rendered account record exceeds {MAX_ACCOUNT_RECORD_BYTES} bytes (the 4096-byte ABI buffer includes its terminating NUL)" ))); diff --git a/docker/build/darwin.Dockerfile b/docker/build/darwin.Dockerfile index 8d4bda8f99..b14c5531ef 100644 --- a/docker/build/darwin.Dockerfile +++ b/docker/build/darwin.Dockerfile @@ -11,7 +11,7 @@ ARG TARGET=aarch64-apple-darwin ARG CLANG=aarch64-apple-darwin20.4 ARG BUILD_PROFILE=debug ARG CACHE_PLATFORM=darwin-arm64 -ARG RUST_TOOLCHAIN=1.91.1 +ARG RUST_TOOLCHAIN=1.94.0 ENV SDK=/root/osxcross/target/SDK/MacOSX11.3.sdk \ RUSTC_WRAPPER=sccache \ diff --git a/docker/build/linux-gnu.Dockerfile b/docker/build/linux-gnu.Dockerfile index 42f434513d..64441c3eed 100644 --- a/docker/build/linux-gnu.Dockerfile +++ b/docker/build/linux-gnu.Dockerfile @@ -5,7 +5,7 @@ # Cargo caches can be persisted with BuildKit/GHA like the Darwin build. FROM ubuntu:24.04 -ARG RUST_TOOLCHAIN=1.91.1 +ARG RUST_TOOLCHAIN=1.94.0 ARG TARGET=x86_64-unknown-linux-gnu ARG BUILD_PROFILE=debug ARG CACHE_PLATFORM=linux-x64-gnu @@ -98,6 +98,7 @@ RUN --mount=type=cache,id=cargo-registry-agentos-${CACHE_PLATFORM},target=/usr/l -C link-arg=/tmp/agentos_gettid_shim.o \ -C link-arg=/tmp/agentos_renameat2_shim.o \ ${RUSTFLAGS:-}"; \ + cargo test -p agentos-execution wasmtime --lib --target "$TARGET"; \ if [ "$BUILD_PROFILE" = "release" ]; then FLAG="--release"; PROF=release; else FLAG=""; PROF=debug; fi; \ cargo build $FLAG -p agentos-sidecar -p agentos-native-sidecar --target "$TARGET"; \ mkdir -p /artifacts; \ diff --git a/docs/design/wasmtime-executor.md b/docs/design/wasmtime-executor.md index ad4c356d0a..5c24948d20 100644 --- a/docs/design/wasmtime-executor.md +++ b/docs/design/wasmtime-executor.md @@ -218,18 +218,16 @@ model as every other child. ## 6. Code organization -The expected initial organization is: +The implemented initial organization is: ```text -crates/execution/src/wasm/ - mod.rs public facade and backend selection - types.rs runtime-neutral requests, events, limits, errors - backend.rs internal dual-backend selection contract +crates/execution/src/wasm.rs + retained V8-WASM implementation and dual-backend + facade; keeping this working compatibility backend + intact avoids coupling Wasmtime to a wholesale move - v8_compat/ - mod.rs current implementation moved mostly intact - prewarm.rs - memory_limits.rs +crates/execution/src/wasm/ + profile.rs shared wasmparser proposal profile wasmtime/ mod.rs execution and engine facade @@ -240,25 +238,28 @@ crates/execution/src/wasm/ limits.rs memory, stack, CPU, and cancellation lifecycle.rs start, exit, traps, signals, teardown memory.rs checked guest-memory ABI primitives + error.rs stable AgentOS outcome normalization linker/ - mod.rs + mod.rs generated-registry trampoline and signal checkpoints preview1.rs filesystem.rs network.rs process.rs terminal.rs - identity.rs + user.rs - threads/ later milestone, absent from initial parity + threads/ Phase 4 only; absent from initial parity mod.rs group.rs admission.rs ``` -The current `crates/execution/src/wasm.rs` should initially move mostly intact -under `v8_compat/`. The Wasmtime migration must not be coupled to a wholesale -cleanup of the working compatibility implementation. +Rust permits `wasm.rs` and the `wasm/` submodule directory to coexist. Retaining +the established V8-WASM implementation in `wasm.rs` is deliberate: it keeps the +compatibility backend independently reviewable while new Wasmtime code is +split by responsibility. A later mechanical move under `v8_compat/` would not +change ownership or behavior and is not a prerequisite for this project. Runtime-neutral host operations do not belong exclusively under `wasm/`. Existing kernel and sidecar operations should be exposed through small @@ -660,7 +661,7 @@ revision has been sealed: - [x] Phase 0: specification, inventory, baseline, and locked decisions. - [x] Phase 1: complete runtime-neutral kernel/executor prerequisite. -- [ ] Phase 2: production Wasmtime executor at current V8-WASM parity. +- [x] Phase 2: production Wasmtime executor at current V8-WASM parity. - [ ] Phase 3: performance decision, preferred-backend rollout, and initial project completion. - [ ] Phase 4: separately gated threaded-WASM roadmap completion. @@ -770,72 +771,89 @@ revision has been sealed: ### Phase 2 revision: Add Wasmtime at full current feature parity -- [ ] Pin one reviewed Wasmtime version and revalidate the referenced API +- [x] Pin one reviewed Wasmtime version and revalidate the referenced API defaults, safety contracts, supported platforms, and Cargo feature set. -- [ ] Add the multi-file `crates/execution/src/wasm/wasmtime/` Engine, Store, +- [x] Add the multi-file `crates/execution/src/wasm/wasmtime/` Engine, Store, Module cache, Linker, ABI, memory, error, interruption, and execution modules without creating a new crate or giant source file. -- [ ] Configure a bounded process-wide Engine registry keyed by the exact +- [x] Configure a bounded process-wide Engine registry keyed by the exact AgentOS feature profile and stack cap; enforce the eight-profile default limit and 80% warning. -- [ ] Prevalidate every module with the shared `wasmparser` profile so V8-WASM +- [x] Prevalidate every module with the shared `wasmparser` profile so V8-WASM and Wasmtime accept and reject the same features independently of engine defaults. -- [ ] Add the bounded per-Engine 32-entry/256 MiB charged in-memory Module LRU, +- [x] Add the bounded per-Engine 32-entry/256 MiB charged in-memory Module LRU, exact cache keys, metrics, and eviction behavior; never deserialize native artifacts. -- [ ] Build Store context from trusted VM generation, kernel PID, permission +- [x] Build Store context from trusted VM generation, kernel PID, permission profile, limit ledger, cancellation state, and shared host-service handles only. -- [ ] Enforce linear-memory, table, instance, stack, aggregate memory, active +- [x] Enforce linear-memory, table, instance, stack, aggregate memory, active CPU, optional wall-clock, deterministic-fuel, and interruption limits with typed errors. -- [ ] Implement epoch-based termination and active-CPU accounting that pauses +- [x] Implement epoch-based termination and active-CPU accounting that pauses while an import is asynchronously waiting. -- [ ] Implement cooperative caught-signal delivery at import/safe-point +- [x] Implement cooperative caught-signal delivery at import/safe-point boundaries with exact trampoline validation, inherited-mask setup, one-at-a-time LIFO token settlement, nested delivery, and shared completion/partial-result versus `SA_RESTART` arbitration; use epochs only for STOP scheduling and terminal interruption. -- [ ] Generate and link the owned Preview1 ABI, `wasi_unstable` alias, and every +- [x] Generate and link the owned Preview1 ABI, `wasi_unstable` alias, and every `host_fs`, `host_net`, `host_process`, `host_tty`, and `host_user` function/version over the Phase 1 shared operations using the generated registry and one dynamic `func_new_async` trampoline; no handwritten import-name switchboard is permitted. -- [ ] Do not create a `wasmtime-wasi` context or install ambient filesystem, +- [x] Do not create a `wasmtime-wasi` context or install ambient filesystem, network, process, environment, clock, random, or stdio capabilities. -- [ ] Apply the three-phase async guest-memory contract to every waiting import: +- [x] Apply the three-phase async guest-memory contract to every waiting import: validate/copy bounded input, await with no guest borrow, then reacquire and revalidate output before commit. -- [ ] Prevalidate all output ranges before side effects and make fd/resource +- [x] Prevalidate all output ranges before side effects and make fd/resource allocation transactional when result encoding can fail. -- [ ] Run guest code only on the bounded non-Tokio VM executor while async host +- [x] Run guest code only on the bounded non-Tokio VM executor while async host work continues to use the one sidecar Tokio runtime and its direct waiters. -- [ ] Normalize validation failures, traps, stack exhaustion, cancellation, +- [x] Normalize validation failures, traps, stack exhaustion, cancellation, timeout, fuel exhaustion, exit, terminating signal, errno, and internal faults into stable AgentOS typed outcomes. -- [ ] Add the optional sealed `wasmtime`/`v8` protocol and client selector; +- [x] Add the optional sealed `wasmtime`/`v8` protocol and client selector; omission remains the sidecar-owned V8 default during Phase 2. -- [ ] Keep V8 permanently for JavaScript and keep V8-WASM as an independent, +- [x] Keep V8 permanently for JavaScript and keep V8-WASM as an independent, maintained compatibility backend; add no V8-to-Wasmtime bridge. -- [ ] Keep shared memory, threads, memory64, multi-memory, relaxed SIMD, tail +- [x] Keep shared memory, threads, memory64, multi-memory, relaxed SIMD, tail calls, GC/function references, exceptions, components, custom page sizes, AOT, pooling, Wizer, and live snapshots disabled for initial parity. -- [ ] Pass the complete differential ABI and working-software corpus—including +- [x] Pass the complete differential ABI and working-software corpus—including `ls`, `vim`, `grep`, `curl`, shell pipelines, sqlite, git, tar/gzip, and metadata tools—against both standalone-WASM backends. -- [ ] Pass permission-tier, errno, malformed-module, hostile-import, +- [x] Pass permission-tier, errno, malformed-module, hostile-import, cancellation, signal, fd/process/TTY/network, and every limit-at/over-bound test against Wasmtime with no ambient-host escape. -- [ ] Pass full Linux x86-64 conformance plus Linux arm64 and macOS x86-64/arm64 +- [x] Pass full Linux x86-64 conformance plus Linux arm64 and macOS x86-64/arm64 build and smoke/conformance release gates; keep browser builds out of scope. -- [ ] Verify teardown releases Store, waiter, fd, socket, process, memory, +- [x] Verify teardown releases Store, waiter, fd, socket, process, memory, compiled-code, and kernel reservations without cross-VM state retention. -- [ ] Seal the complete executor and parity/safety proof as one independently +- [x] Seal the complete executor and parity/safety proof as one independently reviewable Phase 2 JJ revision on top of Phase 1; do not land a partial linker or spike. +Phase 2 evidence (Rust 1.94.0, Linux x86-64 canonical workspace): + +- `just tools-rebuild` rebuilt and audited all 166 default commands; the + required focused Vim build raised the corpus to 167 commands/136 modules, + with all 145 live imports declared in the 169-function/29-signature ABI. +- Native workspace check, all-target strict clippy, formatting, protocol tests, + client tests, fixed-version checks, protocol-inventory checks, TypeScript + request mapping, and workflow YAML parsing passed. Root `pnpm check-types` + passed all 146 tasks. +- Wasmtime units passed 15/15; architecture guards 61/61; safety/limits/ambient + denial 7/7; raw differential ABI 9/9; and the serial real-software corpus + 5/5 in 220.82 seconds (`ls`, real HTTP `curl`, `grep`, sqlite, git, tar/gzip, + metadata, shell/child affinity, and focused Vim). +- Release gates pin the reviewed Wasmtime MSRV and require Linux x86-64/arm64 + builds plus native macOS x86-64/arm64 smoke tests before assets can publish; + browser entrypoints remain excluded. + ### Phase 3 revision: Measure and enable the preferred backend - [ ] Run release builds of both backends on the same canonical machine with diff --git a/packages/core/src/runtime-compat.ts b/packages/core/src/runtime-compat.ts index 116d266d0e..ad8cd34b6a 100644 --- a/packages/core/src/runtime-compat.ts +++ b/packages/core/src/runtime-compat.ts @@ -267,6 +267,8 @@ export interface ExecOptions { filePath?: string; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; + /** Selects standalone WASM commands only; JavaScript remains on V8. */ + wasmBackend?: "v8" | "wasmtime"; } export interface ExecResult { diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index a4d81607e0..06600c474c 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -162,6 +162,8 @@ export interface ExecOptions { filePath?: string; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; + /** Selects standalone WASM commands only; JavaScript remains on V8. */ + wasmBackend?: "v8" | "wasmtime"; } export interface ExecResult { diff --git a/packages/core/src/sidecar/rpc-client.ts b/packages/core/src/sidecar/rpc-client.ts index 84c41c78db..4ad819b138 100644 --- a/packages/core/src/sidecar/rpc-client.ts +++ b/packages/core/src/sidecar/rpc-client.ts @@ -307,6 +307,7 @@ interface TrackedProcessEntry { driver: string; cwd: string; env: Record; + wasmBackend: "v8" | "wasmtime" | undefined; startTime: number; exitTime: number | null; hostPid: number | null; @@ -776,6 +777,7 @@ export class NativeSidecarKernelProxy { ...(options?.env ?? {}), ...(options?.streamStdin ? { AGENTOS_KEEP_STDIN_OPEN: "1" } : {}), }, + wasmBackend: options?.wasmBackend, startTime: Date.now(), exitTime: null, hostPid: null, @@ -1827,6 +1829,7 @@ export class NativeSidecarKernelProxy { args: entry.args, env: entry.env, cwd: entry.cwd, + wasmBackend: entry.wasmBackend, }); entry.hostPid = started.pid; entry.started = true; diff --git a/packages/runtime-core/src/generated-protocol.ts b/packages/runtime-core/src/generated-protocol.ts index 923e8b9d8f..95965393ec 100644 --- a/packages/runtime-core/src/generated-protocol.ts +++ b/packages/runtime-core/src/generated-protocol.ts @@ -1157,6 +1157,39 @@ export function writeWasmPermissionTier(bc: bare.ByteCursor, x: WasmPermissionTi } } +export enum StandaloneWasmBackend { + V8 = "V8", + Wasmtime = "Wasmtime", +} + +export function readStandaloneWasmBackend(bc: bare.ByteCursor): StandaloneWasmBackend { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return StandaloneWasmBackend.V8 + case 1: + return StandaloneWasmBackend.Wasmtime + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeStandaloneWasmBackend(bc: bare.ByteCursor, x: StandaloneWasmBackend): void { + switch (x) { + case StandaloneWasmBackend.V8: { + bare.writeU8(bc, 0) + break + } + case StandaloneWasmBackend.Wasmtime: { + bare.writeU8(bc, 1) + break + } + } +} + /** * agentOS package descriptor. `path` is the trusted host path of the package: * normally the packed `.aospkg` file (header + vbare manifest + mount index + @@ -1988,6 +2021,17 @@ function write25(bc: bare.ByteCursor, x: WasmPermissionTier | null): void { } } +function read26(bc: bare.ByteCursor): StandaloneWasmBackend | null { + return bare.readBool(bc) ? readStandaloneWasmBackend(bc) : null +} + +function write26(bc: bare.ByteCursor, x: StandaloneWasmBackend | null): void { + bare.writeBool(bc, x != null) + if (x != null) { + writeStandaloneWasmBackend(bc, x) + } +} + export type ExecuteRequest = { readonly processId: string readonly command: string | null @@ -1997,6 +2041,7 @@ export type ExecuteRequest = { readonly env: ReadonlyMap readonly cwd: string | null readonly wasmPermissionTier: WasmPermissionTier | null + readonly wasmBackend: StandaloneWasmBackend | null } export function readExecuteRequest(bc: bare.ByteCursor): ExecuteRequest { @@ -2009,6 +2054,7 @@ export function readExecuteRequest(bc: bare.ByteCursor): ExecuteRequest { env: read1(bc), cwd: read0(bc), wasmPermissionTier: read25(bc), + wasmBackend: read26(bc), } } @@ -2021,6 +2067,7 @@ export function writeExecuteRequest(bc: bare.ByteCursor, x: ExecuteRequest): voi write1(bc, x.env) write0(bc, x.cwd) write25(bc, x.wasmPermissionTier) + write26(bc, x.wasmBackend) } export type WriteStdinRequest = { @@ -2095,11 +2142,11 @@ export type GetProcessSnapshotRequest = null export type GetResourceSnapshotRequest = null -function read26(bc: bare.ByteCursor): u16 | null { +function read27(bc: bare.ByteCursor): u16 | null { return bare.readBool(bc) ? bare.readU16(bc) : null } -function write26(bc: bare.ByteCursor, x: u16 | null): void { +function write27(bc: bare.ByteCursor, x: u16 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeU16(bc, x) @@ -2115,14 +2162,14 @@ export type FindListenerRequest = { export function readFindListenerRequest(bc: bare.ByteCursor): FindListenerRequest { return { host: read0(bc), - port: read26(bc), + port: read27(bc), path: read0(bc), } } export function writeFindListenerRequest(bc: bare.ByteCursor, x: FindListenerRequest): void { write0(bc, x.host) - write26(bc, x.port) + write27(bc, x.port) write0(bc, x.path) } @@ -2134,13 +2181,13 @@ export type FindBoundUdpRequest = { export function readFindBoundUdpRequest(bc: bare.ByteCursor): FindBoundUdpRequest { return { host: read0(bc), - port: read26(bc), + port: read27(bc), } } export function writeFindBoundUdpRequest(bc: bare.ByteCursor, x: FindBoundUdpRequest): void { write0(bc, x.host) - write26(bc, x.port) + write27(bc, x.port) } export type GetSignalStateRequest = { @@ -2897,7 +2944,7 @@ export function writeGuestDirEntry(bc: bare.ByteCursor, x: GuestDirEntry): void bare.writeU64(bc, x.size) } -function read27(bc: bare.ByteCursor): readonly GuestDirEntry[] { +function read28(bc: bare.ByteCursor): readonly GuestDirEntry[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -2909,40 +2956,40 @@ function read27(bc: bare.ByteCursor): readonly GuestDirEntry[] { return result } -function write27(bc: bare.ByteCursor, x: readonly GuestDirEntry[]): void { +function write28(bc: bare.ByteCursor, x: readonly GuestDirEntry[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeGuestDirEntry(bc, x[i]) } } -function read28(bc: bare.ByteCursor): readonly GuestDirEntry[] | null { - return bare.readBool(bc) ? read27(bc) : null +function read29(bc: bare.ByteCursor): readonly GuestDirEntry[] | null { + return bare.readBool(bc) ? read28(bc) : null } -function write28(bc: bare.ByteCursor, x: readonly GuestDirEntry[] | null): void { +function write29(bc: bare.ByteCursor, x: readonly GuestDirEntry[] | null): void { bare.writeBool(bc, x != null) if (x != null) { - write27(bc, x) + write28(bc, x) } } -function read29(bc: bare.ByteCursor): GuestFilesystemStat | null { +function read30(bc: bare.ByteCursor): GuestFilesystemStat | null { return bare.readBool(bc) ? readGuestFilesystemStat(bc) : null } -function write29(bc: bare.ByteCursor, x: GuestFilesystemStat | null): void { +function write30(bc: bare.ByteCursor, x: GuestFilesystemStat | null): void { bare.writeBool(bc, x != null) if (x != null) { writeGuestFilesystemStat(bc, x) } } -function read30(bc: bare.ByteCursor): boolean | null { +function read31(bc: bare.ByteCursor): boolean | null { return bare.readBool(bc) ? bare.readBool(bc) : null } -function write30(bc: bare.ByteCursor, x: boolean | null): void { +function write31(bc: bare.ByteCursor, x: boolean | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeBool(bc, x) @@ -2966,9 +3013,9 @@ export function readGuestFilesystemResultResponse(bc: bare.ByteCursor): GuestFil path: bare.readString(bc), content: read0(bc), encoding: read3(bc), - entries: read28(bc), - stat: read29(bc), - exists: read30(bc), + entries: read29(bc), + stat: read30(bc), + exists: read31(bc), target: read0(bc), } } @@ -2978,9 +3025,9 @@ export function writeGuestFilesystemResultResponse(bc: bare.ByteCursor, x: Guest bare.writeString(bc, x.path) write0(bc, x.content) write3(bc, x.encoding) - write28(bc, x.entries) - write29(bc, x.stat) - write30(bc, x.exists) + write29(bc, x.entries) + write30(bc, x.stat) + write31(bc, x.exists) write0(bc, x.target) } @@ -3012,7 +3059,7 @@ export function writeRootFilesystemSnapshotResponse(bc: bare.ByteCursor, x: Root write4(bc, x.entries) } -function read31(bc: bare.ByteCursor): readonly MountInfo[] { +function read32(bc: bare.ByteCursor): readonly MountInfo[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -3024,7 +3071,7 @@ function read31(bc: bare.ByteCursor): readonly MountInfo[] { return result } -function write31(bc: bare.ByteCursor, x: readonly MountInfo[]): void { +function write32(bc: bare.ByteCursor, x: readonly MountInfo[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeMountInfo(bc, x[i]) @@ -3037,12 +3084,12 @@ export type ListMountsResponse = { export function readListMountsResponse(bc: bare.ByteCursor): ListMountsResponse { return { - mounts: read31(bc), + mounts: read32(bc), } } export function writeListMountsResponse(bc: bare.ByteCursor, x: ListMountsResponse): void { - write31(bc, x.mounts) + write32(bc, x.mounts) } export type ProcessStartedResponse = { @@ -3167,11 +3214,11 @@ export function writeProcessSnapshotStatus(bc: bare.ByteCursor, x: ProcessSnapsh } } -function read32(bc: bare.ByteCursor): i32 | null { +function read33(bc: bare.ByteCursor): i32 | null { return bare.readBool(bc) ? bare.readI32(bc) : null } -function write32(bc: bare.ByteCursor, x: i32 | null): void { +function write33(bc: bare.ByteCursor, x: i32 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeI32(bc, x) @@ -3204,7 +3251,7 @@ export function readProcessSnapshotEntry(bc: bare.ByteCursor): ProcessSnapshotEn args: read6(bc), cwd: bare.readString(bc), status: readProcessSnapshotStatus(bc), - exitCode: read32(bc), + exitCode: read33(bc), } } @@ -3219,10 +3266,10 @@ export function writeProcessSnapshotEntry(bc: bare.ByteCursor, x: ProcessSnapsho write6(bc, x.args) bare.writeString(bc, x.cwd) writeProcessSnapshotStatus(bc, x.status) - write32(bc, x.exitCode) + write33(bc, x.exitCode) } -function read33(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { +function read34(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -3234,7 +3281,7 @@ function read33(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { return result } -function write33(bc: bare.ByteCursor, x: readonly ProcessSnapshotEntry[]): void { +function write34(bc: bare.ByteCursor, x: readonly ProcessSnapshotEntry[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeProcessSnapshotEntry(bc, x[i]) @@ -3247,12 +3294,12 @@ export type ProcessSnapshotResponse = { export function readProcessSnapshotResponse(bc: bare.ByteCursor): ProcessSnapshotResponse { return { - processes: read33(bc), + processes: read34(bc), } } export function writeProcessSnapshotResponse(bc: bare.ByteCursor, x: ProcessSnapshotResponse): void { - write33(bc, x.processes) + write34(bc, x.processes) } export type QueueSnapshotEntry = { @@ -3284,7 +3331,7 @@ export function writeQueueSnapshotEntry(bc: bare.ByteCursor, x: QueueSnapshotEnt bare.writeU64(bc, x.fillPercent) } -function read34(bc: bare.ByteCursor): readonly QueueSnapshotEntry[] { +function read35(bc: bare.ByteCursor): readonly QueueSnapshotEntry[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -3296,7 +3343,7 @@ function read34(bc: bare.ByteCursor): readonly QueueSnapshotEntry[] { return result } -function write34(bc: bare.ByteCursor, x: readonly QueueSnapshotEntry[]): void { +function write35(bc: bare.ByteCursor, x: readonly QueueSnapshotEntry[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeQueueSnapshotEntry(bc, x[i]) @@ -3339,7 +3386,7 @@ export function readResourceSnapshotResponse(bc: bare.ByteCursor): ResourceSnaps socketConnections: bare.readU64(bc), socketBufferedBytes: bare.readU64(bc), socketDatagramQueueLen: bare.readU64(bc), - queueSnapshots: read34(bc), + queueSnapshots: read35(bc), } } @@ -3359,7 +3406,7 @@ export function writeResourceSnapshotResponse(bc: bare.ByteCursor, x: ResourceSn bare.writeU64(bc, x.socketConnections) bare.writeU64(bc, x.socketBufferedBytes) bare.writeU64(bc, x.socketDatagramQueueLen) - write34(bc, x.queueSnapshots) + write35(bc, x.queueSnapshots) } export type SocketStateEntry = { @@ -3373,7 +3420,7 @@ export function readSocketStateEntry(bc: bare.ByteCursor): SocketStateEntry { return { processId: bare.readString(bc), host: read0(bc), - port: read26(bc), + port: read27(bc), path: read0(bc), } } @@ -3381,15 +3428,15 @@ export function readSocketStateEntry(bc: bare.ByteCursor): SocketStateEntry { export function writeSocketStateEntry(bc: bare.ByteCursor, x: SocketStateEntry): void { bare.writeString(bc, x.processId) write0(bc, x.host) - write26(bc, x.port) + write27(bc, x.port) write0(bc, x.path) } -function read35(bc: bare.ByteCursor): SocketStateEntry | null { +function read36(bc: bare.ByteCursor): SocketStateEntry | null { return bare.readBool(bc) ? readSocketStateEntry(bc) : null } -function write35(bc: bare.ByteCursor, x: SocketStateEntry | null): void { +function write36(bc: bare.ByteCursor, x: SocketStateEntry | null): void { bare.writeBool(bc, x != null) if (x != null) { writeSocketStateEntry(bc, x) @@ -3402,12 +3449,12 @@ export type ListenerSnapshotResponse = { export function readListenerSnapshotResponse(bc: bare.ByteCursor): ListenerSnapshotResponse { return { - listener: read35(bc), + listener: read36(bc), } } export function writeListenerSnapshotResponse(bc: bare.ByteCursor, x: ListenerSnapshotResponse): void { - write35(bc, x.listener) + write36(bc, x.listener) } export type BoundUdpSnapshotResponse = { @@ -3416,12 +3463,12 @@ export type BoundUdpSnapshotResponse = { export function readBoundUdpSnapshotResponse(bc: bare.ByteCursor): BoundUdpSnapshotResponse { return { - socket: read35(bc), + socket: read36(bc), } } export function writeBoundUdpSnapshotResponse(bc: bare.ByteCursor, x: BoundUdpSnapshotResponse): void { - write35(bc, x.socket) + write36(bc, x.socket) } export enum SignalDispositionAction { @@ -3484,7 +3531,7 @@ export function writeSignalHandlerRegistration(bc: bare.ByteCursor, x: SignalHan bare.writeU32(bc, x.flags) } -function read36(bc: bare.ByteCursor): ReadonlyMap { +function read37(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -3499,7 +3546,7 @@ function read36(bc: bare.ByteCursor): ReadonlyMap): void { +function write37(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeU32(bc, kv[0]) @@ -3515,13 +3562,13 @@ export type SignalStateResponse = { export function readSignalStateResponse(bc: bare.ByteCursor): SignalStateResponse { return { processId: bare.readString(bc), - handlers: read36(bc), + handlers: read37(bc), } } export function writeSignalStateResponse(bc: bare.ByteCursor, x: SignalStateResponse): void { bare.writeString(bc, x.processId) - write36(bc, x.handlers) + write37(bc, x.handlers) } export type ZombieTimerCountResponse = { @@ -3645,7 +3692,7 @@ export function readRejectedResponse(bc: bare.ByteCursor): RejectedResponse { capabilityId: read21(bc), operation: read0(bc), configurationPath: read0(bc), - retryable: read30(bc), + retryable: read31(bc), errno: read0(bc), } } @@ -3664,7 +3711,7 @@ export function writeRejectedResponse(bc: bare.ByteCursor, x: RejectedResponse): write21(bc, x.capabilityId) write0(bc, x.operation) write0(bc, x.configurationPath) - write30(bc, x.retryable) + write31(bc, x.retryable) write0(bc, x.errno) } @@ -4355,11 +4402,11 @@ export function writeSidecarRequestFrame(bc: bare.ByteCursor, x: SidecarRequestF writeSidecarRequestPayload(bc, x.payload) } -function read37(bc: bare.ByteCursor): JsonUtf8 | null { +function read38(bc: bare.ByteCursor): JsonUtf8 | null { return bare.readBool(bc) ? readJsonUtf8(bc) : null } -function write37(bc: bare.ByteCursor, x: JsonUtf8 | null): void { +function write38(bc: bare.ByteCursor, x: JsonUtf8 | null): void { bare.writeBool(bc, x != null) if (x != null) { writeJsonUtf8(bc, x) @@ -4375,14 +4422,14 @@ export type HostCallbackResultResponse = { export function readHostCallbackResultResponse(bc: bare.ByteCursor): HostCallbackResultResponse { return { invocationId: bare.readString(bc), - result: read37(bc), + result: read38(bc), error: read0(bc), } } export function writeHostCallbackResultResponse(bc: bare.ByteCursor, x: HostCallbackResultResponse): void { bare.writeString(bc, x.invocationId) - write37(bc, x.result) + write38(bc, x.result) write0(bc, x.error) } @@ -4395,14 +4442,14 @@ export type JsBridgeResultResponse = { export function readJsBridgeResultResponse(bc: bare.ByteCursor): JsBridgeResultResponse { return { callId: bare.readString(bc), - result: read37(bc), + result: read38(bc), error: read0(bc), } } export function writeJsBridgeResultResponse(bc: bare.ByteCursor, x: JsBridgeResultResponse): void { bare.writeString(bc, x.callId) - write37(bc, x.result) + write38(bc, x.result) write0(bc, x.error) } diff --git a/packages/runtime-core/src/kernel-proxy.ts b/packages/runtime-core/src/kernel-proxy.ts index 92d569b6f0..dd34fb975e 100644 --- a/packages/runtime-core/src/kernel-proxy.ts +++ b/packages/runtime-core/src/kernel-proxy.ts @@ -307,6 +307,7 @@ interface TrackedProcessEntry { driver: string; cwd: string; env: Record; + wasmBackend: "v8" | "wasmtime" | undefined; startTime: number; exitTime: number | null; hostPid: number | null; @@ -652,6 +653,7 @@ export class NativeSidecarKernelProxy { ...(options?.env ?? {}), ...(options?.streamStdin ? { AGENTOS_KEEP_STDIN_OPEN: "1" } : {}), }, + wasmBackend: options?.wasmBackend, startTime: Date.now(), exitTime: null, hostPid: null, @@ -1528,6 +1530,7 @@ export class NativeSidecarKernelProxy { args: entry.args, env: entry.env, cwd: entry.cwd, + wasmBackend: entry.wasmBackend, }); entry.hostPid = started.pid; entry.started = true; diff --git a/packages/runtime-core/src/protocol-maps.ts b/packages/runtime-core/src/protocol-maps.ts index fbc6f50b30..896672eb1e 100644 --- a/packages/runtime-core/src/protocol-maps.ts +++ b/packages/runtime-core/src/protocol-maps.ts @@ -13,6 +13,7 @@ export type LiveWasmPermissionTier = | "read-write" | "read-only" | "isolated"; +export type LiveStandaloneWasmBackend = "v8" | "wasmtime"; export type LivePermissionMode = "allow" | "ask" | "deny"; export type LiveGuestFilesystemOperation = | "read_file" @@ -147,6 +148,17 @@ export function toGeneratedWasmPermissionTier( } } +export function toGeneratedStandaloneWasmBackend( + backend: LiveStandaloneWasmBackend, +): protocol.StandaloneWasmBackend { + switch (backend) { + case "v8": + return protocol.StandaloneWasmBackend.V8; + case "wasmtime": + return protocol.StandaloneWasmBackend.Wasmtime; + } +} + export function toGeneratedGuestFilesystemOperation( operation: LiveGuestFilesystemOperation, ): protocol.GuestFilesystemOperation { diff --git a/packages/runtime-core/src/request-payloads.ts b/packages/runtime-core/src/request-payloads.ts index 47437818a9..2dd736ac15 100644 --- a/packages/runtime-core/src/request-payloads.ts +++ b/packages/runtime-core/src/request-payloads.ts @@ -31,6 +31,7 @@ import { type LiveGuestRuntimeKind, type LiveRootFilesystemMode, type LiveWasmPermissionTier, + type LiveStandaloneWasmBackend, toGeneratedDisposeReason, toGeneratedFilesystemOperation, toGeneratedGuestFilesystemOperation, @@ -38,6 +39,7 @@ import { toGeneratedRootFilesystemEntryEncoding, toGeneratedRootFilesystemMode, toGeneratedWasmPermissionTier, + toGeneratedStandaloneWasmBackend, } from "./protocol-maps.js"; export interface LiveRegisteredHostCallbackExample { @@ -170,6 +172,7 @@ export type LiveRequestPayload = env?: Record; cwd?: string; wasm_permission_tier?: LiveWasmPermissionTier; + wasm_backend?: LiveStandaloneWasmBackend; } | { type: "write_stdin"; @@ -439,6 +442,10 @@ export function toGeneratedRequestPayload( payload.wasm_permission_tier === undefined ? null : toGeneratedWasmPermissionTier(payload.wasm_permission_tier), + wasmBackend: + payload.wasm_backend === undefined + ? null + : toGeneratedStandaloneWasmBackend(payload.wasm_backend), }, }; case "write_stdin": diff --git a/packages/runtime-core/src/sidecar-process.ts b/packages/runtime-core/src/sidecar-process.ts index f1995ae45b..6e0ced16f4 100644 --- a/packages/runtime-core/src/sidecar-process.ts +++ b/packages/runtime-core/src/sidecar-process.ts @@ -68,6 +68,7 @@ type GuestRuntimeKind = Extract< "java_script" | "python" | "web_assembly" >; type WasmPermissionTier = LiveWasmPermissionTier; +type StandaloneWasmBackend = "v8" | "wasmtime"; type RootFilesystemEntryEncoding = LiveRootFilesystemEntryEncoding; type RootFilesystemDescriptor = { @@ -1232,6 +1233,7 @@ export class SidecarProcess { env?: Record; cwd?: string; wasmPermissionTier?: WasmPermissionTier; + wasmBackend?: StandaloneWasmBackend; }, ): Promise<{ pid: number | null }> { const response = await this.sendRequest({ @@ -1253,6 +1255,9 @@ export class SidecarProcess { ...(options.wasmPermissionTier ? { wasm_permission_tier: options.wasmPermissionTier } : {}), + ...(options.wasmBackend + ? { wasm_backend: options.wasmBackend } + : {}), }, }); if (response.payload.type !== "process_started") { diff --git a/packages/runtime-core/src/test-runtime.ts b/packages/runtime-core/src/test-runtime.ts index b18c8d052d..27ab90c229 100644 --- a/packages/runtime-core/src/test-runtime.ts +++ b/packages/runtime-core/src/test-runtime.ts @@ -284,6 +284,7 @@ export interface ExecOptions { filePath?: string; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; + wasmBackend?: "v8" | "wasmtime"; } export interface ExecResult { diff --git a/packages/runtime-core/tests/request-payloads.test.ts b/packages/runtime-core/tests/request-payloads.test.ts index 18fa645900..77b2e0fdf7 100644 --- a/packages/runtime-core/tests/request-payloads.test.ts +++ b/packages/runtime-core/tests/request-payloads.test.ts @@ -205,6 +205,7 @@ describe("request payload conversion", () => { env: new Map([["A", "1"]]), cwd: null, wasmPermissionTier: protocol.WasmPermissionTier.Isolated, + wasmBackend: null, }, }); }); From 0f137a9b82f399b9431beb5ff498facaac4b6003 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Mon, 20 Jul 2026 22:21:51 -0700 Subject: [PATCH 4/6] perf: benchmark Wasmtime executor --- .../execution/assets/runners/wasm-runner.mjs | 44 +- .../src/wasm/wasmtime/diagnostics.rs | 200 + .../execution/src/wasm/wasmtime/lifecycle.rs | 118 +- .../src/wasm/wasmtime/linker/process.rs | 2 +- crates/execution/src/wasm/wasmtime/mod.rs | 1 + crates/execution/src/wasm/wasmtime/module.rs | 37 +- crates/execution/src/wasm/wasmtime/store.rs | 23 + .../src/execution/child_process.rs | 76 +- .../src/execution/coordinator.rs | 20 +- .../src/execution/process_events.rs | 18 +- .../tests/architecture_guards.rs | 35 + .../protocol/agentos_sidecar_v1.bare | 10 + docs/design/wasmtime-executor.md | 71 +- docs/wasmvm/executors.md | 164 + packages/runtime-benchmarks/package.json | 1 + .../results/wasm-backend-comparison.json | 61444 ++++++++++++++++ .../focused/wasm-backend-comparison.bench.ts | 954 + packages/runtime-benchmarks/src/lib/memory.ts | 96 +- packages/runtime-benchmarks/src/lib/vm.ts | 96 +- .../runtime-core/src/generated-protocol.ts | 30 + packages/runtime-core/src/node-runtime.ts | 50 +- .../runtime-core/src/response-payloads.ts | 54 + packages/runtime-core/src/sidecar-process.ts | 95 +- packages/runtime-core/src/test-runtime.ts | 10 + 24 files changed, 63481 insertions(+), 168 deletions(-) create mode 100644 crates/execution/src/wasm/wasmtime/diagnostics.rs create mode 100644 docs/wasmvm/executors.md create mode 100644 packages/runtime-benchmarks/results/wasm-backend-comparison.json create mode 100644 packages/runtime-benchmarks/src/focused/wasm-backend-comparison.bench.ts diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 99ce792dbb..4e431192d0 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -507,6 +507,7 @@ function __agentOSWasmEmitPhaseMetrics(reason, extra = {}) { process.stderr.write(`__AGENTOS_WASM_PHASE_METRICS__:${JSON.stringify({ reason, modulePath, + sourceModuleBytes: typeof moduleBytes !== 'undefined' ? moduleBytes.byteLength : null, moduleBytes: typeof moduleBinary !== 'undefined' ? moduleBinary.byteLength : null, phases: __agentOSWasmPhaseTimings, ...extra, @@ -3744,7 +3745,13 @@ function writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr) { nreadPtr, writeBytesToGuestIovs(iovs, iovsLen, bytes), ); - } catch { + } catch (error) { + const memoryBytes = instanceMemory instanceof WebAssembly.Memory + ? instanceMemory.buffer.byteLength + : 0; + process.stderr.write( + `[agentos] ERR_AGENTOS_V8_HOSTNET_GUEST_WRITE: failed to commit socket bytes to guest memory (iovs=${Number(iovs) >>> 0}, iovsLen=${Number(iovsLen) >>> 0}, nread=${Number(nreadPtr) >>> 0}, sourceBytes=${Buffer.from(bytes ?? []).length}, memoryBytes=${memoryBytes}): ${error?.message ?? String(error)}\n`, + ); return WASI_ERRNO_FAULT; } } @@ -3772,14 +3779,9 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr, iovReques : hasConfiguredTimeout ? configuredTimeout : unixConnectTimeoutMs; const startedAt = Date.now(); for (;;) { - if (!nonblocking) { - const remaining = Math.max(0, waitLimit - (Date.now() - startedAt)); - const readable = waitManagedHostNetReadable(socket, remaining, true); - if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; - if (!readable) { - return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; - } - } + // Linux's nonblocking-I/O pattern is read, then wait, then retry. + // Probing first also closes the race where bytes become durable just + // before a readiness subscription is armed and its wake is coalesced. const result = callSyncRpc('process.hostnet_recv', [ targetFd, requestedLength, @@ -3794,7 +3796,29 @@ function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr, iovReques } if (result == null) return writeGuestUint32(nreadPtr, 0); if (nonblocking) return WASI_ERRNO_AGAIN; - if (Date.now() - startedAt >= waitLimit) { + const remaining = Math.max(0, waitLimit - (Date.now() - startedAt)); + if (remaining === 0) { + return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; + } + const readable = waitManagedHostNetReadable(socket, remaining, true); + if (readable === WASI_ERRNO_INTR) return WASI_ERRNO_INTR; + if (!readable) { + // Readiness is only a hint. A final receive probe wins over a + // simultaneous timeout and prevents a coalesced wake from turning + // already-buffered bytes into a spurious 30-second failure. + const finalResult = callSyncRpc('process.hostnet_recv', [ + targetFd, + requestedLength, + 0, + 0, + ]); + if (finalResult != null && !isHostNetWouldBlock(finalResult)) { + const bytes = finalResult?.type === 'message' + ? hostNetDatagramBytes(finalResult) + : decodeFsBytesPayload(finalResult, 'managed host-network read'); + return writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr); + } + if (finalResult == null) return writeGuestUint32(nreadPtr, 0); return hasConfiguredTimeout ? WASI_ERRNO_AGAIN : WASI_ERRNO_TIMEDOUT; } } diff --git a/crates/execution/src/wasm/wasmtime/diagnostics.rs b/crates/execution/src/wasm/wasmtime/diagnostics.rs new file mode 100644 index 0000000000..d9f4e4f896 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/diagnostics.rs @@ -0,0 +1,200 @@ +//! Opt-in, best-effort per-execution benchmark diagnostics. + +use serde_json::json; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +pub const PHASE_METRICS_PREFIX: &str = "__AGENTOS_WASM_PHASE_METRICS__:"; + +#[derive(Debug, Default)] +struct DiagnosticState { + phases: Vec<(&'static str, Duration)>, + first_host_call: Option, + first_guest_host_call: Option, + first_output: Option, + module_bytes: Option, + module_cache_hit: Option, + guest_linear_memory_bytes: usize, + async_stack_bytes: usize, + reserved_store_bytes: usize, +} + +#[derive(Debug)] +pub struct ExecutionDiagnostics { + enabled: bool, + started: Instant, + state: Mutex, +} + +impl ExecutionDiagnostics { + pub fn new(enabled: bool) -> Self { + Self { + enabled, + started: Instant::now(), + state: Mutex::new(DiagnosticState::default()), + } + } + + pub fn enabled(&self) -> bool { + self.enabled + } + + pub fn phase(&self, name: &'static str, elapsed: Duration) { + if !self.enabled { + return; + } + self.with_state(|state| state.phases.push((name, elapsed))); + } + + pub fn first_host_call(&self) { + if self.enabled { + let elapsed = self.started.elapsed(); + self.with_state(|state| { + state.first_host_call.get_or_insert(elapsed); + }); + } + } + + pub fn first_guest_host_call(&self) { + if self.enabled { + let elapsed = self.started.elapsed(); + self.with_state(|state| { + state.first_guest_host_call.get_or_insert(elapsed); + }); + } + } + + pub fn first_output(&self) { + if self.enabled { + let elapsed = self.started.elapsed(); + self.with_state(|state| { + state.first_output.get_or_insert(elapsed); + }); + } + } + + pub fn module(&self, bytes: usize, cache_hit: bool) { + if self.enabled { + self.with_state(|state| { + state.module_bytes = Some(bytes); + state.module_cache_hit = Some(cache_hit); + }); + } + } + + pub fn store_memory( + &self, + guest_linear_memory_bytes: usize, + async_stack_bytes: usize, + reserved_store_bytes: usize, + ) { + if self.enabled { + self.with_state(|state| { + state.guest_linear_memory_bytes = state + .guest_linear_memory_bytes + .max(guest_linear_memory_bytes); + state.async_stack_bytes = state.async_stack_bytes.max(async_stack_bytes); + state.reserved_store_bytes = state.reserved_store_bytes.max(reserved_store_bytes); + }); + } + } + + pub fn line(&self, reason: &str, module_path: &str) -> Option> { + if !self.enabled { + return None; + } + let state = self.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_WASMTIME_DIAGNOSTICS_POISONED: recovering phase diagnostic state" + ); + poisoned.into_inner() + }); + let phases = state + .phases + .iter() + .map(|(name, elapsed)| json!({ "name": name, "ms": millis(*elapsed) })) + .collect::>(); + let payload = json!({ + "reason": reason, + "backend": "wasmtime", + "modulePath": module_path, + "sourceModuleBytes": state.module_bytes, + "moduleBytes": state.module_bytes, + "moduleCacheHit": state.module_cache_hit, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": state.first_host_call.map(millis), + "firstGuestHostCallMs": state.first_guest_host_call.map(millis), + "firstOutputMs": state.first_output.map(millis), + "guestLinearMemoryBytes": state.guest_linear_memory_bytes, + "asyncStackBytes": state.async_stack_bytes, + "reservedStoreBytes": state.reserved_store_bytes, + "totalMs": millis(self.started.elapsed()), + "phases": phases, + }); + serde_json::to_vec(&payload).ok().map(|mut bytes| { + let mut line = PHASE_METRICS_PREFIX.as_bytes().to_vec(); + line.append(&mut bytes); + line.push(b'\n'); + line + }) + } + + fn with_state(&self, update: impl FnOnce(&mut DiagnosticState) -> T) -> T { + let mut state = self.state.lock().unwrap_or_else(|poisoned| { + eprintln!( + "ERR_AGENTOS_WASMTIME_DIAGNOSTICS_POISONED: recovering phase diagnostic state" + ); + poisoned.into_inner() + }); + update(&mut state) + } +} + +fn millis(duration: Duration) -> f64 { + duration.as_secs_f64() * 1_000.0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn enabled_diagnostics_use_the_shared_phase_marker_and_stable_fields() { + let diagnostics = ExecutionDiagnostics::new(true); + diagnostics.phase("moduleRead", Duration::from_millis(2)); + diagnostics.first_host_call(); + diagnostics.first_guest_host_call(); + diagnostics.first_output(); + diagnostics.module(1234, true); + diagnostics.store_memory(65_536, 2 * 1024 * 1024, 128 * 1024 * 1024); + let line = String::from_utf8( + diagnostics + .line("completed", "/bin/example") + .expect("enabled line"), + ) + .expect("utf8 diagnostics"); + let payload: serde_json::Value = serde_json::from_str( + line.strip_prefix(PHASE_METRICS_PREFIX) + .expect("shared phase prefix"), + ) + .expect("phase JSON"); + assert_eq!(payload["backend"], "wasmtime"); + assert_eq!(payload["modulePath"], "/bin/example"); + assert_eq!(payload["sourceModuleBytes"], 1234); + assert_eq!(payload["moduleBytes"], 1234); + assert_eq!(payload["moduleCacheHit"], true); + assert_eq!(payload["memoryAllocation"], "on-demand"); + assert_eq!(payload["memoryInitCow"], true); + assert_eq!(payload["guestLinearMemoryBytes"], 65_536); + assert_eq!(payload["phases"][0]["name"], "moduleRead"); + } + + #[test] + fn disabled_diagnostics_emit_nothing() { + assert!(ExecutionDiagnostics::new(false) + .line("completed", "/bin/example") + .is_none()); + } +} diff --git a/crates/execution/src/wasm/wasmtime/lifecycle.rs b/crates/execution/src/wasm/wasmtime/lifecycle.rs index 0d38d6b726..6edae37a1e 100644 --- a/crates/execution/src/wasm/wasmtime/lifecycle.rs +++ b/crates/execution/src/wasm/wasmtime/lifecycle.rs @@ -1,9 +1,11 @@ //! Execution lifecycle, cancellation, interruption, and teardown. use super::super::{StartWasmExecutionRequest, WasmExecutionError, WasmExecutionEvent}; +use super::diagnostics::ExecutionDiagnostics; use super::engine::{ WasmtimeEngineHandle, WasmtimeEngineProfile, WasmtimeEngineRegistry, WasmtimeMetricsSnapshot, }; +use super::limits; use super::linker; use super::store::{self, WasmtimeHostClient}; use crate::backend::{ @@ -22,7 +24,7 @@ use std::collections::VecDeque; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::sync::Notify; const MODULE_READ_CHUNK_BYTES: usize = 512 * 1024; @@ -466,6 +468,12 @@ async fn run_execution( event_sender: Sender, event_notify: Option>, ) -> Result { + let diagnostics = Arc::new(ExecutionDiagnostics::new( + request + .env + .get("AGENTOS_WASM_WARMUP_DEBUG") + .is_some_and(|value| value == "1"), + )); wait_until_started(&control).await?; let host = wait_for_host(&host_latch, &control.cancelled).await?; let host = WasmtimeHostClient::new( @@ -477,9 +485,12 @@ async fn run_execution( Arc::clone(runtime.resources()), event_sender, event_notify, - ); + ) + .with_diagnostics(Arc::clone(&diagnostics)); + let engine_started = Instant::now(); let profile = WasmtimeEngineProfile::new(request.limits.max_stack_bytes)?; let engine = WasmtimeEngineRegistry::process().get_or_create(profile)?; + diagnostics.phase("Engine", engine_started.elapsed()); control .engine .lock() @@ -492,16 +503,17 @@ async fn run_execution( .replace(Arc::clone(&engine)); let future = run_loaded_module( - module_path, + module_path.clone(), request.clone(), runtime, - host, + host.clone(), engine, profile, Arc::clone(&control.paused), Arc::clone(&control.pause_notify), + Arc::clone(&diagnostics), ); - if let Some(limit_ms) = request.limits.wall_clock_limit_ms { + let result = if let Some(limit_ms) = request.limits.wall_clock_limit_ms { match tokio::time::timeout(Duration::from_millis(limit_ms), future).await { Ok(result) => result, Err(_) => { @@ -518,7 +530,23 @@ async fn run_execution( } } else { future.await + }; + if diagnostics.enabled() { + let reason = if result.is_ok() { + "completed" + } else { + "failed" + }; + if let Some(line) = diagnostics.line(reason, &module_path) { + if let Err(error) = host.publish_stderr(line).await { + eprintln!( + "ERR_AGENTOS_WASMTIME_DIAGNOSTICS_PUBLISH: failed to publish phase diagnostics: {}: {}", + error.code, error.message + ); + } + } } + result } #[allow(clippy::too_many_arguments)] @@ -531,19 +559,31 @@ async fn run_loaded_module( profile: WasmtimeEngineProfile, paused: Arc, pause_notify: Arc, + diagnostics: Arc, ) -> Result { // The kernel owns the authoritative WASI capability roots and descriptor // numbers. Initialize them before guest code starts so both executors see // the same fd namespace without maintaining a Wasmtime-local projection. + let preopens_started = Instant::now(); host.submit( HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens), 0, ) .await?; + diagnostics.phase("canonicalPreopens", preopens_started.elapsed()); + let module_read_started = Instant::now(); let bytes = load_module(&host, &module_path, &request).await?; - let mut module = super::module::compile_module(&engine, &bytes)?; + diagnostics.phase("moduleRead", module_read_started.elapsed()); + let compiled = super::module::compile_module(&engine, &bytes)?; + diagnostics.phase("profileValidation", compiled.profile_validation); + diagnostics.phase("moduleCompile", compiled.compilation); + diagnostics.module(bytes.len(), compiled.cache_hit); + let mut module = compiled.module; let mut request = request; + let import_validation_started = Instant::now(); linker::validate_module_imports(&module, request.permission_tier)?; + diagnostics.phase("importValidation", import_validation_started.elapsed()); + let linker_started = Instant::now(); let linker = linker::build_linker(engine.engine(), request.permission_tier).map_err(|error| { eprintln!("ERR_AGENTOS_WASMTIME_LINKER: private linker diagnostic: {error:#}"); @@ -552,10 +592,12 @@ async fn run_loaded_module( "failed to build the AgentOS WebAssembly host linker", ) })?; + diagnostics.phase("Linker", linker_started.elapsed()); // One process image may replace itself repeatedly with fexecve. Preserve // the same active-CPU origin across Stores so exec cannot reset its budget. let active_cpu_started_ns = store::thread_cpu_time_ns(); loop { + let store_started = Instant::now(); let mut store = store::create_store( Arc::clone(&engine), &runtime, @@ -566,13 +608,22 @@ async fn run_loaded_module( Arc::clone(&paused), Arc::clone(&pause_notify), )?; + diagnostics.phase("Store", store_started.elapsed()); + let async_stack_bytes = profile.async_stack_bytes()?; + let reserved_store_bytes = async_stack_bytes + .saturating_add(limits::aggregate_store_memory_bytes(&request.limits)?); + let instantiate_started = Instant::now(); let instance = linker .instantiate_async(&mut store, &module) .await .map_err(|error| { super::error::normalize("ERR_AGENTOS_WASMTIME_INSTANTIATE", &error, false) })?; + diagnostics.phase("Instance", instantiate_started.elapsed()); + let signal_started = Instant::now(); linker::initialize_inherited_signal_mask(&mut store, &instance).await?; + diagnostics.phase("signalMaskInit", signal_started.elapsed()); + let entrypoint_started = Instant::now(); let start = instance .get_typed_func::<(), ()>(&mut store, "_start") .map_err(|error| { @@ -584,10 +635,35 @@ async fn run_loaded_module( "WebAssembly module does not export a valid _start function", ) })?; - match start.call_async(&mut store, ()).await { - Ok(()) => return Ok(store.data().exit_code.unwrap_or(0)), + diagnostics.phase("entrypointLookup", entrypoint_started.elapsed()); + diagnostics.store_memory( + guest_linear_memory_bytes(&instance, &mut store), + async_stack_bytes, + reserved_store_bytes, + ); + let call_started = Instant::now(); + let call_result = start.call_async(&mut store, ()).await; + diagnostics.phase("wasi.start", call_started.elapsed()); + diagnostics.store_memory( + guest_linear_memory_bytes(&instance, &mut store), + async_stack_bytes, + reserved_store_bytes, + ); + match call_result { + Ok(()) => { + let exit_code = store.data().exit_code.unwrap_or(0); + let teardown_started = Instant::now(); + drop(start); + drop(store); + diagnostics.phase("Store.teardown", teardown_started.elapsed()); + return Ok(exit_code); + } Err(error) => { if let Some(exit_code) = store.data().exit_code { + let teardown_started = Instant::now(); + drop(start); + drop(store); + diagnostics.phase("Store.teardown", teardown_started.elapsed()); return Ok(exit_code); } if let Some(replacement) = store.data_mut().pending_exec_replacement.take() { @@ -595,24 +671,46 @@ async fn run_loaded_module( request.env = replacement.env; module = replacement.module; linker::validate_module_imports(&module, request.permission_tier)?; + let teardown_started = Instant::now(); + drop(start); + drop(store); + diagnostics.phase("Store.teardown", teardown_started.elapsed()); continue; } if store.data().exec_replaced { + let teardown_started = Instant::now(); + drop(start); + drop(store); + diagnostics.phase("Store.teardown", teardown_started.elapsed()); return Err(HostServiceError::new( "ERR_AGENTOS_EXEC_REPLACED", "the kernel committed a replacement process image", )); } - return Err(super::error::normalize( + let normalized = super::error::normalize( "ERR_AGENTOS_WASMTIME_TRAP", &error, store.data().canceled(), - )); + ); + let teardown_started = Instant::now(); + drop(start); + drop(store); + diagnostics.phase("Store.teardown", teardown_started.elapsed()); + return Err(normalized); } } } } +fn guest_linear_memory_bytes( + instance: &wasmtime::Instance, + store: &mut wasmtime::Store, +) -> usize { + instance + .get_memory(&mut *store, "memory") + .map_or(0, |memory| memory.data_size(&*store)) +} + async fn wait_until_started(control: &Control) -> Result<(), HostServiceError> { loop { let notified = control.start_notify.notified(); diff --git a/crates/execution/src/wasm/wasmtime/linker/process.rs b/crates/execution/src/wasm/wasmtime/linker/process.rs index 647595c906..7d2487d634 100644 --- a/crates/execution/src/wasm/wasmtime/linker/process.rs +++ b/crates/execution/src/wasm/wasmtime/linker/process.rs @@ -639,7 +639,7 @@ async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd Err(error) => return errno(&error), }; let compiled = match module::compile_module(&engine, &bytes) { - Ok(module) => module, + Ok(compiled) => compiled.module, Err(error) if error.code == "ERR_AGENTOS_WASM_INVALID_MODULE" => { return ERRNO_NOEXEC; } diff --git a/crates/execution/src/wasm/wasmtime/mod.rs b/crates/execution/src/wasm/wasmtime/mod.rs index 0489783298..270738e11a 100644 --- a/crates/execution/src/wasm/wasmtime/mod.rs +++ b/crates/execution/src/wasm/wasmtime/mod.rs @@ -6,6 +6,7 @@ //! `wasmtime-wasi` context. mod cache; +mod diagnostics; mod engine; mod error; mod lifecycle; diff --git a/crates/execution/src/wasm/wasmtime/module.rs b/crates/execution/src/wasm/wasmtime/module.rs index a7adbc872f..ca3837b067 100644 --- a/crates/execution/src/wasm/wasmtime/module.rs +++ b/crates/execution/src/wasm/wasmtime/module.rs @@ -4,21 +4,36 @@ use super::super::profile::validate_locked_profile; use super::engine::WasmtimeEngineHandle; use crate::backend::HostServiceError; use std::sync::Arc; +use std::time::{Duration, Instant}; use wasmtime::Module; +pub struct CompiledModule { + pub module: Arc, + pub cache_hit: bool, + pub profile_validation: Duration, + pub compilation: Duration, +} + pub fn compile_module( engine: &WasmtimeEngineHandle, bytes: &[u8], -) -> Result, HostServiceError> { +) -> Result { + let validation_started = Instant::now(); validate_locked_profile(bytes)?; - engine - .modules() - .lock() - .map_err(|_| { - HostServiceError::new( - "ERR_AGENTOS_WASMTIME_MODULE_CACHE_POISONED", - "compiled Module cache lock is poisoned", - ) - })? - .get_or_compile(engine.engine(), bytes) + let profile_validation = validation_started.elapsed(); + let mut modules = engine.modules().lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_MODULE_CACHE_POISONED", + "compiled Module cache lock is poisoned", + ) + })?; + let before = modules.metrics(); + let module = modules.get_or_compile(engine.engine(), bytes)?; + let after = modules.metrics(); + Ok(CompiledModule { + module, + cache_hit: after.hits > before.hits, + profile_validation, + compilation: after.compile_time.saturating_sub(before.compile_time), + }) } diff --git a/crates/execution/src/wasm/wasmtime/store.rs b/crates/execution/src/wasm/wasmtime/store.rs index ced6241ea3..023cde2b15 100644 --- a/crates/execution/src/wasm/wasmtime/store.rs +++ b/crates/execution/src/wasm/wasmtime/store.rs @@ -1,6 +1,7 @@ //! Per-execution Store state backed by AgentOS host capabilities. use super::super::{guest_visible_wasm_env, StartWasmExecutionRequest, WasmExecutionEvent}; +use super::diagnostics::ExecutionDiagnostics; use super::engine::{WasmtimeEngineHandle, WasmtimeEngineProfile}; use super::lifecycle::QueuedWasmtimeEvent; use super::limits; @@ -39,6 +40,7 @@ pub struct WasmtimeHostClient { resources: Arc, events: Sender, event_notify: Option>, + diagnostics: Option>, } impl WasmtimeHostClient { @@ -63,9 +65,15 @@ impl WasmtimeHostClient { resources, events, event_notify, + diagnostics: None, } } + pub fn with_diagnostics(mut self, diagnostics: Arc) -> Self { + self.diagnostics = Some(diagnostics); + self + } + pub fn process(&self) -> HostProcessContext { self.host.process() } @@ -83,6 +91,9 @@ impl WasmtimeHostClient { operation: HostOperation, retained_request_bytes: usize, ) -> Result { + if let Some(diagnostics) = self.diagnostics.as_ref() { + diagnostics.first_host_call(); + } if self.canceled() { return Err(HostServiceError::new( "ECANCELED", @@ -128,6 +139,18 @@ impl WasmtimeHostClient { args: Vec, raw_bytes_args: HashMap>, ) -> Result { + if let Some(diagnostics) = self.diagnostics.as_ref() { + diagnostics.first_host_call(); + diagnostics.first_guest_host_call(); + if matches!(method.as_str(), "__kernel_stdio_write" | "process.fd_write") + && args + .first() + .and_then(serde_json::Value::as_u64) + .is_some_and(|fd| matches!(fd, 1 | 2)) + { + diagnostics.first_output(); + } + } if self.canceled() { return Err(HostServiceError::new( "ECANCELED", diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index 730e86c66a..73b8a9b2e2 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -4139,7 +4139,13 @@ where process_id, &parent_path, &child_process_id, - )?; + ) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_CHILD_DEFERRED_RPC_RECHECK: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + error + })?; self.service_descendant_guest_wait( vm_id, process_id, @@ -4149,7 +4155,13 @@ where .collect::>() .as_slice(), None, - )?; + ) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_CHILD_WAIT_RECHECK: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + error + })?; self.service_descendant_kernel_read( vm_id, process_id, @@ -4159,14 +4171,26 @@ where .collect::>() .as_slice(), None, - )?; + ) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_CHILD_READ_RECHECK: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + error + })?; self.expire_child_process_sync_if_needed( vm_id, process_id, &parent_path, &child_process_id, - )?; + ) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_CHILD_SYNC_EXPIRY: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + error + })?; let guest_owns_child_output = self .vms @@ -4198,7 +4222,12 @@ where { Ok(event) => event, Err(error) if is_javascript_child_process_gone_error(&error) => continue, - Err(error) => return Err(error), + Err(error) => { + eprintln!( + "ERR_AGENTOS_CHILD_EVENT_POLL: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + return Err(error); + } }; if event.is_null() { continue; @@ -4209,7 +4238,14 @@ where &parent_path, &child_process_id, event, - )? { + ) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_CHILD_EVENT_ROUTE: vm={vm_id} process={process_id} child={child_process_id} error={error}" + ); + error + })? + { yielded = true; delivery_backpressured = true; break; @@ -9922,15 +9958,29 @@ where InheritedOutputStream::Stdout => 1, InheritedOutputStream::Stderr => 2, }; - let description = vm - .kernel - .fd_description_identity(EXECUTION_DRIVER_NAME, child.kernel_pid, source_fd) - .map_err(kernel_error)? - .0; - let path = vm + let description = match vm.kernel.fd_description_identity( + EXECUTION_DRIVER_NAME, + child.kernel_pid, + source_fd, + ) { + Ok((description, _)) => description, + Err(error) if error.code() == "EBADF" => { + // Executor diagnostics and already-buffered output can arrive + // after the guest closes its inherited destination during + // process teardown. A closed descriptor has no parent stream + // route; it is not a fatal process-pump failure. + return Ok(None); + } + Err(error) => return Err(kernel_error(error)), + }; + let path = match vm .kernel .fd_path(EXECUTION_DRIVER_NAME, child.kernel_pid, source_fd) - .map_err(kernel_error)?; + { + Ok(path) => path, + Err(error) if error.code() == "EBADF" => return Ok(None), + Err(error) => return Err(kernel_error(error)), + }; Ok(classify_inherited_output_stream(description, path.as_str())) } diff --git a/crates/native-sidecar/src/execution/coordinator.rs b/crates/native-sidecar/src/execution/coordinator.rs index 6cb16f3656..6d69bb881d 100644 --- a/crates/native-sidecar/src/execution/coordinator.rs +++ b/crates/native-sidecar/src/execution/coordinator.rs @@ -492,11 +492,14 @@ where "process://resources", )?; - let snapshot = self + let vm = self .vms .get(&vm_id) - .map(|vm| vm.kernel.resource_snapshot()) - .unwrap_or_default(); + .ok_or_else(|| missing_vm_error(&vm_id))?; + let snapshot = vm.kernel.resource_snapshot(); + let wasm_reserved_memory_bytes = + vm.resources.usage(ResourceClass::WasmMemoryBytes).used as u64; + let wasmtime = self.wasm_engine.wasmtime_metrics()?; let queue_snapshots = queue_tracker::queue_snapshot() .into_iter() .map(|queue| QueueSnapshotEntry { @@ -528,6 +531,17 @@ where socket_connections: snapshot.socket_connections as u64, socket_buffered_bytes: snapshot.socket_buffered_bytes as u64, socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64, + wasm_reserved_memory_bytes, + wasmtime_engine_profiles: wasmtime.engine_profiles as u64, + wasmtime_module_entries: wasmtime.module_entries as u64, + wasmtime_module_cache_hits: wasmtime.module_cache_hits, + wasmtime_module_cache_misses: wasmtime.module_cache_misses, + wasmtime_module_cache_evictions: wasmtime.module_cache_evictions, + wasmtime_compiled_source_bytes: wasmtime.compiled_source_bytes, + wasmtime_charged_module_bytes: wasmtime.charged_module_bytes as u64, + wasmtime_compile_time_micros: u64::try_from(wasmtime.compile_time.as_micros()) + .unwrap_or(u64::MAX), + wasmtime_process_retained_rss_bytes: wasmtime.process_retained_rss_bytes, queue_snapshots, }), ), diff --git a/crates/native-sidecar/src/execution/process_events.rs b/crates/native-sidecar/src/execution/process_events.rs index ac31f1f1fd..f1b0d332bd 100644 --- a/crates/native-sidecar/src/execution/process_events.rs +++ b/crates/native-sidecar/src/execution/process_events.rs @@ -1355,9 +1355,16 @@ where match event { ActiveExecutionEvent::Common(ExecutionEvent::HostCall { operation, reply }) => { + let operation_debug = format!("{operation:?}"); let Some((operation, reply)) = dispatch_context_host_operation(self, vm_id, process_id, operation, reply) - .await? + .await + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_HOST_OPERATION_CONTEXT_DISPATCH: vm={vm_id} process={process_id} operation={operation_debug} error={error}" + ); + error + })? else { return Ok(None); }; @@ -1381,8 +1388,13 @@ where ); return Ok(None); }; - let effects = - dispatch_host_operation(generation, kernel, process, operation, reply)?; + let effects = dispatch_host_operation(generation, kernel, process, operation, reply) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_HOST_OPERATION_DISPATCH: vm={vm_id} process={process_id} operation={operation_debug} error={error}" + ); + error + })?; if effects.may_make_fd_readable { Self::wake_ready_deferred_fd_reads(vm)?; } diff --git a/crates/native-sidecar/tests/architecture_guards.rs b/crates/native-sidecar/tests/architecture_guards.rs index 4dbcae0438..d9083b8f7e 100644 --- a/crates/native-sidecar/tests/architecture_guards.rs +++ b/crates/native-sidecar/tests/architecture_guards.rs @@ -264,6 +264,41 @@ fn managed_blocking_socket_operations_wait_through_posix_poll() { } } +#[test] +fn managed_wasm_socket_reads_probe_before_and_after_readiness_waits() { + let runner = std::fs::read_to_string( + repo_root().join("crates/execution/assets/runners/wasm-runner.mjs"), + ) + .expect("read managed WASM runner"); + let read = runner + .split("function readHostNetSocketToGuestIovs(") + .nth(1) + .expect("managed WASM socket read helper") + .split("\nfunction writeHostNetSocketFromGuestIovs(") + .next() + .expect("managed WASM socket read boundary"); + let managed = read + .split("if (socket?.managed === true)") + .nth(1) + .expect("managed socket branch"); + let first_receive = managed + .find("callSyncRpc('process.hostnet_recv'") + .expect("initial receive probe"); + let readiness_wait = managed + .find("waitManagedHostNetReadable(socket, remaining, true)") + .expect("readiness wait"); + assert!( + first_receive < readiness_wait, + "managed reads must follow the Linux read-then-wait pattern" + ); + let after_wait = &managed[readiness_wait..]; + assert!( + after_wait.contains("const finalResult = callSyncRpc('process.hostnet_recv'") + && after_wait.contains("if (finalResult == null)"), + "a timeout-boundary receive probe must win over a coalesced readiness wake" + ); +} + #[test] fn sidecar_publishes_only_one_signal_delivery_scope_at_a_time() { let root = repo_root(); diff --git a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare index 3ad407c148..81306ecad2 100644 --- a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare +++ b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare @@ -690,6 +690,16 @@ type ResourceSnapshotResponse struct { socketConnections: u64 socketBufferedBytes: u64 socketDatagramQueueLen: u64 + wasmReservedMemoryBytes: u64 + wasmtimeEngineProfiles: u64 + wasmtimeModuleEntries: u64 + wasmtimeModuleCacheHits: u64 + wasmtimeModuleCacheMisses: u64 + wasmtimeModuleCacheEvictions: u64 + wasmtimeCompiledSourceBytes: u64 + wasmtimeChargedModuleBytes: u64 + wasmtimeCompileTimeMicros: u64 + wasmtimeProcessRetainedRssBytes: optional queueSnapshots: list } diff --git a/docs/design/wasmtime-executor.md b/docs/design/wasmtime-executor.md index 5c24948d20..96f1ef25c8 100644 --- a/docs/design/wasmtime-executor.md +++ b/docs/design/wasmtime-executor.md @@ -662,7 +662,7 @@ revision has been sealed: - [x] Phase 0: specification, inventory, baseline, and locked decisions. - [x] Phase 1: complete runtime-neutral kernel/executor prerequisite. - [x] Phase 2: production Wasmtime executor at current V8-WASM parity. -- [ ] Phase 3: performance decision, preferred-backend rollout, and initial +- [x] Phase 3: performance decision, preferred-backend rollout, and initial project completion. - [ ] Phase 4: separately gated threaded-WASM roadmap completion. @@ -856,43 +856,86 @@ Phase 2 evidence (Rust 1.94.0, Linux x86-64 canonical workspace): ### Phase 3 revision: Measure and enable the preferred backend -- [ ] Run release builds of both backends on the same canonical machine with +- [x] Run release builds of both backends on the same canonical machine with identical module bytes, host-service path, output capture, permissions, limits, and cache state. -- [ ] Measure at least five independent fresh-cache processes with five samples +- [x] Measure at least five independent fresh-cache processes with five samples each, plus warm cache-hit runs, for trivial, coreutils, shell, curl, sqlite, vim, large-module, compute-heavy, and host-call-heavy workloads. -- [ ] Run concurrency 1/10/50/100/200 with repeated-module and diverse-module +- [x] Run concurrency 1/10/50/100/200 with repeated-module and diverse-module workloads, success, denial, cancellation, and resource-limit paths. -- [ ] Record phase timing for VM/package setup, Engine lookup, module read, +- [x] Record phase timing for VM/package setup, Engine lookup, module read, validation, compilation/cache, linking, Store/async stack, instantiation, memory initialization, first host call, first output byte, completion, and teardown. -- [ ] Record baseline/incremental/peak RSS and PSS, VIRT separately, committed +- [x] Record baseline/incremental/peak RSS and PSS, VIRT separately, committed linear memory, compiled-code/cache bytes, async-stack bytes, kernel buffers, page faults, and retained memory after teardown. -- [ ] Benchmark on-demand memory allocation and eligible copy-on-write module +- [x] Benchmark on-demand memory allocation and eligible copy-on-write module memory initialization; do not enable pooling, AOT, Wizer, or live snapshots in this phase. -- [ ] Tune only evidence-supported Engine, Module-cache, memory-reservation, +- [x] Tune only evidence-supported Engine, Module-cache, memory-reservation, async-stack, and concurrency defaults while retaining all named bounds. -- [ ] Require zero correctness/safety regression, geometric-mean p50 regression +- [x] Require zero correctness/safety regression, geometric-mean p50 regression no worse than 10%, no individual p95 regression worse than 20%, throughput regression no worse than 10%, and retained RSS/PSS regression no worse than the greater of 10% or 4 MiB. -- [ ] If every preferred-backend threshold passes, make omission select +- [x] If every preferred-backend threshold passes, make omission select Wasmtime; otherwise keep omission on V8 while leaving Wasmtime explicitly selectable and record the failed thresholds. -- [ ] Add operator metrics, warnings, explicit backend override, rollback +- [x] Add operator metrics, warnings, explicit backend override, rollback control, cache/profile-limit visibility, and stable error attribution. -- [ ] Keep V8-WASM selectable, supported, and on the shared parity suite; never +- [x] Keep V8-WASM selectable, supported, and on the shared parity suite; never shadow-run side-effecting executions through both engines. -- [ ] Commit the raw benchmark results, selected defaults, threshold decision, +- [x] Commit the raw benchmark results, selected defaults, threshold decision, rollback criteria, and operator documentation with the implementation. -- [ ] Seal Phase 3 as one independently reviewable JJ revision on top of Phase +- [x] Seal Phase 3 as one independently reviewable JJ revision on top of Phase 2. Completion of this checkbox means the initial production Wasmtime project is complete. +Phase 3 evidence (Rust 1.94.0, release profile, Linux x86-64 canonical +workspace, 2026-07-20 Pacific): + +- The canonical matrix used the same release sidecar, host-service route, + permissions, limits, output capture, and SHA-256-inventoried source modules + for both engines. V8's existing safety transform adds a maximum to an + uncapped memory section (two bytes for this corpus); diagnostics record both + the identical source size and the transformed executable size. +- Five fresh sidecar processes per engine ran five samples for each of nine + distinct workload modules, including a real recursive `find` host-call case. + Per-execution diagnostics record setup, Engine, read, profile validation, + compile/cache, Linker, Store/async stack, Instance (including memory + initialization), first host call, first guest host call, first output, + completion, teardown, guest linear memory, and Store reservations. +- Repeated and diverse 1/10/50/100/200 concurrency rows completed. The 20-way + executor bound and 128-frame ingress bound produced their documented typed + admission failures above capacity; every comparable successful Wasmtime row + exceeded V8 throughput. Permission denial, abort cancellation, and active-CPU + limit paths passed for both engines. +- Correctness, geometric-mean p50 (`0.2972` Wasmtime/V8), and throughput gates + passed. Individual p95 failed because cold compilation dominates substantial + modules. Retained RSS failed (`127,930,368` V8 versus `264,069,120` Wasmtime) + and retained PSS failed (`129,094,656` versus `264,836,096`). Omission + therefore remains V8; explicit Wasmtime and V8 overrides remain available. +- No Engine, module-cache, memory, async-stack, or concurrency default was tuned: + the evidence supports Wasmtime for repeated warm modules but did not identify + a bounded default change that clears the cold-p95 and retained-memory gates. + Pooling, AOT, Wizer, serialized artifacts, and live snapshots remain off; + on-demand allocation and eligible copy-on-write initialization remain on. +- Resource snapshots expose live WASM reservations, Engine profiles, cache + entries/hits/misses/evictions, source and charged cache bytes, compile time, + and whole-process Linux RSS. Near-limit warnings and stable typed failures + name their configuration bounds. Rollback is the explicit V8 selector or + removal of a Wasmtime override. +- The readiness correction found during measurement passed 200 V8 curl samples + across 20 fresh sidecars (plus 200 matching Wasmtime samples) without the + former 30-second lost-wake failure. The canonical nine-workload result then + completed with zero validation failures. +- Raw samples and environment/module provenance are committed in + `packages/runtime-benchmarks/results/wasm-backend-comparison.json`; operator + interpretation, override, rollback, metrics, cold-start, memory, and snapshot + guidance is in `docs/wasmvm/executors.md`. + ### Phase 4 revision: Threading as a separate later project - [ ] Confirm Phase 3 is complete before enabling shared memory or threads. diff --git a/docs/wasmvm/executors.md b/docs/wasmvm/executors.md new file mode 100644 index 0000000000..da3dc41727 --- /dev/null +++ b/docs/wasmvm/executors.md @@ -0,0 +1,164 @@ +# Standalone WebAssembly executors + +AgentOS has two maintained native standalone-WebAssembly executors: V8-WASM +and Wasmtime. JavaScript always remains in V8; the selector described here +only chooses the engine for a standalone WASM command. The executors do not +share guest memory and there is no V8-to-Wasmtime bridge. + +## Production decision + +Omitting the selector currently chooses **V8**. Wasmtime is production-ready +and explicitly selectable, but the July 2026 canonical comparison did not pass +the cold-start p95, low-concurrency throughput, or retained RSS/PSS gates. +Warm Wasmtime module-cache hits were generally much faster than V8-WASM, so an +explicit Wasmtime selection is appropriate for a process expected to reuse a +small module set. It is not yet the safe fleet-wide default for cold or +module-diverse traffic. + +Select a backend per execution: + +```ts +await vm.execCommand("ls", ["-la"], { wasmBackend: "wasmtime" }); +await vm.execCommand("ls", ["-la"], { wasmBackend: "v8" }); +``` + +The selector is sealed to `"wasmtime" | "v8"`. Omission is the sidecar-owned +default; clients must not invent another default. The immediate rollback for a +Wasmtime workload is an explicit `wasmBackend: "v8"`. A fleet rollback is to +omit Wasmtime overrides; no code, cache, or data migration is required. + +## Shared host behavior and safety + +Both executors use the same sidecar-owned kernel, VFS, process table, fd and +socket tables, signal broker, permissions, and resource ledger. Wasmtime links +the AgentOS-owned Preview1/POSIX ABI directly. It does not construct a +`wasmtime-wasi` context and receives no ambient host filesystem, network, +process, environment, clock, random, or stdio authority. + +All filesystem and network waits use owned request/result buffers. Wasmtime +validates and copies guest input before awaiting, holds no guest-memory borrow +across the wait, then reacquires and revalidates every output range before +commit. Guest execution runs on the bounded non-Tokio VM executor; host I/O +continues on the process's one Tokio runtime. + +V8-WASM remains on the same parity and safety suite. AgentOS never shadow-runs +a side-effecting command through both engines. + +## Metrics and warnings + +`getResourceSnapshot()` exposes these process/VM diagnostics: + +| Field | Meaning | +| --- | --- | +| `wasmReservedMemoryBytes` | Live ledger charge for WASM linear memory, tables, and Wasmtime async stacks. It must return to zero after execution teardown. | +| `wasmtimeEngineProfiles` | Process-wide exact Engine profiles retained for distinct stack/feature configurations. | +| `wasmtimeModuleEntries` | Compiled modules currently retained across Engine-profile caches. | +| `wasmtimeModuleCacheHits`, `wasmtimeModuleCacheMisses`, `wasmtimeModuleCacheEvictions` | Cumulative cache behavior. A rising miss or eviction rate predicts cold-start latency. | +| `wasmtimeCompiledSourceBytes` | Cumulative source bytes compiled; this is a counter, not current resident memory. | +| `wasmtimeChargedModuleBytes` | Conservative current compiled-module cache charge used for bounded admission/eviction. | +| `wasmtimeCompileTimeMicros` | Cumulative synchronous Wasmtime compilation time. | +| `wasmtimeProcessRetainedRssBytes` | Whole-sidecar process RSS sampled on Linux. It is intentionally not presented as Wasmtime-only RSS. | + +Operators should alert on sustained module-cache misses/evictions, nonzero +`wasmReservedMemoryBytes` after all executions drain, or profile counts near +the configured bound. The runtime emits host-visible warnings before the +bounded Engine-profile and module-cache limits and typed errors at the limit: + +- `WARN_AGENTOS_WASMTIME_ENGINE_PROFILES_NEAR_LIMIT` / `ERR_AGENTOS_WASMTIME_ENGINE_PROFILE_LIMIT` +- `WARN_AGENTOS_WASMTIME_MODULE_CACHE_NEAR_LIMIT` / `ERR_AGENTOS_WASMTIME_MODULE_CACHE_LIMIT` +- `WARN_AGENTOS_WASMTIME_LIMIT_WARNING` for aggregate Store reservations + +Limit errors include `limitName`, `limit`, and `observed` details where those +values apply. Guest traps use `ERR_AGENTOS_WASM_TRAP` plus a stable `trapKind`; +memory, table, stack, active-CPU, fuel, wall-clock, cancellation, invalid-module, +and instantiation outcomes have stable AgentOS codes. Raw Wasmtime validation +or trap strings are private diagnostics and are not an API contract. + +## Compilation, cold start, and memory + +The Wasmtime backend performs ordinary in-process compilation and keeps a +bounded, SHA-256-keyed `Module` LRU per exact Engine profile. Cache input is the +original trusted module bytes and the configured feature/stack profile; there +is no serialized or externally supplied native artifact. + +The production configuration uses on-demand linear-memory allocation and +Wasmtime's eligible copy-on-write module-memory initialization. It does **not** +use pooling allocation, AOT deserialization, Wizer, or a live Store/Instance +snapshot. Wasmtime does not provide a general live-process snapshot/fork for +AgentOS: copying linear memory alone would omit kernel process, fd, socket, +signal, waiter, and thread state. V8's JavaScript heap snapshot remains a +JavaScript startup optimization and is not a cross-engine WASM snapshot. + +Memory figures must be kept separate: + +- RSS/PSS measure committed process memory; the benchmark records baseline, + peak, end, and retained-after-teardown values from `/proc`. +- VIRT includes Wasmtime guard/address-space reservations and is not committed + memory. It can rise sharply at concurrency without an equivalent RSS rise. +- `guestLinearMemoryBytes`, `asyncStackBytes`, and `reservedStoreBytes` are + recorded per Wasmtime execution in opt-in phase diagnostics. +- compiled-module charge and kernel buffered bytes are reported separately; + neither is guest linear memory. + +## Canonical benchmark and rollback criteria + +The raw release result is +[`packages/runtime-benchmarks/results/wasm-backend-comparison.json`](../../packages/runtime-benchmarks/results/wasm-backend-comparison.json). +It uses identical hashed source modules and host-service paths on one machine, +five independent sidecar processes per engine, five samples per workload, and +warm cache hits. V8 adds its existing two-byte memory-maximum rewrite before +compilation; both the identical source byte count and the transformed V8 byte +count are retained in phase diagnostics. + +The matrix covers trivial, coreutils, shell pipeline, loopback curl, sqlite, +Vim, large-module git, compute-heavy SHA-256, and host-call-heavy filesystem +work, plus repeated/diverse concurrency at 1/10/50/100/200 and permission +denial, cancellation, and CPU-limit paths. + +The canonical result completed with this decision table: + +| Gate | Result | Evidence | +| --- | --- | --- | +| Correctness and safety | Pass | Zero V8 or Wasmtime workload validation failures; denial, cancellation, and CPU-limit paths passed. | +| Geometric-mean p50 | Pass | Wasmtime/V8 ratio `0.2972` (about 70% lower latency across the mixed sample set). | +| Individual p95 | **Fail** | Cold Wasmtime compilation dominates substantive-module p95; several workload ratios exceed the `1.20` ceiling. | +| Throughput | Pass | Wasmtime exceeded V8 on every comparable 1/10/50/100/200 repeated/diverse row; overload produced typed executor/protocol admission errors. | +| Retained RSS | **Fail** | V8 `127,930,368` bytes; Wasmtime `264,069,120` bytes. | +| Retained PSS | **Fail** | V8 `129,094,656` bytes; Wasmtime `264,836,096` bytes. | + +Across workload medians, Wasmtime cold p50 ranged from approximately equal to +V8 for the trivial module to about 17× slower for Vim; warm p50 was about +2.5–4.6× faster. These results support explicit warm-cache use, but the failed +p95 and retained-memory gates require the omission default to remain V8. + +Run it from the repository root with a release sidecar and rebuilt canonical +commands: + +```bash +AGENTOS_SIDECAR_BIN=/absolute/path/to/release/agentos-sidecar \ +AGENTOS_WASM_COMMANDS_DIR=/absolute/path/to/packages/runtime-core/commands \ +pnpm --dir packages/runtime-benchmarks bench:wasm-backends +``` + +Wasmtime may become the omission default only when the same canonical run has +zero correctness/safety regressions and passes every locked threshold: + +- geometric-mean p50 no more than 10% slower; +- no individual p95 more than 20% slower; +- throughput no more than 10% lower; +- retained RSS and PSS no more than the greater of 10% or 4 MiB above V8. + +Keep or restore V8 as the default when any threshold fails, a stable typed +outcome diverges, cache misses become the dominant traffic shape, Store/kernel +resources do not drain, or Wasmtime causes a production safety regression. +An individual workload can still opt into Wasmtime when its own warm-cache and +memory evidence supports that choice. + +## Threads + +Shared WebAssembly memory and pthreads are not enabled by the initial +Wasmtime executor. AgentOS currently does not rely on shared memory between V8 +isolates, and no memory is shared between V8 and Wasmtime. Threading requires +the separately specified worker-process isolation, bounded thread admission, +threaded sysroot/libc, per-thread signal state, atomic wait/notify, teardown, +and hostile-VM conformance milestone before the feature profile can be enabled. diff --git a/packages/runtime-benchmarks/package.json b/packages/runtime-benchmarks/package.json index 968ac867b7..1bdc9d3936 100644 --- a/packages/runtime-benchmarks/package.json +++ b/packages/runtime-benchmarks/package.json @@ -10,6 +10,7 @@ "bench:gate": "tsx src/quick-gate.ts", "bench:memory": "node --expose-gc --import tsx/esm memory.bench.ts", "bench:matrix": "tsx src/run-all.ts", + "bench:wasm-backends": "tsx src/focused/wasm-backend-comparison.bench.ts", "check-types": "pnpm --dir ../runtime-core build && tsc --noEmit" }, "dependencies": { diff --git a/packages/runtime-benchmarks/results/wasm-backend-comparison.json b/packages/runtime-benchmarks/results/wasm-backend-comparison.json new file mode 100644 index 0000000000..5eaaaed6ca --- /dev/null +++ b/packages/runtime-benchmarks/results/wasm-backend-comparison.json @@ -0,0 +1,61444 @@ +{ + "metadata": { + "startedAt": "2026-07-21T06:44:20.865Z", + "hostname": "nathan-dev", + "platform": "linux", + "arch": "x64", + "cpuModel": "12th Gen Intel(R) Core(TM) i7-12700KF", + "logicalCpus": 20, + "totalMemoryBytes": 67170398208, + "kernel": "Linux 6.1.0-41-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.158-1 (2025-11-09) x86_64", + "node": "v24.17.0", + "sidecar": { + "path": "/tmp/release/agentos-sidecar", + "profile": "release", + "mtimeMs": 1784616006071.5117, + "mtimeIso": "2026-07-20T23:40:06.072-07:00", + "sizeBytes": 144867856 + }, + "commandsDir": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands", + "freshProcesses": 5, + "samplesPerProcess": 5, + "concurrencyLevels": [ + 1, + 10, + 50, + 100, + 200 + ], + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "pooling": false, + "aot": false, + "wizer": false, + "liveSnapshots": false + }, + "modules": [ + { + "command": "basename", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/basename", + "bytes": 517345, + "sha256": "4925e6fd365a649a8ae2defb56efb793b37046ad9e4df093f74a3f189cfc428e" + }, + { + "command": "curl", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/curl", + "bytes": 1561500, + "sha256": "5d44e7e68808294b7a155a6ae3c030b02adbf8969657b0ac41802d43b2ea6444" + }, + { + "command": "date", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/date", + "bytes": 2618604, + "sha256": "f49cbb0b6b9aedd645900c41b121485582d72944291a90db385618cbd9f27f76" + }, + { + "command": "dirname", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/dirname", + "bytes": 498912, + "sha256": "773449f7d0723bac7355b6d4ff4149707b877b98f2fdfab9fc6af160ff20a701" + }, + { + "command": "find", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/find", + "bytes": 1509578, + "sha256": "79174d7fa9bbf2a72747a71cc3b1e113a0a1768e95d456c7813e39a7559a1e25" + }, + { + "command": "git", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/git", + "bytes": 3397393, + "sha256": "0491fbbbfcf192e28872c1fe37819861fd54c4f8cb18f28e5ddf8a5297081d2e" + }, + { + "command": "id", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/id", + "bytes": 57250, + "sha256": "1d56d6c9b893f89919b17acc491579ee5f216e3b706ed009308eac594b80ac26" + }, + { + "command": "ls", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/ls", + "bytes": 1208565, + "sha256": "86da1fbf155760c33aa2519e058b6ea95266c9239def7f72faa136b1dcbc9b4f" + }, + { + "command": "printf", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/printf", + "bytes": 652796, + "sha256": "e8a170ba2b94f8f906c2a69b355d13a45f3ef9006143ed63f6994b6f22a8c01a" + }, + { + "command": "pwd", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/pwd", + "bytes": 501598, + "sha256": "40bf2b78bd7c081b6fbafb26c6641404f6062f8fd065e85dcf217b98e269ee86" + }, + { + "command": "sh", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sh", + "bytes": 3082692, + "sha256": "25042baa43977d9b31e302d6a008e172d722f8d9df83b6cab9d84db71618713a" + }, + { + "command": "sha256sum", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sha256sum", + "bytes": 1349812, + "sha256": "7e9fca50d94a69d0854f3092c33565ff0f82af80320f732889623b091e4a73fd" + }, + { + "command": "sqlite3", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sqlite3", + "bytes": 878882, + "sha256": "3f87de5e79cf6cc55eac3e3eeb643f2522b838c1829d7644ab13afd7d917d881" + }, + { + "command": "true", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/true", + "bytes": 28203, + "sha256": "d8cee1b90b65bd7571197ac6fde57f46d538bea6c1b09a1ac404b5368631b2bc" + }, + { + "command": "uname", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/uname", + "bytes": 502129, + "sha256": "70bf67158f10102c5af11b7e6cf7ba5d937b0a9a49ddad4e05bf3fc80726606e" + }, + { + "command": "vim", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/vim", + "bytes": 2854951, + "sha256": "d1db095826460b79b4970bebe4d3bb1d4b754ba82a93a36a207f37e4dc053121" + } + ], + "fresh": [ + { + "backend": "v8", + "processIndex": 0, + "vmSetupMs": 462.35331400000007, + "fixtureSetupMs": 385.8766160000001, + "baseline": { + "rssBytes": 240922624, + "peakRssBytes": 248254464, + "pssBytes": 241631232, + "virtualBytes": 3886268416, + "minorFaults": 55510, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352833536, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 368852992, + "peakRssBytes": 479719424, + "pssBytes": 370725888, + "virtualBytes": 4053340160, + "minorFaults": 835244, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 127930368, + "peakRssBytes": 231464960, + "pssBytes": 129094656, + "virtualBytes": 167071744, + "minorFaults": 779734, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 71.192906, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.640908 + }, + { + "name": "WebAssembly.Module", + "ms": 0.134945 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.074338 + }, + { + "name": "wasi.start", + "ms": 0.09099 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 240922624, + "peakRssBytes": 248254464, + "pssBytes": 241639424, + "virtualBytes": 3888381952, + "minorFaults": 55512, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276721664, + "peakRssBytes": 276721664, + "pssBytes": 276758528, + "virtualBytes": 4640604160, + "minorFaults": 66409, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261775360, + "peakRssBytes": 276721664, + "pssBytes": 262774784, + "virtualBytes": 3955490816, + "minorFaults": 66409, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240922624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261775360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 63.86656200000016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.145307 + }, + { + "name": "WebAssembly.Module", + "ms": 1.159899 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.06795 + }, + { + "name": "wasi.start", + "ms": 0.095417 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261775360, + "peakRssBytes": 276721664, + "pssBytes": 262774784, + "virtualBytes": 3955490816, + "minorFaults": 66409, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276566016, + "peakRssBytes": 276865024, + "pssBytes": 277713920, + "virtualBytes": 4640595968, + "minorFaults": 72712, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261918720, + "peakRssBytes": 276865024, + "pssBytes": 262996992, + "virtualBytes": 3955490816, + "minorFaults": 72712, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261775360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261918720, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 61.319097000000056, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.297265 + }, + { + "name": "WebAssembly.Module", + "ms": 0.15285 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.060684 + }, + { + "name": "wasi.start", + "ms": 0.08518 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261918720, + "peakRssBytes": 276865024, + "pssBytes": 262996992, + "virtualBytes": 3955490816, + "minorFaults": 72712, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276639744, + "peakRssBytes": 276873216, + "pssBytes": 277881856, + "virtualBytes": 4640595968, + "minorFaults": 78986, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261926912, + "peakRssBytes": 276873216, + "pssBytes": 263099392, + "virtualBytes": 3955490816, + "minorFaults": 78986, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261918720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261926912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 60.44943699999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.075238 + }, + { + "name": "WebAssembly.Module", + "ms": 0.140589 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.061157 + }, + { + "name": "wasi.start", + "ms": 0.085867 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261926912, + "peakRssBytes": 276873216, + "pssBytes": 263099392, + "virtualBytes": 3955490816, + "minorFaults": 78986, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276652032, + "peakRssBytes": 276873216, + "pssBytes": 277447680, + "virtualBytes": 4640739328, + "minorFaults": 85246, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261910528, + "peakRssBytes": 276873216, + "pssBytes": 263140352, + "virtualBytes": 3955490816, + "minorFaults": 85246, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261926912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261910528, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 71.48805900000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.124553 + }, + { + "name": "WebAssembly.Module", + "ms": 0.140377 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.115037 + }, + { + "name": "wasi.start", + "ms": 0.129849 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261910528, + "peakRssBytes": 276873216, + "pssBytes": 263140352, + "virtualBytes": 3955490816, + "minorFaults": 85246, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276623360, + "peakRssBytes": 276877312, + "pssBytes": 277955584, + "virtualBytes": 4640739328, + "minorFaults": 91507, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261931008, + "peakRssBytes": 276877312, + "pssBytes": 263193600, + "virtualBytes": 3955490816, + "minorFaults": 91507, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261910528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261931008, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 221.05488400000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.131845 + }, + { + "name": "WebAssembly.Module", + "ms": 2.597381 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.306069 + }, + { + "name": "wasi.start", + "ms": 106.145185 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261931008, + "peakRssBytes": 276877312, + "pssBytes": 263193600, + "virtualBytes": 3955490816, + "minorFaults": 91507, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 334594048, + "peakRssBytes": 335134720, + "pssBytes": 330317824, + "virtualBytes": 4696231936, + "minorFaults": 111228, + "majorFaults": 0 + }, + "end": { + "rssBytes": 283340800, + "peakRssBytes": 335134720, + "pssBytes": 284398592, + "virtualBytes": 3957592064, + "minorFaults": 111228, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261931008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283340800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 195.67816399999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.696064 + }, + { + "name": "WebAssembly.Module", + "ms": 0.943434 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.265517 + }, + { + "name": "wasi.start", + "ms": 85.006597 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 283340800, + "peakRssBytes": 335134720, + "pssBytes": 284398592, + "virtualBytes": 3957592064, + "minorFaults": 111228, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 343375872, + "peakRssBytes": 343916544, + "pssBytes": 341574656, + "virtualBytes": 4695969792, + "minorFaults": 130470, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293322752, + "peakRssBytes": 343916544, + "pssBytes": 294061056, + "virtualBytes": 3957592064, + "minorFaults": 130470, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283340800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293322752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 215.86742700000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.348983 + }, + { + "name": "WebAssembly.Module", + "ms": 1.020886 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.106312 + }, + { + "name": "wasi.start", + "ms": 96.090949 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293322752, + "peakRssBytes": 343916544, + "pssBytes": 294061056, + "virtualBytes": 3957592064, + "minorFaults": 130470, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 347852800, + "peakRssBytes": 348123136, + "pssBytes": 348930048, + "virtualBytes": 4696231936, + "minorFaults": 144872, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293441536, + "peakRssBytes": 348123136, + "pssBytes": 294401024, + "virtualBytes": 3957592064, + "minorFaults": 144872, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293322752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293441536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 188.8706070000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.23384 + }, + { + "name": "WebAssembly.Module", + "ms": 1.028721 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.28526 + }, + { + "name": "wasi.start", + "ms": 80.182999 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293441536, + "peakRssBytes": 348123136, + "pssBytes": 294401024, + "virtualBytes": 3957592064, + "minorFaults": 144872, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 352415744, + "peakRssBytes": 353226752, + "pssBytes": 350245888, + "virtualBytes": 4695969792, + "minorFaults": 160310, + "majorFaults": 0 + }, + "end": { + "rssBytes": 300548096, + "peakRssBytes": 353226752, + "pssBytes": 301687808, + "virtualBytes": 3957592064, + "minorFaults": 160310, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293441536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 300548096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 207.83000399999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.863399 + }, + { + "name": "WebAssembly.Module", + "ms": 1.274193 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.270926 + }, + { + "name": "wasi.start", + "ms": 94.19146 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 300548096, + "peakRssBytes": 353226752, + "pssBytes": 301687808, + "virtualBytes": 3957592064, + "minorFaults": 160310, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 357429248, + "peakRssBytes": 357429248, + "pssBytes": 358658048, + "virtualBytes": 4696494080, + "minorFaults": 175237, + "majorFaults": 0 + }, + "end": { + "rssBytes": 302886912, + "peakRssBytes": 357429248, + "pssBytes": 304164864, + "virtualBytes": 3957592064, + "minorFaults": 175237, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 300548096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 302886912, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 280.2332799999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.191276 + }, + { + "name": "WebAssembly.Module", + "ms": 3.150473 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.729143 + }, + { + "name": "wasi.start", + "ms": 101.563967 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 302886912, + "peakRssBytes": 357429248, + "pssBytes": 304164864, + "virtualBytes": 3957592064, + "minorFaults": 175237, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 409567232, + "peakRssBytes": 409595904, + "pssBytes": 410780672, + "virtualBytes": 5471977472, + "minorFaults": 207749, + "majorFaults": 0 + }, + "end": { + "rssBytes": 328683520, + "peakRssBytes": 409595904, + "pssBytes": 330650624, + "virtualBytes": 4029874176, + "minorFaults": 207749, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 302886912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328683520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 292.09251200000017, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 51.035447 + }, + { + "name": "WebAssembly.Module", + "ms": 3.204424 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.539671 + }, + { + "name": "wasi.start", + "ms": 103.528698 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 328683520, + "peakRssBytes": 409595904, + "pssBytes": 330650624, + "virtualBytes": 4029874176, + "minorFaults": 207749, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425496576, + "peakRssBytes": 425545728, + "pssBytes": 427261952, + "virtualBytes": 5472600064, + "minorFaults": 233822, + "majorFaults": 0 + }, + "end": { + "rssBytes": 329900032, + "peakRssBytes": 425545728, + "pssBytes": 331432960, + "virtualBytes": 4030496768, + "minorFaults": 233822, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328683520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329900032, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 278.16768500000035, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 45.415725 + }, + { + "name": "WebAssembly.Module", + "ms": 5.330718 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.032248 + }, + { + "name": "wasi.start", + "ms": 103.415371 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329900032, + "peakRssBytes": 425545728, + "pssBytes": 331432960, + "virtualBytes": 4030496768, + "minorFaults": 233822, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426459136, + "peakRssBytes": 426541056, + "pssBytes": 428139520, + "virtualBytes": 5472862208, + "minorFaults": 262286, + "majorFaults": 0 + }, + "end": { + "rssBytes": 329863168, + "peakRssBytes": 426541056, + "pssBytes": 331644928, + "virtualBytes": 4030496768, + "minorFaults": 262286, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329900032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329863168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 310.06658800000014, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 50.006927 + }, + { + "name": "WebAssembly.Module", + "ms": 3.44763 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.5085 + }, + { + "name": "wasi.start", + "ms": 106.775862 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329863168, + "peakRssBytes": 426541056, + "pssBytes": 331645952, + "virtualBytes": 4030496768, + "minorFaults": 262286, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422764544, + "peakRssBytes": 426541056, + "pssBytes": 424350720, + "virtualBytes": 5446418432, + "minorFaults": 290877, + "majorFaults": 0 + }, + "end": { + "rssBytes": 329777152, + "peakRssBytes": 426541056, + "pssBytes": 331715584, + "virtualBytes": 4030496768, + "minorFaults": 290877, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329863168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329777152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 608.1495689999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 52.653588 + }, + { + "name": "WebAssembly.Module", + "ms": 3.523389 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.548872 + }, + { + "name": "wasi.start", + "ms": 413.573847 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329777152, + "peakRssBytes": 426541056, + "pssBytes": 331715584, + "virtualBytes": 4030496768, + "minorFaults": 290877, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425988096, + "peakRssBytes": 426541056, + "pssBytes": 428077056, + "virtualBytes": 5474844672, + "minorFaults": 316692, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330027008, + "peakRssBytes": 426541056, + "pssBytes": 331755520, + "virtualBytes": 4032598016, + "minorFaults": 316692, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329777152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330027008, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 157.78206600000067, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.597966 + }, + { + "name": "WebAssembly.Module", + "ms": 1.485847 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.440458 + }, + { + "name": "wasi.start", + "ms": 22.48777 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330027008, + "peakRssBytes": 426541056, + "pssBytes": 331755520, + "virtualBytes": 4032598016, + "minorFaults": 316692, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 400359424, + "peakRssBytes": 426541056, + "pssBytes": 403080192, + "virtualBytes": 4790272000, + "minorFaults": 330976, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330174464, + "peakRssBytes": 426541056, + "pssBytes": 332682240, + "virtualBytes": 4032598016, + "minorFaults": 330976, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330027008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330174464, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 154.4236090000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.253365 + }, + { + "name": "WebAssembly.Module", + "ms": 2.170875 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.377855 + }, + { + "name": "wasi.start", + "ms": 23.024413 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330174464, + "peakRssBytes": 426541056, + "pssBytes": 332682240, + "virtualBytes": 4032598016, + "minorFaults": 330976, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 399826944, + "peakRssBytes": 426541056, + "pssBytes": 402412544, + "virtualBytes": 4790009856, + "minorFaults": 345593, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330297344, + "peakRssBytes": 426541056, + "pssBytes": 332686336, + "virtualBytes": 4032598016, + "minorFaults": 345593, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330174464, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330297344, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 156.1578559999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.152246 + }, + { + "name": "WebAssembly.Module", + "ms": 1.628889 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.250513 + }, + { + "name": "wasi.start", + "ms": 22.23381 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330297344, + "peakRssBytes": 426541056, + "pssBytes": 332686336, + "virtualBytes": 4032598016, + "minorFaults": 345593, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 398901248, + "peakRssBytes": 426541056, + "pssBytes": 401379328, + "virtualBytes": 4790272000, + "minorFaults": 360977, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330219520, + "peakRssBytes": 426541056, + "pssBytes": 332693504, + "virtualBytes": 4032598016, + "minorFaults": 360977, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330297344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330219520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 162.08501200000046, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.690534 + }, + { + "name": "WebAssembly.Module", + "ms": 2.387504 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.265318 + }, + { + "name": "wasi.start", + "ms": 22.948819 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330219520, + "peakRssBytes": 426541056, + "pssBytes": 332693504, + "virtualBytes": 4032598016, + "minorFaults": 360977, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 400441344, + "peakRssBytes": 426541056, + "pssBytes": 403178496, + "virtualBytes": 4790272000, + "minorFaults": 376800, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330145792, + "peakRssBytes": 426541056, + "pssBytes": 332698624, + "virtualBytes": 4032598016, + "minorFaults": 376800, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330219520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330145792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 157.13079000000016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.344841 + }, + { + "name": "WebAssembly.Module", + "ms": 2.721921 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.240962 + }, + { + "name": "wasi.start", + "ms": 23.112668 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330145792, + "peakRssBytes": 426541056, + "pssBytes": 332698624, + "virtualBytes": 4032598016, + "minorFaults": 376800, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 400052224, + "peakRssBytes": 426541056, + "pssBytes": 402519040, + "virtualBytes": 4790534144, + "minorFaults": 391950, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330371072, + "peakRssBytes": 426541056, + "pssBytes": 332706816, + "virtualBytes": 4032598016, + "minorFaults": 391950, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330145792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330371072, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 135.88163299999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.511913 + }, + { + "name": "WebAssembly.Module", + "ms": 1.528626 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.409826 + }, + { + "name": "wasi.start", + "ms": 18.000498 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330371072, + "peakRssBytes": 426541056, + "pssBytes": 332706816, + "virtualBytes": 4032598016, + "minorFaults": 391950, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374738944, + "peakRssBytes": 426541056, + "pssBytes": 375877632, + "virtualBytes": 4759707648, + "minorFaults": 408223, + "majorFaults": 0 + }, + "end": { + "rssBytes": 344530944, + "peakRssBytes": 426541056, + "pssBytes": 286053376, + "virtualBytes": 4032598016, + "minorFaults": 408223, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330371072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 344801280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 136.7787189999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 16.366847 + }, + { + "name": "WebAssembly.Module", + "ms": 1.203659 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.336131 + }, + { + "name": "wasi.start", + "ms": 15.553189 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 344801280, + "peakRssBytes": 426541056, + "pssBytes": 100873216, + "virtualBytes": 4032598016, + "minorFaults": 408270, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395223040, + "peakRssBytes": 426541056, + "pssBytes": 385964032, + "virtualBytes": 4760113152, + "minorFaults": 430113, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356159488, + "peakRssBytes": 426541056, + "pssBytes": 359445504, + "virtualBytes": 4032598016, + "minorFaults": 430113, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 344801280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356700160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 125.2132759999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.645916 + }, + { + "name": "WebAssembly.Module", + "ms": 1.039816 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.494999 + }, + { + "name": "wasi.start", + "ms": 16.00055 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356700160, + "peakRssBytes": 426541056, + "pssBytes": 360481792, + "virtualBytes": 4032598016, + "minorFaults": 430248, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 405954560, + "peakRssBytes": 426541056, + "pssBytes": 396841984, + "virtualBytes": 4759969792, + "minorFaults": 452681, + "majorFaults": 0 + }, + "end": { + "rssBytes": 372457472, + "peakRssBytes": 426541056, + "pssBytes": 230272000, + "virtualBytes": 4032598016, + "minorFaults": 452681, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356700160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372727808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 132.0835049999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.437725 + }, + { + "name": "WebAssembly.Module", + "ms": 1.100311 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.384881 + }, + { + "name": "wasi.start", + "ms": 16.865595 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 372727808, + "peakRssBytes": 426541056, + "pssBytes": 376210432, + "virtualBytes": 4032598016, + "minorFaults": 452820, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 421924864, + "peakRssBytes": 426541056, + "pssBytes": 404717568, + "virtualBytes": 4759969792, + "minorFaults": 469224, + "majorFaults": 0 + }, + "end": { + "rssBytes": 359366656, + "peakRssBytes": 426541056, + "pssBytes": 361902080, + "virtualBytes": 4032598016, + "minorFaults": 469224, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372727808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 359366656, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 134.0147479999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 16.545871 + }, + { + "name": "WebAssembly.Module", + "ms": 1.228851 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.613312 + }, + { + "name": "wasi.start", + "ms": 15.549568 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 359366656, + "peakRssBytes": 426541056, + "pssBytes": 361902080, + "virtualBytes": 4032598016, + "minorFaults": 469224, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 404578304, + "peakRssBytes": 426541056, + "pssBytes": 405999616, + "virtualBytes": 4759969792, + "minorFaults": 487228, + "majorFaults": 0 + }, + "end": { + "rssBytes": 357441536, + "peakRssBytes": 426541056, + "pssBytes": 360361984, + "virtualBytes": 4032598016, + "minorFaults": 487228, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 359366656, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358252544, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 223.0375840000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.002169 + }, + { + "name": "WebAssembly.Module", + "ms": 4.712706 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.062092 + }, + { + "name": "wasi.start", + "ms": 33.785827 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 358522880, + "peakRssBytes": 426541056, + "pssBytes": 360985600, + "virtualBytes": 4032598016, + "minorFaults": 487480, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 434802688, + "peakRssBytes": 434950144, + "pssBytes": 436261888, + "virtualBytes": 4823556096, + "minorFaults": 506132, + "majorFaults": 0 + }, + "end": { + "rssBytes": 341352448, + "peakRssBytes": 434950144, + "pssBytes": 343802880, + "virtualBytes": 4032598016, + "minorFaults": 506132, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358522880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 341352448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 223.17298599999958, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 50.045534 + }, + { + "name": "WebAssembly.Module", + "ms": 3.335846 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.773437 + }, + { + "name": "wasi.start", + "ms": 39.224609 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 341352448, + "peakRssBytes": 434950144, + "pssBytes": 343802880, + "virtualBytes": 4032598016, + "minorFaults": 506132, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 434999296, + "peakRssBytes": 435150848, + "pssBytes": 437518336, + "virtualBytes": 4823293952, + "minorFaults": 521587, + "majorFaults": 0 + }, + "end": { + "rssBytes": 341192704, + "peakRssBytes": 435150848, + "pssBytes": 343805952, + "virtualBytes": 4032598016, + "minorFaults": 521587, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 341352448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 341192704, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 216.91449300000022, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 43.405143 + }, + { + "name": "WebAssembly.Module", + "ms": 4.041598 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.713397 + }, + { + "name": "wasi.start", + "ms": 44.081026 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 341192704, + "peakRssBytes": 435150848, + "pssBytes": 343805952, + "virtualBytes": 4032598016, + "minorFaults": 521587, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 436109312, + "peakRssBytes": 436375552, + "pssBytes": 438744064, + "virtualBytes": 4823556096, + "minorFaults": 538418, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343502848, + "peakRssBytes": 436375552, + "pssBytes": 346088448, + "virtualBytes": 4032598016, + "minorFaults": 538418, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 341192704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343502848, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 246.54707899999994, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 51.684533 + }, + { + "name": "WebAssembly.Module", + "ms": 3.46854 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.50259 + }, + { + "name": "wasi.start", + "ms": 51.117106 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343502848, + "peakRssBytes": 436375552, + "pssBytes": 346088448, + "virtualBytes": 4032598016, + "minorFaults": 538418, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 437399552, + "peakRssBytes": 437616640, + "pssBytes": 440049664, + "virtualBytes": 4823556096, + "minorFaults": 554916, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343646208, + "peakRssBytes": 437616640, + "pssBytes": 346157056, + "virtualBytes": 4032598016, + "minorFaults": 554916, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343502848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343646208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 228.50945799999954, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.01954 + }, + { + "name": "WebAssembly.Module", + "ms": 4.924515 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.025321 + }, + { + "name": "wasi.start", + "ms": 41.568719 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343646208, + "peakRssBytes": 437616640, + "pssBytes": 346157056, + "virtualBytes": 4032598016, + "minorFaults": 554916, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 439185408, + "peakRssBytes": 439201792, + "pssBytes": 441795584, + "virtualBytes": 4823293952, + "minorFaults": 571108, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343498752, + "peakRssBytes": 439201792, + "pssBytes": 346219520, + "virtualBytes": 4032598016, + "minorFaults": 571108, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343646208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343498752, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 245.6948569999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 73.056233 + }, + { + "name": "WebAssembly.Module", + "ms": 4.640735 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.797029 + }, + { + "name": "wasi.start", + "ms": 5.6424 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343498752, + "peakRssBytes": 439201792, + "pssBytes": 346219520, + "virtualBytes": 4032598016, + "minorFaults": 571108, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477859840, + "peakRssBytes": 477929472, + "pssBytes": 479682560, + "virtualBytes": 4813361152, + "minorFaults": 595515, + "majorFaults": 0 + }, + "end": { + "rssBytes": 347381760, + "peakRssBytes": 477929472, + "pssBytes": 349204480, + "virtualBytes": 4033179648, + "minorFaults": 595515, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343498752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347381760, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 254.20395999999982, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 84.303827 + }, + { + "name": "WebAssembly.Module", + "ms": 4.904965 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.41379 + }, + { + "name": "wasi.start", + "ms": 5.0944 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 347381760, + "peakRssBytes": 477929472, + "pssBytes": 349204480, + "virtualBytes": 4033179648, + "minorFaults": 595515, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479404032, + "peakRssBytes": 479510528, + "pssBytes": 481244160, + "virtualBytes": 4813750272, + "minorFaults": 621251, + "majorFaults": 0 + }, + "end": { + "rssBytes": 347443200, + "peakRssBytes": 479510528, + "pssBytes": 349246464, + "virtualBytes": 4033568768, + "minorFaults": 621251, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347381760, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347443200, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 244.22940000000017, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 70.853979 + }, + { + "name": "WebAssembly.Module", + "ms": 3.887805 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.414502 + }, + { + "name": "wasi.start", + "ms": 7.501861 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 347443200, + "peakRssBytes": 479510528, + "pssBytes": 349246464, + "virtualBytes": 4033568768, + "minorFaults": 621251, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479522816, + "peakRssBytes": 479662080, + "pssBytes": 481320960, + "virtualBytes": 4813606912, + "minorFaults": 643925, + "majorFaults": 0 + }, + "end": { + "rssBytes": 347598848, + "peakRssBytes": 479662080, + "pssBytes": 349343744, + "virtualBytes": 4033568768, + "minorFaults": 643925, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347443200, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347598848, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 236.7261499999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 72.332437 + }, + { + "name": "WebAssembly.Module", + "ms": 4.646658 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.156985 + }, + { + "name": "wasi.start", + "ms": 5.388219 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 347598848, + "peakRssBytes": 479662080, + "pssBytes": 349343744, + "virtualBytes": 4033568768, + "minorFaults": 643925, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479571968, + "peakRssBytes": 479715328, + "pssBytes": 481346560, + "virtualBytes": 4813606912, + "minorFaults": 666581, + "majorFaults": 0 + }, + "end": { + "rssBytes": 347648000, + "peakRssBytes": 479715328, + "pssBytes": 349344768, + "virtualBytes": 4033568768, + "minorFaults": 666581, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347598848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347648000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 261.0528170000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.032616 + }, + { + "name": "WebAssembly.Module", + "ms": 5.96914 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.413362 + }, + { + "name": "wasi.start", + "ms": 8.161347 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 347648000, + "peakRssBytes": 479715328, + "pssBytes": 349344768, + "virtualBytes": 4033568768, + "minorFaults": 666581, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479510528, + "peakRssBytes": 479719424, + "pssBytes": 481341440, + "virtualBytes": 4813606912, + "minorFaults": 688144, + "majorFaults": 0 + }, + "end": { + "rssBytes": 347656192, + "peakRssBytes": 479719424, + "pssBytes": 349344768, + "virtualBytes": 4033568768, + "minorFaults": 688144, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347648000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347656192, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 183.8470639999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.729588 + }, + { + "name": "WebAssembly.Module", + "ms": 0.916904 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.408246 + }, + { + "name": "wasi.start", + "ms": 74.069697 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 347656192, + "peakRssBytes": 479719424, + "pssBytes": 349344768, + "virtualBytes": 4033568768, + "minorFaults": 688144, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403779584, + "peakRssBytes": 479719424, + "pssBytes": 401036288, + "virtualBytes": 4771852288, + "minorFaults": 703409, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351784960, + "peakRssBytes": 479719424, + "pssBytes": 353625088, + "virtualBytes": 4034527232, + "minorFaults": 703409, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 347656192, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351784960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 195.03728500000034, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.910266 + }, + { + "name": "WebAssembly.Module", + "ms": 0.728188 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.637834 + }, + { + "name": "wasi.start", + "ms": 79.402046 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351784960, + "peakRssBytes": 479719424, + "pssBytes": 353625088, + "virtualBytes": 4034527232, + "minorFaults": 703409, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403562496, + "peakRssBytes": 479719424, + "pssBytes": 405716992, + "virtualBytes": 4771852288, + "minorFaults": 718669, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351723520, + "peakRssBytes": 479719424, + "pssBytes": 353689600, + "virtualBytes": 4034527232, + "minorFaults": 718669, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351784960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351723520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 197.47316900000078, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.566617 + }, + { + "name": "WebAssembly.Module", + "ms": 0.792176 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.37971 + }, + { + "name": "wasi.start", + "ms": 68.890478 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351723520, + "peakRssBytes": 479719424, + "pssBytes": 353689600, + "virtualBytes": 4034527232, + "minorFaults": 718669, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403718144, + "peakRssBytes": 479719424, + "pssBytes": 405745664, + "virtualBytes": 4771590144, + "minorFaults": 733401, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351772672, + "peakRssBytes": 479719424, + "pssBytes": 353690624, + "virtualBytes": 4034527232, + "minorFaults": 733401, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351723520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351772672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 959.1709859999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.357998 + }, + { + "name": "WebAssembly.Module", + "ms": 1.307329 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.370345 + }, + { + "name": "wasi.start", + "ms": 66.601038 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351772672, + "peakRssBytes": 479719424, + "pssBytes": 353690624, + "virtualBytes": 4034527232, + "minorFaults": 733401, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 405032960, + "peakRssBytes": 479719424, + "pssBytes": 407245824, + "virtualBytes": 4771590144, + "minorFaults": 746446, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351653888, + "peakRssBytes": 479719424, + "pssBytes": 353690624, + "virtualBytes": 4034527232, + "minorFaults": 746446, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351772672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351653888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 364.37481800000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.128657 + }, + { + "name": "WebAssembly.Module", + "ms": 0.822044 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.480099 + }, + { + "name": "wasi.start", + "ms": 66.265046 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351653888, + "peakRssBytes": 479719424, + "pssBytes": 353690624, + "virtualBytes": 4034527232, + "minorFaults": 746446, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403714048, + "peakRssBytes": 479719424, + "pssBytes": 405791744, + "virtualBytes": 4771852288, + "minorFaults": 760664, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351842304, + "peakRssBytes": 479719424, + "pssBytes": 353715200, + "virtualBytes": 4034527232, + "minorFaults": 760664, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351653888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351842304, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 138.22382399999879, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.341263 + }, + { + "name": "WebAssembly.Module", + "ms": 2.214492 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.334328 + }, + { + "name": "wasi.start", + "ms": 13.486 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351842304, + "peakRssBytes": 479719424, + "pssBytes": 353715200, + "virtualBytes": 4034527232, + "minorFaults": 760664, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422232064, + "peakRssBytes": 479719424, + "pssBytes": 424219648, + "virtualBytes": 4792430592, + "minorFaults": 776494, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352604160, + "peakRssBytes": 479719424, + "pssBytes": 354440192, + "virtualBytes": 4036038656, + "minorFaults": 776494, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351842304, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352604160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 614.0596300000016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.001429 + }, + { + "name": "WebAssembly.Module", + "ms": 1.127536 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.122331 + }, + { + "name": "wasi.start", + "ms": 13.929302 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352604160, + "peakRssBytes": 479719424, + "pssBytes": 354440192, + "virtualBytes": 4036038656, + "minorFaults": 776494, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422899712, + "peakRssBytes": 479719424, + "pssBytes": 424813568, + "virtualBytes": 4792954880, + "minorFaults": 790760, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352960512, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 790760, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352604160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352960512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 1351.7398759999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.034602 + }, + { + "name": "WebAssembly.Module", + "ms": 1.158415 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.153199 + }, + { + "name": "wasi.start", + "ms": 14.097119 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352960512, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 790760, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420638720, + "peakRssBytes": 479719424, + "pssBytes": 422749184, + "virtualBytes": 4792692736, + "minorFaults": 804967, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352796672, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 804967, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352960512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352796672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 863.6683400000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.99465 + }, + { + "name": "WebAssembly.Module", + "ms": 1.901692 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.353872 + }, + { + "name": "wasi.start", + "ms": 13.299731 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352796672, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 804967, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420433920, + "peakRssBytes": 479719424, + "pssBytes": 422745088, + "virtualBytes": 4792430592, + "minorFaults": 819173, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352653312, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 819173, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352796672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352653312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 1017.7249879999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616260935350038/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 34.280095 + }, + { + "name": "WebAssembly.Module", + "ms": 1.275506 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.601533 + }, + { + "name": "wasi.start", + "ms": 23.134165 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352653312, + "peakRssBytes": 479719424, + "pssBytes": 354710528, + "virtualBytes": 4036300800, + "minorFaults": 819173, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420847616, + "peakRssBytes": 479719424, + "pssBytes": 422744064, + "virtualBytes": 4792168448, + "minorFaults": 833379, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352833536, + "peakRssBytes": 479719424, + "pssBytes": 354709504, + "virtualBytes": 4036300800, + "minorFaults": 833379, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352653312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352833536, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 0, + "vmSetupMs": 531.360544000001, + "fixtureSetupMs": 623.8085960000008, + "baseline": { + "rssBytes": 241324032, + "peakRssBytes": 246824960, + "pssBytes": 243825664, + "virtualBytes": 3886268416, + "minorFaults": 55596, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 474136576, + "peakRssBytes": 510603264, + "pssBytes": 477432832, + "virtualBytes": 4186484736, + "minorFaults": 111136, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 232812544, + "peakRssBytes": 263778304, + "pssBytes": 233607168, + "virtualBytes": 300216320, + "minorFaults": 55540, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 67.37656699999934, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.077777, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 49.853775000000006, + "phases": [ + { + "name": "Engine", + "ms": 0.069743 + }, + { + "name": "canonicalPreopens", + "ms": 0.111865 + }, + { + "name": "moduleRead", + "ms": 2.469189 + }, + { + "name": "profileValidation", + "ms": 0.22827899999999998 + }, + { + "name": "moduleCompile", + "ms": 46.53285 + }, + { + "name": "importValidation", + "ms": 0.002982 + }, + { + "name": "Linker", + "ms": 0.184001 + }, + { + "name": "Store", + "ms": 0.015347999999999999 + }, + { + "name": "Instance", + "ms": 0.040787000000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.080376 + }, + { + "name": "entrypointLookup", + "ms": 0.002446 + }, + { + "name": "wasi.start", + "ms": 0.021463 + }, + { + "name": "Store.teardown", + "ms": 0.021322999999999998 + } + ] + }, + "memory": { + "start": { + "rssBytes": 241324032, + "peakRssBytes": 246824960, + "pssBytes": 243834880, + "virtualBytes": 3888381952, + "minorFaults": 55598, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56755, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56755, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 241324032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 418.2421639999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018851, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 6.406117999999999, + "phases": [ + { + "name": "Engine", + "ms": 0.004809 + }, + { + "name": "canonicalPreopens", + "ms": 0.161895 + }, + { + "name": "moduleRead", + "ms": 3.361224 + }, + { + "name": "profileValidation", + "ms": 0.241623 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00348 + }, + { + "name": "Linker", + "ms": 0.220318 + }, + { + "name": "Store", + "ms": 0.01617 + }, + { + "name": "Instance", + "ms": 1.092364 + }, + { + "name": "signalMaskInit", + "ms": 1.159582 + }, + { + "name": "entrypointLookup", + "ms": 0.004668 + }, + { + "name": "wasi.start", + "ms": 0.030423 + }, + { + "name": "Store.teardown", + "ms": 0.022071 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56755, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957792768, + "minorFaults": 56772, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56772, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 1251.2706390000021, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012617, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 5.491859, + "phases": [ + { + "name": "Engine", + "ms": 0.001894 + }, + { + "name": "canonicalPreopens", + "ms": 0.14543499999999998 + }, + { + "name": "moduleRead", + "ms": 3.616097 + }, + { + "name": "profileValidation", + "ms": 0.228891 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003626 + }, + { + "name": "Linker", + "ms": 0.193938 + }, + { + "name": "Store", + "ms": 0.016661 + }, + { + "name": "Instance", + "ms": 0.037789 + }, + { + "name": "signalMaskInit", + "ms": 0.574045 + }, + { + "name": "entrypointLookup", + "ms": 0.007913 + }, + { + "name": "wasi.start", + "ms": 0.563396 + }, + { + "name": "Store.teardown", + "ms": 0.019294 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56772, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 8319868928, + "minorFaults": 56789, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957780480, + "minorFaults": 56789, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 1586.4033530000015, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017814, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 5.1694439999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.002452 + }, + { + "name": "canonicalPreopens", + "ms": 0.18720900000000001 + }, + { + "name": "moduleRead", + "ms": 2.274387 + }, + { + "name": "profileValidation", + "ms": 0.282852 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.005403 + }, + { + "name": "Linker", + "ms": 0.210053 + }, + { + "name": "Store", + "ms": 0.015437000000000001 + }, + { + "name": "Instance", + "ms": 0.875718 + }, + { + "name": "signalMaskInit", + "ms": 1.1668850000000002 + }, + { + "name": "entrypointLookup", + "ms": 0.00591 + }, + { + "name": "wasi.start", + "ms": 0.024697 + }, + { + "name": "Store.teardown", + "ms": 0.021924 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957780480, + "minorFaults": 56789, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957792768, + "minorFaults": 56806, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56806, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 1336.6324569999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.020026000000000002, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.740638000000001, + "phases": [ + { + "name": "Engine", + "ms": 0.002415 + }, + { + "name": "canonicalPreopens", + "ms": 0.128829 + }, + { + "name": "moduleRead", + "ms": 3.423451 + }, + { + "name": "profileValidation", + "ms": 0.224277 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00367 + }, + { + "name": "Linker", + "ms": 0.214164 + }, + { + "name": "Store", + "ms": 0.024994000000000002 + }, + { + "name": "Instance", + "ms": 0.22109800000000002 + }, + { + "name": "signalMaskInit", + "ms": 0.371105 + }, + { + "name": "entrypointLookup", + "ms": 0.0026939999999999998 + }, + { + "name": "wasi.start", + "ms": 0.021845 + }, + { + "name": "Store.teardown", + "ms": 0.016290000000000002 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56806, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957792768, + "minorFaults": 56823, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254397440, + "virtualBytes": 3957780480, + "minorFaults": 56823, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1124.7428459999974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016328, + "firstGuestHostCallMs": 1061.508306, + "firstOutputMs": 1108.869899, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1109.0353340000001, + "phases": [ + { + "name": "Engine", + "ms": 0.004245 + }, + { + "name": "canonicalPreopens", + "ms": 0.10760800000000001 + }, + { + "name": "moduleRead", + "ms": 6.634766 + }, + { + "name": "profileValidation", + "ms": 3.883921 + }, + { + "name": "moduleCompile", + "ms": 1048.884603 + }, + { + "name": "importValidation", + "ms": 0.007522 + }, + { + "name": "Linker", + "ms": 0.184113 + }, + { + "name": "Store", + "ms": 0.018465 + }, + { + "name": "Instance", + "ms": 0.950882 + }, + { + "name": "signalMaskInit", + "ms": 0.060176 + }, + { + "name": "entrypointLookup", + "ms": 0.005346 + }, + { + "name": "wasi.start", + "ms": 47.604285000000004 + }, + { + "name": "Store.teardown", + "ms": 0.051248999999999996 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251830272, + "peakRssBytes": 252014592, + "pssBytes": 254398464, + "virtualBytes": 3957780480, + "minorFaults": 56823, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290689024, + "peakRssBytes": 290816000, + "pssBytes": 293396480, + "virtualBytes": 8327684096, + "minorFaults": 63641, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63641, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 46532, + "wasmtimeProcessRetainedRssBytes": 251830272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 78.23481399999946, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.007637, + "firstGuestHostCallMs": 11.551303, + "firstOutputMs": 59.102554000000005, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.639654, + "phases": [ + { + "name": "Engine", + "ms": 0.001531 + }, + { + "name": "canonicalPreopens", + "ms": 0.103702 + }, + { + "name": "moduleRead", + "ms": 6.548881000000001 + }, + { + "name": "profileValidation", + "ms": 3.844878 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006933000000000001 + }, + { + "name": "Linker", + "ms": 0.19606800000000002 + }, + { + "name": "Store", + "ms": 0.014416 + }, + { + "name": "Instance", + "ms": 0.042204 + }, + { + "name": "signalMaskInit", + "ms": 0.045578 + }, + { + "name": "entrypointLookup", + "ms": 0.002775 + }, + { + "name": "wasi.start", + "ms": 47.802757 + }, + { + "name": "Store.teardown", + "ms": 1.382968 + } + ] + }, + "memory": { + "start": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63641, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290689024, + "peakRssBytes": 290816000, + "pssBytes": 293404672, + "virtualBytes": 8327684096, + "minorFaults": 63710, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63710, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 82.85998599999948, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013369, + "firstGuestHostCallMs": 11.382633, + "firstOutputMs": 64.936493, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 65.143438, + "phases": [ + { + "name": "Engine", + "ms": 0.0020729999999999998 + }, + { + "name": "canonicalPreopens", + "ms": 0.148362 + }, + { + "name": "moduleRead", + "ms": 6.2666520000000006 + }, + { + "name": "profileValidation", + "ms": 3.9174420000000003 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007718 + }, + { + "name": "Linker", + "ms": 0.19852699999999998 + }, + { + "name": "Store", + "ms": 0.016405999999999997 + }, + { + "name": "Instance", + "ms": 0.03468 + }, + { + "name": "signalMaskInit", + "ms": 0.046298000000000006 + }, + { + "name": "entrypointLookup", + "ms": 0.0028339999999999997 + }, + { + "name": "wasi.start", + "ms": 53.846704 + }, + { + "name": "Store.teardown", + "ms": 0.050155 + } + ] + }, + "memory": { + "start": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63710, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290689024, + "peakRssBytes": 290816000, + "pssBytes": 293404672, + "virtualBytes": 8327684096, + "minorFaults": 63779, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292789248, + "virtualBytes": 3963228160, + "minorFaults": 63779, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 86.26025200000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018510000000000002, + "firstGuestHostCallMs": 11.976538, + "firstOutputMs": 67.658539, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 68.91123400000001, + "phases": [ + { + "name": "Engine", + "ms": 0.00435 + }, + { + "name": "canonicalPreopens", + "ms": 0.10670099999999999 + }, + { + "name": "moduleRead", + "ms": 6.531972 + }, + { + "name": "profileValidation", + "ms": 4.2227380000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007377 + }, + { + "name": "Linker", + "ms": 0.19972199999999998 + }, + { + "name": "Store", + "ms": 0.019344 + }, + { + "name": "Instance", + "ms": 0.041643 + }, + { + "name": "signalMaskInit", + "ms": 0.08043800000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.004815 + }, + { + "name": "wasi.start", + "ms": 55.970748 + }, + { + "name": "Store.teardown", + "ms": 1.105901 + } + ] + }, + "memory": { + "start": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292789248, + "virtualBytes": 3963228160, + "minorFaults": 63779, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290689024, + "peakRssBytes": 290816000, + "pssBytes": 293404672, + "virtualBytes": 8327684096, + "minorFaults": 63848, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292789248, + "virtualBytes": 3963228160, + "minorFaults": 63848, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 81.29399299999932, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019625, + "firstGuestHostCallMs": 12.437336, + "firstOutputMs": 63.053554, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 63.692968, + "phases": [ + { + "name": "Engine", + "ms": 0.0030800000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.17827 + }, + { + "name": "moduleRead", + "ms": 7.246903 + }, + { + "name": "profileValidation", + "ms": 3.8814230000000003 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007282 + }, + { + "name": "Linker", + "ms": 0.207531 + }, + { + "name": "Store", + "ms": 0.016937 + }, + { + "name": "Instance", + "ms": 0.041381999999999995 + }, + { + "name": "signalMaskInit", + "ms": 0.052983999999999996 + }, + { + "name": "entrypointLookup", + "ms": 0.004219 + }, + { + "name": "wasi.start", + "ms": 50.905937 + }, + { + "name": "Store.teardown", + "ms": 0.531018 + } + ] + }, + "memory": { + "start": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292789248, + "virtualBytes": 3963228160, + "minorFaults": 63848, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290689024, + "peakRssBytes": 290816000, + "pssBytes": 293404672, + "virtualBytes": 8327684096, + "minorFaults": 63917, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63917, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3795.777968000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012696, + "firstGuestHostCallMs": 3393.193308, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3777.901216, + "phases": [ + { + "name": "Engine", + "ms": 0.00188 + }, + { + "name": "canonicalPreopens", + "ms": 0.107557 + }, + { + "name": "moduleRead", + "ms": 11.439091 + }, + { + "name": "profileValidation", + "ms": 12.523351 + }, + { + "name": "moduleCompile", + "ms": 3367.1836810000004 + }, + { + "name": "importValidation", + "ms": 0.008178 + }, + { + "name": "Linker", + "ms": 0.194549 + }, + { + "name": "Store", + "ms": 0.017093 + }, + { + "name": "Instance", + "ms": 0.214475 + }, + { + "name": "signalMaskInit", + "ms": 0.079719 + }, + { + "name": "entrypointLookup", + "ms": 0.004699 + }, + { + "name": "wasi.start", + "ms": 384.62218199999995 + }, + { + "name": "Store.teardown", + "ms": 0.056703 + } + ] + }, + "memory": { + "start": { + "rssBytes": 290091008, + "peakRssBytes": 290816000, + "pssBytes": 292790272, + "virtualBytes": 3963228160, + "minorFaults": 63917, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402534400, + "peakRssBytes": 402563072, + "pssBytes": 405364736, + "virtualBytes": 12846841856, + "minorFaults": 81368, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81368, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1095417, + "wasmtimeProcessRetainedRssBytes": 290091008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 72.93382199999905, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013221, + "firstGuestHostCallMs": 25.834743, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.031023000000005, + "phases": [ + { + "name": "Engine", + "ms": 0.002173 + }, + { + "name": "canonicalPreopens", + "ms": 0.113479 + }, + { + "name": "moduleRead", + "ms": 11.456417 + }, + { + "name": "profileValidation", + "ms": 12.344904999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010192 + }, + { + "name": "Linker", + "ms": 0.214815 + }, + { + "name": "Store", + "ms": 0.017332 + }, + { + "name": "Instance", + "ms": 0.165403 + }, + { + "name": "signalMaskInit", + "ms": 0.083807 + }, + { + "name": "entrypointLookup", + "ms": 0.003925 + }, + { + "name": "wasi.start", + "ms": 26.145743 + }, + { + "name": "Store.teardown", + "ms": 0.043008 + } + ] + }, + "memory": { + "start": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81368, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 371265536, + "virtualBytes": 12846841856, + "minorFaults": 81448, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81448, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 104.91694800000187, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017025000000000002, + "firstGuestHostCallMs": 32.341138, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 75.90491300000001, + "phases": [ + { + "name": "Engine", + "ms": 0.002131 + }, + { + "name": "canonicalPreopens", + "ms": 0.144797 + }, + { + "name": "moduleRead", + "ms": 12.657837 + }, + { + "name": "profileValidation", + "ms": 16.407049999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.01292 + }, + { + "name": "Linker", + "ms": 0.227029 + }, + { + "name": "Store", + "ms": 0.021662 + }, + { + "name": "Instance", + "ms": 1.213704 + }, + { + "name": "signalMaskInit", + "ms": 0.171128 + }, + { + "name": "entrypointLookup", + "ms": 0.00933 + }, + { + "name": "wasi.start", + "ms": 43.51650600000001 + }, + { + "name": "Store.teardown", + "ms": 0.058765 + } + ] + }, + "memory": { + "start": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81448, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 371268608, + "virtualBytes": 12846841856, + "minorFaults": 81528, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370461696, + "virtualBytes": 4117929984, + "minorFaults": 81528, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 84.50312599999961, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01715, + "firstGuestHostCallMs": 26.05142, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 64.727763, + "phases": [ + { + "name": "Engine", + "ms": 0.001902 + }, + { + "name": "canonicalPreopens", + "ms": 0.165409 + }, + { + "name": "moduleRead", + "ms": 11.121178 + }, + { + "name": "profileValidation", + "ms": 12.825125 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010856000000000001 + }, + { + "name": "Linker", + "ms": 0.21124199999999999 + }, + { + "name": "Store", + "ms": 0.017666 + }, + { + "name": "Instance", + "ms": 0.198411 + }, + { + "name": "signalMaskInit", + "ms": 0.071546 + }, + { + "name": "entrypointLookup", + "ms": 0.0037790000000000002 + }, + { + "name": "wasi.start", + "ms": 37.829837999999995 + }, + { + "name": "Store.teardown", + "ms": 0.8352010000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370461696, + "virtualBytes": 4117929984, + "minorFaults": 81528, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 371265536, + "virtualBytes": 12846841856, + "minorFaults": 81608, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370461696, + "virtualBytes": 4117929984, + "minorFaults": 81608, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 72.26765800000067, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017858000000000002, + "firstGuestHostCallMs": 26.149328, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.482522, + "phases": [ + { + "name": "Engine", + "ms": 0.0048720000000000005 + }, + { + "name": "canonicalPreopens", + "ms": 0.10901999999999999 + }, + { + "name": "moduleRead", + "ms": 11.718914 + }, + { + "name": "profileValidation", + "ms": 12.471981999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009758000000000001 + }, + { + "name": "Linker", + "ms": 0.216997 + }, + { + "name": "Store", + "ms": 0.024301999999999997 + }, + { + "name": "Instance", + "ms": 0.060159 + }, + { + "name": "signalMaskInit", + "ms": 0.085983 + }, + { + "name": "entrypointLookup", + "ms": 0.0038900000000000002 + }, + { + "name": "wasi.start", + "ms": 25.305093 + }, + { + "name": "Store.teardown", + "ms": 0.044338 + } + ] + }, + "memory": { + "start": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370461696, + "virtualBytes": 4117929984, + "minorFaults": 81608, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 371273728, + "virtualBytes": 12846841856, + "minorFaults": 81688, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81688, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1576.7306449999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.024142, + "firstGuestHostCallMs": 1552.747485, + "firstOutputMs": 1557.6860550000001, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1557.954826, + "phases": [ + { + "name": "Engine", + "ms": 0.00178 + }, + { + "name": "canonicalPreopens", + "ms": 0.114755 + }, + { + "name": "moduleRead", + "ms": 6.860246 + }, + { + "name": "profileValidation", + "ms": 6.44599 + }, + { + "name": "moduleCompile", + "ms": 1538.138361 + }, + { + "name": "importValidation", + "ms": 0.010327 + }, + { + "name": "Linker", + "ms": 0.183435 + }, + { + "name": "Store", + "ms": 0.018148 + }, + { + "name": "Instance", + "ms": 0.128397 + }, + { + "name": "signalMaskInit", + "ms": 0.06425700000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.003143 + }, + { + "name": "wasi.start", + "ms": 5.133499 + }, + { + "name": "Store.teardown", + "ms": 0.044094999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 366706688, + "peakRssBytes": 402563072, + "pssBytes": 370462720, + "virtualBytes": 4117929984, + "minorFaults": 81688, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374370304, + "peakRssBytes": 402563072, + "pssBytes": 378585088, + "virtualBytes": 8489783296, + "minorFaults": 82062, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378585088, + "virtualBytes": 4125327360, + "minorFaults": 82062, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4822829, + "wasmtimeProcessRetainedRssBytes": 366706688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 37.231541000001016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009952, + "firstGuestHostCallMs": 14.628169999999999, + "firstOutputMs": 20.179352, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 20.452526, + "phases": [ + { + "name": "Engine", + "ms": 0.00185 + }, + { + "name": "canonicalPreopens", + "ms": 0.157437 + }, + { + "name": "moduleRead", + "ms": 6.756956 + }, + { + "name": "profileValidation", + "ms": 6.3736749999999995 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011679 + }, + { + "name": "Linker", + "ms": 0.20787 + }, + { + "name": "Store", + "ms": 0.019066 + }, + { + "name": "Instance", + "ms": 0.249052 + }, + { + "name": "signalMaskInit", + "ms": 0.086079 + }, + { + "name": "entrypointLookup", + "ms": 0.0028729999999999997 + }, + { + "name": "wasi.start", + "ms": 5.773434 + }, + { + "name": "Store.teardown", + "ms": 0.038797 + } + ] + }, + "memory": { + "start": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378585088, + "virtualBytes": 4125327360, + "minorFaults": 82062, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 379109376, + "virtualBytes": 8489783296, + "minorFaults": 82111, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378605568, + "virtualBytes": 4125327360, + "minorFaults": 82111, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 43.12155999999959, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014445, + "firstGuestHostCallMs": 14.538836, + "firstOutputMs": 21.083975000000002, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 21.36503, + "phases": [ + { + "name": "Engine", + "ms": 0.0017439999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.146638 + }, + { + "name": "moduleRead", + "ms": 6.796615 + }, + { + "name": "profileValidation", + "ms": 6.368701 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011654000000000001 + }, + { + "name": "Linker", + "ms": 0.197826 + }, + { + "name": "Store", + "ms": 0.017739 + }, + { + "name": "Instance", + "ms": 0.18695499999999998 + }, + { + "name": "signalMaskInit", + "ms": 0.051441 + }, + { + "name": "entrypointLookup", + "ms": 0.002982 + }, + { + "name": "wasi.start", + "ms": 6.771829 + }, + { + "name": "Store.teardown", + "ms": 0.042896 + } + ] + }, + "memory": { + "start": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378605568, + "virtualBytes": 4125327360, + "minorFaults": 82111, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378694656, + "virtualBytes": 8489783296, + "minorFaults": 82162, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378694656, + "virtualBytes": 4125327360, + "minorFaults": 82162, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 43.51996599999984, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014998, + "firstGuestHostCallMs": 17.10951, + "firstOutputMs": 23.284587, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 25.365605, + "phases": [ + { + "name": "Engine", + "ms": 0.001893 + }, + { + "name": "canonicalPreopens", + "ms": 0.16628500000000002 + }, + { + "name": "moduleRead", + "ms": 6.465062 + }, + { + "name": "profileValidation", + "ms": 9.356294 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012204999999999999 + }, + { + "name": "Linker", + "ms": 0.207336 + }, + { + "name": "Store", + "ms": 0.016658 + }, + { + "name": "Instance", + "ms": 0.0425 + }, + { + "name": "signalMaskInit", + "ms": 0.08383700000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.003026 + }, + { + "name": "wasi.start", + "ms": 6.414785 + }, + { + "name": "Store.teardown", + "ms": 1.791828 + } + ] + }, + "memory": { + "start": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378695680, + "virtualBytes": 4125327360, + "minorFaults": 82162, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 379179008, + "virtualBytes": 8489783296, + "minorFaults": 82206, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378694656, + "virtualBytes": 4125327360, + "minorFaults": 82206, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 44.15762099999847, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01703, + "firstGuestHostCallMs": 15.786297000000001, + "firstOutputMs": 22.440211, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.686424, + "phases": [ + { + "name": "Engine", + "ms": 0.001632 + }, + { + "name": "canonicalPreopens", + "ms": 0.119492 + }, + { + "name": "moduleRead", + "ms": 7.3562080000000005 + }, + { + "name": "profileValidation", + "ms": 6.445315 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013699000000000001 + }, + { + "name": "Linker", + "ms": 0.208089 + }, + { + "name": "Store", + "ms": 0.01718 + }, + { + "name": "Instance", + "ms": 0.782356 + }, + { + "name": "signalMaskInit", + "ms": 0.062974 + }, + { + "name": "entrypointLookup", + "ms": 0.003703 + }, + { + "name": "wasi.start", + "ms": 6.869784999999999 + }, + { + "name": "Store.teardown", + "ms": 0.032755 + } + ] + }, + "memory": { + "start": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378694656, + "virtualBytes": 4125327360, + "minorFaults": 82206, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 379202560, + "virtualBytes": 8489783296, + "minorFaults": 82252, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378702848, + "virtualBytes": 4125327360, + "minorFaults": 82252, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1259.5405269999974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.026413, + "firstGuestHostCallMs": 1240.326054, + "firstOutputMs": 1242.406992, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1243.0985480000002, + "phases": [ + { + "name": "Engine", + "ms": 0.002378 + }, + { + "name": "canonicalPreopens", + "ms": 0.138906 + }, + { + "name": "moduleRead", + "ms": 5.283956 + }, + { + "name": "profileValidation", + "ms": 7.443357000000001 + }, + { + "name": "moduleCompile", + "ms": 1226.6817720000001 + }, + { + "name": "importValidation", + "ms": 0.00824 + }, + { + "name": "Linker", + "ms": 0.18231 + }, + { + "name": "Store", + "ms": 0.015856 + }, + { + "name": "Instance", + "ms": 0.072437 + }, + { + "name": "signalMaskInit", + "ms": 0.050062 + }, + { + "name": "entrypointLookup", + "ms": 0.003189 + }, + { + "name": "wasi.start", + "ms": 2.148416 + }, + { + "name": "Store.teardown", + "ms": 0.569563 + } + ] + }, + "memory": { + "start": { + "rssBytes": 374136832, + "peakRssBytes": 402563072, + "pssBytes": 378702848, + "virtualBytes": 4125327360, + "minorFaults": 82252, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 379740160, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 8495251456, + "minorFaults": 83151, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83151, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6360967, + "wasmtimeProcessRetainedRssBytes": 374136832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 30.554088000000775, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.055433, + "firstGuestHostCallMs": 11.984889, + "firstOutputMs": 13.923414000000001, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 15.308697, + "phases": [ + { + "name": "Engine", + "ms": 0.003029 + }, + { + "name": "canonicalPreopens", + "ms": 0.121433 + }, + { + "name": "moduleRead", + "ms": 5.168081 + }, + { + "name": "profileValidation", + "ms": 4.8841730000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011154 + }, + { + "name": "Linker", + "ms": 0.221547 + }, + { + "name": "Store", + "ms": 0.015455 + }, + { + "name": "Instance", + "ms": 1.023467 + }, + { + "name": "signalMaskInit", + "ms": 0.031403 + }, + { + "name": "entrypointLookup", + "ms": 0.004493 + }, + { + "name": "wasi.start", + "ms": 2.0292399999999997 + }, + { + "name": "Store.teardown", + "ms": 1.27677 + } + ] + }, + "memory": { + "start": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83151, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 381702144, + "peakRssBytes": 402563072, + "pssBytes": 384225280, + "virtualBytes": 8495251456, + "minorFaults": 83196, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83196, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 32.13724799999909, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013348, + "firstGuestHostCallMs": 10.886108, + "firstOutputMs": 12.779648, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 12.904636, + "phases": [ + { + "name": "Engine", + "ms": 0.002039 + }, + { + "name": "canonicalPreopens", + "ms": 0.10202 + }, + { + "name": "moduleRead", + "ms": 5.154426999999999 + }, + { + "name": "profileValidation", + "ms": 4.849709 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010469 + }, + { + "name": "Linker", + "ms": 0.20843899999999999 + }, + { + "name": "Store", + "ms": 0.017898 + }, + { + "name": "Instance", + "ms": 0.04786 + }, + { + "name": "signalMaskInit", + "ms": 0.05564 + }, + { + "name": "entrypointLookup", + "ms": 0.002807 + }, + { + "name": "wasi.start", + "ms": 1.955994 + }, + { + "name": "Store.teardown", + "ms": 0.036592 + } + ] + }, + "memory": { + "start": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83196, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384225280, + "virtualBytes": 4130807808, + "minorFaults": 83241, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83241, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 31.011916999999812, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01815, + "firstGuestHostCallMs": 11.532642, + "firstOutputMs": 14.212207, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 15.306528, + "phases": [ + { + "name": "Engine", + "ms": 0.002059 + }, + { + "name": "canonicalPreopens", + "ms": 0.106028 + }, + { + "name": "moduleRead", + "ms": 5.290444 + }, + { + "name": "profileValidation", + "ms": 4.85742 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009807 + }, + { + "name": "Linker", + "ms": 0.205369 + }, + { + "name": "Store", + "ms": 0.016007 + }, + { + "name": "Instance", + "ms": 0.526057 + }, + { + "name": "signalMaskInit", + "ms": 0.073899 + }, + { + "name": "entrypointLookup", + "ms": 0.00378 + }, + { + "name": "wasi.start", + "ms": 2.746105 + }, + { + "name": "Store.teardown", + "ms": 0.9790440000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83241, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 381702144, + "peakRssBytes": 402563072, + "pssBytes": 384225280, + "virtualBytes": 8495251456, + "minorFaults": 83286, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83286, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 33.786996999999246, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010386, + "firstGuestHostCallMs": 10.940913, + "firstOutputMs": 12.888109, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.013050999999999, + "phases": [ + { + "name": "Engine", + "ms": 0.0018139999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.104883 + }, + { + "name": "moduleRead", + "ms": 5.1910799999999995 + }, + { + "name": "profileValidation", + "ms": 4.8371070000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008834999999999999 + }, + { + "name": "Linker", + "ms": 0.205214 + }, + { + "name": "Store", + "ms": 0.018691000000000003 + }, + { + "name": "Instance", + "ms": 0.091408 + }, + { + "name": "signalMaskInit", + "ms": 0.049530000000000005 + }, + { + "name": "entrypointLookup", + "ms": 0.002868 + }, + { + "name": "wasi.start", + "ms": 2.006271 + }, + { + "name": "Store.teardown", + "ms": 0.036646 + } + ] + }, + "memory": { + "start": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384172032, + "virtualBytes": 4130795520, + "minorFaults": 83286, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384225280, + "virtualBytes": 4130807808, + "minorFaults": 83331, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384171008, + "virtualBytes": 4130795520, + "minorFaults": 83331, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3657.8598960000018, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.027126, + "firstGuestHostCallMs": 3630.178667, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3639.43458, + "phases": [ + { + "name": "Engine", + "ms": 0.003003 + }, + { + "name": "canonicalPreopens", + "ms": 0.182014 + }, + { + "name": "moduleRead", + "ms": 10.366237 + }, + { + "name": "profileValidation", + "ms": 14.986934999999999 + }, + { + "name": "moduleCompile", + "ms": 3602.553933 + }, + { + "name": "importValidation", + "ms": 0.014263 + }, + { + "name": "Linker", + "ms": 0.221169 + }, + { + "name": "Store", + "ms": 0.027171 + }, + { + "name": "Instance", + "ms": 0.234337 + }, + { + "name": "signalMaskInit", + "ms": 0.127839 + }, + { + "name": "entrypointLookup", + "ms": 0.005123 + }, + { + "name": "wasi.start", + "ms": 9.130937000000001 + }, + { + "name": "Store.teardown", + "ms": 0.184101 + } + ] + }, + "memory": { + "start": { + "rssBytes": 379604992, + "peakRssBytes": 402563072, + "pssBytes": 384171008, + "virtualBytes": 4130795520, + "minorFaults": 83331, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417447936, + "peakRssBytes": 417853440, + "pssBytes": 422349824, + "virtualBytes": 8511668224, + "minorFaults": 85965, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 85965, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7587649, + "wasmtimeProcessRetainedRssBytes": 379604992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 61.33905300000333, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.046305, + "firstGuestHostCallMs": 28.496993000000003, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 40.573258, + "phases": [ + { + "name": "Engine", + "ms": 0.00196 + }, + { + "name": "canonicalPreopens", + "ms": 0.10951999999999999 + }, + { + "name": "moduleRead", + "ms": 11.147934999999999 + }, + { + "name": "profileValidation", + "ms": 14.834339 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013940000000000001 + }, + { + "name": "Linker", + "ms": 0.207594 + }, + { + "name": "Store", + "ms": 0.017113 + }, + { + "name": "Instance", + "ms": 0.6997140000000001 + }, + { + "name": "signalMaskInit", + "ms": 0.06952499999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.00362 + }, + { + "name": "wasi.start", + "ms": 12.044228 + }, + { + "name": "Store.teardown", + "ms": 0.048964 + } + ] + }, + "memory": { + "start": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 85965, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417673216, + "peakRssBytes": 417853440, + "pssBytes": 422334464, + "virtualBytes": 8511668224, + "minorFaults": 86057, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86057, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 68.08935799999745, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.037327, + "firstGuestHostCallMs": 28.847617999999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 49.743404, + "phases": [ + { + "name": "Engine", + "ms": 0.002053 + }, + { + "name": "canonicalPreopens", + "ms": 0.114009 + }, + { + "name": "moduleRead", + "ms": 11.522437 + }, + { + "name": "profileValidation", + "ms": 14.801686 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.017720999999999997 + }, + { + "name": "Linker", + "ms": 0.239096 + }, + { + "name": "Store", + "ms": 0.019154 + }, + { + "name": "Instance", + "ms": 0.6583840000000001 + }, + { + "name": "signalMaskInit", + "ms": 0.072448 + }, + { + "name": "entrypointLookup", + "ms": 0.003801 + }, + { + "name": "wasi.start", + "ms": 20.304040999999998 + }, + { + "name": "Store.teardown", + "ms": 0.578925 + } + ] + }, + "memory": { + "start": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86057, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417673216, + "peakRssBytes": 417853440, + "pssBytes": 422318080, + "virtualBytes": 8511668224, + "minorFaults": 86149, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86149, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 67.42290700000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014248, + "firstGuestHostCallMs": 28.515314, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.152271000000006, + "phases": [ + { + "name": "Engine", + "ms": 0.001505 + }, + { + "name": "canonicalPreopens", + "ms": 0.11147800000000001 + }, + { + "name": "moduleRead", + "ms": 11.318063 + }, + { + "name": "profileValidation", + "ms": 14.618831 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013841000000000001 + }, + { + "name": "Linker", + "ms": 0.22315200000000002 + }, + { + "name": "Store", + "ms": 0.020041 + }, + { + "name": "Instance", + "ms": 0.801594 + }, + { + "name": "signalMaskInit", + "ms": 0.053794 + }, + { + "name": "entrypointLookup", + "ms": 0.0036509999999999997 + }, + { + "name": "wasi.start", + "ms": 18.963081 + }, + { + "name": "Store.teardown", + "ms": 0.680912 + } + ] + }, + "memory": { + "start": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86149, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417673216, + "peakRssBytes": 417853440, + "pssBytes": 422318080, + "virtualBytes": 8511668224, + "minorFaults": 86241, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86241, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 66.88658299999952, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011387, + "firstGuestHostCallMs": 29.120886, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.325106999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.001543 + }, + { + "name": "canonicalPreopens", + "ms": 0.13368200000000002 + }, + { + "name": "moduleRead", + "ms": 11.337175 + }, + { + "name": "profileValidation", + "ms": 14.599274000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.018767 + }, + { + "name": "Linker", + "ms": 0.253212 + }, + { + "name": "Store", + "ms": 0.023176 + }, + { + "name": "Instance", + "ms": 1.313951 + }, + { + "name": "signalMaskInit", + "ms": 0.07313499999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.0041860000000000005 + }, + { + "name": "wasi.start", + "ms": 18.58893 + }, + { + "name": "Store.teardown", + "ms": 0.623773 + } + ] + }, + "memory": { + "start": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86241, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417673216, + "peakRssBytes": 417853440, + "pssBytes": 422318080, + "virtualBytes": 8511668224, + "minorFaults": 86333, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86333, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3915.2825840000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018409, + "firstGuestHostCallMs": 3892.997982, + "firstOutputMs": 3893.70391, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3893.908436, + "phases": [ + { + "name": "Engine", + "ms": 0.00212 + }, + { + "name": "canonicalPreopens", + "ms": 0.109275 + }, + { + "name": "moduleRead", + "ms": 12.716283 + }, + { + "name": "profileValidation", + "ms": 16.040612 + }, + { + "name": "moduleCompile", + "ms": 3862.015511 + }, + { + "name": "importValidation", + "ms": 0.021248 + }, + { + "name": "Linker", + "ms": 0.217808 + }, + { + "name": "Store", + "ms": 0.017920000000000002 + }, + { + "name": "Instance", + "ms": 0.203038 + }, + { + "name": "signalMaskInit", + "ms": 0.083246 + }, + { + "name": "entrypointLookup", + "ms": 0.003903 + }, + { + "name": "wasi.start", + "ms": 0.84733 + }, + { + "name": "Store.teardown", + "ms": 0.038488999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 417128448, + "peakRssBytes": 417853440, + "pssBytes": 421695488, + "virtualBytes": 4147212288, + "minorFaults": 86333, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463466496, + "virtualBytes": 4165820416, + "minorFaults": 94046, + "majorFaults": 0 + }, + "end": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94046, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11190203, + "wasmtimeProcessRetainedRssBytes": 417128448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 54.4134269999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.053399999999999996, + "firstGuestHostCallMs": 31.363006, + "firstOutputMs": 32.181767, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 34.196902, + "phases": [ + { + "name": "Engine", + "ms": 0.00185 + }, + { + "name": "canonicalPreopens", + "ms": 0.105996 + }, + { + "name": "moduleRead", + "ms": 13.104493999999999 + }, + { + "name": "profileValidation", + "ms": 16.182724 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015872 + }, + { + "name": "Linker", + "ms": 0.218911 + }, + { + "name": "Store", + "ms": 0.017964 + }, + { + "name": "Instance", + "ms": 0.047303 + }, + { + "name": "signalMaskInit", + "ms": 0.054543 + }, + { + "name": "entrypointLookup", + "ms": 0.0036959999999999996 + }, + { + "name": "wasi.start", + "ms": 1.004823 + }, + { + "name": "Store.teardown", + "ms": 1.767368 + } + ] + }, + "memory": { + "start": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94046, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463519744, + "virtualBytes": 8530264064, + "minorFaults": 94086, + "majorFaults": 0 + }, + "end": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94086, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 62.07325200000196, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.025736, + "firstGuestHostCallMs": 31.247548, + "firstOutputMs": 32.090654, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 32.347820999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.0022570000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.145104 + }, + { + "name": "moduleRead", + "ms": 12.912056999999999 + }, + { + "name": "profileValidation", + "ms": 16.013036 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.017227999999999997 + }, + { + "name": "Linker", + "ms": 0.21460500000000002 + }, + { + "name": "Store", + "ms": 0.017811 + }, + { + "name": "Instance", + "ms": 0.05136 + }, + { + "name": "signalMaskInit", + "ms": 0.055899000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.003502 + }, + { + "name": "wasi.start", + "ms": 1.256423 + }, + { + "name": "Store.teardown", + "ms": 0.044518 + } + ] + }, + "memory": { + "start": { + "rssBytes": 458899456, + "peakRssBytes": 459612160, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94086, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463519744, + "virtualBytes": 4165820416, + "minorFaults": 94126, + "majorFaults": 0 + }, + "end": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94126, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 59.127190999999584, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021773, + "firstGuestHostCallMs": 31.698695, + "firstOutputMs": 32.495598, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 32.685308000000006, + "phases": [ + { + "name": "Engine", + "ms": 0.0024 + }, + { + "name": "canonicalPreopens", + "ms": 0.126084 + }, + { + "name": "moduleRead", + "ms": 12.748461 + }, + { + "name": "profileValidation", + "ms": 15.916625999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015579 + }, + { + "name": "Linker", + "ms": 0.221053 + }, + { + "name": "Store", + "ms": 0.017507 + }, + { + "name": "Instance", + "ms": 0.772464 + }, + { + "name": "signalMaskInit", + "ms": 0.060207000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.003603 + }, + { + "name": "wasi.start", + "ms": 1.184288 + }, + { + "name": "Store.teardown", + "ms": 0.035535 + } + ] + }, + "memory": { + "start": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94126, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463519744, + "virtualBytes": 4165820416, + "minorFaults": 94166, + "majorFaults": 0 + }, + "end": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94166, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 58.004785999997694, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023132, + "firstGuestHostCallMs": 32.308884000000006, + "firstOutputMs": 33.415071, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 35.199652, + "phases": [ + { + "name": "Engine", + "ms": 0.006376 + }, + { + "name": "canonicalPreopens", + "ms": 0.129889 + }, + { + "name": "moduleRead", + "ms": 12.857828 + }, + { + "name": "profileValidation", + "ms": 17.089573 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015143 + }, + { + "name": "Linker", + "ms": 0.21824600000000002 + }, + { + "name": "Store", + "ms": 0.017720999999999997 + }, + { + "name": "Instance", + "ms": 0.052632 + }, + { + "name": "signalMaskInit", + "ms": 0.068241 + }, + { + "name": "entrypointLookup", + "ms": 0.0029289999999999997 + }, + { + "name": "wasi.start", + "ms": 1.540336 + }, + { + "name": "Store.teardown", + "ms": 1.5709140000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94166, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 460996608, + "peakRssBytes": 461582336, + "pssBytes": 463519744, + "virtualBytes": 8530264064, + "minorFaults": 94206, + "majorFaults": 0 + }, + "end": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94206, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 703.8645800000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.024147000000000002, + "firstGuestHostCallMs": 637.994198, + "firstOutputMs": 682.268669, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 682.460022, + "phases": [ + { + "name": "Engine", + "ms": 0.0014550000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.14455300000000001 + }, + { + "name": "moduleRead", + "ms": 5.964077 + }, + { + "name": "profileValidation", + "ms": 2.5592319999999997 + }, + { + "name": "moduleCompile", + "ms": 627.9780350000001 + }, + { + "name": "importValidation", + "ms": 0.007736 + }, + { + "name": "Linker", + "ms": 0.186486 + }, + { + "name": "Store", + "ms": 0.019391000000000002 + }, + { + "name": "Instance", + "ms": 0.28867699999999996 + }, + { + "name": "signalMaskInit", + "ms": 0.080349 + }, + { + "name": "entrypointLookup", + "ms": 0.0045390000000000005 + }, + { + "name": "wasi.start", + "ms": 44.465249 + }, + { + "name": "Store.teardown", + "ms": 0.049147 + } + ] + }, + "memory": { + "start": { + "rssBytes": 458899456, + "peakRssBytes": 461582336, + "pssBytes": 463466496, + "virtualBytes": 4165808128, + "minorFaults": 94206, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462966784, + "peakRssBytes": 463261696, + "pssBytes": 467841024, + "virtualBytes": 8534126592, + "minorFaults": 95225, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95225, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15052218, + "wasmtimeProcessRetainedRssBytes": 458899456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 72.94723299999896, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.048605, + "firstGuestHostCallMs": 10.497879999999999, + "firstOutputMs": 52.028798, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.205588, + "phases": [ + { + "name": "Engine", + "ms": 0.0019700000000000004 + }, + { + "name": "canonicalPreopens", + "ms": 0.108936 + }, + { + "name": "moduleRead", + "ms": 6.611573 + }, + { + "name": "profileValidation", + "ms": 2.4618919999999997 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008185 + }, + { + "name": "Linker", + "ms": 0.209928 + }, + { + "name": "Store", + "ms": 0.016596 + }, + { + "name": "Instance", + "ms": 0.266276 + }, + { + "name": "signalMaskInit", + "ms": 0.024406999999999998 + }, + { + "name": "entrypointLookup", + "ms": 0.002915 + }, + { + "name": "wasi.start", + "ms": 41.735661 + }, + { + "name": "Store.teardown", + "ms": 0.037163 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95225, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467718144, + "virtualBytes": 8534126592, + "minorFaults": 95271, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95271, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 68.29262800000288, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016232, + "firstGuestHostCallMs": 10.812624999999999, + "firstOutputMs": 50.748013, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.035719, + "phases": [ + { + "name": "Engine", + "ms": 0.001737 + }, + { + "name": "canonicalPreopens", + "ms": 0.106798 + }, + { + "name": "moduleRead", + "ms": 6.668284 + }, + { + "name": "profileValidation", + "ms": 2.4807230000000002 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007275 + }, + { + "name": "Linker", + "ms": 0.209907 + }, + { + "name": "Store", + "ms": 0.016483 + }, + { + "name": "Instance", + "ms": 0.5151640000000001 + }, + { + "name": "signalMaskInit", + "ms": 0.044981 + }, + { + "name": "entrypointLookup", + "ms": 0.004828 + }, + { + "name": "wasi.start", + "ms": 40.134564 + }, + { + "name": "Store.teardown", + "ms": 0.156711 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95271, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467718144, + "virtualBytes": 8534126592, + "minorFaults": 95317, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95317, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 69.62612400000216, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009507, + "firstGuestHostCallMs": 10.65728, + "firstOutputMs": 51.852059, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.007997, + "phases": [ + { + "name": "Engine", + "ms": 0.001526 + }, + { + "name": "canonicalPreopens", + "ms": 0.10165199999999999 + }, + { + "name": "moduleRead", + "ms": 6.6870389999999995 + }, + { + "name": "profileValidation", + "ms": 2.465608 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008134 + }, + { + "name": "Linker", + "ms": 0.206928 + }, + { + "name": "Store", + "ms": 0.016433999999999997 + }, + { + "name": "Instance", + "ms": 0.356693 + }, + { + "name": "signalMaskInit", + "ms": 0.045947 + }, + { + "name": "entrypointLookup", + "ms": 0.003056 + }, + { + "name": "wasi.start", + "ms": 41.406305999999994 + }, + { + "name": "Store.teardown", + "ms": 0.037814 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95317, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467718144, + "virtualBytes": 8534126592, + "minorFaults": 95363, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95363, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 75.65971600000194, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014688000000000001, + "firstGuestHostCallMs": 11.344003, + "firstOutputMs": 53.630810000000004, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.796012999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.001581 + }, + { + "name": "canonicalPreopens", + "ms": 0.111753 + }, + { + "name": "moduleRead", + "ms": 6.599048 + }, + { + "name": "profileValidation", + "ms": 2.466167 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008671 + }, + { + "name": "Linker", + "ms": 0.22565000000000002 + }, + { + "name": "Store", + "ms": 0.017644 + }, + { + "name": "Instance", + "ms": 1.00731 + }, + { + "name": "signalMaskInit", + "ms": 0.063101 + }, + { + "name": "entrypointLookup", + "ms": 0.007284 + }, + { + "name": "wasi.start", + "ms": 42.57071199999999 + }, + { + "name": "Store.teardown", + "ms": 0.03578799999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95363, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467718144, + "virtualBytes": 8534126592, + "minorFaults": 95409, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95409, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1412.0978959999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.024413, + "firstGuestHostCallMs": 1387.500144, + "firstOutputMs": 1390.9356690000002, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1393.1221200000002, + "phases": [ + { + "name": "Engine", + "ms": 0.0020559999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.105142 + }, + { + "name": "moduleRead", + "ms": 6.777931 + }, + { + "name": "profileValidation", + "ms": 4.487435 + }, + { + "name": "moduleCompile", + "ms": 1374.948169 + }, + { + "name": "importValidation", + "ms": 0.008157 + }, + { + "name": "Linker", + "ms": 0.189802 + }, + { + "name": "Store", + "ms": 0.016905000000000003 + }, + { + "name": "Instance", + "ms": 0.173211 + }, + { + "name": "signalMaskInit", + "ms": 0.04776 + }, + { + "name": "entrypointLookup", + "ms": 0.004542 + }, + { + "name": "wasi.start", + "ms": 5.482611 + }, + { + "name": "Store.teardown", + "ms": 0.06352100000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462761984, + "peakRssBytes": 463261696, + "pssBytes": 467329024, + "virtualBytes": 4169670656, + "minorFaults": 95409, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 510603264, + "peakRssBytes": 510603264, + "pssBytes": 515387392, + "virtualBytes": 8540446720, + "minorFaults": 111021, + "majorFaults": 0 + }, + "end": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475447296, + "virtualBytes": 4175990784, + "minorFaults": 111021, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15680196, + "wasmtimeProcessRetainedRssBytes": 462761984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 38.60826399999496, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.028265000000000002, + "firstGuestHostCallMs": 12.765191999999999, + "firstOutputMs": 16.202755999999997, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.668888, + "phases": [ + { + "name": "Engine", + "ms": 0.0020090000000000004 + }, + { + "name": "canonicalPreopens", + "ms": 0.107037 + }, + { + "name": "moduleRead", + "ms": 7.0349319999999995 + }, + { + "name": "profileValidation", + "ms": 4.531029 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009853 + }, + { + "name": "Linker", + "ms": 0.213304 + }, + { + "name": "Store", + "ms": 0.017139 + }, + { + "name": "Instance", + "ms": 0.039613999999999996 + }, + { + "name": "signalMaskInit", + "ms": 0.063491 + }, + { + "name": "entrypointLookup", + "ms": 0.002732 + }, + { + "name": "wasi.start", + "ms": 4.834636 + }, + { + "name": "Store.teardown", + "ms": 0.045047 + } + ] + }, + "memory": { + "start": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475447296, + "virtualBytes": 4175990784, + "minorFaults": 111021, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475728896, + "virtualBytes": 8540446720, + "minorFaults": 111049, + "majorFaults": 0 + }, + "end": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111049, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 44.58055500000046, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023274, + "firstGuestHostCallMs": 16.764792, + "firstOutputMs": 21.856983, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 24.921938, + "phases": [ + { + "name": "Engine", + "ms": 0.002279 + }, + { + "name": "canonicalPreopens", + "ms": 0.114716 + }, + { + "name": "moduleRead", + "ms": 8.128485 + }, + { + "name": "profileValidation", + "ms": 7.244612999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012313000000000001 + }, + { + "name": "Linker", + "ms": 0.3472 + }, + { + "name": "Store", + "ms": 0.028036000000000002 + }, + { + "name": "Instance", + "ms": 0.055784 + }, + { + "name": "signalMaskInit", + "ms": 0.04908 + }, + { + "name": "entrypointLookup", + "ms": 0.003638 + }, + { + "name": "wasi.start", + "ms": 7.1062449999999995 + }, + { + "name": "Store.teardown", + "ms": 1.0046810000000002 + } + ] + }, + "memory": { + "start": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111049, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475728896, + "virtualBytes": 8540446720, + "minorFaults": 111077, + "majorFaults": 0 + }, + "end": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111077, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 42.81555500000104, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.025814999999999998, + "firstGuestHostCallMs": 15.197968, + "firstOutputMs": 19.712019, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 21.386492999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.002836 + }, + { + "name": "canonicalPreopens", + "ms": 0.16833 + }, + { + "name": "moduleRead", + "ms": 7.355294 + }, + { + "name": "profileValidation", + "ms": 5.0344370000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.018998 + }, + { + "name": "Linker", + "ms": 0.290096 + }, + { + "name": "Store", + "ms": 0.020446 + }, + { + "name": "Instance", + "ms": 1.451995 + }, + { + "name": "signalMaskInit", + "ms": 0.075739 + }, + { + "name": "entrypointLookup", + "ms": 0.005278 + }, + { + "name": "wasi.start", + "ms": 6.132608 + }, + { + "name": "Store.teardown", + "ms": 0.041971999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111077, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475732992, + "virtualBytes": 8540446720, + "minorFaults": 111105, + "majorFaults": 0 + }, + "end": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111105, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 37.660821000004944, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018862999999999998, + "firstGuestHostCallMs": 12.498234, + "firstOutputMs": 15.838011999999999, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.289661000000002, + "phases": [ + { + "name": "Engine", + "ms": 0.0019470000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.112665 + }, + { + "name": "moduleRead", + "ms": 6.892033 + }, + { + "name": "profileValidation", + "ms": 4.414136 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008518999999999999 + }, + { + "name": "Linker", + "ms": 0.206916 + }, + { + "name": "Store", + "ms": 0.016117 + }, + { + "name": "Instance", + "ms": 0.040067 + }, + { + "name": "signalMaskInit", + "ms": 0.059646 + }, + { + "name": "entrypointLookup", + "ms": 0.002842 + }, + { + "name": "wasi.start", + "ms": 4.73712 + }, + { + "name": "Store.teardown", + "ms": 0.041646 + } + ] + }, + "memory": { + "start": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475446272, + "virtualBytes": 4175990784, + "minorFaults": 111105, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475729920, + "virtualBytes": 8540446720, + "minorFaults": 111133, + "majorFaults": 0 + }, + "end": { + "rssBytes": 470880256, + "peakRssBytes": 510603264, + "pssBytes": 475447296, + "virtualBytes": 4175990784, + "minorFaults": 111133, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17055144, + "wasmtimeProcessRetainedRssBytes": 470880256, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 1, + "vmSetupMs": 449.754022000001, + "fixtureSetupMs": 388.4685830000017, + "baseline": { + "rssBytes": 242298880, + "peakRssBytes": 249667584, + "pssBytes": 244297728, + "virtualBytes": 3886759936, + "minorFaults": 53703, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375681024, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 391061504, + "peakRssBytes": 500350976, + "pssBytes": 393019392, + "virtualBytes": 4051726336, + "minorFaults": 877276, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 148762624, + "peakRssBytes": 250683392, + "pssBytes": 148721664, + "virtualBytes": 164966400, + "minorFaults": 823573, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 72.01046599999972, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.214856 + }, + { + "name": "WebAssembly.Module", + "ms": 0.304634 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.225594 + }, + { + "name": "wasi.start", + "ms": 0.097381 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 242298880, + "peakRssBytes": 249667584, + "pssBytes": 244305920, + "virtualBytes": 3888873472, + "minorFaults": 53705, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277237760, + "peakRssBytes": 278360064, + "pssBytes": 265433088, + "virtualBytes": 4641087488, + "minorFaults": 64600, + "majorFaults": 0 + }, + "end": { + "rssBytes": 263413760, + "peakRssBytes": 278360064, + "pssBytes": 265433088, + "virtualBytes": 3955982336, + "minorFaults": 64600, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 242298880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263413760, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 67.55463299999974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 2.635889 + }, + { + "name": "WebAssembly.Module", + "ms": 0.188504 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.059614 + }, + { + "name": "wasi.start", + "ms": 0.089495 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 263413760, + "peakRssBytes": 278360064, + "pssBytes": 265433088, + "virtualBytes": 3955982336, + "minorFaults": 64600, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 278200320, + "peakRssBytes": 278454272, + "pssBytes": 265605120, + "virtualBytes": 4641087488, + "minorFaults": 70890, + "majorFaults": 0 + }, + "end": { + "rssBytes": 263512064, + "peakRssBytes": 278454272, + "pssBytes": 265605120, + "virtualBytes": 3955982336, + "minorFaults": 70890, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263413760, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263512064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 60.996517999999924, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.057166 + }, + { + "name": "WebAssembly.Module", + "ms": 0.15937 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.06364 + }, + { + "name": "wasi.start", + "ms": 0.087326 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 263512064, + "peakRssBytes": 278454272, + "pssBytes": 265605120, + "virtualBytes": 3955982336, + "minorFaults": 70890, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 278224896, + "peakRssBytes": 278474752, + "pssBytes": 267698176, + "virtualBytes": 4641349632, + "minorFaults": 77155, + "majorFaults": 0 + }, + "end": { + "rssBytes": 263528448, + "peakRssBytes": 278474752, + "pssBytes": 265674752, + "virtualBytes": 3955982336, + "minorFaults": 77155, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263512064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263528448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 59.57136900000478, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.033827 + }, + { + "name": "WebAssembly.Module", + "ms": 0.141204 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.063533 + }, + { + "name": "wasi.start", + "ms": 0.084687 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 263528448, + "peakRssBytes": 278474752, + "pssBytes": 265674752, + "virtualBytes": 3955982336, + "minorFaults": 77155, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277983232, + "peakRssBytes": 278474752, + "pssBytes": 279420928, + "virtualBytes": 4640825344, + "minorFaults": 83412, + "majorFaults": 0 + }, + "end": { + "rssBytes": 263528448, + "peakRssBytes": 278474752, + "pssBytes": 265707520, + "virtualBytes": 3955982336, + "minorFaults": 83412, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263528448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263528448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 60.252863000001526, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.217148 + }, + { + "name": "WebAssembly.Module", + "ms": 0.144045 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.062027 + }, + { + "name": "wasi.start", + "ms": 0.090621 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 263528448, + "peakRssBytes": 278474752, + "pssBytes": 265707520, + "virtualBytes": 3955982336, + "minorFaults": 83412, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 278245376, + "peakRssBytes": 278482944, + "pssBytes": 265752576, + "virtualBytes": 4641087488, + "minorFaults": 89671, + "majorFaults": 0 + }, + "end": { + "rssBytes": 263536640, + "peakRssBytes": 278482944, + "pssBytes": 265752576, + "virtualBytes": 3955982336, + "minorFaults": 89671, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263528448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263536640, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 204.3435249999966, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.513438 + }, + { + "name": "WebAssembly.Module", + "ms": 1.191057 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.1187 + }, + { + "name": "wasi.start", + "ms": 87.925247 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 263536640, + "peakRssBytes": 278482944, + "pssBytes": 265752576, + "virtualBytes": 3955982336, + "minorFaults": 89671, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 336879616, + "peakRssBytes": 337420288, + "pssBytes": 332718080, + "virtualBytes": 4696461312, + "minorFaults": 110327, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285777920, + "peakRssBytes": 337420288, + "pssBytes": 287305728, + "virtualBytes": 3958083584, + "minorFaults": 110327, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 263536640, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 285777920, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 200.9365289999987, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.163334 + }, + { + "name": "WebAssembly.Module", + "ms": 1.005392 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.857214 + }, + { + "name": "wasi.start", + "ms": 86.773696 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 285777920, + "peakRssBytes": 337420288, + "pssBytes": 287305728, + "virtualBytes": 3958083584, + "minorFaults": 110327, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 343089152, + "peakRssBytes": 344170496, + "pssBytes": 341663744, + "virtualBytes": 4696461312, + "minorFaults": 128018, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293359616, + "peakRssBytes": 344170496, + "pssBytes": 294830080, + "virtualBytes": 3958083584, + "minorFaults": 128018, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 285777920, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293359616, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 199.48389499999757, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.627458 + }, + { + "name": "WebAssembly.Module", + "ms": 0.993258 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.297612 + }, + { + "name": "wasi.start", + "ms": 88.204314 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293359616, + "peakRssBytes": 344170496, + "pssBytes": 294830080, + "virtualBytes": 3958083584, + "minorFaults": 128018, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 349605888, + "peakRssBytes": 349876224, + "pssBytes": 351162368, + "virtualBytes": 4696461312, + "minorFaults": 141797, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293445632, + "peakRssBytes": 349876224, + "pssBytes": 295055360, + "virtualBytes": 3958083584, + "minorFaults": 141797, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293359616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293445632, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 204.37370799999917, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.823595 + }, + { + "name": "WebAssembly.Module", + "ms": 1.016929 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.303438 + }, + { + "name": "wasi.start", + "ms": 90.153756 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293445632, + "peakRssBytes": 349876224, + "pssBytes": 295055360, + "virtualBytes": 3958083584, + "minorFaults": 141797, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 354238464, + "peakRssBytes": 354508800, + "pssBytes": 350404608, + "virtualBytes": 4696723456, + "minorFaults": 157672, + "majorFaults": 0 + }, + "end": { + "rssBytes": 303022080, + "peakRssBytes": 354508800, + "pssBytes": 304521216, + "virtualBytes": 3958083584, + "minorFaults": 157672, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293445632, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303022080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 192.86305600000196, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.435626 + }, + { + "name": "WebAssembly.Module", + "ms": 1.387056 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.277312 + }, + { + "name": "wasi.start", + "ms": 81.819752 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 303022080, + "peakRssBytes": 354508800, + "pssBytes": 304521216, + "virtualBytes": 3958083584, + "minorFaults": 157672, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 362676224, + "peakRssBytes": 362946560, + "pssBytes": 358785024, + "virtualBytes": 4696461312, + "minorFaults": 174326, + "majorFaults": 0 + }, + "end": { + "rssBytes": 312528896, + "peakRssBytes": 362946560, + "pssBytes": 314077184, + "virtualBytes": 3958083584, + "minorFaults": 174326, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303022080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 312528896, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 291.80294899999717, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 53.283734 + }, + { + "name": "WebAssembly.Module", + "ms": 3.90142 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.223291 + }, + { + "name": "wasi.start", + "ms": 102.361833 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 312528896, + "peakRssBytes": 362946560, + "pssBytes": 314077184, + "virtualBytes": 3958083584, + "minorFaults": 174326, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 418631680, + "peakRssBytes": 418820096, + "pssBytes": 420237312, + "virtualBytes": 5472464896, + "minorFaults": 204133, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338370560, + "peakRssBytes": 418820096, + "pssBytes": 340185088, + "virtualBytes": 4030361600, + "minorFaults": 204133, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 312528896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338370560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 284.0986570000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.834385 + }, + { + "name": "WebAssembly.Module", + "ms": 3.204116 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.451194 + }, + { + "name": "wasi.start", + "ms": 104.104857 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338370560, + "peakRssBytes": 418820096, + "pssBytes": 340185088, + "virtualBytes": 4030361600, + "minorFaults": 204133, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431165440, + "peakRssBytes": 431427584, + "pssBytes": 433102848, + "virtualBytes": 5472202752, + "minorFaults": 231481, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338509824, + "peakRssBytes": 431427584, + "pssBytes": 340279296, + "virtualBytes": 4030361600, + "minorFaults": 231481, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338370560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338509824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 300.051148999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.871135 + }, + { + "name": "WebAssembly.Module", + "ms": 3.942472 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.312982 + }, + { + "name": "wasi.start", + "ms": 102.78105 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338509824, + "peakRssBytes": 431427584, + "pssBytes": 340279296, + "virtualBytes": 4030361600, + "minorFaults": 231481, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 434548736, + "peakRssBytes": 434712576, + "pssBytes": 436559872, + "virtualBytes": 5472202752, + "minorFaults": 255743, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338411520, + "peakRssBytes": 434712576, + "pssBytes": 340418560, + "virtualBytes": 4030361600, + "minorFaults": 255743, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338509824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338411520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 290.1620610000027, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.228924 + }, + { + "name": "WebAssembly.Module", + "ms": 4.241497 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.461204 + }, + { + "name": "wasi.start", + "ms": 108.525666 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338411520, + "peakRssBytes": 434712576, + "pssBytes": 340418560, + "virtualBytes": 4030361600, + "minorFaults": 255743, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 434716672, + "peakRssBytes": 434782208, + "pssBytes": 436649984, + "virtualBytes": 5472870400, + "minorFaults": 282001, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338767872, + "peakRssBytes": 434782208, + "pssBytes": 340586496, + "virtualBytes": 4030361600, + "minorFaults": 282001, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338411520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338767872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 275.38358199999493, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.582654 + }, + { + "name": "WebAssembly.Module", + "ms": 2.861484 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.580765 + }, + { + "name": "wasi.start", + "ms": 92.458849 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338767872, + "peakRssBytes": 434782208, + "pssBytes": 340586496, + "virtualBytes": 4030361600, + "minorFaults": 282001, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433459200, + "peakRssBytes": 434782208, + "pssBytes": 435458048, + "virtualBytes": 5446021120, + "minorFaults": 309242, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338771968, + "peakRssBytes": 434782208, + "pssBytes": 340590592, + "virtualBytes": 4030361600, + "minorFaults": 309242, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338767872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338771968, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 178.32801499999914, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.161452 + }, + { + "name": "WebAssembly.Module", + "ms": 2.64522 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.035415 + }, + { + "name": "wasi.start", + "ms": 24.327466 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338771968, + "peakRssBytes": 434782208, + "pssBytes": 340590592, + "virtualBytes": 4030361600, + "minorFaults": 309242, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 408698880, + "peakRssBytes": 434782208, + "pssBytes": 411258880, + "virtualBytes": 4788035584, + "minorFaults": 323416, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338833408, + "peakRssBytes": 434782208, + "pssBytes": 341250048, + "virtualBytes": 4030361600, + "minorFaults": 323416, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338771968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338833408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 149.7120619999987, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.347109 + }, + { + "name": "WebAssembly.Module", + "ms": 1.852786 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.515943 + }, + { + "name": "wasi.start", + "ms": 19.264369 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338833408, + "peakRssBytes": 434782208, + "pssBytes": 341250048, + "virtualBytes": 4030361600, + "minorFaults": 323416, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 408178688, + "peakRssBytes": 434782208, + "pssBytes": 410656768, + "virtualBytes": 4788035584, + "minorFaults": 338447, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338980864, + "peakRssBytes": 434782208, + "pssBytes": 341319680, + "virtualBytes": 4030361600, + "minorFaults": 338447, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338833408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338980864, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 155.11269500000344, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.427282 + }, + { + "name": "WebAssembly.Module", + "ms": 2.273313 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.335936 + }, + { + "name": "wasi.start", + "ms": 19.986326 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338980864, + "peakRssBytes": 434782208, + "pssBytes": 341319680, + "virtualBytes": 4030361600, + "minorFaults": 338447, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 409292800, + "peakRssBytes": 434782208, + "pssBytes": 412090368, + "virtualBytes": 4788035584, + "minorFaults": 353318, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338649088, + "peakRssBytes": 434782208, + "pssBytes": 341327872, + "virtualBytes": 4030361600, + "minorFaults": 353318, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338980864, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338649088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 172.44962400000077, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.704015 + }, + { + "name": "WebAssembly.Module", + "ms": 2.359496 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.254819 + }, + { + "name": "wasi.start", + "ms": 29.494973 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338649088, + "peakRssBytes": 434782208, + "pssBytes": 341327872, + "virtualBytes": 4030361600, + "minorFaults": 353318, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 408748032, + "peakRssBytes": 434782208, + "pssBytes": 411414528, + "virtualBytes": 4788178944, + "minorFaults": 368533, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338595840, + "peakRssBytes": 434782208, + "pssBytes": 341327872, + "virtualBytes": 4030361600, + "minorFaults": 368533, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338649088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338595840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 150.85821700000088, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.60733 + }, + { + "name": "WebAssembly.Module", + "ms": 1.946016 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.258881 + }, + { + "name": "wasi.start", + "ms": 20.577832 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338595840, + "peakRssBytes": 434782208, + "pssBytes": 341327872, + "virtualBytes": 4030361600, + "minorFaults": 368533, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 407695360, + "peakRssBytes": 434782208, + "pssBytes": 410742784, + "virtualBytes": 4787773440, + "minorFaults": 382051, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338456576, + "peakRssBytes": 434782208, + "pssBytes": 341336064, + "virtualBytes": 4030361600, + "minorFaults": 382051, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338595840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338456576, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 120.82420999999886, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.222519 + }, + { + "name": "WebAssembly.Module", + "ms": 1.482689 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.326544 + }, + { + "name": "wasi.start", + "ms": 16.570396 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338456576, + "peakRssBytes": 434782208, + "pssBytes": 341336064, + "virtualBytes": 4030361600, + "minorFaults": 382051, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 384806912, + "peakRssBytes": 434782208, + "pssBytes": 386400256, + "virtualBytes": 4757209088, + "minorFaults": 397260, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353132544, + "peakRssBytes": 434782208, + "pssBytes": 236849152, + "virtualBytes": 4030361600, + "minorFaults": 397260, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338456576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353132544, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 133.66149700000096, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.463073 + }, + { + "name": "WebAssembly.Module", + "ms": 1.276179 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.747232 + }, + { + "name": "wasi.start", + "ms": 15.674002 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353402880, + "peakRssBytes": 434782208, + "pssBytes": 234055680, + "virtualBytes": 4030361600, + "minorFaults": 397335, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 404406272, + "peakRssBytes": 434782208, + "pssBytes": 393400320, + "virtualBytes": 4757614592, + "minorFaults": 418348, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356339712, + "peakRssBytes": 434782208, + "pssBytes": 358858752, + "virtualBytes": 4030361600, + "minorFaults": 418348, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353402880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356339712, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 128.89805299999716, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 15.912174 + }, + { + "name": "WebAssembly.Module", + "ms": 2.082593 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.388194 + }, + { + "name": "wasi.start", + "ms": 15.735463 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356339712, + "peakRssBytes": 434782208, + "pssBytes": 358858752, + "virtualBytes": 4030361600, + "minorFaults": 418348, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 404258816, + "peakRssBytes": 434782208, + "pssBytes": 407220224, + "virtualBytes": 4757209088, + "minorFaults": 437891, + "majorFaults": 0 + }, + "end": { + "rssBytes": 358084608, + "peakRssBytes": 434782208, + "pssBytes": 361394176, + "virtualBytes": 4030361600, + "minorFaults": 437891, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356339712, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358895616, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 124.31854400000157, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 16.246494 + }, + { + "name": "WebAssembly.Module", + "ms": 1.032855 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.37551 + }, + { + "name": "wasi.start", + "ms": 15.642322 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 358895616, + "peakRssBytes": 434782208, + "pssBytes": 361603072, + "virtualBytes": 4030361600, + "minorFaults": 438041, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 405360640, + "peakRssBytes": 434782208, + "pssBytes": 406749184, + "virtualBytes": 4757733376, + "minorFaults": 455579, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353722368, + "peakRssBytes": 434782208, + "pssBytes": 141404160, + "virtualBytes": 4030361600, + "minorFaults": 455579, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358895616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353992704, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 127.7930850000048, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 15.522533 + }, + { + "name": "WebAssembly.Module", + "ms": 1.833034 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.503706 + }, + { + "name": "wasi.start", + "ms": 16.617017 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353992704, + "peakRssBytes": 434782208, + "pssBytes": 129894400, + "virtualBytes": 4030361600, + "minorFaults": 455642, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403996672, + "peakRssBytes": 434782208, + "pssBytes": 400924672, + "virtualBytes": 4757471232, + "minorFaults": 477908, + "majorFaults": 0 + }, + "end": { + "rssBytes": 368353280, + "peakRssBytes": 434782208, + "pssBytes": 374280192, + "virtualBytes": 4030361600, + "minorFaults": 477908, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353992704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371593216, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 234.06816999999864, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 50.474391 + }, + { + "name": "WebAssembly.Module", + "ms": 4.047127 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.104174 + }, + { + "name": "wasi.start", + "ms": 31.399917 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 371593216, + "peakRssBytes": 434782208, + "pssBytes": 375521280, + "virtualBytes": 4030361600, + "minorFaults": 478730, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 456003584, + "peakRssBytes": 456204288, + "pssBytes": 458817536, + "virtualBytes": 4821581824, + "minorFaults": 500161, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362328064, + "peakRssBytes": 456204288, + "pssBytes": 365191168, + "virtualBytes": 4030361600, + "minorFaults": 500161, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371593216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362328064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 226.30407099999866, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.159219 + }, + { + "name": "WebAssembly.Module", + "ms": 3.413095 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.439653 + }, + { + "name": "wasi.start", + "ms": 41.113641 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362328064, + "peakRssBytes": 456204288, + "pssBytes": 365191168, + "virtualBytes": 4030361600, + "minorFaults": 500161, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 456179712, + "peakRssBytes": 456286208, + "pssBytes": 459030528, + "virtualBytes": 4821319680, + "minorFaults": 516162, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364568576, + "peakRssBytes": 456286208, + "pssBytes": 367476736, + "virtualBytes": 4030361600, + "minorFaults": 516162, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362328064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364568576, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 237.54077400000097, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 47.677327 + }, + { + "name": "WebAssembly.Module", + "ms": 4.290145 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.869441 + }, + { + "name": "wasi.start", + "ms": 42.668108 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364568576, + "peakRssBytes": 456286208, + "pssBytes": 367476736, + "virtualBytes": 4030361600, + "minorFaults": 516162, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 460369920, + "peakRssBytes": 460521472, + "pssBytes": 460345344, + "virtualBytes": 4821319680, + "minorFaults": 531869, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364724224, + "peakRssBytes": 460521472, + "pssBytes": 367542272, + "virtualBytes": 4030361600, + "minorFaults": 531869, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364568576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364724224, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 228.71948300000076, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 39.355453 + }, + { + "name": "WebAssembly.Module", + "ms": 3.030255 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.241297 + }, + { + "name": "wasi.start", + "ms": 40.782734 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364724224, + "peakRssBytes": 460521472, + "pssBytes": 367542272, + "virtualBytes": 4030361600, + "minorFaults": 531869, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 457207808, + "peakRssBytes": 460521472, + "pssBytes": 460402688, + "virtualBytes": 4743098368, + "minorFaults": 550866, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364806144, + "peakRssBytes": 460521472, + "pssBytes": 367607808, + "virtualBytes": 4030361600, + "minorFaults": 550866, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364724224, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364806144, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 231.94582800000353, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.541531 + }, + { + "name": "WebAssembly.Module", + "ms": 3.790248 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.063435 + }, + { + "name": "wasi.start", + "ms": 43.212322 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364806144, + "peakRssBytes": 460521472, + "pssBytes": 367607808, + "virtualBytes": 4030361600, + "minorFaults": 550866, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 458743808, + "peakRssBytes": 460521472, + "pssBytes": 461426688, + "virtualBytes": 4821319680, + "minorFaults": 566813, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364974080, + "peakRssBytes": 460521472, + "pssBytes": 367607808, + "virtualBytes": 4030361600, + "minorFaults": 566813, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364806144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364974080, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 263.1384560000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 77.916547 + }, + { + "name": "WebAssembly.Module", + "ms": 4.194864 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.50456 + }, + { + "name": "wasi.start", + "ms": 6.762414 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364974080, + "peakRssBytes": 460521472, + "pssBytes": 367607808, + "virtualBytes": 4030361600, + "minorFaults": 566813, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 499445760, + "peakRssBytes": 499662848, + "pssBytes": 501907456, + "virtualBytes": 4812226560, + "minorFaults": 601833, + "majorFaults": 0 + }, + "end": { + "rssBytes": 367771648, + "peakRssBytes": 499662848, + "pssBytes": 370085888, + "virtualBytes": 4032188416, + "minorFaults": 601833, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364974080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367771648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 260.85875300000043, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.059322 + }, + { + "name": "WebAssembly.Module", + "ms": 3.380625 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.663806 + }, + { + "name": "wasi.start", + "ms": 5.185433 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 367771648, + "peakRssBytes": 499662848, + "pssBytes": 370085888, + "virtualBytes": 4032188416, + "minorFaults": 601833, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 500002816, + "peakRssBytes": 500228096, + "pssBytes": 502157312, + "virtualBytes": 4812759040, + "minorFaults": 627065, + "majorFaults": 0 + }, + "end": { + "rssBytes": 368156672, + "peakRssBytes": 500228096, + "pssBytes": 370151424, + "virtualBytes": 4032577536, + "minorFaults": 627065, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367771648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368156672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 239.3497520000019, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 70.441655 + }, + { + "name": "WebAssembly.Module", + "ms": 3.809011 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.403075 + }, + { + "name": "wasi.start", + "ms": 7.051986 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 368156672, + "peakRssBytes": 500228096, + "pssBytes": 370151424, + "virtualBytes": 4032577536, + "minorFaults": 627065, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 500236288, + "peakRssBytes": 500350976, + "pssBytes": 502169600, + "virtualBytes": 4812615680, + "minorFaults": 661425, + "majorFaults": 0 + }, + "end": { + "rssBytes": 368283648, + "peakRssBytes": 500350976, + "pssBytes": 370192384, + "virtualBytes": 4032577536, + "minorFaults": 661425, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368156672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368283648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 265.6803609999988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.892187 + }, + { + "name": "WebAssembly.Module", + "ms": 3.43383 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.404278 + }, + { + "name": "wasi.start", + "ms": 5.088939 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 368283648, + "peakRssBytes": 500350976, + "pssBytes": 370191360, + "virtualBytes": 4032577536, + "minorFaults": 661425, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 500174848, + "peakRssBytes": 500350976, + "pssBytes": 502194176, + "virtualBytes": 4812615680, + "minorFaults": 695837, + "majorFaults": 0 + }, + "end": { + "rssBytes": 368058368, + "peakRssBytes": 500350976, + "pssBytes": 370192384, + "virtualBytes": 4032577536, + "minorFaults": 695837, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368283648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368058368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 250.42661999999837, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 77.150218 + }, + { + "name": "WebAssembly.Module", + "ms": 3.782099 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.411356 + }, + { + "name": "wasi.start", + "ms": 6.50456 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 368058368, + "peakRssBytes": 500350976, + "pssBytes": 370192384, + "virtualBytes": 4032577536, + "minorFaults": 695837, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 499781632, + "peakRssBytes": 500350976, + "pssBytes": 502198272, + "virtualBytes": 4812615680, + "minorFaults": 729737, + "majorFaults": 0 + }, + "end": { + "rssBytes": 367845376, + "peakRssBytes": 500350976, + "pssBytes": 370192384, + "virtualBytes": 4032577536, + "minorFaults": 729737, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 368058368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367845376, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 192.66382500000327, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.463579 + }, + { + "name": "WebAssembly.Module", + "ms": 0.801827 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.362264 + }, + { + "name": "wasi.start", + "ms": 76.034887 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 367845376, + "peakRssBytes": 500350976, + "pssBytes": 370192384, + "virtualBytes": 4032577536, + "minorFaults": 729737, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426328064, + "peakRssBytes": 500350976, + "pssBytes": 423374848, + "virtualBytes": 4770603008, + "minorFaults": 745234, + "majorFaults": 0 + }, + "end": { + "rssBytes": 373755904, + "peakRssBytes": 500350976, + "pssBytes": 376053760, + "virtualBytes": 4033540096, + "minorFaults": 745234, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367845376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 373755904, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 185.70017100000405, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.169945 + }, + { + "name": "WebAssembly.Module", + "ms": 1.793762 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.36939 + }, + { + "name": "wasi.start", + "ms": 70.543662 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 373755904, + "peakRssBytes": 500350976, + "pssBytes": 376053760, + "virtualBytes": 4033540096, + "minorFaults": 745234, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426053632, + "peakRssBytes": 500350976, + "pssBytes": 428183552, + "virtualBytes": 4770603008, + "minorFaults": 759472, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374177792, + "peakRssBytes": 500350976, + "pssBytes": 376168448, + "virtualBytes": 4033540096, + "minorFaults": 759472, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 373755904, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374177792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 200.99755100000039, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.666623 + }, + { + "name": "WebAssembly.Module", + "ms": 0.723126 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.37021 + }, + { + "name": "wasi.start", + "ms": 76.334211 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374177792, + "peakRssBytes": 500350976, + "pssBytes": 376168448, + "virtualBytes": 4033540096, + "minorFaults": 759472, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425971712, + "peakRssBytes": 500350976, + "pssBytes": 428261376, + "virtualBytes": 4770340864, + "minorFaults": 774204, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374059008, + "peakRssBytes": 500350976, + "pssBytes": 376168448, + "virtualBytes": 4033540096, + "minorFaults": 774204, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374177792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374059008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 192.66364199999953, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.756104 + }, + { + "name": "WebAssembly.Module", + "ms": 0.812517 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.016016 + }, + { + "name": "wasi.start", + "ms": 69.560719 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374059008, + "peakRssBytes": 500350976, + "pssBytes": 376168448, + "virtualBytes": 4033540096, + "minorFaults": 774204, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426004480, + "peakRssBytes": 500350976, + "pssBytes": 428306432, + "virtualBytes": 4770603008, + "minorFaults": 787923, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374165504, + "peakRssBytes": 500350976, + "pssBytes": 376209408, + "virtualBytes": 4033540096, + "minorFaults": 787923, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374059008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374165504, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 194.20882300000085, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.564405 + }, + { + "name": "WebAssembly.Module", + "ms": 0.841714 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.380179 + }, + { + "name": "wasi.start", + "ms": 75.017863 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374165504, + "peakRssBytes": 500350976, + "pssBytes": 376209408, + "virtualBytes": 4033540096, + "minorFaults": 787923, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426119168, + "peakRssBytes": 500350976, + "pssBytes": 428310528, + "virtualBytes": 4770865152, + "minorFaults": 802144, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374280192, + "peakRssBytes": 500350976, + "pssBytes": 376209408, + "virtualBytes": 4033540096, + "minorFaults": 802144, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374165504, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374280192, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 163.27174699999887, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.177298 + }, + { + "name": "WebAssembly.Module", + "ms": 1.271275 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.864047 + }, + { + "name": "wasi.start", + "ms": 15.47904 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374280192, + "peakRssBytes": 500350976, + "pssBytes": 376209408, + "virtualBytes": 4033540096, + "minorFaults": 802144, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 443592704, + "peakRssBytes": 500350976, + "pssBytes": 445485056, + "virtualBytes": 4791439360, + "minorFaults": 817676, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375771136, + "peakRssBytes": 500350976, + "pssBytes": 377446400, + "virtualBytes": 4035047424, + "minorFaults": 817676, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374280192, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375771136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 150.51854699999967, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.887988 + }, + { + "name": "WebAssembly.Module", + "ms": 1.108154 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.887407 + }, + { + "name": "wasi.start", + "ms": 15.007711 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375771136, + "peakRssBytes": 500350976, + "pssBytes": 377446400, + "virtualBytes": 4035047424, + "minorFaults": 817676, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 443502592, + "peakRssBytes": 500350976, + "pssBytes": 445751296, + "virtualBytes": 4791705600, + "minorFaults": 833481, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375623680, + "peakRssBytes": 500350976, + "pssBytes": 377712640, + "virtualBytes": 4035313664, + "minorFaults": 833481, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375771136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375623680, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 160.33649200000218, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.423321 + }, + { + "name": "WebAssembly.Module", + "ms": 2.834103 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.336674 + }, + { + "name": "wasi.start", + "ms": 18.776903 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375623680, + "peakRssBytes": 500350976, + "pssBytes": 377712640, + "virtualBytes": 4035313664, + "minorFaults": 833481, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 445591552, + "peakRssBytes": 500350976, + "pssBytes": 447737856, + "virtualBytes": 4791705600, + "minorFaults": 848172, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375750656, + "peakRssBytes": 500350976, + "pssBytes": 377711616, + "virtualBytes": 4035313664, + "minorFaults": 848172, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375623680, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375750656, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 155.98187499999767, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.024031 + }, + { + "name": "WebAssembly.Module", + "ms": 2.885652 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.349823 + }, + { + "name": "wasi.start", + "ms": 15.051145 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375750656, + "peakRssBytes": 500350976, + "pssBytes": 377711616, + "virtualBytes": 4035313664, + "minorFaults": 848172, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 443580416, + "peakRssBytes": 500350976, + "pssBytes": 445730816, + "virtualBytes": 4791443456, + "minorFaults": 861875, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375611392, + "peakRssBytes": 500350976, + "pssBytes": 377716736, + "virtualBytes": 4035313664, + "minorFaults": 861875, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375750656, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375611392, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 141.74686500000098, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616301847839526/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.745871 + }, + { + "name": "WebAssembly.Module", + "ms": 2.443167 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.354343 + }, + { + "name": "wasi.start", + "ms": 14.376328 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375611392, + "peakRssBytes": 500350976, + "pssBytes": 377716736, + "virtualBytes": 4035313664, + "minorFaults": 861875, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 443666432, + "peakRssBytes": 500350976, + "pssBytes": 445759488, + "virtualBytes": 4791443456, + "minorFaults": 875570, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375681024, + "peakRssBytes": 500350976, + "pssBytes": 377716736, + "virtualBytes": 4035313664, + "minorFaults": 875570, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375611392, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375681024, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 1, + "vmSetupMs": 433.3178589999952, + "fixtureSetupMs": 385.4503810000024, + "baseline": { + "rssBytes": 242221056, + "peakRssBytes": 247803904, + "pssBytes": 243678208, + "virtualBytes": 3886272512, + "minorFaults": 56103, + "majorFaults": 1 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 506327040, + "peakRssBytes": 512724992, + "pssBytes": 508514304, + "virtualBytes": 4202934272, + "minorFaults": 112566, + "majorFaults": 1 + }, + "retainedDelta": { + "rssBytes": 264105984, + "peakRssBytes": 264921088, + "pssBytes": 264836096, + "virtualBytes": 316661760, + "minorFaults": 56463, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 66.00842499999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.075331, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.454142999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.069009 + }, + { + "name": "canonicalPreopens", + "ms": 0.100437 + }, + { + "name": "moduleRead", + "ms": 3.639694 + }, + { + "name": "profileValidation", + "ms": 0.221688 + }, + { + "name": "moduleCompile", + "ms": 43.919 + }, + { + "name": "importValidation", + "ms": 0.002813 + }, + { + "name": "Linker", + "ms": 0.18588 + }, + { + "name": "Store", + "ms": 0.020979 + }, + { + "name": "Instance", + "ms": 0.047691000000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.103853 + }, + { + "name": "entrypointLookup", + "ms": 0.002866 + }, + { + "name": "wasi.start", + "ms": 0.03536 + }, + { + "name": "Store.teardown", + "ms": 0.022309 + } + ] + }, + "memory": { + "start": { + "rssBytes": 242221056, + "peakRssBytes": 247803904, + "pssBytes": 243686400, + "virtualBytes": 3888386048, + "minorFaults": 56105, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57264, + "majorFaults": 1 + }, + "end": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57264, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 242221056, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 19.949939999998605, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.0085, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3.514233, + "phases": [ + { + "name": "Engine", + "ms": 0.00237 + }, + { + "name": "canonicalPreopens", + "ms": 0.103288 + }, + { + "name": "moduleRead", + "ms": 2.221672 + }, + { + "name": "profileValidation", + "ms": 0.215212 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003742 + }, + { + "name": "Linker", + "ms": 0.19856 + }, + { + "name": "Store", + "ms": 0.014898 + }, + { + "name": "Instance", + "ms": 0.040981 + }, + { + "name": "signalMaskInit", + "ms": 0.586701 + }, + { + "name": "entrypointLookup", + "ms": 0.0036279999999999997 + }, + { + "name": "wasi.start", + "ms": 0.028031 + }, + { + "name": "Store.teardown", + "ms": 0.019632 + } + ] + }, + "memory": { + "start": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57264, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57281, + "majorFaults": 1 + }, + "end": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57281, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 20.03107099999761, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010869, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3.6084099999999997, + "phases": [ + { + "name": "Engine", + "ms": 0.001566 + }, + { + "name": "canonicalPreopens", + "ms": 0.101209 + }, + { + "name": "moduleRead", + "ms": 2.277304 + }, + { + "name": "profileValidation", + "ms": 0.230694 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003412 + }, + { + "name": "Linker", + "ms": 0.192529 + }, + { + "name": "Store", + "ms": 0.013111000000000001 + }, + { + "name": "Instance", + "ms": 0.029765 + }, + { + "name": "signalMaskInit", + "ms": 0.610697 + }, + { + "name": "entrypointLookup", + "ms": 0.008173 + }, + { + "name": "wasi.start", + "ms": 0.043123 + }, + { + "name": "Store.teardown", + "ms": 0.018088 + } + ] + }, + "memory": { + "start": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57281, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57298, + "majorFaults": 1 + }, + "end": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57298, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 20.711374999998952, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010091000000000001, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.650357, + "phases": [ + { + "name": "Engine", + "ms": 0.001709 + }, + { + "name": "canonicalPreopens", + "ms": 0.101807 + }, + { + "name": "moduleRead", + "ms": 3.347341 + }, + { + "name": "profileValidation", + "ms": 0.210862 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003015 + }, + { + "name": "Linker", + "ms": 0.19871 + }, + { + "name": "Store", + "ms": 0.014842 + }, + { + "name": "Instance", + "ms": 0.031411 + }, + { + "name": "signalMaskInit", + "ms": 0.620579 + }, + { + "name": "entrypointLookup", + "ms": 0.002261 + }, + { + "name": "wasi.start", + "ms": 0.025026 + }, + { + "name": "Store.teardown", + "ms": 0.018873 + } + ] + }, + "memory": { + "start": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57298, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57315, + "majorFaults": 1 + }, + "end": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57315, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 20.159522999994806, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008516, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.7169550000000005, + "phases": [ + { + "name": "Engine", + "ms": 0.0020719999999999996 + }, + { + "name": "canonicalPreopens", + "ms": 0.09665 + }, + { + "name": "moduleRead", + "ms": 3.3859179999999998 + }, + { + "name": "profileValidation", + "ms": 0.22516999999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0028959999999999997 + }, + { + "name": "Linker", + "ms": 0.19832899999999998 + }, + { + "name": "Store", + "ms": 0.014566 + }, + { + "name": "Instance", + "ms": 0.032508 + }, + { + "name": "signalMaskInit", + "ms": 0.617624 + }, + { + "name": "entrypointLookup", + "ms": 0.007113 + }, + { + "name": "wasi.start", + "ms": 0.041624 + }, + { + "name": "Store.teardown", + "ms": 0.018418 + } + ] + }, + "memory": { + "start": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57315, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57332, + "majorFaults": 1 + }, + "end": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57332, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1128.4819590000043, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010921, + "firstGuestHostCallMs": 1058.701968, + "firstOutputMs": 1112.462373, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1112.769727, + "phases": [ + { + "name": "Engine", + "ms": 0.00184 + }, + { + "name": "canonicalPreopens", + "ms": 0.106064 + }, + { + "name": "moduleRead", + "ms": 6.49883 + }, + { + "name": "profileValidation", + "ms": 3.882606 + }, + { + "name": "moduleCompile", + "ms": 1046.845964 + }, + { + "name": "importValidation", + "ms": 0.00646 + }, + { + "name": "Linker", + "ms": 0.184895 + }, + { + "name": "Store", + "ms": 0.017319 + }, + { + "name": "Instance", + "ms": 0.17515799999999998 + }, + { + "name": "signalMaskInit", + "ms": 0.085281 + }, + { + "name": "entrypointLookup", + "ms": 0.00396 + }, + { + "name": "wasi.start", + "ms": 54.187881 + }, + { + "name": "Store.teardown", + "ms": 0.123178 + } + ] + }, + "memory": { + "start": { + "rssBytes": 252710912, + "peakRssBytes": 252895232, + "pssBytes": 254185472, + "virtualBytes": 3957784576, + "minorFaults": 57332, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294793216, + "virtualBytes": 8327688192, + "minorFaults": 64523, + "majorFaults": 1 + }, + "end": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64523, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 43919, + "wasmtimeProcessRetainedRssBytes": 252710912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 404.8369820000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009909, + "firstGuestHostCallMs": 12.478771, + "firstOutputMs": 68.20988, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 68.437725, + "phases": [ + { + "name": "Engine", + "ms": 0.001603 + }, + { + "name": "canonicalPreopens", + "ms": 0.11317100000000001 + }, + { + "name": "moduleRead", + "ms": 6.800821 + }, + { + "name": "profileValidation", + "ms": 4.246131 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009082999999999999 + }, + { + "name": "Linker", + "ms": 0.259418 + }, + { + "name": "Store", + "ms": 0.020854 + }, + { + "name": "Instance", + "ms": 0.048094 + }, + { + "name": "signalMaskInit", + "ms": 0.068902 + }, + { + "name": "entrypointLookup", + "ms": 0.005719 + }, + { + "name": "wasi.start", + "ms": 56.149476 + }, + { + "name": "Store.teardown", + "ms": 0.06765700000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64523, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 293113856, + "peakRssBytes": 293240832, + "pssBytes": 294866944, + "virtualBytes": 8327688192, + "minorFaults": 64592, + "majorFaults": 1 + }, + "end": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294251520, + "virtualBytes": 3963232256, + "minorFaults": 64592, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 78.87716600000567, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016784, + "firstGuestHostCallMs": 11.763308, + "firstOutputMs": 61.299693000000005, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.465053, + "phases": [ + { + "name": "Engine", + "ms": 0.0023420000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.116363 + }, + { + "name": "moduleRead", + "ms": 6.622362 + }, + { + "name": "profileValidation", + "ms": 3.9091539999999996 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006856999999999999 + }, + { + "name": "Linker", + "ms": 0.196662 + }, + { + "name": "Store", + "ms": 0.017431 + }, + { + "name": "Instance", + "ms": 0.044178999999999996 + }, + { + "name": "signalMaskInit", + "ms": 0.060365 + }, + { + "name": "entrypointLookup", + "ms": 0.004575 + }, + { + "name": "wasi.start", + "ms": 49.829534 + }, + { + "name": "Store.teardown", + "ms": 0.047617999999999994 + } + ] + }, + "memory": { + "start": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294251520, + "virtualBytes": 3963232256, + "minorFaults": 64592, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 293113856, + "peakRssBytes": 293240832, + "pssBytes": 294866944, + "virtualBytes": 8327688192, + "minorFaults": 64661, + "majorFaults": 1 + }, + "end": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64661, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 77.96589200000017, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019468, + "firstGuestHostCallMs": 12.227234, + "firstOutputMs": 59.837459, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.139533, + "phases": [ + { + "name": "Engine", + "ms": 0.0023599999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.164259 + }, + { + "name": "moduleRead", + "ms": 7.1132 + }, + { + "name": "profileValidation", + "ms": 3.899779 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0067599999999999995 + }, + { + "name": "Linker", + "ms": 0.197245 + }, + { + "name": "Store", + "ms": 0.015007999999999999 + }, + { + "name": "Instance", + "ms": 0.042367 + }, + { + "name": "signalMaskInit", + "ms": 0.033242 + }, + { + "name": "entrypointLookup", + "ms": 0.002787 + }, + { + "name": "wasi.start", + "ms": 47.851653 + }, + { + "name": "Store.teardown", + "ms": 0.200886 + } + ] + }, + "memory": { + "start": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64661, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 293113856, + "peakRssBytes": 293240832, + "pssBytes": 294866944, + "virtualBytes": 8327688192, + "minorFaults": 64730, + "majorFaults": 1 + }, + "end": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64730, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 76.78484700000263, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012098, + "firstGuestHostCallMs": 11.508545, + "firstOutputMs": 59.680321, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.01153, + "phases": [ + { + "name": "Engine", + "ms": 0.0019549999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.105661 + }, + { + "name": "moduleRead", + "ms": 6.438865 + }, + { + "name": "profileValidation", + "ms": 3.884595 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007199 + }, + { + "name": "Linker", + "ms": 0.202755 + }, + { + "name": "Store", + "ms": 0.016524 + }, + { + "name": "Instance", + "ms": 0.041731 + }, + { + "name": "signalMaskInit", + "ms": 0.058959000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.002783 + }, + { + "name": "wasi.start", + "ms": 48.420509 + }, + { + "name": "Store.teardown", + "ms": 0.229583 + } + ] + }, + "memory": { + "start": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64730, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 293113856, + "peakRssBytes": 293240832, + "pssBytes": 294866944, + "virtualBytes": 8327688192, + "minorFaults": 64799, + "majorFaults": 1 + }, + "end": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64799, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3803.7957689999967, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012211999999999999, + "firstGuestHostCallMs": 3396.199138, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3785.681113, + "phases": [ + { + "name": "Engine", + "ms": 0.001701 + }, + { + "name": "canonicalPreopens", + "ms": 0.120094 + }, + { + "name": "moduleRead", + "ms": 11.407523 + }, + { + "name": "profileValidation", + "ms": 12.507064 + }, + { + "name": "moduleCompile", + "ms": 3370.21783 + }, + { + "name": "importValidation", + "ms": 0.009226 + }, + { + "name": "Linker", + "ms": 0.18563100000000002 + }, + { + "name": "Store", + "ms": 0.017879 + }, + { + "name": "Instance", + "ms": 0.224741 + }, + { + "name": "signalMaskInit", + "ms": 0.086308 + }, + { + "name": "entrypointLookup", + "ms": 0.0038550000000000004 + }, + { + "name": "wasi.start", + "ms": 389.404115 + }, + { + "name": "Store.teardown", + "ms": 0.054597 + } + ] + }, + "memory": { + "start": { + "rssBytes": 292515840, + "peakRssBytes": 293240832, + "pssBytes": 294252544, + "virtualBytes": 3963232256, + "minorFaults": 64799, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 420278272, + "peakRssBytes": 420438016, + "pssBytes": 423219200, + "virtualBytes": 12846845952, + "minorFaults": 82139, + "majorFaults": 1 + }, + "end": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82139, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1090764, + "wasmtimeProcessRetainedRssBytes": 292515840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 84.3335750000042, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012916, + "firstGuestHostCallMs": 27.560829, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 62.541521, + "phases": [ + { + "name": "Engine", + "ms": 0.001759 + }, + { + "name": "canonicalPreopens", + "ms": 0.122433 + }, + { + "name": "moduleRead", + "ms": 11.669237 + }, + { + "name": "profileValidation", + "ms": 12.704429999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010983 + }, + { + "name": "Linker", + "ms": 0.215113 + }, + { + "name": "Store", + "ms": 0.020108 + }, + { + "name": "Instance", + "ms": 1.283641 + }, + { + "name": "signalMaskInit", + "ms": 0.060499 + }, + { + "name": "entrypointLookup", + "ms": 0.008926 + }, + { + "name": "wasi.start", + "ms": 34.946284 + }, + { + "name": "Store.teardown", + "ms": 0.038495999999999996 + } + ] + }, + "memory": { + "start": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82139, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 423431168, + "virtualBytes": 12846845952, + "minorFaults": 82219, + "majorFaults": 1 + }, + "end": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422628352, + "virtualBytes": 4117934080, + "minorFaults": 82219, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 83.3109970000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019636, + "firstGuestHostCallMs": 28.340047, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 63.257904, + "phases": [ + { + "name": "Engine", + "ms": 0.004707 + }, + { + "name": "canonicalPreopens", + "ms": 0.11118800000000001 + }, + { + "name": "moduleRead", + "ms": 11.954361 + }, + { + "name": "profileValidation", + "ms": 12.605722 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.018307 + }, + { + "name": "Linker", + "ms": 0.288585 + }, + { + "name": "Store", + "ms": 0.031010000000000003 + }, + { + "name": "Instance", + "ms": 1.733786 + }, + { + "name": "signalMaskInit", + "ms": 0.078704 + }, + { + "name": "entrypointLookup", + "ms": 0.006706 + }, + { + "name": "wasi.start", + "ms": 34.88951 + }, + { + "name": "Store.teardown", + "ms": 0.028922 + } + ] + }, + "memory": { + "start": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422628352, + "virtualBytes": 4117934080, + "minorFaults": 82219, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 423431168, + "virtualBytes": 12846845952, + "minorFaults": 82299, + "majorFaults": 1 + }, + "end": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82299, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 74.20443399999931, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.026414, + "firstGuestHostCallMs": 25.672956, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 55.324585, + "phases": [ + { + "name": "Engine", + "ms": 0.00232 + }, + { + "name": "canonicalPreopens", + "ms": 0.118256 + }, + { + "name": "moduleRead", + "ms": 11.353478 + }, + { + "name": "profileValidation", + "ms": 12.399578 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009715 + }, + { + "name": "Linker", + "ms": 0.21296400000000001 + }, + { + "name": "Store", + "ms": 0.016106000000000002 + }, + { + "name": "Instance", + "ms": 0.045786 + }, + { + "name": "signalMaskInit", + "ms": 0.07439 + }, + { + "name": "entrypointLookup", + "ms": 0.005028 + }, + { + "name": "wasi.start", + "ms": 29.61886 + }, + { + "name": "Store.teardown", + "ms": 0.034907 + } + ] + }, + "memory": { + "start": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82299, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 423432192, + "virtualBytes": 12846845952, + "minorFaults": 82379, + "majorFaults": 1 + }, + "end": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82379, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 80.53494499999942, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015120999999999999, + "firstGuestHostCallMs": 25.794923, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.766546, + "phases": [ + { + "name": "Engine", + "ms": 0.002272 + }, + { + "name": "canonicalPreopens", + "ms": 0.11395899999999999 + }, + { + "name": "moduleRead", + "ms": 11.545235 + }, + { + "name": "profileValidation", + "ms": 12.232415999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011718000000000001 + }, + { + "name": "Linker", + "ms": 0.219149 + }, + { + "name": "Store", + "ms": 0.020413 + }, + { + "name": "Instance", + "ms": 0.060995 + }, + { + "name": "signalMaskInit", + "ms": 0.103214 + }, + { + "name": "entrypointLookup", + "ms": 0.007019 + }, + { + "name": "wasi.start", + "ms": 34.975736 + }, + { + "name": "Store.teardown", + "ms": 0.034524 + } + ] + }, + "memory": { + "start": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82379, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 423432192, + "virtualBytes": 12846845952, + "minorFaults": 82459, + "majorFaults": 1 + }, + "end": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82459, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1560.877677000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011351, + "firstGuestHostCallMs": 1533.927711, + "firstOutputMs": 1540.2962389999998, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1540.647248, + "phases": [ + { + "name": "Engine", + "ms": 0.001819 + }, + { + "name": "canonicalPreopens", + "ms": 0.105862 + }, + { + "name": "moduleRead", + "ms": 6.792631 + }, + { + "name": "profileValidation", + "ms": 6.350484 + }, + { + "name": "moduleCompile", + "ms": 1519.480349 + }, + { + "name": "importValidation", + "ms": 0.012649 + }, + { + "name": "Linker", + "ms": 0.193672 + }, + { + "name": "Store", + "ms": 0.01745 + }, + { + "name": "Instance", + "ms": 0.16103499999999998 + }, + { + "name": "signalMaskInit", + "ms": 0.049632 + }, + { + "name": "entrypointLookup", + "ms": 0.0033799999999999998 + }, + { + "name": "wasi.start", + "ms": 6.5971210000000005 + }, + { + "name": "Store.teardown", + "ms": 0.061951000000000006 + } + ] + }, + "memory": { + "start": { + "rssBytes": 419704832, + "peakRssBytes": 420438016, + "pssBytes": 422629376, + "virtualBytes": 4117934080, + "minorFaults": 82459, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430710784, + "virtualBytes": 8489787392, + "minorFaults": 83341, + "majorFaults": 1 + }, + "end": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430678016, + "virtualBytes": 4125331456, + "minorFaults": 83341, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4821794, + "wasmtimeProcessRetainedRssBytes": 419704832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 42.9066909999965, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017711, + "firstGuestHostCallMs": 15.606427, + "firstOutputMs": 22.354183, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.566658999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.002352 + }, + { + "name": "canonicalPreopens", + "ms": 0.16414700000000002 + }, + { + "name": "moduleRead", + "ms": 6.763737 + }, + { + "name": "profileValidation", + "ms": 6.456537 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011916000000000001 + }, + { + "name": "Linker", + "ms": 0.206943 + }, + { + "name": "Store", + "ms": 0.016761 + }, + { + "name": "Instance", + "ms": 1.132673 + }, + { + "name": "signalMaskInit", + "ms": 0.082495 + }, + { + "name": "entrypointLookup", + "ms": 0.003888 + }, + { + "name": "wasi.start", + "ms": 6.916743 + }, + { + "name": "Store.teardown", + "ms": 0.038721 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430678016, + "virtualBytes": 4125331456, + "minorFaults": 83341, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 431185920, + "virtualBytes": 8489787392, + "minorFaults": 83388, + "majorFaults": 1 + }, + "end": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430813184, + "virtualBytes": 4125331456, + "minorFaults": 83388, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 42.8490199999942, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01332, + "firstGuestHostCallMs": 15.802931, + "firstOutputMs": 22.23611, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.44385, + "phases": [ + { + "name": "Engine", + "ms": 0.0019649999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.105282 + }, + { + "name": "moduleRead", + "ms": 6.855995999999999 + }, + { + "name": "profileValidation", + "ms": 6.6795789999999995 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011314999999999999 + }, + { + "name": "Linker", + "ms": 0.22917 + }, + { + "name": "Store", + "ms": 0.020597 + }, + { + "name": "Instance", + "ms": 1.040288 + }, + { + "name": "signalMaskInit", + "ms": 0.07212199999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.005967 + }, + { + "name": "wasi.start", + "ms": 6.595519 + }, + { + "name": "Store.teardown", + "ms": 0.037308999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430813184, + "virtualBytes": 4125331456, + "minorFaults": 83388, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 431328256, + "virtualBytes": 8489787392, + "minorFaults": 83436, + "majorFaults": 1 + }, + "end": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430828544, + "virtualBytes": 4125331456, + "minorFaults": 83436, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 44.693249999996624, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015056, + "firstGuestHostCallMs": 15.982173999999999, + "firstOutputMs": 26.544175, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 26.754569, + "phases": [ + { + "name": "Engine", + "ms": 0.001885 + }, + { + "name": "canonicalPreopens", + "ms": 0.13317299999999999 + }, + { + "name": "moduleRead", + "ms": 7.114203000000001 + }, + { + "name": "profileValidation", + "ms": 6.6696610000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010998 + }, + { + "name": "Linker", + "ms": 0.21922 + }, + { + "name": "Store", + "ms": 0.018049 + }, + { + "name": "Instance", + "ms": 0.779051 + }, + { + "name": "signalMaskInit", + "ms": 0.08615500000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.003242 + }, + { + "name": "wasi.start", + "ms": 10.899567000000001 + }, + { + "name": "Store.teardown", + "ms": 0.040689 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430828544, + "virtualBytes": 4125331456, + "minorFaults": 83436, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 431311872, + "virtualBytes": 8489787392, + "minorFaults": 83486, + "majorFaults": 1 + }, + "end": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430853120, + "virtualBytes": 4125331456, + "minorFaults": 83486, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 44.221361000003526, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012723, + "firstGuestHostCallMs": 15.027787, + "firstOutputMs": 20.789949, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 21.080572, + "phases": [ + { + "name": "Engine", + "ms": 0.001621 + }, + { + "name": "canonicalPreopens", + "ms": 0.150213 + }, + { + "name": "moduleRead", + "ms": 5.899695 + }, + { + "name": "profileValidation", + "ms": 6.953133 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013132000000000001 + }, + { + "name": "Linker", + "ms": 0.213317 + }, + { + "name": "Store", + "ms": 0.020085000000000002 + }, + { + "name": "Instance", + "ms": 0.952646 + }, + { + "name": "signalMaskInit", + "ms": 0.050483 + }, + { + "name": "entrypointLookup", + "ms": 0.003359 + }, + { + "name": "wasi.start", + "ms": 5.967141 + }, + { + "name": "Store.teardown", + "ms": 0.051079 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430853120, + "virtualBytes": 4125331456, + "minorFaults": 83486, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 431356928, + "virtualBytes": 8489787392, + "minorFaults": 83530, + "majorFaults": 1 + }, + "end": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430853120, + "virtualBytes": 4125331456, + "minorFaults": 83530, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1272.3886730000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021094, + "firstGuestHostCallMs": 1246.8107479999999, + "firstOutputMs": 1249.351722, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1249.5206560000001, + "phases": [ + { + "name": "Engine", + "ms": 0.0024040000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.18917 + }, + { + "name": "moduleRead", + "ms": 5.064836 + }, + { + "name": "profileValidation", + "ms": 7.224682 + }, + { + "name": "moduleCompile", + "ms": 1232.409154 + }, + { + "name": "importValidation", + "ms": 0.008881 + }, + { + "name": "Linker", + "ms": 0.22217 + }, + { + "name": "Store", + "ms": 0.021626 + }, + { + "name": "Instance", + "ms": 1.094878 + }, + { + "name": "signalMaskInit", + "ms": 0.08205899999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.0028640000000000002 + }, + { + "name": "wasi.start", + "ms": 2.605254 + }, + { + "name": "Store.teardown", + "ms": 0.045815 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427134976, + "peakRssBytes": 427765760, + "pssBytes": 430853120, + "virtualBytes": 4125331456, + "minorFaults": 83530, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432668672, + "peakRssBytes": 435081216, + "pssBytes": 436321280, + "virtualBytes": 4130811904, + "minorFaults": 83918, + "majorFaults": 1 + }, + "end": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436321280, + "virtualBytes": 4130799616, + "minorFaults": 83918, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6341274, + "wasmtimeProcessRetainedRssBytes": 427134976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 37.06238999999914, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.037495, + "firstGuestHostCallMs": 12.917858, + "firstOutputMs": 15.637014, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 16.999359, + "phases": [ + { + "name": "Engine", + "ms": 0.0026690000000000004 + }, + { + "name": "canonicalPreopens", + "ms": 0.099213 + }, + { + "name": "moduleRead", + "ms": 5.169914 + }, + { + "name": "profileValidation", + "ms": 6.806414999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009562 + }, + { + "name": "Linker", + "ms": 0.235417 + }, + { + "name": "Store", + "ms": 0.025866 + }, + { + "name": "Instance", + "ms": 0.069908 + }, + { + "name": "signalMaskInit", + "ms": 0.03664 + }, + { + "name": "entrypointLookup", + "ms": 0.003715 + }, + { + "name": "wasi.start", + "ms": 2.792843 + }, + { + "name": "Store.teardown", + "ms": 1.2318369999999998 + } + ] + }, + "memory": { + "start": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436321280, + "virtualBytes": 4130799616, + "minorFaults": 83918, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436374528, + "virtualBytes": 8495255552, + "minorFaults": 83963, + "majorFaults": 1 + }, + "end": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436321280, + "virtualBytes": 4130799616, + "minorFaults": 83963, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 33.370695999998134, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.029078, + "firstGuestHostCallMs": 11.559945, + "firstOutputMs": 13.472192, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.596939, + "phases": [ + { + "name": "Engine", + "ms": 0.002356 + }, + { + "name": "canonicalPreopens", + "ms": 0.225158 + }, + { + "name": "moduleRead", + "ms": 5.302289 + }, + { + "name": "profileValidation", + "ms": 4.962522 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009176 + }, + { + "name": "Linker", + "ms": 0.204871 + }, + { + "name": "Store", + "ms": 0.020469 + }, + { + "name": "Instance", + "ms": 0.342897 + }, + { + "name": "signalMaskInit", + "ms": 0.031559000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.003699 + }, + { + "name": "wasi.start", + "ms": 1.977539 + }, + { + "name": "Store.teardown", + "ms": 0.033613 + } + ] + }, + "memory": { + "start": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436321280, + "virtualBytes": 4130799616, + "minorFaults": 83963, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436374528, + "virtualBytes": 4130811904, + "minorFaults": 84008, + "majorFaults": 1 + }, + "end": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84008, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 33.9224549999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013356, + "firstGuestHostCallMs": 11.370468, + "firstOutputMs": 13.257235, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.381692000000001, + "phases": [ + { + "name": "Engine", + "ms": 0.001639 + }, + { + "name": "canonicalPreopens", + "ms": 0.13160599999999997 + }, + { + "name": "moduleRead", + "ms": 5.236548 + }, + { + "name": "profileValidation", + "ms": 4.880217 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009295 + }, + { + "name": "Linker", + "ms": 0.20366199999999998 + }, + { + "name": "Store", + "ms": 0.016602000000000002 + }, + { + "name": "Instance", + "ms": 0.424291 + }, + { + "name": "signalMaskInit", + "ms": 0.02403 + }, + { + "name": "entrypointLookup", + "ms": 0.003578 + }, + { + "name": "wasi.start", + "ms": 1.950099 + }, + { + "name": "Store.teardown", + "ms": 0.037252999999999994 + } + ] + }, + "memory": { + "start": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84008, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436375552, + "virtualBytes": 4130811904, + "minorFaults": 84053, + "majorFaults": 1 + }, + "end": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84053, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 32.89695299999585, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010702, + "firstGuestHostCallMs": 10.838602, + "firstOutputMs": 13.026743, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.303722, + "phases": [ + { + "name": "Engine", + "ms": 0.001574 + }, + { + "name": "canonicalPreopens", + "ms": 0.106597 + }, + { + "name": "moduleRead", + "ms": 5.166288 + }, + { + "name": "profileValidation", + "ms": 4.815324 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00938 + }, + { + "name": "Linker", + "ms": 0.20196799999999998 + }, + { + "name": "Store", + "ms": 0.015780000000000002 + }, + { + "name": "Instance", + "ms": 0.043707 + }, + { + "name": "signalMaskInit", + "ms": 0.045685 + }, + { + "name": "entrypointLookup", + "ms": 0.002423 + }, + { + "name": "wasi.start", + "ms": 2.251949 + }, + { + "name": "Store.teardown", + "ms": 0.18077100000000002 + } + ] + }, + "memory": { + "start": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84053, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 434700288, + "peakRssBytes": 435081216, + "pssBytes": 436375552, + "virtualBytes": 8495255552, + "minorFaults": 84098, + "majorFaults": 1 + }, + "end": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84098, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3629.959167000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016931, + "firstGuestHostCallMs": 3603.774853, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3612.1925610000003, + "phases": [ + { + "name": "Engine", + "ms": 0.001874 + }, + { + "name": "canonicalPreopens", + "ms": 0.1079 + }, + { + "name": "moduleRead", + "ms": 11.282687 + }, + { + "name": "profileValidation", + "ms": 14.6115 + }, + { + "name": "moduleCompile", + "ms": 3575.9013400000003 + }, + { + "name": "importValidation", + "ms": 0.012571 + }, + { + "name": "Linker", + "ms": 0.198677 + }, + { + "name": "Store", + "ms": 0.017587 + }, + { + "name": "Instance", + "ms": 0.176303 + }, + { + "name": "signalMaskInit", + "ms": 0.081768 + }, + { + "name": "entrypointLookup", + "ms": 0.004393 + }, + { + "name": "wasi.start", + "ms": 6.931978999999999 + }, + { + "name": "Store.teardown", + "ms": 1.480488 + } + ] + }, + "memory": { + "start": { + "rssBytes": 432603136, + "peakRssBytes": 435081216, + "pssBytes": 436322304, + "virtualBytes": 4130799616, + "minorFaults": 84098, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 449675264, + "peakRssBytes": 449859584, + "pssBytes": 453513216, + "virtualBytes": 8511672320, + "minorFaults": 84673, + "majorFaults": 1 + }, + "end": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84673, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7573683, + "wasmtimeProcessRetainedRssBytes": 432603136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 63.756554999999935, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014396, + "firstGuestHostCallMs": 30.160346, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 42.558349, + "phases": [ + { + "name": "Engine", + "ms": 0.0015400000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.192687 + }, + { + "name": "moduleRead", + "ms": 11.857724 + }, + { + "name": "profileValidation", + "ms": 14.724352 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012067 + }, + { + "name": "Linker", + "ms": 0.21478 + }, + { + "name": "Store", + "ms": 0.018566000000000003 + }, + { + "name": "Instance", + "ms": 1.602462 + }, + { + "name": "signalMaskInit", + "ms": 0.047481999999999996 + }, + { + "name": "entrypointLookup", + "ms": 0.003603 + }, + { + "name": "wasi.start", + "ms": 12.486236 + }, + { + "name": "Store.teardown", + "ms": 0.047205 + } + ] + }, + "memory": { + "start": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84673, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 449679360, + "peakRssBytes": 449859584, + "pssBytes": 453492736, + "virtualBytes": 8511672320, + "minorFaults": 84765, + "majorFaults": 1 + }, + "end": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84765, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 66.69951600000059, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018142, + "firstGuestHostCallMs": 26.85655, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 45.782784, + "phases": [ + { + "name": "Engine", + "ms": 0.0018679999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.15296 + }, + { + "name": "moduleRead", + "ms": 10.36478 + }, + { + "name": "profileValidation", + "ms": 14.630517 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013163 + }, + { + "name": "Linker", + "ms": 0.222352 + }, + { + "name": "Store", + "ms": 0.016777 + }, + { + "name": "Instance", + "ms": 0.049852 + }, + { + "name": "signalMaskInit", + "ms": 0.047182 + }, + { + "name": "entrypointLookup", + "ms": 0.0032990000000000003 + }, + { + "name": "wasi.start", + "ms": 18.887677 + }, + { + "name": "Store.teardown", + "ms": 0.053292 + } + ] + }, + "memory": { + "start": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84765, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 449679360, + "peakRssBytes": 449859584, + "pssBytes": 453492736, + "virtualBytes": 8511672320, + "minorFaults": 84857, + "majorFaults": 1 + }, + "end": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84857, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 67.80699500000628, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016323, + "firstGuestHostCallMs": 27.68156, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 46.548728, + "phases": [ + { + "name": "Engine", + "ms": 0.0018670000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.143442 + }, + { + "name": "moduleRead", + "ms": 11.144565 + }, + { + "name": "profileValidation", + "ms": 14.68189 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012179 + }, + { + "name": "Linker", + "ms": 0.213482 + }, + { + "name": "Store", + "ms": 0.01626 + }, + { + "name": "Instance", + "ms": 0.05248 + }, + { + "name": "signalMaskInit", + "ms": 0.060179 + }, + { + "name": "entrypointLookup", + "ms": 0.002777 + }, + { + "name": "wasi.start", + "ms": 18.785084 + }, + { + "name": "Store.teardown", + "ms": 0.064206 + } + ] + }, + "memory": { + "start": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 84857, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 449679360, + "peakRssBytes": 449859584, + "pssBytes": 453492736, + "virtualBytes": 8511672320, + "minorFaults": 84949, + "majorFaults": 1 + }, + "end": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452852736, + "virtualBytes": 4147216384, + "minorFaults": 84949, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 76.93699599999673, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015975, + "firstGuestHostCallMs": 29.511064, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.435483, + "phases": [ + { + "name": "Engine", + "ms": 0.0017740000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.111033 + }, + { + "name": "moduleRead", + "ms": 11.959935999999999 + }, + { + "name": "profileValidation", + "ms": 15.13673 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013767999999999999 + }, + { + "name": "Linker", + "ms": 0.227633 + }, + { + "name": "Store", + "ms": 0.019871999999999997 + }, + { + "name": "Instance", + "ms": 0.566874 + }, + { + "name": "signalMaskInit", + "ms": 0.06333000000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.006179 + }, + { + "name": "wasi.start", + "ms": 23.923156000000002 + }, + { + "name": "Store.teardown", + "ms": 0.047311 + } + ] + }, + "memory": { + "start": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452852736, + "virtualBytes": 4147216384, + "minorFaults": 84949, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 449679360, + "peakRssBytes": 449859584, + "pssBytes": 453491712, + "virtualBytes": 8511672320, + "minorFaults": 85041, + "majorFaults": 1 + }, + "end": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452853760, + "virtualBytes": 4147216384, + "minorFaults": 85041, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3922.614549999991, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01836, + "firstGuestHostCallMs": 3898.524316, + "firstOutputMs": 3899.483375, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3899.677127, + "phases": [ + { + "name": "Engine", + "ms": 0.002194 + }, + { + "name": "canonicalPreopens", + "ms": 0.119818 + }, + { + "name": "moduleRead", + "ms": 13.104294000000001 + }, + { + "name": "profileValidation", + "ms": 17.104912 + }, + { + "name": "moduleCompile", + "ms": 3866.0648229999997 + }, + { + "name": "importValidation", + "ms": 0.012643000000000001 + }, + { + "name": "Linker", + "ms": 0.194666 + }, + { + "name": "Store", + "ms": 0.017099 + }, + { + "name": "Instance", + "ms": 0.189267 + }, + { + "name": "signalMaskInit", + "ms": 0.15115699999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.003473 + }, + { + "name": "wasi.start", + "ms": 1.0983939999999999 + }, + { + "name": "Store.teardown", + "ms": 0.038051 + } + ] + }, + "memory": { + "start": { + "rssBytes": 449134592, + "peakRssBytes": 449859584, + "pssBytes": 452852736, + "virtualBytes": 4147216384, + "minorFaults": 85041, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165824512, + "minorFaults": 87096, + "majorFaults": 1 + }, + "end": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87096, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11149584, + "wasmtimeProcessRetainedRssBytes": 449134592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 54.252659000005224, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.054388, + "firstGuestHostCallMs": 31.857898000000002, + "firstOutputMs": 32.525272, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 32.695422, + "phases": [ + { + "name": "Engine", + "ms": 0.002082 + }, + { + "name": "canonicalPreopens", + "ms": 0.15085500000000002 + }, + { + "name": "moduleRead", + "ms": 13.894985 + }, + { + "name": "profileValidation", + "ms": 15.842924000000002 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015312 + }, + { + "name": "Linker", + "ms": 0.211474 + }, + { + "name": "Store", + "ms": 0.016334 + }, + { + "name": "Instance", + "ms": 0.052773 + }, + { + "name": "signalMaskInit", + "ms": 0.057472 + }, + { + "name": "entrypointLookup", + "ms": 0.003374 + }, + { + "name": "wasi.start", + "ms": 0.811986 + }, + { + "name": "Store.teardown", + "ms": 0.028612 + } + ] + }, + "memory": { + "start": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87096, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471502848, + "virtualBytes": 4165824512, + "minorFaults": 87136, + "majorFaults": 1 + }, + "end": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87136, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 53.15050099999644, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.030997999999999998, + "firstGuestHostCallMs": 32.957245, + "firstOutputMs": 34.055076, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 34.246184, + "phases": [ + { + "name": "Engine", + "ms": 0.0020150000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.129224 + }, + { + "name": "moduleRead", + "ms": 13.991803 + }, + { + "name": "profileValidation", + "ms": 16.182624 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015164 + }, + { + "name": "Linker", + "ms": 0.205462 + }, + { + "name": "Store", + "ms": 0.016874 + }, + { + "name": "Instance", + "ms": 0.047436 + }, + { + "name": "signalMaskInit", + "ms": 0.08208399999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.003502 + }, + { + "name": "wasi.start", + "ms": 1.9538 + }, + { + "name": "Store.teardown", + "ms": 0.029964 + } + ] + }, + "memory": { + "start": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87136, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471502848, + "virtualBytes": 8527900672, + "minorFaults": 87176, + "majorFaults": 1 + }, + "end": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87176, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 54.66141800000332, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.026013, + "firstGuestHostCallMs": 31.142785999999997, + "firstOutputMs": 32.180074000000005, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 32.404469999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.00178 + }, + { + "name": "canonicalPreopens", + "ms": 0.11684499999999999 + }, + { + "name": "moduleRead", + "ms": 13.082787 + }, + { + "name": "profileValidation", + "ms": 16.00039 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016422 + }, + { + "name": "Linker", + "ms": 0.209936 + }, + { + "name": "Store", + "ms": 0.016984 + }, + { + "name": "Instance", + "ms": 0.050358 + }, + { + "name": "signalMaskInit", + "ms": 0.064784 + }, + { + "name": "entrypointLookup", + "ms": 0.003 + }, + { + "name": "wasi.start", + "ms": 1.22723 + }, + { + "name": "Store.teardown", + "ms": 0.028846 + } + ] + }, + "memory": { + "start": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87176, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471502848, + "virtualBytes": 4165824512, + "minorFaults": 87216, + "majorFaults": 1 + }, + "end": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87216, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 52.47720500000287, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016782, + "firstGuestHostCallMs": 31.11912, + "firstOutputMs": 31.949743, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.107655, + "phases": [ + { + "name": "Engine", + "ms": 0.001896 + }, + { + "name": "canonicalPreopens", + "ms": 0.14228400000000002 + }, + { + "name": "moduleRead", + "ms": 12.713519 + }, + { + "name": "profileValidation", + "ms": 16.331533 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.014652 + }, + { + "name": "Linker", + "ms": 0.209861 + }, + { + "name": "Store", + "ms": 0.016907 + }, + { + "name": "Instance", + "ms": 0.047333 + }, + { + "name": "signalMaskInit", + "ms": 0.074714 + }, + { + "name": "entrypointLookup", + "ms": 0.003478 + }, + { + "name": "wasi.start", + "ms": 0.980503 + }, + { + "name": "Store.teardown", + "ms": 0.9905370000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87216, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471502848, + "virtualBytes": 8530268160, + "minorFaults": 87256, + "majorFaults": 1 + }, + "end": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87256, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 708.9819379999972, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.028624, + "firstGuestHostCallMs": 643.8717819999999, + "firstOutputMs": 685.661085, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 685.87258, + "phases": [ + { + "name": "Engine", + "ms": 0.0020109999999999998 + }, + { + "name": "canonicalPreopens", + "ms": 0.12375900000000001 + }, + { + "name": "moduleRead", + "ms": 6.658805 + }, + { + "name": "profileValidation", + "ms": 2.483806 + }, + { + "name": "moduleCompile", + "ms": 633.2536560000001 + }, + { + "name": "importValidation", + "ms": 0.005856 + }, + { + "name": "Linker", + "ms": 0.17985099999999998 + }, + { + "name": "Store", + "ms": 0.017355 + }, + { + "name": "Instance", + "ms": 0.25339 + }, + { + "name": "signalMaskInit", + "ms": 0.11831499999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.004201 + }, + { + "name": "wasi.start", + "ms": 41.996002999999995 + }, + { + "name": "Store.teardown", + "ms": 0.046192000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 467730432, + "peakRssBytes": 468443136, + "pssBytes": 471449600, + "virtualBytes": 4165812224, + "minorFaults": 87256, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471797760, + "peakRssBytes": 472092672, + "pssBytes": 475824128, + "virtualBytes": 8534130688, + "minorFaults": 88275, + "majorFaults": 1 + }, + "end": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88275, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15015649, + "wasmtimeProcessRetainedRssBytes": 467730432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 73.43025200000557, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.04977, + "firstGuestHostCallMs": 10.704102, + "firstOutputMs": 54.470574, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 55.087679, + "phases": [ + { + "name": "Engine", + "ms": 0.002249 + }, + { + "name": "canonicalPreopens", + "ms": 0.105942 + }, + { + "name": "moduleRead", + "ms": 6.625045 + }, + { + "name": "profileValidation", + "ms": 2.471615 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006433 + }, + { + "name": "Linker", + "ms": 0.208022 + }, + { + "name": "Store", + "ms": 0.016398000000000003 + }, + { + "name": "Instance", + "ms": 0.452528 + }, + { + "name": "signalMaskInit", + "ms": 0.019969 + }, + { + "name": "entrypointLookup", + "ms": 0.003111 + }, + { + "name": "wasi.start", + "ms": 43.985507 + }, + { + "name": "Store.teardown", + "ms": 0.47248599999999996 + } + ] + }, + "memory": { + "start": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88275, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475701248, + "virtualBytes": 8534130688, + "minorFaults": 88321, + "majorFaults": 1 + }, + "end": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88321, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 68.36556399999245, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.020966, + "firstGuestHostCallMs": 10.462942, + "firstOutputMs": 50.182412, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 50.446725, + "phases": [ + { + "name": "Engine", + "ms": 0.0019039999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.155709 + }, + { + "name": "moduleRead", + "ms": 6.77997 + }, + { + "name": "profileValidation", + "ms": 2.4724809999999997 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006719 + }, + { + "name": "Linker", + "ms": 0.210719 + }, + { + "name": "Store", + "ms": 0.016037 + }, + { + "name": "Instance", + "ms": 0.044599 + }, + { + "name": "signalMaskInit", + "ms": 0.020175 + }, + { + "name": "entrypointLookup", + "ms": 0.002384 + }, + { + "name": "wasi.start", + "ms": 39.913662 + }, + { + "name": "Store.teardown", + "ms": 0.136809 + } + ] + }, + "memory": { + "start": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88321, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475701248, + "virtualBytes": 8534130688, + "minorFaults": 88367, + "majorFaults": 1 + }, + "end": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88367, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 74.93622299999697, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016564, + "firstGuestHostCallMs": 10.230995, + "firstOutputMs": 55.052034, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 55.523348999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.002364 + }, + { + "name": "canonicalPreopens", + "ms": 0.140239 + }, + { + "name": "moduleRead", + "ms": 5.600063 + }, + { + "name": "profileValidation", + "ms": 2.427388 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007386 + }, + { + "name": "Linker", + "ms": 0.210812 + }, + { + "name": "Store", + "ms": 0.015747 + }, + { + "name": "Instance", + "ms": 0.707229 + }, + { + "name": "signalMaskInit", + "ms": 0.052401 + }, + { + "name": "entrypointLookup", + "ms": 0.003254 + }, + { + "name": "wasi.start", + "ms": 45.323211 + }, + { + "name": "Store.teardown", + "ms": 0.329769 + } + ] + }, + "memory": { + "start": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475312128, + "virtualBytes": 4169674752, + "minorFaults": 88367, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475701248, + "virtualBytes": 8534130688, + "minorFaults": 88413, + "majorFaults": 1 + }, + "end": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475311104, + "virtualBytes": 4169674752, + "minorFaults": 88413, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 75.6135890000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.022342, + "firstGuestHostCallMs": 10.432131, + "firstOutputMs": 57.012179, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 57.183820000000004, + "phases": [ + { + "name": "Engine", + "ms": 0.003077 + }, + { + "name": "canonicalPreopens", + "ms": 0.165987 + }, + { + "name": "moduleRead", + "ms": 6.289592 + }, + { + "name": "profileValidation", + "ms": 2.466158 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006955 + }, + { + "name": "Linker", + "ms": 0.212495 + }, + { + "name": "Store", + "ms": 0.019295 + }, + { + "name": "Instance", + "ms": 0.47042999999999996 + }, + { + "name": "signalMaskInit", + "ms": 0.02554 + }, + { + "name": "entrypointLookup", + "ms": 0.004715 + }, + { + "name": "wasi.start", + "ms": 46.786477 + }, + { + "name": "Store.teardown", + "ms": 0.042261 + } + ] + }, + "memory": { + "start": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475311104, + "virtualBytes": 4169674752, + "minorFaults": 88413, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475700224, + "virtualBytes": 8534130688, + "minorFaults": 88459, + "majorFaults": 1 + }, + "end": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475311104, + "virtualBytes": 4169674752, + "minorFaults": 88459, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1421.7791129999969, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016541999999999998, + "firstGuestHostCallMs": 1395.464066, + "firstOutputMs": 1398.939856, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1400.591579, + "phases": [ + { + "name": "Engine", + "ms": 0.001986 + }, + { + "name": "canonicalPreopens", + "ms": 0.114727 + }, + { + "name": "moduleRead", + "ms": 6.860707 + }, + { + "name": "profileValidation", + "ms": 4.7144070000000005 + }, + { + "name": "moduleCompile", + "ms": 1382.49439 + }, + { + "name": "importValidation", + "ms": 0.007627999999999999 + }, + { + "name": "Linker", + "ms": 0.19300099999999998 + }, + { + "name": "Store", + "ms": 0.017404000000000003 + }, + { + "name": "Instance", + "ms": 0.260704 + }, + { + "name": "signalMaskInit", + "ms": 0.058336 + }, + { + "name": "entrypointLookup", + "ms": 0.003373 + }, + { + "name": "wasi.start", + "ms": 5.032476999999999 + }, + { + "name": "Store.teardown", + "ms": 0.047165 + } + ] + }, + "memory": { + "start": { + "rssBytes": 471592960, + "peakRssBytes": 472092672, + "pssBytes": 475311104, + "virtualBytes": 4169674752, + "minorFaults": 88459, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 512724992, + "peakRssBytes": 512724992, + "pssBytes": 516644864, + "virtualBytes": 8567570432, + "minorFaults": 112453, + "majorFaults": 1 + }, + "end": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112453, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15648903, + "wasmtimeProcessRetainedRssBytes": 471592960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 37.559370999995735, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.070012, + "firstGuestHostCallMs": 12.563459, + "firstOutputMs": 16.898556, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 18.937925, + "phases": [ + { + "name": "Engine", + "ms": 0.001742 + }, + { + "name": "canonicalPreopens", + "ms": 0.131475 + }, + { + "name": "moduleRead", + "ms": 6.862518 + }, + { + "name": "profileValidation", + "ms": 4.46258 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009195 + }, + { + "name": "Linker", + "ms": 0.208369 + }, + { + "name": "Store", + "ms": 0.017485 + }, + { + "name": "Instance", + "ms": 0.038277 + }, + { + "name": "signalMaskInit", + "ms": 0.044709 + }, + { + "name": "entrypointLookup", + "ms": 0.0027240000000000003 + }, + { + "name": "wasi.start", + "ms": 5.820106 + }, + { + "name": "Store.teardown", + "ms": 0.535585 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112453, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508911616, + "virtualBytes": 8567570432, + "minorFaults": 112481, + "majorFaults": 1 + }, + "end": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112481, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 39.89948699998786, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016022, + "firstGuestHostCallMs": 14.680318, + "firstOutputMs": 18.11314, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 21.06682, + "phases": [ + { + "name": "Engine", + "ms": 0.001392 + }, + { + "name": "canonicalPreopens", + "ms": 0.104177 + }, + { + "name": "moduleRead", + "ms": 6.810982 + }, + { + "name": "profileValidation", + "ms": 5.878817 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008468999999999999 + }, + { + "name": "Linker", + "ms": 0.205752 + }, + { + "name": "Store", + "ms": 0.017426 + }, + { + "name": "Instance", + "ms": 0.038133 + }, + { + "name": "signalMaskInit", + "ms": 0.053328 + }, + { + "name": "entrypointLookup", + "ms": 0.002713 + }, + { + "name": "wasi.start", + "ms": 5.676348 + }, + { + "name": "Store.teardown", + "ms": 1.511477 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112481, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508682240, + "virtualBytes": 8567570432, + "minorFaults": 112509, + "majorFaults": 1 + }, + "end": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112509, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 38.21304200000304, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013808, + "firstGuestHostCallMs": 13.035504999999999, + "firstOutputMs": 17.68133, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 20.094084000000002, + "phases": [ + { + "name": "Engine", + "ms": 0.0015149999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.113777 + }, + { + "name": "moduleRead", + "ms": 7.401845 + }, + { + "name": "profileValidation", + "ms": 4.4217960000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008851999999999999 + }, + { + "name": "Linker", + "ms": 0.21296299999999999 + }, + { + "name": "Store", + "ms": 0.018002 + }, + { + "name": "Instance", + "ms": 0.048428 + }, + { + "name": "signalMaskInit", + "ms": 0.041161 + }, + { + "name": "entrypointLookup", + "ms": 0.003211 + }, + { + "name": "wasi.start", + "ms": 6.047591 + }, + { + "name": "Store.teardown", + "ms": 0.9464579999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112509, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508911616, + "virtualBytes": 8567570432, + "minorFaults": 112537, + "majorFaults": 1 + }, + "end": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112537, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 38.71507000000565, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015006, + "firstGuestHostCallMs": 12.448465, + "firstOutputMs": 15.927726999999999, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.351165, + "phases": [ + { + "name": "Engine", + "ms": 0.001973 + }, + { + "name": "canonicalPreopens", + "ms": 0.111391 + }, + { + "name": "moduleRead", + "ms": 6.859867 + }, + { + "name": "profileValidation", + "ms": 4.383782999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009745 + }, + { + "name": "Linker", + "ms": 0.209543 + }, + { + "name": "Store", + "ms": 0.017062 + }, + { + "name": "Instance", + "ms": 0.040367999999999994 + }, + { + "name": "signalMaskInit", + "ms": 0.068771 + }, + { + "name": "entrypointLookup", + "ms": 0.003382 + }, + { + "name": "wasi.start", + "ms": 4.857032 + }, + { + "name": "Store.teardown", + "ms": 0.037765 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112537, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508911616, + "virtualBytes": 8567570432, + "minorFaults": 112565, + "majorFaults": 1 + }, + "end": { + "rssBytes": 504909824, + "peakRssBytes": 512724992, + "pssBytes": 508628992, + "virtualBytes": 4203114496, + "minorFaults": 112565, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17031397, + "wasmtimeProcessRetainedRssBytes": 504909824, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 2, + "vmSetupMs": 444.05221399999573, + "fixtureSetupMs": 394.11860099999467, + "baseline": { + "rssBytes": 240877568, + "peakRssBytes": 248307712, + "pssBytes": 241887232, + "virtualBytes": 3886764032, + "minorFaults": 55053, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362541056, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 378028032, + "peakRssBytes": 488099840, + "pssBytes": 380020736, + "virtualBytes": 4051148800, + "minorFaults": 835577, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 137150464, + "peakRssBytes": 239792128, + "pssBytes": 138133504, + "virtualBytes": 164384768, + "minorFaults": 780524, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 61.38473900000099, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 0.984192 + }, + { + "name": "WebAssembly.Module", + "ms": 0.1306 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.058555 + }, + { + "name": "wasi.start", + "ms": 0.088413 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 240877568, + "peakRssBytes": 248307712, + "pssBytes": 241895424, + "virtualBytes": 3888877568, + "minorFaults": 55055, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276123648, + "peakRssBytes": 276721664, + "pssBytes": 263124992, + "virtualBytes": 4641091584, + "minorFaults": 65960, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261775360, + "peakRssBytes": 276721664, + "pssBytes": 263124992, + "virtualBytes": 3955986432, + "minorFaults": 65960, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240877568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261775360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 61.66583300000639, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.043755 + }, + { + "name": "WebAssembly.Module", + "ms": 0.179088 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.067636 + }, + { + "name": "wasi.start", + "ms": 0.093588 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261775360, + "peakRssBytes": 276721664, + "pssBytes": 263124992, + "virtualBytes": 3955986432, + "minorFaults": 65960, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276230144, + "peakRssBytes": 276766720, + "pssBytes": 263215104, + "virtualBytes": 4640567296, + "minorFaults": 72231, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261820416, + "peakRssBytes": 276766720, + "pssBytes": 263215104, + "virtualBytes": 3955986432, + "minorFaults": 72231, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261775360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261820416, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 59.991568000012194, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 2.098807 + }, + { + "name": "WebAssembly.Module", + "ms": 0.142355 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.060481 + }, + { + "name": "wasi.start", + "ms": 0.088968 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261820416, + "peakRssBytes": 276766720, + "pssBytes": 263215104, + "virtualBytes": 3955986432, + "minorFaults": 72231, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276275200, + "peakRssBytes": 276766720, + "pssBytes": 263272448, + "virtualBytes": 4640829440, + "minorFaults": 78492, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261804032, + "peakRssBytes": 276766720, + "pssBytes": 263272448, + "virtualBytes": 3955986432, + "minorFaults": 78492, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261820416, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261804032, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 62.526891999994405, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.274063 + }, + { + "name": "WebAssembly.Module", + "ms": 0.171957 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.109132 + }, + { + "name": "wasi.start", + "ms": 0.140346 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261804032, + "peakRssBytes": 276766720, + "pssBytes": 263272448, + "virtualBytes": 3955986432, + "minorFaults": 78492, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276566016, + "peakRssBytes": 276795392, + "pssBytes": 263526400, + "virtualBytes": 4641353728, + "minorFaults": 84802, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261849088, + "peakRssBytes": 276795392, + "pssBytes": 263526400, + "virtualBytes": 3955986432, + "minorFaults": 84802, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261804032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261849088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 66.481970000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.122779 + }, + { + "name": "WebAssembly.Module", + "ms": 0.189539 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.062428 + }, + { + "name": "wasi.start", + "ms": 0.09084 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261849088, + "peakRssBytes": 276795392, + "pssBytes": 263526400, + "virtualBytes": 3955986432, + "minorFaults": 84802, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276987904, + "peakRssBytes": 277209088, + "pssBytes": 263791616, + "virtualBytes": 4641353728, + "minorFaults": 91115, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262262784, + "peakRssBytes": 277209088, + "pssBytes": 263791616, + "virtualBytes": 3955986432, + "minorFaults": 91115, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261849088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262262784, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 202.68641900000512, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.841274 + }, + { + "name": "WebAssembly.Module", + "ms": 1.682913 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.291341 + }, + { + "name": "wasi.start", + "ms": 83.719234 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262262784, + "peakRssBytes": 277209088, + "pssBytes": 263791616, + "virtualBytes": 3955986432, + "minorFaults": 91115, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 334647296, + "peakRssBytes": 335187968, + "pssBytes": 330475520, + "virtualBytes": 4694364160, + "minorFaults": 111248, + "majorFaults": 0 + }, + "end": { + "rssBytes": 283631616, + "peakRssBytes": 335187968, + "pssBytes": 284968960, + "virtualBytes": 3955986432, + "minorFaults": 111248, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262262784, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283631616, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 188.18507199999294, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.467958 + }, + { + "name": "WebAssembly.Module", + "ms": 0.961674 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.283951 + }, + { + "name": "wasi.start", + "ms": 79.247946 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 283631616, + "peakRssBytes": 335187968, + "pssBytes": 284968960, + "virtualBytes": 3955986432, + "minorFaults": 111248, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 338411520, + "peakRssBytes": 338411520, + "pssBytes": 339830784, + "virtualBytes": 4694507520, + "minorFaults": 125660, + "majorFaults": 0 + }, + "end": { + "rssBytes": 283889664, + "peakRssBytes": 338411520, + "pssBytes": 285337600, + "virtualBytes": 3955986432, + "minorFaults": 125660, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283631616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283889664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 311.08343899999454, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 34.376692 + }, + { + "name": "WebAssembly.Module", + "ms": 1.073219 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.632798 + }, + { + "name": "wasi.start", + "ms": 84.260056 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 283889664, + "peakRssBytes": 338411520, + "pssBytes": 285337600, + "virtualBytes": 3955986432, + "minorFaults": 125660, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 344772608, + "peakRssBytes": 345313280, + "pssBytes": 341596160, + "virtualBytes": 4696727552, + "minorFaults": 143210, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293392384, + "peakRssBytes": 345313280, + "pssBytes": 172806144, + "virtualBytes": 3958087680, + "minorFaults": 143210, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283889664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293392384, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 193.88430799999333, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.104642 + }, + { + "name": "WebAssembly.Module", + "ms": 1.089826 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.310481 + }, + { + "name": "wasi.start", + "ms": 85.300298 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293662720, + "peakRssBytes": 345313280, + "pssBytes": 194698240, + "virtualBytes": 3958087680, + "minorFaults": 143263, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 349429760, + "peakRssBytes": 349429760, + "pssBytes": 351021056, + "virtualBytes": 4696465408, + "minorFaults": 157452, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293752832, + "peakRssBytes": 349429760, + "pssBytes": 295326720, + "virtualBytes": 3958087680, + "minorFaults": 157452, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293662720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293752832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 214.82807399999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.955644 + }, + { + "name": "WebAssembly.Module", + "ms": 0.964332 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.550431 + }, + { + "name": "wasi.start", + "ms": 92.519548 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293752832, + "peakRssBytes": 349429760, + "pssBytes": 295326720, + "virtualBytes": 3958087680, + "minorFaults": 157452, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 350797824, + "peakRssBytes": 350797824, + "pssBytes": 352180224, + "virtualBytes": 4696465408, + "minorFaults": 171326, + "majorFaults": 0 + }, + "end": { + "rssBytes": 296337408, + "peakRssBytes": 350797824, + "pssBytes": 297687040, + "virtualBytes": 3958087680, + "minorFaults": 171326, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293752832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 296337408, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 282.89680800000497, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.929239 + }, + { + "name": "WebAssembly.Module", + "ms": 3.237841 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.782477 + }, + { + "name": "wasi.start", + "ms": 102.913305 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 296337408, + "peakRssBytes": 350797824, + "pssBytes": 297687040, + "virtualBytes": 3958087680, + "minorFaults": 171326, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402612224, + "peakRssBytes": 402710528, + "pssBytes": 403892224, + "virtualBytes": 5472210944, + "minorFaults": 201639, + "majorFaults": 0 + }, + "end": { + "rssBytes": 321990656, + "peakRssBytes": 402710528, + "pssBytes": 323773440, + "virtualBytes": 4030369792, + "minorFaults": 201639, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 296337408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 321990656, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 284.06915200001094, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.879439 + }, + { + "name": "WebAssembly.Module", + "ms": 2.735955 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.516343 + }, + { + "name": "wasi.start", + "ms": 98.72775 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 321990656, + "peakRssBytes": 402710528, + "pssBytes": 323773440, + "virtualBytes": 4030369792, + "minorFaults": 201639, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 418299904, + "peakRssBytes": 418623488, + "pssBytes": 420145152, + "virtualBytes": 5472735232, + "minorFaults": 229133, + "majorFaults": 0 + }, + "end": { + "rssBytes": 326774784, + "peakRssBytes": 418623488, + "pssBytes": 328624128, + "virtualBytes": 4030369792, + "minorFaults": 229133, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 321990656, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 326774784, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 296.07076300001063, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 39.136873 + }, + { + "name": "WebAssembly.Module", + "ms": 3.446276 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.044836 + }, + { + "name": "wasi.start", + "ms": 111.710612 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 326774784, + "peakRssBytes": 418623488, + "pssBytes": 328624128, + "virtualBytes": 4030369792, + "minorFaults": 229133, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431554560, + "peakRssBytes": 431718400, + "pssBytes": 433435648, + "virtualBytes": 5471948800, + "minorFaults": 259583, + "majorFaults": 0 + }, + "end": { + "rssBytes": 335798272, + "peakRssBytes": 431718400, + "pssBytes": 337503232, + "virtualBytes": 4030369792, + "minorFaults": 259583, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 326774784, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335798272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 289.89052000000083, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.754002 + }, + { + "name": "WebAssembly.Module", + "ms": 2.679932 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.362198 + }, + { + "name": "wasi.start", + "ms": 106.178816 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335798272, + "peakRssBytes": 431718400, + "pssBytes": 337503232, + "virtualBytes": 4030369792, + "minorFaults": 259583, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431706112, + "peakRssBytes": 431833088, + "pssBytes": 433637376, + "virtualBytes": 5472210944, + "minorFaults": 285876, + "majorFaults": 0 + }, + "end": { + "rssBytes": 335826944, + "peakRssBytes": 431833088, + "pssBytes": 337672192, + "virtualBytes": 4030369792, + "minorFaults": 285876, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335798272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335826944, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 289.740850999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 51.996592 + }, + { + "name": "WebAssembly.Module", + "ms": 3.354929 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.075706 + }, + { + "name": "wasi.start", + "ms": 101.079241 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335826944, + "peakRssBytes": 431833088, + "pssBytes": 337672192, + "virtualBytes": 4030369792, + "minorFaults": 285876, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430583808, + "peakRssBytes": 431833088, + "pssBytes": 432583680, + "virtualBytes": 5445767168, + "minorFaults": 311729, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336089088, + "peakRssBytes": 431833088, + "pssBytes": 337852416, + "virtualBytes": 4030369792, + "minorFaults": 311729, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335826944, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336089088, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 149.62543899999582, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.095981 + }, + { + "name": "WebAssembly.Module", + "ms": 2.10386 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.264495 + }, + { + "name": "wasi.start", + "ms": 20.295646 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336089088, + "peakRssBytes": 431833088, + "pssBytes": 337852416, + "virtualBytes": 4030369792, + "minorFaults": 311729, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 406024192, + "peakRssBytes": 431833088, + "pssBytes": 408557568, + "virtualBytes": 4787781632, + "minorFaults": 327452, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336220160, + "peakRssBytes": 431833088, + "pssBytes": 338507776, + "virtualBytes": 4030369792, + "minorFaults": 327452, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336089088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336220160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 176.44064400000207, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 34.752531 + }, + { + "name": "WebAssembly.Module", + "ms": 2.402678 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.282322 + }, + { + "name": "wasi.start", + "ms": 27.406405 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336220160, + "peakRssBytes": 431833088, + "pssBytes": 338507776, + "virtualBytes": 4030369792, + "minorFaults": 327452, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 405368832, + "peakRssBytes": 431833088, + "pssBytes": 407946240, + "virtualBytes": 4787781632, + "minorFaults": 342493, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336076800, + "peakRssBytes": 431833088, + "pssBytes": 338576384, + "virtualBytes": 4030369792, + "minorFaults": 342493, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336220160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336076800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 157.3340850000095, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.735807 + }, + { + "name": "WebAssembly.Module", + "ms": 1.559336 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.800855 + }, + { + "name": "wasi.start", + "ms": 22.406167 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336076800, + "peakRssBytes": 431833088, + "pssBytes": 338576384, + "virtualBytes": 4030369792, + "minorFaults": 342493, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 404639744, + "peakRssBytes": 431833088, + "pssBytes": 407267328, + "virtualBytes": 4787781632, + "minorFaults": 357879, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336220160, + "peakRssBytes": 431833088, + "pssBytes": 338589696, + "virtualBytes": 4030369792, + "minorFaults": 357879, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336076800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336220160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 145.81927399999404, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.549391 + }, + { + "name": "WebAssembly.Module", + "ms": 1.745678 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.424479 + }, + { + "name": "wasi.start", + "ms": 19.843364 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336220160, + "peakRssBytes": 431833088, + "pssBytes": 338589696, + "virtualBytes": 4030369792, + "minorFaults": 357879, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 406155264, + "peakRssBytes": 431833088, + "pssBytes": 408717312, + "virtualBytes": 4787781632, + "minorFaults": 373100, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336084992, + "peakRssBytes": 431833088, + "pssBytes": 338589696, + "virtualBytes": 4030369792, + "minorFaults": 373100, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336220160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336084992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 167.99799600000551, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.513274 + }, + { + "name": "WebAssembly.Module", + "ms": 2.165273 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.566373 + }, + { + "name": "wasi.start", + "ms": 21.93143 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336084992, + "peakRssBytes": 431833088, + "pssBytes": 338589696, + "virtualBytes": 4030369792, + "minorFaults": 373100, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 405585920, + "peakRssBytes": 431833088, + "pssBytes": 408044544, + "virtualBytes": 4787781632, + "minorFaults": 387137, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336228352, + "peakRssBytes": 431833088, + "pssBytes": 338592768, + "virtualBytes": 4030369792, + "minorFaults": 387137, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336084992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336228352, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 123.0204389999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 16.338071 + }, + { + "name": "WebAssembly.Module", + "ms": 2.271482 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.334244 + }, + { + "name": "wasi.start", + "ms": 14.900835 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336228352, + "peakRssBytes": 431833088, + "pssBytes": 338592768, + "virtualBytes": 4030369792, + "minorFaults": 387137, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 381968384, + "peakRssBytes": 431833088, + "pssBytes": 381302784, + "virtualBytes": 4757217280, + "minorFaults": 404468, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352485376, + "peakRssBytes": 431833088, + "pssBytes": 251857920, + "virtualBytes": 4030369792, + "minorFaults": 404468, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336228352, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353026048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 125.96926199999871, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.525467 + }, + { + "name": "WebAssembly.Module", + "ms": 1.439256 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.365624 + }, + { + "name": "wasi.start", + "ms": 15.883563 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353026048, + "peakRssBytes": 431833088, + "pssBytes": 177538048, + "virtualBytes": 4030369792, + "minorFaults": 404571, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403091456, + "peakRssBytes": 431833088, + "pssBytes": 384047104, + "virtualBytes": 4757741568, + "minorFaults": 423624, + "majorFaults": 0 + }, + "end": { + "rssBytes": 349966336, + "peakRssBytes": 431833088, + "pssBytes": 248654848, + "virtualBytes": 4030369792, + "minorFaults": 423624, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353026048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 350507008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 133.6294130000024, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 14.957418 + }, + { + "name": "WebAssembly.Module", + "ms": 1.947519 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.768637 + }, + { + "name": "wasi.start", + "ms": 15.831258 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 350777344, + "peakRssBytes": 431833088, + "pssBytes": 171523072, + "virtualBytes": 4030369792, + "minorFaults": 423825, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 401104896, + "peakRssBytes": 431833088, + "pssBytes": 403641344, + "virtualBytes": 4757622784, + "minorFaults": 442967, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351064064, + "peakRssBytes": 431833088, + "pssBytes": 353703936, + "virtualBytes": 4030369792, + "minorFaults": 442967, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 350777344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351064064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 143.77228000000468, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.053022 + }, + { + "name": "WebAssembly.Module", + "ms": 1.0715 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.786163 + }, + { + "name": "wasi.start", + "ms": 16.448607 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351064064, + "peakRssBytes": 431833088, + "pssBytes": 353702912, + "virtualBytes": 4030369792, + "minorFaults": 442967, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 401199104, + "peakRssBytes": 431833088, + "pssBytes": 403706880, + "virtualBytes": 4757479424, + "minorFaults": 465173, + "majorFaults": 0 + }, + "end": { + "rssBytes": 359239680, + "peakRssBytes": 431833088, + "pssBytes": 121806848, + "virtualBytes": 4030369792, + "minorFaults": 465173, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351064064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 359510016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 128.27110800000082, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 14.844181 + }, + { + "name": "WebAssembly.Module", + "ms": 1.117248 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.657341 + }, + { + "name": "wasi.start", + "ms": 14.535872 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 359510016, + "peakRssBytes": 431833088, + "pssBytes": 133087232, + "virtualBytes": 4030369792, + "minorFaults": 465235, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 410398720, + "peakRssBytes": 431833088, + "pssBytes": 412612608, + "virtualBytes": 4757479424, + "minorFaults": 486125, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362213376, + "peakRssBytes": 431833088, + "pssBytes": 364648448, + "virtualBytes": 4030369792, + "minorFaults": 486125, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 359510016, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362213376, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 215.65722700000333, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 44.98062 + }, + { + "name": "WebAssembly.Module", + "ms": 3.584527 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.829447 + }, + { + "name": "wasi.start", + "ms": 32.170364 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362213376, + "peakRssBytes": 431833088, + "pssBytes": 364926976, + "virtualBytes": 4030369792, + "minorFaults": 486125, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 441397248, + "peakRssBytes": 441417728, + "pssBytes": 443905024, + "virtualBytes": 4821065728, + "minorFaults": 506398, + "majorFaults": 0 + }, + "end": { + "rssBytes": 349720576, + "peakRssBytes": 441417728, + "pssBytes": 352601088, + "virtualBytes": 4030369792, + "minorFaults": 506398, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362213376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349720576, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 228.70629899999767, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 45.799486 + }, + { + "name": "WebAssembly.Module", + "ms": 3.514496 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.441962 + }, + { + "name": "wasi.start", + "ms": 37.291317 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349720576, + "peakRssBytes": 441417728, + "pssBytes": 352601088, + "virtualBytes": 4030369792, + "minorFaults": 506398, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 445591552, + "peakRssBytes": 445763584, + "pssBytes": 448538624, + "virtualBytes": 4821327872, + "minorFaults": 523372, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351879168, + "peakRssBytes": 445763584, + "pssBytes": 354711552, + "virtualBytes": 4030369792, + "minorFaults": 523372, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349720576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351879168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 238.25868600000103, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 47.621135 + }, + { + "name": "WebAssembly.Module", + "ms": 4.124215 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.237672 + }, + { + "name": "wasi.start", + "ms": 47.839973 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351879168, + "peakRssBytes": 445763584, + "pssBytes": 354711552, + "virtualBytes": 4030369792, + "minorFaults": 523372, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 447541248, + "peakRssBytes": 447758336, + "pssBytes": 450401280, + "virtualBytes": 4821327872, + "minorFaults": 539564, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352014336, + "peakRssBytes": 447758336, + "pssBytes": 354711552, + "virtualBytes": 4030369792, + "minorFaults": 539564, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351879168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352014336, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 222.7493439999962, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.061795 + }, + { + "name": "WebAssembly.Module", + "ms": 2.956558 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.920744 + }, + { + "name": "wasi.start", + "ms": 46.779355 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352014336, + "peakRssBytes": 447758336, + "pssBytes": 354711552, + "virtualBytes": 4030369792, + "minorFaults": 539564, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 445915136, + "peakRssBytes": 447758336, + "pssBytes": 448436224, + "virtualBytes": 4821590016, + "minorFaults": 554482, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352317440, + "peakRssBytes": 447758336, + "pssBytes": 354846720, + "virtualBytes": 4030369792, + "minorFaults": 554482, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352014336, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352317440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 230.37622799999372, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.642271 + }, + { + "name": "WebAssembly.Module", + "ms": 4.932981 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.29168 + }, + { + "name": "wasi.start", + "ms": 37.878412 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352317440, + "peakRssBytes": 447758336, + "pssBytes": 354846720, + "virtualBytes": 4030369792, + "minorFaults": 554482, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444760064, + "peakRssBytes": 447758336, + "pssBytes": 447707136, + "virtualBytes": 4769169408, + "minorFaults": 569475, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352083968, + "peakRssBytes": 447758336, + "pssBytes": 354846720, + "virtualBytes": 4030369792, + "minorFaults": 569475, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352317440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352083968, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 261.1860420000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.331135 + }, + { + "name": "WebAssembly.Module", + "ms": 3.543257 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.737777 + }, + { + "name": "wasi.start", + "ms": 5.140779 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352083968, + "peakRssBytes": 447758336, + "pssBytes": 354846720, + "virtualBytes": 4030369792, + "minorFaults": 569475, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 485330944, + "peakRssBytes": 485404672, + "pssBytes": 487420928, + "virtualBytes": 4810989568, + "minorFaults": 590979, + "majorFaults": 0 + }, + "end": { + "rssBytes": 354430976, + "peakRssBytes": 485404672, + "pssBytes": 356440064, + "virtualBytes": 4030951424, + "minorFaults": 590979, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352083968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 354430976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 299.49480400000175, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 73.03197 + }, + { + "name": "WebAssembly.Module", + "ms": 4.696651 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.373326 + }, + { + "name": "wasi.start", + "ms": 6.805341 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 354430976, + "peakRssBytes": 485404672, + "pssBytes": 356440064, + "virtualBytes": 4030951424, + "minorFaults": 590979, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 487419904, + "peakRssBytes": 487546880, + "pssBytes": 489563136, + "virtualBytes": 4812001280, + "minorFaults": 615955, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355393536, + "peakRssBytes": 487546880, + "pssBytes": 357557248, + "virtualBytes": 4031963136, + "minorFaults": 615955, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 354430976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355393536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 248.55402199999662, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.357207 + }, + { + "name": "WebAssembly.Module", + "ms": 3.443155 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.394758 + }, + { + "name": "wasi.start", + "ms": 5.619792 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355393536, + "peakRssBytes": 487546880, + "pssBytes": 357557248, + "virtualBytes": 4031963136, + "minorFaults": 615955, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 487890944, + "peakRssBytes": 488009728, + "pssBytes": 489850880, + "virtualBytes": 4812001280, + "minorFaults": 638632, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355938304, + "peakRssBytes": 488009728, + "pssBytes": 357849088, + "virtualBytes": 4031963136, + "minorFaults": 638632, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355393536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355938304, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 232.23686199999065, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 72.796015 + }, + { + "name": "WebAssembly.Module", + "ms": 4.615626 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.472647 + }, + { + "name": "wasi.start", + "ms": 5.497737 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355938304, + "peakRssBytes": 488009728, + "pssBytes": 357849088, + "virtualBytes": 4031963136, + "minorFaults": 638632, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 487972864, + "peakRssBytes": 488030208, + "pssBytes": 489853952, + "virtualBytes": 4811739136, + "minorFaults": 661287, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355966976, + "peakRssBytes": 488030208, + "pssBytes": 357852160, + "virtualBytes": 4031963136, + "minorFaults": 661287, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355938304, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355966976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 252.451550999991, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 67.100467 + }, + { + "name": "WebAssembly.Module", + "ms": 3.966363 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.570022 + }, + { + "name": "wasi.start", + "ms": 5.489674 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355966976, + "peakRssBytes": 488030208, + "pssBytes": 357852160, + "virtualBytes": 4031963136, + "minorFaults": 661287, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488038400, + "peakRssBytes": 488099840, + "pssBytes": 489859072, + "virtualBytes": 4812144640, + "minorFaults": 688032, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356024320, + "peakRssBytes": 488099840, + "pssBytes": 357853184, + "virtualBytes": 4031963136, + "minorFaults": 688032, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355966976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356024320, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 221.2920529999974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.732346 + }, + { + "name": "WebAssembly.Module", + "ms": 0.957014 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.3952 + }, + { + "name": "wasi.start", + "ms": 101.144013 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356024320, + "peakRssBytes": 488099840, + "pssBytes": 357853184, + "virtualBytes": 4031963136, + "minorFaults": 688032, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413663232, + "peakRssBytes": 488099840, + "pssBytes": 409445376, + "virtualBytes": 4769783808, + "minorFaults": 704185, + "majorFaults": 0 + }, + "end": { + "rssBytes": 361930752, + "peakRssBytes": 488099840, + "pssBytes": 363660288, + "virtualBytes": 4032196608, + "minorFaults": 704185, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356024320, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361930752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 188.6756730000052, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.982335 + }, + { + "name": "WebAssembly.Module", + "ms": 0.866806 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.367965 + }, + { + "name": "wasi.start", + "ms": 70.86028 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 361930752, + "peakRssBytes": 488099840, + "pssBytes": 363660288, + "virtualBytes": 4032196608, + "minorFaults": 704185, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413810688, + "peakRssBytes": 488099840, + "pssBytes": 415762432, + "virtualBytes": 4770045952, + "minorFaults": 718422, + "majorFaults": 0 + }, + "end": { + "rssBytes": 361881600, + "peakRssBytes": 488099840, + "pssBytes": 363718656, + "virtualBytes": 4032196608, + "minorFaults": 718422, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361930752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361881600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 183.9771100000071, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.37183 + }, + { + "name": "WebAssembly.Module", + "ms": 0.720013 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.361809 + }, + { + "name": "wasi.start", + "ms": 71.2929 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 361881600, + "peakRssBytes": 488099840, + "pssBytes": 363718656, + "virtualBytes": 4032196608, + "minorFaults": 718422, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414076928, + "peakRssBytes": 488099840, + "pssBytes": 415753216, + "virtualBytes": 4768997376, + "minorFaults": 732138, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362037248, + "peakRssBytes": 488099840, + "pssBytes": 363746304, + "virtualBytes": 4032196608, + "minorFaults": 732138, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361881600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362037248, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 192.06318800000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.76168 + }, + { + "name": "WebAssembly.Module", + "ms": 0.885083 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.411798 + }, + { + "name": "wasi.start", + "ms": 66.779844 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362037248, + "peakRssBytes": 488099840, + "pssBytes": 363746304, + "virtualBytes": 4032196608, + "minorFaults": 732138, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 415010816, + "peakRssBytes": 488099840, + "pssBytes": 416835584, + "virtualBytes": 4769259520, + "minorFaults": 746092, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362967040, + "peakRssBytes": 488099840, + "pssBytes": 364738560, + "virtualBytes": 4032196608, + "minorFaults": 746092, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362037248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362967040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 179.40422299999045, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.717542 + }, + { + "name": "WebAssembly.Module", + "ms": 1.054218 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.370945 + }, + { + "name": "wasi.start", + "ms": 68.285362 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362967040, + "peakRssBytes": 488099840, + "pssBytes": 364738560, + "virtualBytes": 4032196608, + "minorFaults": 746092, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414830592, + "peakRssBytes": 488099840, + "pssBytes": 416839680, + "virtualBytes": 4769521664, + "minorFaults": 759292, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362881024, + "peakRssBytes": 488099840, + "pssBytes": 364738560, + "virtualBytes": 4032196608, + "minorFaults": 759292, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362967040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362881024, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 167.4548300000024, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.654119 + }, + { + "name": "WebAssembly.Module", + "ms": 3.51798 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.829393 + }, + { + "name": "wasi.start", + "ms": 17.803801 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362881024, + "peakRssBytes": 488099840, + "pssBytes": 364738560, + "virtualBytes": 4032196608, + "minorFaults": 759292, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430809088, + "peakRssBytes": 488099840, + "pssBytes": 432784384, + "virtualBytes": 4790095872, + "minorFaults": 773502, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362987520, + "peakRssBytes": 488099840, + "pssBytes": 364742656, + "virtualBytes": 4033703936, + "minorFaults": 773502, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362881024, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362987520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 147.43096100000548, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.394425 + }, + { + "name": "WebAssembly.Module", + "ms": 1.227672 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.160525 + }, + { + "name": "wasi.start", + "ms": 14.155551 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362987520, + "peakRssBytes": 488099840, + "pssBytes": 364742656, + "virtualBytes": 4033703936, + "minorFaults": 773502, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430960640, + "peakRssBytes": 488099840, + "pssBytes": 432789504, + "virtualBytes": 4790099968, + "minorFaults": 788733, + "majorFaults": 0 + }, + "end": { + "rssBytes": 363139072, + "peakRssBytes": 488099840, + "pssBytes": 364746752, + "virtualBytes": 4033970176, + "minorFaults": 788733, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362987520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363139072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 154.83912799999234, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.210364 + }, + { + "name": "WebAssembly.Module", + "ms": 1.95364 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.332249 + }, + { + "name": "wasi.start", + "ms": 15.349265 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 363139072, + "peakRssBytes": 488099840, + "pssBytes": 364746752, + "virtualBytes": 4033970176, + "minorFaults": 788733, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432910336, + "peakRssBytes": 488099840, + "pssBytes": 434817024, + "virtualBytes": 4790099968, + "minorFaults": 802413, + "majorFaults": 0 + }, + "end": { + "rssBytes": 363065344, + "peakRssBytes": 488099840, + "pssBytes": 364745728, + "virtualBytes": 4033970176, + "minorFaults": 802413, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363139072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363065344, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 169.68301400000928, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 34.675822 + }, + { + "name": "WebAssembly.Module", + "ms": 2.471248 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.506013 + }, + { + "name": "wasi.start", + "ms": 13.452361 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 363065344, + "peakRssBytes": 488099840, + "pssBytes": 364745728, + "virtualBytes": 4033970176, + "minorFaults": 802413, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430538752, + "peakRssBytes": 488099840, + "pssBytes": 432777216, + "virtualBytes": 4790362112, + "minorFaults": 818153, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362700800, + "peakRssBytes": 488099840, + "pssBytes": 364746752, + "virtualBytes": 4033970176, + "minorFaults": 818153, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363065344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362700800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 150.79418199999782, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616332498569306/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.267395 + }, + { + "name": "WebAssembly.Module", + "ms": 1.908846 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.351671 + }, + { + "name": "wasi.start", + "ms": 14.111396 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362700800, + "peakRssBytes": 488099840, + "pssBytes": 364746752, + "virtualBytes": 4033970176, + "minorFaults": 818153, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430395392, + "peakRssBytes": 488099840, + "pssBytes": 432789504, + "virtualBytes": 4790099968, + "minorFaults": 833893, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362541056, + "peakRssBytes": 488099840, + "pssBytes": 364746752, + "virtualBytes": 4033970176, + "minorFaults": 833893, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362700800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362541056, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 2, + "vmSetupMs": 440.18857100000605, + "fixtureSetupMs": 387.6827609999891, + "baseline": { + "rssBytes": 240869376, + "peakRssBytes": 248401920, + "pssBytes": 241751040, + "virtualBytes": 3886268416, + "minorFaults": 54068, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 491917312, + "peakRssBytes": 523464704, + "pssBytes": 493586432, + "virtualBytes": 4196777984, + "minorFaults": 105255, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 251047936, + "peakRssBytes": 275062784, + "pssBytes": 251835392, + "virtualBytes": 310509568, + "minorFaults": 51187, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 65.0786700000026, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.068066, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.883596000000004, + "phases": [ + { + "name": "Engine", + "ms": 0.062437 + }, + { + "name": "canonicalPreopens", + "ms": 0.100397 + }, + { + "name": "moduleRead", + "ms": 2.508687 + }, + { + "name": "profileValidation", + "ms": 0.232356 + }, + { + "name": "moduleCompile", + "ms": 45.514128 + }, + { + "name": "importValidation", + "ms": 0.002736 + }, + { + "name": "Linker", + "ms": 0.192502 + }, + { + "name": "Store", + "ms": 0.016866000000000003 + }, + { + "name": "Instance", + "ms": 0.047768 + }, + { + "name": "signalMaskInit", + "ms": 0.092418 + }, + { + "name": "entrypointLookup", + "ms": 0.002205 + }, + { + "name": "wasi.start", + "ms": 0.023972999999999998 + }, + { + "name": "Store.teardown", + "ms": 0.016429000000000003 + } + ] + }, + "memory": { + "start": { + "rssBytes": 240869376, + "peakRssBytes": 248401920, + "pssBytes": 241759232, + "virtualBytes": 3888381952, + "minorFaults": 54070, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55210, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55210, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240869376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 20.151596999989124, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008198, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.60818, + "phases": [ + { + "name": "Engine", + "ms": 0.001882 + }, + { + "name": "canonicalPreopens", + "ms": 0.10189000000000001 + }, + { + "name": "moduleRead", + "ms": 3.278462 + }, + { + "name": "profileValidation", + "ms": 0.20768899999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003066 + }, + { + "name": "Linker", + "ms": 0.190805 + }, + { + "name": "Store", + "ms": 0.015026 + }, + { + "name": "Instance", + "ms": 0.030664999999999998 + }, + { + "name": "signalMaskInit", + "ms": 0.645301 + }, + { + "name": "entrypointLookup", + "ms": 0.00627 + }, + { + "name": "wasi.start", + "ms": 0.040631 + }, + { + "name": "Store.teardown", + "ms": 0.015398 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55210, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55227, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55227, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 18.68338100000983, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008163, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.58943, + "phases": [ + { + "name": "Engine", + "ms": 0.001741 + }, + { + "name": "canonicalPreopens", + "ms": 0.09657299999999999 + }, + { + "name": "moduleRead", + "ms": 3.29651 + }, + { + "name": "profileValidation", + "ms": 0.22711699999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002921 + }, + { + "name": "Linker", + "ms": 0.188259 + }, + { + "name": "Store", + "ms": 0.014973 + }, + { + "name": "Instance", + "ms": 0.034969 + }, + { + "name": "signalMaskInit", + "ms": 0.613716 + }, + { + "name": "entrypointLookup", + "ms": 0.002401 + }, + { + "name": "wasi.start", + "ms": 0.02517 + }, + { + "name": "Store.teardown", + "ms": 0.016427 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55227, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55244, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55244, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 20.848127999997814, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009301, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 5.484891, + "phases": [ + { + "name": "Engine", + "ms": 0.001879 + }, + { + "name": "canonicalPreopens", + "ms": 0.10637100000000001 + }, + { + "name": "moduleRead", + "ms": 3.438552 + }, + { + "name": "profileValidation", + "ms": 0.243424 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0030369999999999998 + }, + { + "name": "Linker", + "ms": 0.198416 + }, + { + "name": "Store", + "ms": 0.015305999999999998 + }, + { + "name": "Instance", + "ms": 0.028139 + }, + { + "name": "signalMaskInit", + "ms": 0.596189 + }, + { + "name": "entrypointLookup", + "ms": 0.007324 + }, + { + "name": "wasi.start", + "ms": 0.7555930000000001 + }, + { + "name": "Store.teardown", + "ms": 0.01487 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55244, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 8319868928, + "minorFaults": 55261, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55261, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 19.511495999991894, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.007657, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3.66569, + "phases": [ + { + "name": "Engine", + "ms": 0.0015409999999999998 + }, + { + "name": "canonicalPreopens", + "ms": 0.136563 + }, + { + "name": "moduleRead", + "ms": 2.2864050000000002 + }, + { + "name": "profileValidation", + "ms": 0.22797299999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002579 + }, + { + "name": "Linker", + "ms": 0.181928 + }, + { + "name": "Store", + "ms": 0.015168 + }, + { + "name": "Instance", + "ms": 0.033313999999999996 + }, + { + "name": "signalMaskInit", + "ms": 0.661467 + }, + { + "name": "entrypointLookup", + "ms": 0.007944 + }, + { + "name": "wasi.start", + "ms": 0.02274 + }, + { + "name": "Store.teardown", + "ms": 0.01819 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55261, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55278, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55278, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1122.9707440000057, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008050999999999999, + "firstGuestHostCallMs": 1054.064612, + "firstOutputMs": 1104.004879, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1104.402096, + "phases": [ + { + "name": "Engine", + "ms": 0.0015680000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.10290500000000001 + }, + { + "name": "moduleRead", + "ms": 5.365767 + }, + { + "name": "profileValidation", + "ms": 3.869647 + }, + { + "name": "moduleCompile", + "ms": 1043.494946 + }, + { + "name": "importValidation", + "ms": 0.007481 + }, + { + "name": "Linker", + "ms": 0.19295900000000002 + }, + { + "name": "Store", + "ms": 0.016228000000000003 + }, + { + "name": "Instance", + "ms": 0.175001 + }, + { + "name": "signalMaskInit", + "ms": 0.0821 + }, + { + "name": "entrypointLookup", + "ms": 0.004258 + }, + { + "name": "wasi.start", + "ms": 50.201152 + }, + { + "name": "Store.teardown", + "ms": 0.273067 + } + ] + }, + "memory": { + "start": { + "rssBytes": 251310080, + "peakRssBytes": 251494400, + "pssBytes": 252258304, + "virtualBytes": 3957780480, + "minorFaults": 55278, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291119104, + "peakRssBytes": 291844096, + "pssBytes": 292804608, + "virtualBytes": 8327684096, + "minorFaults": 62467, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291119104, + "peakRssBytes": 291844096, + "pssBytes": 292131840, + "virtualBytes": 3963228160, + "minorFaults": 62467, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45514, + "wasmtimeProcessRetainedRssBytes": 251310080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291119104, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 87.37018300000636, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012241, + "firstGuestHostCallMs": 12.908239, + "firstOutputMs": 68.99328, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 69.661214, + "phases": [ + { + "name": "Engine", + "ms": 0.001989 + }, + { + "name": "canonicalPreopens", + "ms": 0.130829 + }, + { + "name": "moduleRead", + "ms": 6.656314 + }, + { + "name": "profileValidation", + "ms": 4.351324999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00711 + }, + { + "name": "Linker", + "ms": 0.211499 + }, + { + "name": "Store", + "ms": 0.017376000000000003 + }, + { + "name": "Instance", + "ms": 0.472365 + }, + { + "name": "signalMaskInit", + "ms": 0.067275 + }, + { + "name": "entrypointLookup", + "ms": 0.004174 + }, + { + "name": "wasi.start", + "ms": 56.574889000000006 + }, + { + "name": "Store.teardown", + "ms": 0.560476 + } + ] + }, + "memory": { + "start": { + "rssBytes": 291119104, + "peakRssBytes": 291844096, + "pssBytes": 292131840, + "virtualBytes": 3963228160, + "minorFaults": 62467, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291926016, + "peakRssBytes": 292081664, + "pssBytes": 293106688, + "virtualBytes": 8327684096, + "minorFaults": 62624, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292492288, + "virtualBytes": 3963228160, + "minorFaults": 62624, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291119104, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 79.02671099999861, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015212999999999999, + "firstGuestHostCallMs": 11.844105, + "firstOutputMs": 62.279922, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 62.429399, + "phases": [ + { + "name": "Engine", + "ms": 0.0018 + }, + { + "name": "canonicalPreopens", + "ms": 0.120084 + }, + { + "name": "moduleRead", + "ms": 6.709825 + }, + { + "name": "profileValidation", + "ms": 3.9442369999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0075120000000000004 + }, + { + "name": "Linker", + "ms": 0.20987 + }, + { + "name": "Store", + "ms": 0.016568000000000003 + }, + { + "name": "Instance", + "ms": 0.039008 + }, + { + "name": "signalMaskInit", + "ms": 0.045593 + }, + { + "name": "entrypointLookup", + "ms": 0.002705 + }, + { + "name": "wasi.start", + "ms": 50.674448 + }, + { + "name": "Store.teardown", + "ms": 0.046839000000000006 + } + ] + }, + "memory": { + "start": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292492288, + "virtualBytes": 3963228160, + "minorFaults": 62624, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292077568, + "peakRssBytes": 292081664, + "pssBytes": 293107712, + "virtualBytes": 8327684096, + "minorFaults": 62693, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62693, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 79.86813399998937, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011633000000000001, + "firstGuestHostCallMs": 11.540129, + "firstOutputMs": 61.330127, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.488284, + "phases": [ + { + "name": "Engine", + "ms": 0.0021390000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.10405400000000001 + }, + { + "name": "moduleRead", + "ms": 6.536086 + }, + { + "name": "profileValidation", + "ms": 3.8533259999999996 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0069110000000000005 + }, + { + "name": "Linker", + "ms": 0.20085 + }, + { + "name": "Store", + "ms": 0.015129 + }, + { + "name": "Instance", + "ms": 0.043731 + }, + { + "name": "signalMaskInit", + "ms": 0.035157999999999995 + }, + { + "name": "entrypointLookup", + "ms": 0.002984 + }, + { + "name": "wasi.start", + "ms": 50.031594 + }, + { + "name": "Store.teardown", + "ms": 0.049833 + } + ] + }, + "memory": { + "start": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62693, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292077568, + "peakRssBytes": 292081664, + "pssBytes": 293107712, + "virtualBytes": 8327684096, + "minorFaults": 62762, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62762, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 76.65373700000055, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008631999999999999, + "firstGuestHostCallMs": 11.567692, + "firstOutputMs": 58.974425, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 59.160202000000005, + "phases": [ + { + "name": "Engine", + "ms": 0.001831 + }, + { + "name": "canonicalPreopens", + "ms": 0.107157 + }, + { + "name": "moduleRead", + "ms": 6.546948 + }, + { + "name": "profileValidation", + "ms": 3.8554850000000003 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00741 + }, + { + "name": "Linker", + "ms": 0.200268 + }, + { + "name": "Store", + "ms": 0.015995 + }, + { + "name": "Instance", + "ms": 0.041234 + }, + { + "name": "signalMaskInit", + "ms": 0.043062 + }, + { + "name": "entrypointLookup", + "ms": 0.0030800000000000003 + }, + { + "name": "wasi.start", + "ms": 47.656877 + }, + { + "name": "Store.teardown", + "ms": 0.06476900000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62762, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292077568, + "peakRssBytes": 292081664, + "pssBytes": 293107712, + "virtualBytes": 8327684096, + "minorFaults": 62831, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62831, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3801.143683999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012880999999999998, + "firstGuestHostCallMs": 3406.512388, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3780.769378, + "phases": [ + { + "name": "Engine", + "ms": 0.001533 + }, + { + "name": "canonicalPreopens", + "ms": 0.106665 + }, + { + "name": "moduleRead", + "ms": 11.413471 + }, + { + "name": "profileValidation", + "ms": 12.23263 + }, + { + "name": "moduleCompile", + "ms": 3380.854033 + }, + { + "name": "importValidation", + "ms": 0.00822 + }, + { + "name": "Linker", + "ms": 0.185387 + }, + { + "name": "Store", + "ms": 0.015894000000000002 + }, + { + "name": "Instance", + "ms": 0.193454 + }, + { + "name": "signalMaskInit", + "ms": 0.083459 + }, + { + "name": "entrypointLookup", + "ms": 0.004536 + }, + { + "name": "wasi.start", + "ms": 374.167868 + }, + { + "name": "Store.teardown", + "ms": 0.050071000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 291479552, + "peakRssBytes": 292081664, + "pssBytes": 292493312, + "virtualBytes": 3963228160, + "minorFaults": 62831, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422944768, + "peakRssBytes": 423231488, + "pssBytes": 425244672, + "virtualBytes": 12867809280, + "minorFaults": 81335, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81335, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089009, + "wasmtimeProcessRetainedRssBytes": 291479552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 73.11178900000232, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01015, + "firstGuestHostCallMs": 25.796847, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.192125999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.001512 + }, + { + "name": "canonicalPreopens", + "ms": 0.108762 + }, + { + "name": "moduleRead", + "ms": 11.538094000000001 + }, + { + "name": "profileValidation", + "ms": 12.374299 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010424999999999999 + }, + { + "name": "Linker", + "ms": 0.216248 + }, + { + "name": "Store", + "ms": 0.019555000000000003 + }, + { + "name": "Instance", + "ms": 0.049682000000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.057639 + }, + { + "name": "entrypointLookup", + "ms": 0.002854 + }, + { + "name": "wasi.start", + "ms": 25.346062999999997 + }, + { + "name": "Store.teardown", + "ms": 0.043013 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81335, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 425519104, + "virtualBytes": 12867809280, + "minorFaults": 81415, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81415, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 84.84089799999492, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021344000000000002, + "firstGuestHostCallMs": 25.559379, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 65.234834, + "phases": [ + { + "name": "Engine", + "ms": 0.001728 + }, + { + "name": "canonicalPreopens", + "ms": 0.11475400000000001 + }, + { + "name": "moduleRead", + "ms": 11.245973 + }, + { + "name": "profileValidation", + "ms": 12.359098000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010409 + }, + { + "name": "Linker", + "ms": 0.21548699999999998 + }, + { + "name": "Store", + "ms": 0.017407000000000002 + }, + { + "name": "Instance", + "ms": 0.042782 + }, + { + "name": "signalMaskInit", + "ms": 0.101757 + }, + { + "name": "entrypointLookup", + "ms": 0.004042 + }, + { + "name": "wasi.start", + "ms": 38.578088 + }, + { + "name": "Store.teardown", + "ms": 1.0721800000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81415, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422563840, + "peakRssBytes": 423231488, + "pssBytes": 425522176, + "virtualBytes": 12867809280, + "minorFaults": 81495, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424711168, + "virtualBytes": 4138897408, + "minorFaults": 81495, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 78.16584699999657, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018667, + "firstGuestHostCallMs": 28.71895, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 57.935591, + "phases": [ + { + "name": "Engine", + "ms": 0.001856 + }, + { + "name": "canonicalPreopens", + "ms": 0.117876 + }, + { + "name": "moduleRead", + "ms": 11.759865 + }, + { + "name": "profileValidation", + "ms": 13.934269 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009694999999999999 + }, + { + "name": "Linker", + "ms": 0.21284799999999998 + }, + { + "name": "Store", + "ms": 0.017271 + }, + { + "name": "Instance", + "ms": 1.155688 + }, + { + "name": "signalMaskInit", + "ms": 0.061311 + }, + { + "name": "entrypointLookup", + "ms": 0.0038500000000000006 + }, + { + "name": "wasi.start", + "ms": 28.555413 + }, + { + "name": "Store.teardown", + "ms": 0.655259 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424711168, + "virtualBytes": 4138897408, + "minorFaults": 81495, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 425513984, + "virtualBytes": 12867809280, + "minorFaults": 81575, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424711168, + "virtualBytes": 4138897408, + "minorFaults": 81575, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 70.7398749999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012341, + "firstGuestHostCallMs": 25.888113, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.386739, + "phases": [ + { + "name": "Engine", + "ms": 0.00161 + }, + { + "name": "canonicalPreopens", + "ms": 0.10823300000000001 + }, + { + "name": "moduleRead", + "ms": 11.667404 + }, + { + "name": "profileValidation", + "ms": 12.346692 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009638 + }, + { + "name": "Linker", + "ms": 0.20338 + }, + { + "name": "Store", + "ms": 0.017207 + }, + { + "name": "Instance", + "ms": 0.043844999999999995 + }, + { + "name": "signalMaskInit", + "ms": 0.070973 + }, + { + "name": "entrypointLookup", + "ms": 0.0032430000000000002 + }, + { + "name": "wasi.start", + "ms": 25.802134000000002 + }, + { + "name": "Store.teardown", + "ms": 0.684288 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424711168, + "virtualBytes": 4138897408, + "minorFaults": 81575, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 425515008, + "virtualBytes": 12867809280, + "minorFaults": 81655, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81655, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1569.1759480000037, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017918999999999997, + "firstGuestHostCallMs": 1541.230482, + "firstOutputMs": 1547.962236, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1548.303215, + "phases": [ + { + "name": "Engine", + "ms": 0.005853 + }, + { + "name": "canonicalPreopens", + "ms": 0.118066 + }, + { + "name": "moduleRead", + "ms": 6.796978 + }, + { + "name": "profileValidation", + "ms": 6.365356 + }, + { + "name": "moduleCompile", + "ms": 1525.108356 + }, + { + "name": "importValidation", + "ms": 0.010551 + }, + { + "name": "Linker", + "ms": 0.186412 + }, + { + "name": "Store", + "ms": 0.019677 + }, + { + "name": "Instance", + "ms": 1.7579099999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.091444 + }, + { + "name": "entrypointLookup", + "ms": 0.010968 + }, + { + "name": "wasi.start", + "ms": 6.953447 + }, + { + "name": "Store.teardown", + "ms": 0.058511 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422510592, + "peakRssBytes": 423231488, + "pssBytes": 424712192, + "virtualBytes": 4138897408, + "minorFaults": 81655, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430546944, + "peakRssBytes": 430571520, + "pssBytes": 433125376, + "virtualBytes": 8510750720, + "minorFaults": 82541, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433021952, + "virtualBytes": 4146294784, + "minorFaults": 82541, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4818610, + "wasmtimeProcessRetainedRssBytes": 422510592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 44.78237600000284, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015203, + "firstGuestHostCallMs": 15.884624, + "firstOutputMs": 22.858216000000002, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 23.158107, + "phases": [ + { + "name": "Engine", + "ms": 0.00221 + }, + { + "name": "canonicalPreopens", + "ms": 0.119478 + }, + { + "name": "moduleRead", + "ms": 6.917743 + }, + { + "name": "profileValidation", + "ms": 6.68173 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011474999999999999 + }, + { + "name": "Linker", + "ms": 0.249858 + }, + { + "name": "Store", + "ms": 0.017338 + }, + { + "name": "Instance", + "ms": 1.066193 + }, + { + "name": "signalMaskInit", + "ms": 0.046949 + }, + { + "name": "entrypointLookup", + "ms": 0.004058999999999999 + }, + { + "name": "wasi.start", + "ms": 7.1880690000000005 + }, + { + "name": "Store.teardown", + "ms": 0.050822 + } + ] + }, + "memory": { + "start": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433021952, + "virtualBytes": 4146294784, + "minorFaults": 82541, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433542144, + "virtualBytes": 8510750720, + "minorFaults": 82589, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433038336, + "virtualBytes": 4146294784, + "minorFaults": 82589, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 128.26963400001114, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.024671000000000002, + "firstGuestHostCallMs": 14.610481, + "firstOutputMs": 20.162025, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 20.369249, + "phases": [ + { + "name": "Engine", + "ms": 0.001782 + }, + { + "name": "canonicalPreopens", + "ms": 0.128552 + }, + { + "name": "moduleRead", + "ms": 6.97915 + }, + { + "name": "profileValidation", + "ms": 6.4057 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011528 + }, + { + "name": "Linker", + "ms": 0.20011700000000002 + }, + { + "name": "Store", + "ms": 0.014974 + }, + { + "name": "Instance", + "ms": 0.046467 + }, + { + "name": "signalMaskInit", + "ms": 0.055469 + }, + { + "name": "entrypointLookup", + "ms": 0.003052 + }, + { + "name": "wasi.start", + "ms": 5.7157409999999995 + }, + { + "name": "Store.teardown", + "ms": 0.033331000000000006 + } + ] + }, + "memory": { + "start": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433038336, + "virtualBytes": 4146294784, + "minorFaults": 82589, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433551360, + "virtualBytes": 8510750720, + "minorFaults": 82636, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433051648, + "virtualBytes": 4146294784, + "minorFaults": 82636, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 43.421853000007104, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.031873, + "firstGuestHostCallMs": 14.663917, + "firstOutputMs": 22.113442, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.331148, + "phases": [ + { + "name": "Engine", + "ms": 0.00219 + }, + { + "name": "canonicalPreopens", + "ms": 0.117189 + }, + { + "name": "moduleRead", + "ms": 5.891895 + }, + { + "name": "profileValidation", + "ms": 6.33364 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011829 + }, + { + "name": "Linker", + "ms": 0.205434 + }, + { + "name": "Store", + "ms": 0.017595 + }, + { + "name": "Instance", + "ms": 1.278816 + }, + { + "name": "signalMaskInit", + "ms": 0.024203 + }, + { + "name": "entrypointLookup", + "ms": 0.003636 + }, + { + "name": "wasi.start", + "ms": 7.619654 + }, + { + "name": "Store.teardown", + "ms": 0.037776 + } + ] + }, + "memory": { + "start": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433051648, + "virtualBytes": 4146294784, + "minorFaults": 82636, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433104896, + "virtualBytes": 8510750720, + "minorFaults": 82681, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433055744, + "virtualBytes": 4146294784, + "minorFaults": 82681, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 44.34264200000325, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.042276, + "firstGuestHostCallMs": 16.293148, + "firstOutputMs": 23.719592000000002, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 23.980262, + "phases": [ + { + "name": "Engine", + "ms": 0.0027700000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.129019 + }, + { + "name": "moduleRead", + "ms": 6.924676 + }, + { + "name": "profileValidation", + "ms": 6.366410999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013523 + }, + { + "name": "Linker", + "ms": 0.210814 + }, + { + "name": "Store", + "ms": 0.017623 + }, + { + "name": "Instance", + "ms": 1.5932469999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.044675 + }, + { + "name": "entrypointLookup", + "ms": 0.005245 + }, + { + "name": "wasi.start", + "ms": 7.842379 + }, + { + "name": "Store.teardown", + "ms": 0.034186 + } + ] + }, + "memory": { + "start": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433055744, + "virtualBytes": 4146294784, + "minorFaults": 82681, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433580032, + "virtualBytes": 8510750720, + "minorFaults": 82730, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433076224, + "virtualBytes": 4146294784, + "minorFaults": 82730, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1260.929449999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010767, + "firstGuestHostCallMs": 1238.103493, + "firstOutputMs": 1240.785163, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1240.933576, + "phases": [ + { + "name": "Engine", + "ms": 0.001624 + }, + { + "name": "canonicalPreopens", + "ms": 0.14156 + }, + { + "name": "moduleRead", + "ms": 4.649457 + }, + { + "name": "profileValidation", + "ms": 4.9736270000000005 + }, + { + "name": "moduleCompile", + "ms": 1227.534191 + }, + { + "name": "importValidation", + "ms": 0.008388 + }, + { + "name": "Linker", + "ms": 0.187782 + }, + { + "name": "Store", + "ms": 0.016953 + }, + { + "name": "Instance", + "ms": 0.077204 + }, + { + "name": "signalMaskInit", + "ms": 0.077394 + }, + { + "name": "entrypointLookup", + "ms": 0.003273 + }, + { + "name": "wasi.start", + "ms": 2.746651 + }, + { + "name": "Store.teardown", + "ms": 0.041631999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 429940736, + "peakRssBytes": 430571520, + "pssBytes": 433076224, + "virtualBytes": 4146294784, + "minorFaults": 82730, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435585024, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83118, + "majorFaults": 0 + }, + "end": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83118, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6343719, + "wasmtimeProcessRetainedRssBytes": 429940736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 34.05023799999617, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011838999999999999, + "firstGuestHostCallMs": 10.638401, + "firstOutputMs": 13.206055, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.345510999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.001557 + }, + { + "name": "canonicalPreopens", + "ms": 0.139474 + }, + { + "name": "moduleRead", + "ms": 4.611605 + }, + { + "name": "profileValidation", + "ms": 4.876834 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008909 + }, + { + "name": "Linker", + "ms": 0.206108 + }, + { + "name": "Store", + "ms": 0.019074 + }, + { + "name": "Instance", + "ms": 0.27771 + }, + { + "name": "signalMaskInit", + "ms": 0.057892 + }, + { + "name": "entrypointLookup", + "ms": 0.002947 + }, + { + "name": "wasi.start", + "ms": 2.640024 + }, + { + "name": "Store.teardown", + "ms": 0.036833 + } + ] + }, + "memory": { + "start": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83118, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438597632, + "virtualBytes": 4151775232, + "minorFaults": 83163, + "majorFaults": 0 + }, + "end": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83163, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 33.477169999998296, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.025041, + "firstGuestHostCallMs": 11.005286, + "firstOutputMs": 12.947588, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.101084, + "phases": [ + { + "name": "Engine", + "ms": 0.00184 + }, + { + "name": "canonicalPreopens", + "ms": 0.111198 + }, + { + "name": "moduleRead", + "ms": 5.087681 + }, + { + "name": "profileValidation", + "ms": 4.876563000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008486 + }, + { + "name": "Linker", + "ms": 0.20316 + }, + { + "name": "Store", + "ms": 0.016614 + }, + { + "name": "Instance", + "ms": 0.190075 + }, + { + "name": "signalMaskInit", + "ms": 0.058441 + }, + { + "name": "entrypointLookup", + "ms": 0.0030350000000000004 + }, + { + "name": "wasi.start", + "ms": 2.010342 + }, + { + "name": "Store.teardown", + "ms": 0.036652000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83163, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438597632, + "virtualBytes": 4151775232, + "minorFaults": 83208, + "majorFaults": 0 + }, + "end": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83208, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 32.73002400000405, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017314, + "firstGuestHostCallMs": 11.143934, + "firstOutputMs": 13.032101, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.162433, + "phases": [ + { + "name": "Engine", + "ms": 0.001855 + }, + { + "name": "canonicalPreopens", + "ms": 0.11565500000000001 + }, + { + "name": "moduleRead", + "ms": 5.168431 + }, + { + "name": "profileValidation", + "ms": 4.857138000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008807 + }, + { + "name": "Linker", + "ms": 0.207734 + }, + { + "name": "Store", + "ms": 0.017138 + }, + { + "name": "Instance", + "ms": 0.303786 + }, + { + "name": "signalMaskInit", + "ms": 0.022399 + }, + { + "name": "entrypointLookup", + "ms": 0.0022180000000000004 + }, + { + "name": "wasi.start", + "ms": 1.950345 + }, + { + "name": "Store.teardown", + "ms": 0.038332 + } + ] + }, + "memory": { + "start": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83208, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435462144, + "peakRssBytes": 437886976, + "pssBytes": 438597632, + "virtualBytes": 4151775232, + "minorFaults": 83253, + "majorFaults": 0 + }, + "end": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83253, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 31.765849999996135, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015369, + "firstGuestHostCallMs": 11.298737999999998, + "firstOutputMs": 13.437525, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 14.990083, + "phases": [ + { + "name": "Engine", + "ms": 0.001706 + }, + { + "name": "canonicalPreopens", + "ms": 0.105918 + }, + { + "name": "moduleRead", + "ms": 5.181471 + }, + { + "name": "profileValidation", + "ms": 4.891007 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010022 + }, + { + "name": "Linker", + "ms": 0.20929 + }, + { + "name": "Store", + "ms": 0.016535 + }, + { + "name": "Instance", + "ms": 0.408981 + }, + { + "name": "signalMaskInit", + "ms": 0.031304 + }, + { + "name": "entrypointLookup", + "ms": 0.003408 + }, + { + "name": "wasi.start", + "ms": 2.219881 + }, + { + "name": "Store.teardown", + "ms": 1.437429 + } + ] + }, + "memory": { + "start": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83253, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 437506048, + "peakRssBytes": 437886976, + "pssBytes": 438597632, + "virtualBytes": 8516218880, + "minorFaults": 83298, + "majorFaults": 0 + }, + "end": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83298, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3643.197438000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014612, + "firstGuestHostCallMs": 3614.1218670000003, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3621.6687800000004, + "phases": [ + { + "name": "Engine", + "ms": 0.001857 + }, + { + "name": "canonicalPreopens", + "ms": 0.105684 + }, + { + "name": "moduleRead", + "ms": 11.109852 + }, + { + "name": "profileValidation", + "ms": 14.583836 + }, + { + "name": "moduleCompile", + "ms": 3585.341477 + }, + { + "name": "importValidation", + "ms": 0.011316 + }, + { + "name": "Linker", + "ms": 0.188557 + }, + { + "name": "Store", + "ms": 0.018011 + }, + { + "name": "Instance", + "ms": 1.21901 + }, + { + "name": "signalMaskInit", + "ms": 0.079947 + }, + { + "name": "entrypointLookup", + "ms": 0.00464 + }, + { + "name": "wasi.start", + "ms": 7.583456 + }, + { + "name": "Store.teardown", + "ms": 0.050384 + } + ] + }, + "memory": { + "start": { + "rssBytes": 435408896, + "peakRssBytes": 437886976, + "pssBytes": 438544384, + "virtualBytes": 4151762944, + "minorFaults": 83298, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 452542464, + "peakRssBytes": 452661248, + "pssBytes": 455768064, + "virtualBytes": 8532635648, + "minorFaults": 83872, + "majorFaults": 0 + }, + "end": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 83872, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7571253, + "wasmtimeProcessRetainedRssBytes": 435408896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 61.16563000000315, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014633, + "firstGuestHostCallMs": 28.229426, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 40.504315999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.001686 + }, + { + "name": "canonicalPreopens", + "ms": 0.114065 + }, + { + "name": "moduleRead", + "ms": 11.189267 + }, + { + "name": "profileValidation", + "ms": 14.667994 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012591 + }, + { + "name": "Linker", + "ms": 0.215107 + }, + { + "name": "Store", + "ms": 0.016673 + }, + { + "name": "Instance", + "ms": 0.583797 + }, + { + "name": "signalMaskInit", + "ms": 0.052901000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.002742 + }, + { + "name": "wasi.start", + "ms": 12.25802 + }, + { + "name": "Store.teardown", + "ms": 0.047617999999999994 + } + ] + }, + "memory": { + "start": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 83872, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 452481024, + "peakRssBytes": 452661248, + "pssBytes": 455710720, + "virtualBytes": 8532635648, + "minorFaults": 83964, + "majorFaults": 0 + }, + "end": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 83964, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 72.42277299999841, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019708, + "firstGuestHostCallMs": 29.2056, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 50.668994000000005, + "phases": [ + { + "name": "Engine", + "ms": 0.0020150000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.115603 + }, + { + "name": "moduleRead", + "ms": 11.314165 + }, + { + "name": "profileValidation", + "ms": 15.080736000000002 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012425 + }, + { + "name": "Linker", + "ms": 0.22110200000000002 + }, + { + "name": "Store", + "ms": 0.019834 + }, + { + "name": "Instance", + "ms": 0.993963 + }, + { + "name": "signalMaskInit", + "ms": 0.054446 + }, + { + "name": "entrypointLookup", + "ms": 0.004118 + }, + { + "name": "wasi.start", + "ms": 21.42791 + }, + { + "name": "Store.teardown", + "ms": 0.046429 + } + ] + }, + "memory": { + "start": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 83964, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 452481024, + "peakRssBytes": 452661248, + "pssBytes": 455709696, + "virtualBytes": 8532635648, + "minorFaults": 84056, + "majorFaults": 0 + }, + "end": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455070720, + "virtualBytes": 4168179712, + "minorFaults": 84056, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 70.69342399999732, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016488, + "firstGuestHostCallMs": 29.229834999999998, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 49.512273, + "phases": [ + { + "name": "Engine", + "ms": 0.001782 + }, + { + "name": "canonicalPreopens", + "ms": 0.121371 + }, + { + "name": "moduleRead", + "ms": 11.334943 + }, + { + "name": "profileValidation", + "ms": 15.635860000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.014365000000000001 + }, + { + "name": "Linker", + "ms": 0.240902 + }, + { + "name": "Store", + "ms": 0.017626 + }, + { + "name": "Instance", + "ms": 0.440187 + }, + { + "name": "signalMaskInit", + "ms": 0.055048 + }, + { + "name": "entrypointLookup", + "ms": 0.004096000000000001 + }, + { + "name": "wasi.start", + "ms": 19.541490000000003 + }, + { + "name": "Store.teardown", + "ms": 0.743054 + } + ] + }, + "memory": { + "start": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455070720, + "virtualBytes": 4168179712, + "minorFaults": 84056, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 452481024, + "peakRssBytes": 452661248, + "pssBytes": 455693312, + "virtualBytes": 8532635648, + "minorFaults": 84148, + "majorFaults": 0 + }, + "end": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455070720, + "virtualBytes": 4168179712, + "minorFaults": 84148, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 73.34198800000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017564, + "firstGuestHostCallMs": 30.535866, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 54.884629, + "phases": [ + { + "name": "Engine", + "ms": 0.0038480000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.117061 + }, + { + "name": "moduleRead", + "ms": 11.353189 + }, + { + "name": "profileValidation", + "ms": 15.565850000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012897 + }, + { + "name": "Linker", + "ms": 0.216909 + }, + { + "name": "Store", + "ms": 0.018260000000000002 + }, + { + "name": "Instance", + "ms": 1.5631899999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.078075 + }, + { + "name": "entrypointLookup", + "ms": 0.00505 + }, + { + "name": "wasi.start", + "ms": 23.487887 + }, + { + "name": "Store.teardown", + "ms": 1.112001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455070720, + "virtualBytes": 4168179712, + "minorFaults": 84148, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 452481024, + "peakRssBytes": 452661248, + "pssBytes": 455694336, + "virtualBytes": 8532635648, + "minorFaults": 84240, + "majorFaults": 0 + }, + "end": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 84240, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3913.844087999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015861, + "firstGuestHostCallMs": 3892.25091, + "firstOutputMs": 3893.2610990000003, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3893.500203, + "phases": [ + { + "name": "Engine", + "ms": 0.001815 + }, + { + "name": "canonicalPreopens", + "ms": 0.175646 + }, + { + "name": "moduleRead", + "ms": 12.883468 + }, + { + "name": "profileValidation", + "ms": 15.916267000000001 + }, + { + "name": "moduleCompile", + "ms": 3860.175768 + }, + { + "name": "importValidation", + "ms": 0.013166 + }, + { + "name": "Linker", + "ms": 0.181034 + }, + { + "name": "Store", + "ms": 0.017143 + }, + { + "name": "Instance", + "ms": 1.0295969999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.072157 + }, + { + "name": "entrypointLookup", + "ms": 0.004643 + }, + { + "name": "wasi.start", + "ms": 1.4106290000000001 + }, + { + "name": "Store.teardown", + "ms": 0.038013 + } + ] + }, + "memory": { + "start": { + "rssBytes": 451936256, + "peakRssBytes": 452661248, + "pssBytes": 455071744, + "virtualBytes": 4168179712, + "minorFaults": 84240, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 473280512, + "peakRssBytes": 473993216, + "pssBytes": 476416000, + "virtualBytes": 4186787840, + "minorFaults": 89010, + "majorFaults": 0 + }, + "end": { + "rssBytes": 473280512, + "peakRssBytes": 473993216, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89010, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11156594, + "wasmtimeProcessRetainedRssBytes": 451936256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 51.291408000004594, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.073764, + "firstGuestHostCallMs": 31.343071000000002, + "firstOutputMs": 32.060176000000006, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.078453, + "phases": [ + { + "name": "Engine", + "ms": 0.002499 + }, + { + "name": "canonicalPreopens", + "ms": 0.12398500000000001 + }, + { + "name": "moduleRead", + "ms": 12.869465 + }, + { + "name": "profileValidation", + "ms": 16.129123 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016948 + }, + { + "name": "Linker", + "ms": 0.209154 + }, + { + "name": "Store", + "ms": 0.018368000000000002 + }, + { + "name": "Instance", + "ms": 0.049548 + }, + { + "name": "signalMaskInit", + "ms": 0.084713 + }, + { + "name": "entrypointLookup", + "ms": 0.003476 + }, + { + "name": "wasi.start", + "ms": 1.075883 + }, + { + "name": "Store.teardown", + "ms": 0.861922 + } + ] + }, + "memory": { + "start": { + "rssBytes": 473280512, + "peakRssBytes": 473993216, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89010, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475377664, + "peakRssBytes": 475963392, + "pssBytes": 476469248, + "virtualBytes": 8551231488, + "minorFaults": 89050, + "majorFaults": 0 + }, + "end": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89050, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 53.75641600000381, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019792999999999998, + "firstGuestHostCallMs": 30.782222, + "firstOutputMs": 31.490382999999998, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.382580000000004, + "phases": [ + { + "name": "Engine", + "ms": 0.001893 + }, + { + "name": "canonicalPreopens", + "ms": 0.114103 + }, + { + "name": "moduleRead", + "ms": 11.888582 + }, + { + "name": "profileValidation", + "ms": 16.559884 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015656 + }, + { + "name": "Linker", + "ms": 0.23615299999999997 + }, + { + "name": "Store", + "ms": 0.017067000000000002 + }, + { + "name": "Instance", + "ms": 0.048551000000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.063081 + }, + { + "name": "entrypointLookup", + "ms": 0.004222 + }, + { + "name": "wasi.start", + "ms": 1.107321 + }, + { + "name": "Store.teardown", + "ms": 1.6953179999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89050, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475377664, + "peakRssBytes": 475963392, + "pssBytes": 476469248, + "virtualBytes": 8551231488, + "minorFaults": 89090, + "majorFaults": 0 + }, + "end": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476414976, + "virtualBytes": 4186775552, + "minorFaults": 89090, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 58.67849200000637, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021413, + "firstGuestHostCallMs": 34.134806, + "firstOutputMs": 35.19119, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 35.468077, + "phases": [ + { + "name": "Engine", + "ms": 0.002108 + }, + { + "name": "canonicalPreopens", + "ms": 0.135683 + }, + { + "name": "moduleRead", + "ms": 13.076307 + }, + { + "name": "profileValidation", + "ms": 18.706805 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.017374 + }, + { + "name": "Linker", + "ms": 0.220523 + }, + { + "name": "Store", + "ms": 0.019025 + }, + { + "name": "Instance", + "ms": 0.052435 + }, + { + "name": "signalMaskInit", + "ms": 0.055762 + }, + { + "name": "entrypointLookup", + "ms": 0.003389 + }, + { + "name": "wasi.start", + "ms": 1.482103 + }, + { + "name": "Store.teardown", + "ms": 0.071136 + } + ] + }, + "memory": { + "start": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476414976, + "virtualBytes": 4186775552, + "minorFaults": 89090, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476468224, + "virtualBytes": 4186787840, + "minorFaults": 89130, + "majorFaults": 0 + }, + "end": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476414976, + "virtualBytes": 4186775552, + "minorFaults": 89130, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 60.4722299999994, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013624, + "firstGuestHostCallMs": 33.405258, + "firstOutputMs": 34.547243, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 34.856956000000004, + "phases": [ + { + "name": "Engine", + "ms": 0.0017230000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.166701 + }, + { + "name": "moduleRead", + "ms": 13.006474 + }, + { + "name": "profileValidation", + "ms": 16.924562 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016524999999999998 + }, + { + "name": "Linker", + "ms": 0.215157 + }, + { + "name": "Store", + "ms": 0.017897000000000003 + }, + { + "name": "Instance", + "ms": 1.01289 + }, + { + "name": "signalMaskInit", + "ms": 0.063681 + }, + { + "name": "entrypointLookup", + "ms": 0.0036899999999999997 + }, + { + "name": "wasi.start", + "ms": 1.750018 + }, + { + "name": "Store.teardown", + "ms": 0.049678 + } + ] + }, + "memory": { + "start": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476414976, + "virtualBytes": 4186775552, + "minorFaults": 89130, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476468224, + "virtualBytes": 4186787840, + "minorFaults": 89170, + "majorFaults": 0 + }, + "end": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89170, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 701.267989999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.020778, + "firstGuestHostCallMs": 637.0169490000001, + "firstOutputMs": 681.901538, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 682.932886, + "phases": [ + { + "name": "Engine", + "ms": 0.001531 + }, + { + "name": "canonicalPreopens", + "ms": 0.14605 + }, + { + "name": "moduleRead", + "ms": 5.870095 + }, + { + "name": "profileValidation", + "ms": 2.499658 + }, + { + "name": "moduleCompile", + "ms": 627.067177 + }, + { + "name": "importValidation", + "ms": 0.007156 + }, + { + "name": "Linker", + "ms": 0.18549300000000002 + }, + { + "name": "Store", + "ms": 0.019188 + }, + { + "name": "Instance", + "ms": 0.280665 + }, + { + "name": "signalMaskInit", + "ms": 0.053452 + }, + { + "name": "entrypointLookup", + "ms": 0.0037 + }, + { + "name": "wasi.start", + "ms": 45.075098000000004 + }, + { + "name": "Store.teardown", + "ms": 0.869727 + } + ] + }, + "memory": { + "start": { + "rssBytes": 473280512, + "peakRssBytes": 475963392, + "pssBytes": 476416000, + "virtualBytes": 4186775552, + "minorFaults": 89170, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477347840, + "peakRssBytes": 477642752, + "pssBytes": 480790528, + "virtualBytes": 8555094016, + "minorFaults": 90189, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90189, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15016770, + "wasmtimeProcessRetainedRssBytes": 473280512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 77.24019100000442, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.052069, + "firstGuestHostCallMs": 10.311938, + "firstOutputMs": 56.074728, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 56.724537999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.0018050000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.111104 + }, + { + "name": "moduleRead", + "ms": 6.57914 + }, + { + "name": "profileValidation", + "ms": 2.436188 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007672 + }, + { + "name": "Linker", + "ms": 0.21199099999999999 + }, + { + "name": "Store", + "ms": 0.015913 + }, + { + "name": "Instance", + "ms": 0.117839 + }, + { + "name": "signalMaskInit", + "ms": 0.046424999999999994 + }, + { + "name": "entrypointLookup", + "ms": 0.00249 + }, + { + "name": "wasi.start", + "ms": 45.960235000000004 + }, + { + "name": "Store.teardown", + "ms": 0.49905999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90189, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480667648, + "virtualBytes": 8555094016, + "minorFaults": 90235, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480277504, + "virtualBytes": 4190638080, + "minorFaults": 90235, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 76.74016500001017, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.022146000000000002, + "firstGuestHostCallMs": 11.430173, + "firstOutputMs": 53.696803, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.870611, + "phases": [ + { + "name": "Engine", + "ms": 0.00304 + }, + { + "name": "canonicalPreopens", + "ms": 0.168304 + }, + { + "name": "moduleRead", + "ms": 6.628062 + }, + { + "name": "profileValidation", + "ms": 2.4618539999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008036000000000001 + }, + { + "name": "Linker", + "ms": 0.281276 + }, + { + "name": "Store", + "ms": 0.017248000000000003 + }, + { + "name": "Instance", + "ms": 0.674341 + }, + { + "name": "signalMaskInit", + "ms": 0.393251 + }, + { + "name": "entrypointLookup", + "ms": 0.004599000000000001 + }, + { + "name": "wasi.start", + "ms": 42.47754 + }, + { + "name": "Store.teardown", + "ms": 0.042683 + } + ] + }, + "memory": { + "start": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480277504, + "virtualBytes": 4190638080, + "minorFaults": 90235, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480666624, + "virtualBytes": 8555094016, + "minorFaults": 90281, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480277504, + "virtualBytes": 4190638080, + "minorFaults": 90281, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 69.5737999999983, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.026009, + "firstGuestHostCallMs": 10.633286, + "firstOutputMs": 50.183094, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.052511, + "phases": [ + { + "name": "Engine", + "ms": 0.0021119999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.12375299999999999 + }, + { + "name": "moduleRead", + "ms": 6.760506 + }, + { + "name": "profileValidation", + "ms": 2.453287 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007393 + }, + { + "name": "Linker", + "ms": 0.213582 + }, + { + "name": "Store", + "ms": 0.015269 + }, + { + "name": "Instance", + "ms": 0.257516 + }, + { + "name": "signalMaskInit", + "ms": 0.022199 + }, + { + "name": "entrypointLookup", + "ms": 0.002822 + }, + { + "name": "wasi.start", + "ms": 39.746703000000004 + }, + { + "name": "Store.teardown", + "ms": 0.718712 + } + ] + }, + "memory": { + "start": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480277504, + "virtualBytes": 4190638080, + "minorFaults": 90281, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480667648, + "virtualBytes": 8555094016, + "minorFaults": 90327, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90327, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 69.45579399999406, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023451, + "firstGuestHostCallMs": 11.001586000000001, + "firstOutputMs": 52.307385999999994, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.491628999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.002992 + }, + { + "name": "canonicalPreopens", + "ms": 0.181998 + }, + { + "name": "moduleRead", + "ms": 5.401446 + }, + { + "name": "profileValidation", + "ms": 2.51871 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007215 + }, + { + "name": "Linker", + "ms": 0.21313100000000001 + }, + { + "name": "Store", + "ms": 0.016649 + }, + { + "name": "Instance", + "ms": 0.052196 + }, + { + "name": "signalMaskInit", + "ms": 0.036894 + }, + { + "name": "entrypointLookup", + "ms": 0.0033799999999999998 + }, + { + "name": "wasi.start", + "ms": 43.317684 + }, + { + "name": "Store.teardown", + "ms": 0.043441 + } + ] + }, + "memory": { + "start": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90327, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480667648, + "virtualBytes": 8555094016, + "minorFaults": 90373, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90373, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1422.7365940000018, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009145, + "firstGuestHostCallMs": 1397.8471379999999, + "firstOutputMs": 1401.733755, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1405.3472689999999, + "phases": [ + { + "name": "Engine", + "ms": 0.0013930000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.100437 + }, + { + "name": "moduleRead", + "ms": 6.807246 + }, + { + "name": "profileValidation", + "ms": 6.746312 + }, + { + "name": "moduleCompile", + "ms": 1382.653924 + }, + { + "name": "importValidation", + "ms": 0.008564 + }, + { + "name": "Linker", + "ms": 0.185281 + }, + { + "name": "Store", + "ms": 0.017984 + }, + { + "name": "Instance", + "ms": 0.215871 + }, + { + "name": "signalMaskInit", + "ms": 0.176037 + }, + { + "name": "entrypointLookup", + "ms": 0.004046 + }, + { + "name": "wasi.start", + "ms": 6.102162000000001 + }, + { + "name": "Store.teardown", + "ms": 1.5257910000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 477143040, + "peakRssBytes": 477642752, + "pssBytes": 480278528, + "virtualBytes": 4190638080, + "minorFaults": 90373, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 523464704, + "peakRssBytes": 523464704, + "pssBytes": 526821376, + "virtualBytes": 8561414144, + "minorFaults": 105147, + "majorFaults": 0 + }, + "end": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493635584, + "virtualBytes": 4196958208, + "minorFaults": 105147, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15643837, + "wasmtimeProcessRetainedRssBytes": 477143040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 37.25239200000942, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.056794, + "firstGuestHostCallMs": 13.490171, + "firstOutputMs": 16.803251000000003, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 19.14039, + "phases": [ + { + "name": "Engine", + "ms": 0.0020710000000000004 + }, + { + "name": "canonicalPreopens", + "ms": 0.134536 + }, + { + "name": "moduleRead", + "ms": 6.814952 + }, + { + "name": "profileValidation", + "ms": 4.396481 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008737 + }, + { + "name": "Linker", + "ms": 0.212186 + }, + { + "name": "Store", + "ms": 0.016603999999999997 + }, + { + "name": "Instance", + "ms": 0.04591 + }, + { + "name": "signalMaskInit", + "ms": 0.022533 + }, + { + "name": "entrypointLookup", + "ms": 0.002604 + }, + { + "name": "wasi.start", + "ms": 5.761882 + }, + { + "name": "Store.teardown", + "ms": 0.9175989999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493635584, + "virtualBytes": 4196958208, + "minorFaults": 105147, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 492597248, + "peakRssBytes": 523464704, + "pssBytes": 493688832, + "virtualBytes": 8561414144, + "minorFaults": 105173, + "majorFaults": 0 + }, + "end": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493635584, + "virtualBytes": 4196958208, + "minorFaults": 105173, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 40.201015999991796, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018004, + "firstGuestHostCallMs": 11.685701, + "firstOutputMs": 15.312147999999999, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 16.893026, + "phases": [ + { + "name": "Engine", + "ms": 0.0021330000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.16844900000000002 + }, + { + "name": "moduleRead", + "ms": 5.74603 + }, + { + "name": "profileValidation", + "ms": 4.416006 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009075999999999999 + }, + { + "name": "Linker", + "ms": 0.213032 + }, + { + "name": "Store", + "ms": 0.016603 + }, + { + "name": "Instance", + "ms": 0.044102 + }, + { + "name": "signalMaskInit", + "ms": 0.024721 + }, + { + "name": "entrypointLookup", + "ms": 0.0027879999999999997 + }, + { + "name": "wasi.start", + "ms": 5.4550279999999995 + }, + { + "name": "Store.teardown", + "ms": 0.042864 + } + ] + }, + "memory": { + "start": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493635584, + "virtualBytes": 4196958208, + "minorFaults": 105173, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 492597248, + "peakRssBytes": 523464704, + "pssBytes": 496007168, + "virtualBytes": 8561414144, + "minorFaults": 105199, + "majorFaults": 0 + }, + "end": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493634560, + "virtualBytes": 4196958208, + "minorFaults": 105199, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 41.29077300000063, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.019146, + "firstGuestHostCallMs": 11.828559, + "firstOutputMs": 15.334314, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 18.688266, + "phases": [ + { + "name": "Engine", + "ms": 0.001547 + }, + { + "name": "canonicalPreopens", + "ms": 0.150836 + }, + { + "name": "moduleRead", + "ms": 5.78896 + }, + { + "name": "profileValidation", + "ms": 4.761411 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009692 + }, + { + "name": "Linker", + "ms": 0.23539700000000002 + }, + { + "name": "Store", + "ms": 0.018865 + }, + { + "name": "Instance", + "ms": 0.041391 + }, + { + "name": "signalMaskInit", + "ms": 0.051115 + }, + { + "name": "entrypointLookup", + "ms": 0.003875 + }, + { + "name": "wasi.start", + "ms": 5.371792999999999 + }, + { + "name": "Store.teardown", + "ms": 1.4445759999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493634560, + "virtualBytes": 4196958208, + "minorFaults": 105199, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493917184, + "virtualBytes": 8561414144, + "minorFaults": 105227, + "majorFaults": 0 + }, + "end": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493634560, + "virtualBytes": 4196958208, + "minorFaults": 105227, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 44.58231899999373, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010981000000000001, + "firstGuestHostCallMs": 15.680388, + "firstOutputMs": 19.613874, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.142214, + "phases": [ + { + "name": "Engine", + "ms": 0.0019080000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.106236 + }, + { + "name": "moduleRead", + "ms": 7.427302999999999 + }, + { + "name": "profileValidation", + "ms": 4.848232 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015035999999999999 + }, + { + "name": "Linker", + "ms": 0.283439 + }, + { + "name": "Store", + "ms": 0.019556999999999998 + }, + { + "name": "Instance", + "ms": 0.04685 + }, + { + "name": "signalMaskInit", + "ms": 0.060259 + }, + { + "name": "entrypointLookup", + "ms": 0.003311 + }, + { + "name": "wasi.start", + "ms": 8.296223 + }, + { + "name": "Store.teardown", + "ms": 0.241029 + } + ] + }, + "memory": { + "start": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493634560, + "virtualBytes": 4196958208, + "minorFaults": 105227, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 492597248, + "peakRssBytes": 523464704, + "pssBytes": 496034816, + "virtualBytes": 8561414144, + "minorFaults": 105253, + "majorFaults": 0 + }, + "end": { + "rssBytes": 490500096, + "peakRssBytes": 523464704, + "pssBytes": 493634560, + "virtualBytes": 4196958208, + "minorFaults": 105253, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17026491, + "wasmtimeProcessRetainedRssBytes": 490500096, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 3, + "vmSetupMs": 448.1571879999974, + "fixtureSetupMs": 387.02259700000286, + "baseline": { + "rssBytes": 239636480, + "peakRssBytes": 246521856, + "pssBytes": 241855488, + "virtualBytes": 3886764032, + "minorFaults": 55429, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343244800, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 359579648, + "peakRssBytes": 469463040, + "pssBytes": 361319424, + "virtualBytes": 4053245952, + "minorFaults": 817379, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 119943168, + "peakRssBytes": 222941184, + "pssBytes": 119463936, + "virtualBytes": 166481920, + "minorFaults": 761950, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 62.551344999999856, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.027535 + }, + { + "name": "WebAssembly.Module", + "ms": 0.144759 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.056519 + }, + { + "name": "wasi.start", + "ms": 0.089477 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 239636480, + "peakRssBytes": 246521856, + "pssBytes": 241863680, + "virtualBytes": 3888877568, + "minorFaults": 55431, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275525632, + "peakRssBytes": 275820544, + "pssBytes": 263388160, + "virtualBytes": 4641091584, + "minorFaults": 66408, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260874240, + "peakRssBytes": 275820544, + "pssBytes": 263388160, + "virtualBytes": 3955986432, + "minorFaults": 66408, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 239636480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260874240, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 59.87996299999941, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 0.997672 + }, + { + "name": "WebAssembly.Module", + "ms": 0.156684 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.060019 + }, + { + "name": "wasi.start", + "ms": 0.088162 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260874240, + "peakRssBytes": 275820544, + "pssBytes": 263388160, + "virtualBytes": 3955986432, + "minorFaults": 66408, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275369984, + "peakRssBytes": 276213760, + "pssBytes": 263588864, + "virtualBytes": 4640567296, + "minorFaults": 72705, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261267456, + "peakRssBytes": 276213760, + "pssBytes": 263588864, + "virtualBytes": 3955986432, + "minorFaults": 72705, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260874240, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261267456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 60.882388999991235, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.040181 + }, + { + "name": "WebAssembly.Module", + "ms": 0.133264 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.057286 + }, + { + "name": "wasi.start", + "ms": 0.087252 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261267456, + "peakRssBytes": 276213760, + "pssBytes": 263588864, + "virtualBytes": 3955986432, + "minorFaults": 72705, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275980288, + "peakRssBytes": 276213760, + "pssBytes": 263612416, + "virtualBytes": 4641091584, + "minorFaults": 78959, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261255168, + "peakRssBytes": 276213760, + "pssBytes": 263612416, + "virtualBytes": 3955986432, + "minorFaults": 78959, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261267456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261255168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 69.236791000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.122036 + }, + { + "name": "WebAssembly.Module", + "ms": 0.136397 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.086311 + }, + { + "name": "wasi.start", + "ms": 0.10558 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261255168, + "peakRssBytes": 276213760, + "pssBytes": 263612416, + "virtualBytes": 3955986432, + "minorFaults": 78959, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275996672, + "peakRssBytes": 276226048, + "pssBytes": 278460416, + "virtualBytes": 4641091584, + "minorFaults": 85223, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261279744, + "peakRssBytes": 276226048, + "pssBytes": 263673856, + "virtualBytes": 3955986432, + "minorFaults": 85223, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261255168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261279744, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 69.98422399999981, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 2.631698 + }, + { + "name": "WebAssembly.Module", + "ms": 0.136955 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.058705 + }, + { + "name": "wasi.start", + "ms": 0.088483 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261279744, + "peakRssBytes": 276226048, + "pssBytes": 263673856, + "virtualBytes": 3955986432, + "minorFaults": 85223, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275734528, + "peakRssBytes": 276246528, + "pssBytes": 278345728, + "virtualBytes": 4642668544, + "minorFaults": 91486, + "majorFaults": 0 + }, + "end": { + "rssBytes": 261304320, + "peakRssBytes": 276246528, + "pssBytes": 263740416, + "virtualBytes": 3958087680, + "minorFaults": 91486, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261279744, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261304320, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 193.73045200000342, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.745937 + }, + { + "name": "WebAssembly.Module", + "ms": 1.25911 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.839417 + }, + { + "name": "wasi.start", + "ms": 78.454328 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261304320, + "peakRssBytes": 276246528, + "pssBytes": 263740416, + "virtualBytes": 3958087680, + "minorFaults": 91486, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 331591680, + "peakRssBytes": 332402688, + "pssBytes": 329038848, + "virtualBytes": 4696465408, + "minorFaults": 110825, + "majorFaults": 0 + }, + "end": { + "rssBytes": 281292800, + "peakRssBytes": 332402688, + "pssBytes": 282696704, + "virtualBytes": 3958087680, + "minorFaults": 110825, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261304320, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 281292800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 216.3142039999948, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.058934 + }, + { + "name": "WebAssembly.Module", + "ms": 1.464657 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.333507 + }, + { + "name": "wasi.start", + "ms": 105.472731 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 281292800, + "peakRssBytes": 332402688, + "pssBytes": 282696704, + "virtualBytes": 3958087680, + "minorFaults": 110825, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 342351872, + "peakRssBytes": 343162880, + "pssBytes": 336922624, + "virtualBytes": 4696465408, + "minorFaults": 127907, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290656256, + "peakRssBytes": 343162880, + "pssBytes": 199294976, + "virtualBytes": 3958087680, + "minorFaults": 127907, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 281292800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290926592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 201.60102299999562, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.199163 + }, + { + "name": "WebAssembly.Module", + "ms": 1.392289 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.284554 + }, + { + "name": "wasi.start", + "ms": 87.953546 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 290926592, + "peakRssBytes": 343162880, + "pssBytes": 145051648, + "virtualBytes": 3958087680, + "minorFaults": 127938, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 347074560, + "peakRssBytes": 347074560, + "pssBytes": 348552192, + "virtualBytes": 4696727552, + "minorFaults": 141687, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291246080, + "peakRssBytes": 347074560, + "pssBytes": 292649984, + "virtualBytes": 3958087680, + "minorFaults": 141687, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290926592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291246080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 209.6024510000134, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.130117 + }, + { + "name": "WebAssembly.Module", + "ms": 1.412843 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.281598 + }, + { + "name": "wasi.start", + "ms": 94.937303 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 291246080, + "peakRssBytes": 347074560, + "pssBytes": 292649984, + "virtualBytes": 3958087680, + "minorFaults": 141687, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 345739264, + "peakRssBytes": 347074560, + "pssBytes": 347457536, + "virtualBytes": 4696608768, + "minorFaults": 156576, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291246080, + "peakRssBytes": 347074560, + "pssBytes": 292853760, + "virtualBytes": 3958087680, + "minorFaults": 156576, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291246080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291246080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 203.66028599999845, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.958207 + }, + { + "name": "WebAssembly.Module", + "ms": 1.071778 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.643902 + }, + { + "name": "wasi.start", + "ms": 87.898117 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 291246080, + "peakRssBytes": 347074560, + "pssBytes": 292853760, + "virtualBytes": 3958087680, + "minorFaults": 156576, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 348139520, + "peakRssBytes": 348139520, + "pssBytes": 349686784, + "virtualBytes": 4696465408, + "minorFaults": 171452, + "majorFaults": 0 + }, + "end": { + "rssBytes": 293576704, + "peakRssBytes": 348139520, + "pssBytes": 295128064, + "virtualBytes": 3958087680, + "minorFaults": 171452, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291246080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293576704, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 288.921451000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.508649 + }, + { + "name": "WebAssembly.Module", + "ms": 3.57867 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.997754 + }, + { + "name": "wasi.start", + "ms": 110.663375 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293576704, + "peakRssBytes": 348139520, + "pssBytes": 295128064, + "virtualBytes": 3958087680, + "minorFaults": 171452, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 397742080, + "peakRssBytes": 398766080, + "pssBytes": 393649152, + "virtualBytes": 5446287360, + "minorFaults": 202325, + "majorFaults": 0 + }, + "end": { + "rssBytes": 323878912, + "peakRssBytes": 398766080, + "pssBytes": 325634048, + "virtualBytes": 4030365696, + "minorFaults": 202325, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293576704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323878912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 281.22933900001226, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 40.95662 + }, + { + "name": "WebAssembly.Module", + "ms": 4.247875 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.542341 + }, + { + "name": "wasi.start", + "ms": 102.515828 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 323878912, + "peakRssBytes": 398766080, + "pssBytes": 325634048, + "virtualBytes": 4030365696, + "minorFaults": 202325, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419840000, + "peakRssBytes": 419880960, + "pssBytes": 421499904, + "virtualBytes": 5472206848, + "minorFaults": 228585, + "majorFaults": 0 + }, + "end": { + "rssBytes": 324382720, + "peakRssBytes": 419880960, + "pssBytes": 326056960, + "virtualBytes": 4030365696, + "minorFaults": 228585, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323878912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 324382720, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 302.65943400000106, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 59.828208 + }, + { + "name": "WebAssembly.Module", + "ms": 3.084868 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.291481 + }, + { + "name": "wasi.start", + "ms": 107.991284 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 324382720, + "peakRssBytes": 419880960, + "pssBytes": 326056960, + "virtualBytes": 4030365696, + "minorFaults": 228585, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 418791424, + "peakRssBytes": 419880960, + "pssBytes": 420816896, + "virtualBytes": 5474713600, + "minorFaults": 254744, + "majorFaults": 0 + }, + "end": { + "rssBytes": 324378624, + "peakRssBytes": 419880960, + "pssBytes": 326224896, + "virtualBytes": 4032466944, + "minorFaults": 254744, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 324382720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 324378624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 287.537931999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.519574 + }, + { + "name": "WebAssembly.Module", + "ms": 2.952139 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.939056 + }, + { + "name": "wasi.start", + "ms": 93.88884 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 324378624, + "peakRssBytes": 419880960, + "pssBytes": 326224896, + "virtualBytes": 4032466944, + "minorFaults": 254744, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417128448, + "peakRssBytes": 419880960, + "pssBytes": 419163136, + "virtualBytes": 5447864320, + "minorFaults": 281483, + "majorFaults": 0 + }, + "end": { + "rssBytes": 328679424, + "peakRssBytes": 419880960, + "pssBytes": 330926080, + "virtualBytes": 4032466944, + "minorFaults": 281483, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 324378624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328679424, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 286.264995000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 45.167438 + }, + { + "name": "WebAssembly.Module", + "ms": 2.94281 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.496113 + }, + { + "name": "wasi.start", + "ms": 101.729017 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 328679424, + "peakRssBytes": 419880960, + "pssBytes": 330926080, + "virtualBytes": 4032466944, + "minorFaults": 281483, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426598400, + "peakRssBytes": 426799104, + "pssBytes": 428633088, + "virtualBytes": 5474570240, + "minorFaults": 310279, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333553664, + "peakRssBytes": 426799104, + "pssBytes": 335502336, + "virtualBytes": 4032466944, + "minorFaults": 310279, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328679424, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333553664, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 151.78794100000232, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.172214 + }, + { + "name": "WebAssembly.Module", + "ms": 1.526611 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.025625 + }, + { + "name": "wasi.start", + "ms": 20.780235 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333553664, + "peakRssBytes": 426799104, + "pssBytes": 335502336, + "virtualBytes": 4032466944, + "minorFaults": 310279, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402976768, + "peakRssBytes": 426799104, + "pssBytes": 405687296, + "virtualBytes": 4790140928, + "minorFaults": 323849, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333398016, + "peakRssBytes": 426799104, + "pssBytes": 336095232, + "virtualBytes": 4032466944, + "minorFaults": 323849, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333553664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333398016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 178.96058100000664, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 36.237474 + }, + { + "name": "WebAssembly.Module", + "ms": 2.952204 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.323682 + }, + { + "name": "wasi.start", + "ms": 27.442328 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333398016, + "peakRssBytes": 426799104, + "pssBytes": 336095232, + "virtualBytes": 4032466944, + "minorFaults": 323849, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402141184, + "peakRssBytes": 426799104, + "pssBytes": 404859904, + "virtualBytes": 4790403072, + "minorFaults": 338724, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333520896, + "peakRssBytes": 426799104, + "pssBytes": 336165888, + "virtualBytes": 4032466944, + "minorFaults": 338724, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333398016, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333520896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 156.09818099999393, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.334072 + }, + { + "name": "WebAssembly.Module", + "ms": 1.331497 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.298235 + }, + { + "name": "wasi.start", + "ms": 19.919338 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333520896, + "peakRssBytes": 426799104, + "pssBytes": 336165888, + "virtualBytes": 4032466944, + "minorFaults": 338724, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 403525632, + "peakRssBytes": 426799104, + "pssBytes": 406526976, + "virtualBytes": 4789878784, + "minorFaults": 352984, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333426688, + "peakRssBytes": 426799104, + "pssBytes": 336182272, + "virtualBytes": 4032466944, + "minorFaults": 352984, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333520896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333426688, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 149.3080869999976, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.736881 + }, + { + "name": "WebAssembly.Module", + "ms": 2.044763 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.251993 + }, + { + "name": "wasi.start", + "ms": 20.630596 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333426688, + "peakRssBytes": 426799104, + "pssBytes": 336182272, + "virtualBytes": 4032466944, + "minorFaults": 352984, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402960384, + "peakRssBytes": 426799104, + "pssBytes": 405854208, + "virtualBytes": 4790140928, + "minorFaults": 368607, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333512704, + "peakRssBytes": 426799104, + "pssBytes": 336181248, + "virtualBytes": 4032466944, + "minorFaults": 368607, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333426688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333512704, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 150.8167870000034, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.356941 + }, + { + "name": "WebAssembly.Module", + "ms": 1.53092 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.263121 + }, + { + "name": "wasi.start", + "ms": 20.78079 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333512704, + "peakRssBytes": 426799104, + "pssBytes": 336181248, + "virtualBytes": 4032466944, + "minorFaults": 368607, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 402141184, + "peakRssBytes": 426799104, + "pssBytes": 404876288, + "virtualBytes": 4790403072, + "minorFaults": 382969, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333574144, + "peakRssBytes": 426799104, + "pssBytes": 336186368, + "virtualBytes": 4032466944, + "minorFaults": 382969, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333512704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333574144, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 119.87707899999805, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 16.13805 + }, + { + "name": "WebAssembly.Module", + "ms": 2.139888 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.33933 + }, + { + "name": "wasi.start", + "ms": 15.335618 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333574144, + "peakRssBytes": 426799104, + "pssBytes": 336186368, + "virtualBytes": 4032466944, + "minorFaults": 382969, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 385089536, + "peakRssBytes": 426799104, + "pssBytes": 378125312, + "virtualBytes": 4759982080, + "minorFaults": 398225, + "majorFaults": 0 + }, + "end": { + "rssBytes": 348524544, + "peakRssBytes": 426799104, + "pssBytes": 137668608, + "virtualBytes": 4032466944, + "minorFaults": 398225, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333574144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 348794880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 128.70126000000164, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.277527 + }, + { + "name": "WebAssembly.Module", + "ms": 1.020546 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.13505 + }, + { + "name": "wasi.start", + "ms": 14.767001 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349605888, + "peakRssBytes": 426799104, + "pssBytes": 202934272, + "virtualBytes": 4032466944, + "minorFaults": 398495, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 400683008, + "peakRssBytes": 426799104, + "pssBytes": 382372864, + "virtualBytes": 4759838720, + "minorFaults": 417063, + "majorFaults": 0 + }, + "end": { + "rssBytes": 346402816, + "peakRssBytes": 426799104, + "pssBytes": 349240320, + "virtualBytes": 4032466944, + "minorFaults": 417063, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349335552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346402816, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 137.29745800000092, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 15.093761 + }, + { + "name": "WebAssembly.Module", + "ms": 1.887423 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.35427 + }, + { + "name": "wasi.start", + "ms": 14.71256 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346402816, + "peakRssBytes": 426799104, + "pssBytes": 349240320, + "virtualBytes": 4032466944, + "minorFaults": 417063, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 394375168, + "peakRssBytes": 426799104, + "pssBytes": 384719872, + "virtualBytes": 4759457792, + "minorFaults": 435993, + "majorFaults": 0 + }, + "end": { + "rssBytes": 346771456, + "peakRssBytes": 426799104, + "pssBytes": 349096960, + "virtualBytes": 4032466944, + "minorFaults": 435993, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346402816, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346771456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 144.00267899999744, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 18.487234 + }, + { + "name": "WebAssembly.Module", + "ms": 2.316122 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.330165 + }, + { + "name": "wasi.start", + "ms": 15.425251 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346771456, + "peakRssBytes": 426799104, + "pssBytes": 349096960, + "virtualBytes": 4032466944, + "minorFaults": 435993, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 396206080, + "peakRssBytes": 426799104, + "pssBytes": 397436928, + "virtualBytes": 4759052288, + "minorFaults": 455199, + "majorFaults": 0 + }, + "end": { + "rssBytes": 348999680, + "peakRssBytes": 426799104, + "pssBytes": 351909888, + "virtualBytes": 4032466944, + "minorFaults": 455199, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346771456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349270016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 126.28668099999777, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.907533 + }, + { + "name": "WebAssembly.Module", + "ms": 1.737999 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.419431 + }, + { + "name": "wasi.start", + "ms": 15.603728 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349810688, + "peakRssBytes": 426799104, + "pssBytes": 352946176, + "virtualBytes": 4032466944, + "minorFaults": 455398, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395542528, + "peakRssBytes": 426799104, + "pssBytes": 384855040, + "virtualBytes": 4759576576, + "minorFaults": 474464, + "majorFaults": 0 + }, + "end": { + "rssBytes": 344387584, + "peakRssBytes": 426799104, + "pssBytes": 347257856, + "virtualBytes": 4032466944, + "minorFaults": 474464, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349540352, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 344387584, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 211.28133700000762, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.337092 + }, + { + "name": "WebAssembly.Module", + "ms": 3.578023 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.892496 + }, + { + "name": "wasi.start", + "ms": 31.958879 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 344387584, + "peakRssBytes": 426799104, + "pssBytes": 347487232, + "virtualBytes": 4032466944, + "minorFaults": 474468, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427962368, + "peakRssBytes": 428011520, + "pssBytes": 430545920, + "virtualBytes": 4823425024, + "minorFaults": 493042, + "majorFaults": 0 + }, + "end": { + "rssBytes": 332320768, + "peakRssBytes": 428011520, + "pssBytes": 334997504, + "virtualBytes": 4032466944, + "minorFaults": 493042, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 344387584, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 332320768, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 224.99272900000506, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 47.985762 + }, + { + "name": "WebAssembly.Module", + "ms": 3.965453 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.598692 + }, + { + "name": "wasi.start", + "ms": 37.488378 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 332320768, + "peakRssBytes": 428011520, + "pssBytes": 334997504, + "virtualBytes": 4032466944, + "minorFaults": 493042, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426229760, + "peakRssBytes": 428011520, + "pssBytes": 427874304, + "virtualBytes": 4823425024, + "minorFaults": 508921, + "majorFaults": 0 + }, + "end": { + "rssBytes": 332398592, + "peakRssBytes": 428011520, + "pssBytes": 335031296, + "virtualBytes": 4032466944, + "minorFaults": 508921, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 332320768, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 332398592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 228.9931249999936, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 42.500446 + }, + { + "name": "WebAssembly.Module", + "ms": 3.924406 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.363076 + }, + { + "name": "wasi.start", + "ms": 52.643655 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 332398592, + "peakRssBytes": 428011520, + "pssBytes": 335031296, + "virtualBytes": 4032466944, + "minorFaults": 508921, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426311680, + "peakRssBytes": 428011520, + "pssBytes": 427879424, + "virtualBytes": 4823162880, + "minorFaults": 525439, + "majorFaults": 0 + }, + "end": { + "rssBytes": 334622720, + "peakRssBytes": 428011520, + "pssBytes": 337389568, + "virtualBytes": 4032466944, + "minorFaults": 525439, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 332398592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334622720, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 249.97187999999733, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.662683 + }, + { + "name": "WebAssembly.Module", + "ms": 3.98334 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.217912 + }, + { + "name": "wasi.start", + "ms": 48.286444 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 334622720, + "peakRssBytes": 428011520, + "pssBytes": 337389568, + "virtualBytes": 4032466944, + "minorFaults": 525439, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428564480, + "peakRssBytes": 428605440, + "pssBytes": 431213568, + "virtualBytes": 4823425024, + "minorFaults": 544809, + "majorFaults": 0 + }, + "end": { + "rssBytes": 334737408, + "peakRssBytes": 428605440, + "pssBytes": 337456128, + "virtualBytes": 4032466944, + "minorFaults": 544809, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334622720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334737408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 242.7195720000018, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 57.791155 + }, + { + "name": "WebAssembly.Module", + "ms": 3.591065 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.006212 + }, + { + "name": "wasi.start", + "ms": 48.386576 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 334737408, + "peakRssBytes": 428605440, + "pssBytes": 337456128, + "virtualBytes": 4032466944, + "minorFaults": 544809, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427601920, + "peakRssBytes": 428605440, + "pssBytes": 430312448, + "virtualBytes": 4771123200, + "minorFaults": 560837, + "majorFaults": 0 + }, + "end": { + "rssBytes": 335060992, + "peakRssBytes": 428605440, + "pssBytes": 337508352, + "virtualBytes": 4032466944, + "minorFaults": 560837, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335060992, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 256.9399199999898, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 78.999159 + }, + { + "name": "WebAssembly.Module", + "ms": 4.687477 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.141035 + }, + { + "name": "wasi.start", + "ms": 5.66142 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335060992, + "peakRssBytes": 428605440, + "pssBytes": 337508352, + "virtualBytes": 4032466944, + "minorFaults": 560837, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 468639744, + "peakRssBytes": 468836352, + "pssBytes": 470895616, + "virtualBytes": 4812967936, + "minorFaults": 580242, + "majorFaults": 0 + }, + "end": { + "rssBytes": 336924672, + "peakRssBytes": 468836352, + "pssBytes": 339041280, + "virtualBytes": 4033048576, + "minorFaults": 580242, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335060992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336924672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 279.3240050000022, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 87.001015 + }, + { + "name": "WebAssembly.Module", + "ms": 4.373353 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.101403 + }, + { + "name": "wasi.start", + "ms": 6.811082 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336924672, + "peakRssBytes": 468836352, + "pssBytes": 339041280, + "virtualBytes": 4033048576, + "minorFaults": 580242, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 469037056, + "peakRssBytes": 469037056, + "pssBytes": 471144448, + "virtualBytes": 4814098432, + "minorFaults": 601904, + "majorFaults": 0 + }, + "end": { + "rssBytes": 337166336, + "peakRssBytes": 469037056, + "pssBytes": 339147776, + "virtualBytes": 4034060288, + "minorFaults": 601904, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336924672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337166336, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 252.85701199999312, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 79.913787 + }, + { + "name": "WebAssembly.Module", + "ms": 3.720848 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.425466 + }, + { + "name": "wasi.start", + "ms": 5.509757 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 337166336, + "peakRssBytes": 469037056, + "pssBytes": 339147776, + "virtualBytes": 4034060288, + "minorFaults": 601904, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 469221376, + "peakRssBytes": 469315584, + "pssBytes": 471190528, + "virtualBytes": 4814241792, + "minorFaults": 625085, + "majorFaults": 0 + }, + "end": { + "rssBytes": 337244160, + "peakRssBytes": 469315584, + "pssBytes": 339188736, + "virtualBytes": 4034060288, + "minorFaults": 625085, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337166336, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337244160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 254.60162800000398, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.748667 + }, + { + "name": "WebAssembly.Module", + "ms": 3.747941 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.385677 + }, + { + "name": "wasi.start", + "ms": 5.598721 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 337244160, + "peakRssBytes": 469315584, + "pssBytes": 339188736, + "virtualBytes": 4034060288, + "minorFaults": 625085, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 469299200, + "peakRssBytes": 469336064, + "pssBytes": 471194624, + "virtualBytes": 4814098432, + "minorFaults": 648765, + "majorFaults": 0 + }, + "end": { + "rssBytes": 337358848, + "peakRssBytes": 469336064, + "pssBytes": 339188736, + "virtualBytes": 4034060288, + "minorFaults": 648765, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337244160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337358848, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 259.52649499999825, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 78.653276 + }, + { + "name": "WebAssembly.Module", + "ms": 4.03181 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.401701 + }, + { + "name": "wasi.start", + "ms": 8.452253 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 337358848, + "peakRssBytes": 469336064, + "pssBytes": 339188736, + "virtualBytes": 4034060288, + "minorFaults": 648765, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 469348352, + "peakRssBytes": 469463040, + "pssBytes": 471170048, + "virtualBytes": 4814241792, + "minorFaults": 671932, + "majorFaults": 0 + }, + "end": { + "rssBytes": 337461248, + "peakRssBytes": 469463040, + "pssBytes": 339187712, + "virtualBytes": 4034060288, + "minorFaults": 671932, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337358848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337461248, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 204.0650800000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.228178 + }, + { + "name": "WebAssembly.Module", + "ms": 0.74323 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.46641 + }, + { + "name": "wasi.start", + "ms": 72.730725 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 337461248, + "peakRssBytes": 469463040, + "pssBytes": 339187712, + "virtualBytes": 4034060288, + "minorFaults": 671932, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395354112, + "peakRssBytes": 469463040, + "pssBytes": 390736896, + "virtualBytes": 4771356672, + "minorFaults": 687066, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343375872, + "peakRssBytes": 469463040, + "pssBytes": 345017344, + "virtualBytes": 4034293760, + "minorFaults": 687066, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337461248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343375872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 182.86275800000294, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.854853 + }, + { + "name": "WebAssembly.Module", + "ms": 0.784558 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.056165 + }, + { + "name": "wasi.start", + "ms": 65.759949 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343375872, + "peakRssBytes": 469463040, + "pssBytes": 345017344, + "virtualBytes": 4034293760, + "minorFaults": 687066, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395059200, + "peakRssBytes": 469463040, + "pssBytes": 397130752, + "virtualBytes": 4771618816, + "minorFaults": 700779, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343195648, + "peakRssBytes": 469463040, + "pssBytes": 345029632, + "virtualBytes": 4034293760, + "minorFaults": 700779, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343375872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343195648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 206.0170849999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.838748 + }, + { + "name": "WebAssembly.Module", + "ms": 1.890543 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.476438 + }, + { + "name": "wasi.start", + "ms": 76.859856 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343195648, + "peakRssBytes": 469463040, + "pssBytes": 345029632, + "virtualBytes": 4034293760, + "minorFaults": 700779, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395210752, + "peakRssBytes": 469463040, + "pssBytes": 397126656, + "virtualBytes": 4771356672, + "minorFaults": 714515, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343191552, + "peakRssBytes": 469463040, + "pssBytes": 345127936, + "virtualBytes": 4034293760, + "minorFaults": 714515, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343195648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343191552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 177.93755200000305, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.318987 + }, + { + "name": "WebAssembly.Module", + "ms": 0.751106 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.455805 + }, + { + "name": "wasi.start", + "ms": 66.479728 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343191552, + "peakRssBytes": 469463040, + "pssBytes": 345127936, + "virtualBytes": 4034293760, + "minorFaults": 714515, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 395227136, + "peakRssBytes": 469463040, + "pssBytes": 397229056, + "virtualBytes": 4771762176, + "minorFaults": 728738, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343064576, + "peakRssBytes": 469463040, + "pssBytes": 345132032, + "virtualBytes": 4034293760, + "minorFaults": 728738, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343191552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343064576, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 208.87630699999863, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.416105 + }, + { + "name": "WebAssembly.Module", + "ms": 1.802145 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.464722 + }, + { + "name": "wasi.start", + "ms": 84.805979 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343064576, + "peakRssBytes": 469463040, + "pssBytes": 345132032, + "virtualBytes": 4034293760, + "minorFaults": 728738, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 394854400, + "peakRssBytes": 469463040, + "pssBytes": 397232128, + "virtualBytes": 4771880960, + "minorFaults": 742451, + "majorFaults": 0 + }, + "end": { + "rssBytes": 342941696, + "peakRssBytes": 469463040, + "pssBytes": 345136128, + "virtualBytes": 4034293760, + "minorFaults": 742451, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343064576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342941696, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 147.65516300000309, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.335402 + }, + { + "name": "WebAssembly.Module", + "ms": 1.159821 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.123288 + }, + { + "name": "wasi.start", + "ms": 14.274032 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 342941696, + "peakRssBytes": 469463040, + "pssBytes": 345136128, + "virtualBytes": 4034293760, + "minorFaults": 742451, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 410681344, + "peakRssBytes": 469463040, + "pssBytes": 413135872, + "virtualBytes": 4791934976, + "minorFaults": 757170, + "majorFaults": 0 + }, + "end": { + "rssBytes": 342863872, + "peakRssBytes": 469463040, + "pssBytes": 345144320, + "virtualBytes": 4035805184, + "minorFaults": 757170, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342941696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342863872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 154.06495100000757, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.210458 + }, + { + "name": "WebAssembly.Module", + "ms": 1.195261 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.117542 + }, + { + "name": "wasi.start", + "ms": 15.582415 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 342863872, + "peakRssBytes": 469463040, + "pssBytes": 345144320, + "virtualBytes": 4035805184, + "minorFaults": 757170, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 410873856, + "peakRssBytes": 469463040, + "pssBytes": 413219840, + "virtualBytes": 4792459264, + "minorFaults": 772406, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343031808, + "peakRssBytes": 469463040, + "pssBytes": 345147392, + "virtualBytes": 4036067328, + "minorFaults": 772406, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342863872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343031808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 166.72299999999814, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.652448 + }, + { + "name": "WebAssembly.Module", + "ms": 2.663088 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.370068 + }, + { + "name": "wasi.start", + "ms": 18.034706 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343031808, + "peakRssBytes": 469463040, + "pssBytes": 345147392, + "virtualBytes": 4036067328, + "minorFaults": 772406, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 411082752, + "peakRssBytes": 469463040, + "pssBytes": 413191168, + "virtualBytes": 4792459264, + "minorFaults": 786613, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343212032, + "peakRssBytes": 469463040, + "pssBytes": 345148416, + "virtualBytes": 4036067328, + "minorFaults": 786613, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343031808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343212032, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 145.3703519999981, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.427128 + }, + { + "name": "WebAssembly.Module", + "ms": 1.053721 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.554906 + }, + { + "name": "wasi.start", + "ms": 13.19717 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343212032, + "peakRssBytes": 469463040, + "pssBytes": 345148416, + "virtualBytes": 4036067328, + "minorFaults": 786613, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413024256, + "peakRssBytes": 469463040, + "pssBytes": 414968832, + "virtualBytes": 4791934976, + "minorFaults": 801780, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343109632, + "peakRssBytes": 469463040, + "pssBytes": 345148416, + "virtualBytes": 4036067328, + "minorFaults": 801780, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343212032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343109632, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 150.07742300000973, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616363053447532/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.330528 + }, + { + "name": "WebAssembly.Module", + "ms": 1.215589 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.429283 + }, + { + "name": "wasi.start", + "ms": 16.397764 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343109632, + "peakRssBytes": 469463040, + "pssBytes": 345148416, + "virtualBytes": 4036067328, + "minorFaults": 801780, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 411258880, + "peakRssBytes": 469463040, + "pssBytes": 413182976, + "virtualBytes": 4792197120, + "minorFaults": 815987, + "majorFaults": 0 + }, + "end": { + "rssBytes": 343244800, + "peakRssBytes": 469463040, + "pssBytes": 345147392, + "virtualBytes": 4036067328, + "minorFaults": 815987, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343109632, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343244800, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 3, + "vmSetupMs": 433.61118700000225, + "fixtureSetupMs": 391.08179499999096, + "baseline": { + "rssBytes": 240513024, + "peakRssBytes": 247570432, + "pssBytes": 241710080, + "virtualBytes": 3886272512, + "minorFaults": 54992, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 505716736, + "peakRssBytes": 537546752, + "pssBytes": 507726848, + "virtualBytes": 4206153728, + "minorFaults": 122177, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 265203712, + "peakRssBytes": 289976320, + "pssBytes": 266016768, + "virtualBytes": 319881216, + "minorFaults": 67185, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 65.33928299999388, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.061413, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.606175, + "phases": [ + { + "name": "Engine", + "ms": 0.056008 + }, + { + "name": "canonicalPreopens", + "ms": 0.11071 + }, + { + "name": "moduleRead", + "ms": 2.329655 + }, + { + "name": "profileValidation", + "ms": 0.225859 + }, + { + "name": "moduleCompile", + "ms": 45.438172 + }, + { + "name": "importValidation", + "ms": 0.00297 + }, + { + "name": "Linker", + "ms": 0.184504 + }, + { + "name": "Store", + "ms": 0.015281000000000001 + }, + { + "name": "Instance", + "ms": 0.038757999999999994 + }, + { + "name": "signalMaskInit", + "ms": 0.07998899999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.002531 + }, + { + "name": "wasi.start", + "ms": 0.032074 + }, + { + "name": "Store.teardown", + "ms": 0.017374999999999998 + } + ] + }, + "memory": { + "start": { + "rssBytes": 240513024, + "peakRssBytes": 247570432, + "pssBytes": 241718272, + "virtualBytes": 3888386048, + "minorFaults": 54994, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56147, + "majorFaults": 0 + }, + "end": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56147, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240513024, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 21.06144299999869, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008687, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 5.2751079999999995, + "phases": [ + { + "name": "Engine", + "ms": 0.00174 + }, + { + "name": "canonicalPreopens", + "ms": 0.098853 + }, + { + "name": "moduleRead", + "ms": 3.457001 + }, + { + "name": "profileValidation", + "ms": 0.21372200000000002 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002941 + }, + { + "name": "Linker", + "ms": 0.187915 + }, + { + "name": "Store", + "ms": 0.016517 + }, + { + "name": "Instance", + "ms": 0.037071 + }, + { + "name": "signalMaskInit", + "ms": 0.559899 + }, + { + "name": "entrypointLookup", + "ms": 0.006883 + }, + { + "name": "wasi.start", + "ms": 0.583337 + }, + { + "name": "Store.teardown", + "ms": 0.034269 + } + ] + }, + "memory": { + "start": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56147, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 8319873024, + "minorFaults": 56164, + "majorFaults": 0 + }, + "end": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56164, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 20.04453799998737, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.006734, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.676745, + "phases": [ + { + "name": "Engine", + "ms": 0.0015090000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.094358 + }, + { + "name": "moduleRead", + "ms": 3.357549 + }, + { + "name": "profileValidation", + "ms": 0.23646299999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0030180000000000003 + }, + { + "name": "Linker", + "ms": 0.236675 + }, + { + "name": "Store", + "ms": 0.014145 + }, + { + "name": "Instance", + "ms": 0.034228 + }, + { + "name": "signalMaskInit", + "ms": 0.5694969999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.007038 + }, + { + "name": "wasi.start", + "ms": 0.035741999999999996 + }, + { + "name": "Store.teardown", + "ms": 0.015068999999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56164, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56181, + "majorFaults": 0 + }, + "end": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56181, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 20.858960999990813, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013210999999999999, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.615126, + "phases": [ + { + "name": "Engine", + "ms": 0.001547 + }, + { + "name": "canonicalPreopens", + "ms": 0.10448800000000001 + }, + { + "name": "moduleRead", + "ms": 3.290963 + }, + { + "name": "profileValidation", + "ms": 0.227693 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002976 + }, + { + "name": "Linker", + "ms": 0.190635 + }, + { + "name": "Store", + "ms": 0.013686 + }, + { + "name": "Instance", + "ms": 0.0339 + }, + { + "name": "signalMaskInit", + "ms": 0.607518 + }, + { + "name": "entrypointLookup", + "ms": 0.004181000000000001 + }, + { + "name": "wasi.start", + "ms": 0.035851 + }, + { + "name": "Store.teardown", + "ms": 0.017611 + } + ] + }, + "memory": { + "start": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56181, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56198, + "majorFaults": 0 + }, + "end": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56198, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 21.35860700000194, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010881, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 4.660342, + "phases": [ + { + "name": "Engine", + "ms": 0.001869 + }, + { + "name": "canonicalPreopens", + "ms": 0.110219 + }, + { + "name": "moduleRead", + "ms": 3.378627 + }, + { + "name": "profileValidation", + "ms": 0.22754200000000002 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003216 + }, + { + "name": "Linker", + "ms": 0.195656 + }, + { + "name": "Store", + "ms": 0.014620000000000001 + }, + { + "name": "Instance", + "ms": 0.036476 + }, + { + "name": "signalMaskInit", + "ms": 0.576764 + }, + { + "name": "entrypointLookup", + "ms": 0.002791 + }, + { + "name": "wasi.start", + "ms": 0.024122 + }, + { + "name": "Store.teardown", + "ms": 0.014854999999999998 + } + ] + }, + "memory": { + "start": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56198, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56215, + "majorFaults": 0 + }, + "end": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56215, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1132.9555429999891, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010952, + "firstGuestHostCallMs": 1061.427079, + "firstOutputMs": 1115.0437829999998, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1115.875443, + "phases": [ + { + "name": "Engine", + "ms": 0.001584 + }, + { + "name": "canonicalPreopens", + "ms": 0.1004 + }, + { + "name": "moduleRead", + "ms": 6.781451 + }, + { + "name": "profileValidation", + "ms": 4.00545 + }, + { + "name": "moduleCompile", + "ms": 1048.026793 + }, + { + "name": "importValidation", + "ms": 0.006266 + }, + { + "name": "Linker", + "ms": 0.179576 + }, + { + "name": "Store", + "ms": 0.016554000000000003 + }, + { + "name": "Instance", + "ms": 0.28654999999999997 + }, + { + "name": "signalMaskInit", + "ms": 0.076352 + }, + { + "name": "entrypointLookup", + "ms": 0.005327 + }, + { + "name": "wasi.start", + "ms": 55.073003 + }, + { + "name": "Store.teardown", + "ms": 0.674707 + } + ] + }, + "memory": { + "start": { + "rssBytes": 250990592, + "peakRssBytes": 251174912, + "pssBytes": 252267520, + "virtualBytes": 3957784576, + "minorFaults": 56215, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289300480, + "peakRssBytes": 289312768, + "pssBytes": 290601984, + "virtualBytes": 8327688192, + "minorFaults": 65915, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289994752, + "virtualBytes": 3963232256, + "minorFaults": 65915, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45438, + "wasmtimeProcessRetainedRssBytes": 250990592, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 88.90184999999474, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016235999999999997, + "firstGuestHostCallMs": 12.038836, + "firstOutputMs": 70.78366799999999, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 70.92808600000001, + "phases": [ + { + "name": "Engine", + "ms": 0.003307 + }, + { + "name": "canonicalPreopens", + "ms": 0.19168000000000002 + }, + { + "name": "moduleRead", + "ms": 6.356851 + }, + { + "name": "profileValidation", + "ms": 4.316266 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007459 + }, + { + "name": "Linker", + "ms": 0.21665700000000002 + }, + { + "name": "Store", + "ms": 0.021746 + }, + { + "name": "Instance", + "ms": 0.049099 + }, + { + "name": "signalMaskInit", + "ms": 0.092078 + }, + { + "name": "entrypointLookup", + "ms": 0.005686 + }, + { + "name": "wasi.start", + "ms": 59.012100999999994 + }, + { + "name": "Store.teardown", + "ms": 0.046925999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289994752, + "virtualBytes": 3963232256, + "minorFaults": 65915, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289185792, + "peakRssBytes": 289312768, + "pssBytes": 290610176, + "virtualBytes": 8327688192, + "minorFaults": 65984, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 65984, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 79.02812299999641, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015260999999999999, + "firstGuestHostCallMs": 11.676175, + "firstOutputMs": 61.760767, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.967526, + "phases": [ + { + "name": "Engine", + "ms": 0.0018989999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.140845 + }, + { + "name": "moduleRead", + "ms": 6.573293 + }, + { + "name": "profileValidation", + "ms": 3.897814 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007238 + }, + { + "name": "Linker", + "ms": 0.19776200000000002 + }, + { + "name": "Store", + "ms": 0.015880000000000002 + }, + { + "name": "Instance", + "ms": 0.041464 + }, + { + "name": "signalMaskInit", + "ms": 0.046766 + }, + { + "name": "entrypointLookup", + "ms": 0.0025069999999999997 + }, + { + "name": "wasi.start", + "ms": 50.341795000000005 + }, + { + "name": "Store.teardown", + "ms": 0.09295400000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 65984, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289185792, + "peakRssBytes": 289312768, + "pssBytes": 290610176, + "virtualBytes": 8327688192, + "minorFaults": 66053, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66053, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 79.63946199999191, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009699, + "firstGuestHostCallMs": 12.269141000000001, + "firstOutputMs": 62.238201, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 62.451012, + "phases": [ + { + "name": "Engine", + "ms": 0.001612 + }, + { + "name": "canonicalPreopens", + "ms": 0.139873 + }, + { + "name": "moduleRead", + "ms": 7.2093679999999996 + }, + { + "name": "profileValidation", + "ms": 3.884691 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.00643 + }, + { + "name": "Linker", + "ms": 0.195427 + }, + { + "name": "Store", + "ms": 0.015769000000000002 + }, + { + "name": "Instance", + "ms": 0.045524 + }, + { + "name": "signalMaskInit", + "ms": 0.026922 + }, + { + "name": "entrypointLookup", + "ms": 0.003154 + }, + { + "name": "wasi.start", + "ms": 50.231716000000006 + }, + { + "name": "Store.teardown", + "ms": 0.058816 + } + ] + }, + "memory": { + "start": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66053, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289185792, + "peakRssBytes": 289312768, + "pssBytes": 290610176, + "virtualBytes": 8327688192, + "minorFaults": 66122, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66122, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 78.43500699999277, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008574, + "firstGuestHostCallMs": 11.980767, + "firstOutputMs": 61.027173, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.177082, + "phases": [ + { + "name": "Engine", + "ms": 0.001885 + }, + { + "name": "canonicalPreopens", + "ms": 0.113301 + }, + { + "name": "moduleRead", + "ms": 6.632204 + }, + { + "name": "profileValidation", + "ms": 4.170704 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007333999999999999 + }, + { + "name": "Linker", + "ms": 0.200528 + }, + { + "name": "Store", + "ms": 0.019292 + }, + { + "name": "Instance", + "ms": 0.041565 + }, + { + "name": "signalMaskInit", + "ms": 0.040308000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.0035240000000000002 + }, + { + "name": "wasi.start", + "ms": 49.304147 + }, + { + "name": "Store.teardown", + "ms": 0.043954 + } + ] + }, + "memory": { + "start": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66122, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289185792, + "peakRssBytes": 289312768, + "pssBytes": 290610176, + "virtualBytes": 8327688192, + "minorFaults": 66191, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66191, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3833.2702419999987, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012681, + "firstGuestHostCallMs": 3412.4648519999996, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3807.2706630000002, + "phases": [ + { + "name": "Engine", + "ms": 0.0020099999999999996 + }, + { + "name": "canonicalPreopens", + "ms": 0.105832 + }, + { + "name": "moduleRead", + "ms": 11.335995 + }, + { + "name": "profileValidation", + "ms": 12.530371 + }, + { + "name": "moduleCompile", + "ms": 3386.6004470000003 + }, + { + "name": "importValidation", + "ms": 0.009942999999999999 + }, + { + "name": "Linker", + "ms": 0.197655 + }, + { + "name": "Store", + "ms": 0.014624 + }, + { + "name": "Instance", + "ms": 0.20302699999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.042787000000000006 + }, + { + "name": "entrypointLookup", + "ms": 0.005534 + }, + { + "name": "wasi.start", + "ms": 394.69313700000004 + }, + { + "name": "Store.teardown", + "ms": 0.049308000000000005 + } + ] + }, + "memory": { + "start": { + "rssBytes": 288587776, + "peakRssBytes": 289312768, + "pssBytes": 289995776, + "virtualBytes": 3963232256, + "minorFaults": 66191, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424230912, + "peakRssBytes": 424361984, + "pssBytes": 426702848, + "virtualBytes": 12877185024, + "minorFaults": 88924, + "majorFaults": 0 + }, + "end": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426104832, + "virtualBytes": 4148273152, + "minorFaults": 88924, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1093464, + "wasmtimeProcessRetainedRssBytes": 288587776, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 85.88319300000148, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012681, + "firstGuestHostCallMs": 29.604575, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 65.180552, + "phases": [ + { + "name": "Engine", + "ms": 0.003258 + }, + { + "name": "canonicalPreopens", + "ms": 0.11038099999999999 + }, + { + "name": "moduleRead", + "ms": 11.760511 + }, + { + "name": "profileValidation", + "ms": 15.48691 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010331 + }, + { + "name": "Linker", + "ms": 0.21050999999999997 + }, + { + "name": "Store", + "ms": 0.018756000000000002 + }, + { + "name": "Instance", + "ms": 0.48507900000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.053963 + }, + { + "name": "entrypointLookup", + "ms": 0.003765 + }, + { + "name": "wasi.start", + "ms": 35.565020000000004 + }, + { + "name": "Store.teardown", + "ms": 0.038352000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426104832, + "virtualBytes": 4148273152, + "minorFaults": 88924, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426907648, + "virtualBytes": 12877185024, + "minorFaults": 89004, + "majorFaults": 0 + }, + "end": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426104832, + "virtualBytes": 4148273152, + "minorFaults": 89004, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 71.55701199999021, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017585, + "firstGuestHostCallMs": 26.118569, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.312583, + "phases": [ + { + "name": "Engine", + "ms": 0.001794 + }, + { + "name": "canonicalPreopens", + "ms": 0.113924 + }, + { + "name": "moduleRead", + "ms": 11.417188999999999 + }, + { + "name": "profileValidation", + "ms": 12.568963 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009613 + }, + { + "name": "Linker", + "ms": 0.208425 + }, + { + "name": "Store", + "ms": 0.016850999999999998 + }, + { + "name": "Instance", + "ms": 0.278855 + }, + { + "name": "signalMaskInit", + "ms": 0.065811 + }, + { + "name": "entrypointLookup", + "ms": 0.003305 + }, + { + "name": "wasi.start", + "ms": 26.206999000000003 + }, + { + "name": "Store.teardown", + "ms": 0.9852000000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426104832, + "virtualBytes": 4148273152, + "minorFaults": 89004, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426912768, + "virtualBytes": 12877185024, + "minorFaults": 89084, + "majorFaults": 0 + }, + "end": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89084, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 69.20789499999955, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012773, + "firstGuestHostCallMs": 24.914235, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 49.95583, + "phases": [ + { + "name": "Engine", + "ms": 0.002123 + }, + { + "name": "canonicalPreopens", + "ms": 0.107165 + }, + { + "name": "moduleRead", + "ms": 10.474494 + }, + { + "name": "profileValidation", + "ms": 12.565055000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009618 + }, + { + "name": "Linker", + "ms": 0.213699 + }, + { + "name": "Store", + "ms": 0.016427 + }, + { + "name": "Instance", + "ms": 0.072227 + }, + { + "name": "signalMaskInit", + "ms": 0.036454 + }, + { + "name": "entrypointLookup", + "ms": 0.002907 + }, + { + "name": "wasi.start", + "ms": 24.996793999999998 + }, + { + "name": "Store.teardown", + "ms": 0.038175 + } + ] + }, + "memory": { + "start": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89084, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426908672, + "virtualBytes": 12877185024, + "minorFaults": 89164, + "majorFaults": 0 + }, + "end": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89164, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 77.34364999999525, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.02232, + "firstGuestHostCallMs": 26.067353, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 58.710766, + "phases": [ + { + "name": "Engine", + "ms": 0.002842 + }, + { + "name": "canonicalPreopens", + "ms": 0.11732 + }, + { + "name": "moduleRead", + "ms": 11.799558 + }, + { + "name": "profileValidation", + "ms": 12.325028999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009675 + }, + { + "name": "Linker", + "ms": 0.19901200000000002 + }, + { + "name": "Store", + "ms": 0.015527 + }, + { + "name": "Instance", + "ms": 0.052942 + }, + { + "name": "signalMaskInit", + "ms": 0.10354899999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.0043490000000000004 + }, + { + "name": "wasi.start", + "ms": 32.011486999999995 + }, + { + "name": "Store.teardown", + "ms": 0.6261180000000001 + } + ] + }, + "memory": { + "start": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89164, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426912768, + "virtualBytes": 12877185024, + "minorFaults": 89244, + "majorFaults": 0 + }, + "end": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89244, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1578.7898149999965, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011742, + "firstGuestHostCallMs": 1553.553645, + "firstOutputMs": 1560.500973, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1561.76083, + "phases": [ + { + "name": "Engine", + "ms": 0.001927 + }, + { + "name": "canonicalPreopens", + "ms": 0.098175 + }, + { + "name": "moduleRead", + "ms": 6.758533 + }, + { + "name": "profileValidation", + "ms": 6.252835 + }, + { + "name": "moduleCompile", + "ms": 1539.0375760000002 + }, + { + "name": "importValidation", + "ms": 0.010506000000000001 + }, + { + "name": "Linker", + "ms": 0.184134 + }, + { + "name": "Store", + "ms": 0.017853 + }, + { + "name": "Instance", + "ms": 0.35896 + }, + { + "name": "signalMaskInit", + "ms": 0.075096 + }, + { + "name": "entrypointLookup", + "ms": 0.003005 + }, + { + "name": "wasi.start", + "ms": 7.118054000000001 + }, + { + "name": "Store.teardown", + "ms": 1.0608309999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 423641088, + "peakRssBytes": 424361984, + "pssBytes": 426105856, + "virtualBytes": 4148273152, + "minorFaults": 89244, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431263744, + "peakRssBytes": 431775744, + "pssBytes": 434367488, + "virtualBytes": 8520126464, + "minorFaults": 89638, + "majorFaults": 0 + }, + "end": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434367488, + "virtualBytes": 4155670528, + "minorFaults": 89638, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4840160, + "wasmtimeProcessRetainedRssBytes": 423641088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 40.17661799999769, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011593000000000001, + "firstGuestHostCallMs": 15.731381999999998, + "firstOutputMs": 22.019758, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.239863, + "phases": [ + { + "name": "Engine", + "ms": 0.001615 + }, + { + "name": "canonicalPreopens", + "ms": 0.104469 + }, + { + "name": "moduleRead", + "ms": 6.799965 + }, + { + "name": "profileValidation", + "ms": 6.337503000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011392999999999999 + }, + { + "name": "Linker", + "ms": 0.22719099999999998 + }, + { + "name": "Store", + "ms": 0.018712999999999997 + }, + { + "name": "Instance", + "ms": 1.198127 + }, + { + "name": "signalMaskInit", + "ms": 0.07087399999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.004809 + }, + { + "name": "wasi.start", + "ms": 6.668988 + }, + { + "name": "Store.teardown", + "ms": 0.035514 + } + ] + }, + "memory": { + "start": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434367488, + "virtualBytes": 4155670528, + "minorFaults": 89638, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434420736, + "virtualBytes": 8520126464, + "minorFaults": 89684, + "majorFaults": 0 + }, + "end": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434375680, + "virtualBytes": 4155670528, + "minorFaults": 89684, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 44.82561300000816, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016314, + "firstGuestHostCallMs": 15.502453, + "firstOutputMs": 22.976879, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 23.17971, + "phases": [ + { + "name": "Engine", + "ms": 0.00191 + }, + { + "name": "canonicalPreopens", + "ms": 0.138381 + }, + { + "name": "moduleRead", + "ms": 6.916918 + }, + { + "name": "profileValidation", + "ms": 6.527332 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012572 + }, + { + "name": "Linker", + "ms": 0.207378 + }, + { + "name": "Store", + "ms": 0.022816 + }, + { + "name": "Instance", + "ms": 0.8302710000000001 + }, + { + "name": "signalMaskInit", + "ms": 0.037492 + }, + { + "name": "entrypointLookup", + "ms": 0.003389 + }, + { + "name": "wasi.start", + "ms": 7.679703000000001 + }, + { + "name": "Store.teardown", + "ms": 0.032479 + } + ] + }, + "memory": { + "start": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434375680, + "virtualBytes": 4155670528, + "minorFaults": 89684, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434428928, + "virtualBytes": 8520126464, + "minorFaults": 89735, + "majorFaults": 0 + }, + "end": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434404352, + "virtualBytes": 4155670528, + "minorFaults": 89735, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 41.48682199999166, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.01361, + "firstGuestHostCallMs": 15.201557, + "firstOutputMs": 22.378836, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.58944, + "phases": [ + { + "name": "Engine", + "ms": 0.001698 + }, + { + "name": "canonicalPreopens", + "ms": 0.106956 + }, + { + "name": "moduleRead", + "ms": 6.888566 + }, + { + "name": "profileValidation", + "ms": 6.572853 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011196 + }, + { + "name": "Linker", + "ms": 0.203325 + }, + { + "name": "Store", + "ms": 0.017209000000000002 + }, + { + "name": "Instance", + "ms": 0.582656 + }, + { + "name": "signalMaskInit", + "ms": 0.055821 + }, + { + "name": "entrypointLookup", + "ms": 0.002999 + }, + { + "name": "wasi.start", + "ms": 7.34236 + }, + { + "name": "Store.teardown", + "ms": 0.03642 + } + ] + }, + "memory": { + "start": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434404352, + "virtualBytes": 4155670528, + "minorFaults": 89735, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434916352, + "virtualBytes": 8520126464, + "minorFaults": 89780, + "majorFaults": 0 + }, + "end": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434408448, + "virtualBytes": 4155670528, + "minorFaults": 89780, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 42.64855699999316, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013089, + "firstGuestHostCallMs": 15.437503, + "firstOutputMs": 22.284274, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.553601999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.00204 + }, + { + "name": "canonicalPreopens", + "ms": 0.10586699999999999 + }, + { + "name": "moduleRead", + "ms": 6.81738 + }, + { + "name": "profileValidation", + "ms": 6.449603 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011006 + }, + { + "name": "Linker", + "ms": 0.21230000000000002 + }, + { + "name": "Store", + "ms": 0.019447 + }, + { + "name": "Instance", + "ms": 0.9916149999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.062962 + }, + { + "name": "entrypointLookup", + "ms": 0.002722 + }, + { + "name": "wasi.start", + "ms": 7.071784999999999 + }, + { + "name": "Store.teardown", + "ms": 0.038303000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434408448, + "virtualBytes": 4155670528, + "minorFaults": 89780, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434461696, + "virtualBytes": 8520126464, + "minorFaults": 89828, + "majorFaults": 0 + }, + "end": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434424832, + "virtualBytes": 4155670528, + "minorFaults": 89828, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1260.7793850000016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009318, + "firstGuestHostCallMs": 1239.9428560000001, + "firstOutputMs": 1241.894359, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1242.0406500000001, + "phases": [ + { + "name": "Engine", + "ms": 0.002306 + }, + { + "name": "canonicalPreopens", + "ms": 0.097107 + }, + { + "name": "moduleRead", + "ms": 5.148452000000001 + }, + { + "name": "profileValidation", + "ms": 4.852086 + }, + { + "name": "moduleCompile", + "ms": 1229.0389109999999 + }, + { + "name": "importValidation", + "ms": 0.008619 + }, + { + "name": "Linker", + "ms": 0.18545399999999998 + }, + { + "name": "Store", + "ms": 0.016728999999999997 + }, + { + "name": "Instance", + "ms": 0.074301 + }, + { + "name": "signalMaskInit", + "ms": 0.088156 + }, + { + "name": "entrypointLookup", + "ms": 0.003251 + }, + { + "name": "wasi.start", + "ms": 2.013281 + }, + { + "name": "Store.teardown", + "ms": 0.036642 + } + ] + }, + "memory": { + "start": { + "rssBytes": 431144960, + "peakRssBytes": 431775744, + "pssBytes": 434424832, + "virtualBytes": 4155670528, + "minorFaults": 89828, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 438845440, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 8525594624, + "minorFaults": 90727, + "majorFaults": 0 + }, + "end": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90727, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6379197, + "wasmtimeProcessRetainedRssBytes": 431144960, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 34.3704449999932, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014513, + "firstGuestHostCallMs": 11.899265, + "firstOutputMs": 13.717912, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.84792, + "phases": [ + { + "name": "Engine", + "ms": 0.0022 + }, + { + "name": "canonicalPreopens", + "ms": 0.10631199999999999 + }, + { + "name": "moduleRead", + "ms": 5.202622 + }, + { + "name": "profileValidation", + "ms": 4.823577 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008644 + }, + { + "name": "Linker", + "ms": 0.221885 + }, + { + "name": "Store", + "ms": 0.017315 + }, + { + "name": "Instance", + "ms": 1.016242 + }, + { + "name": "signalMaskInit", + "ms": 0.065347 + }, + { + "name": "entrypointLookup", + "ms": 0.0032270000000000003 + }, + { + "name": "wasi.start", + "ms": 1.8790639999999998 + }, + { + "name": "Store.teardown", + "ms": 0.038075 + } + ] + }, + "memory": { + "start": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90727, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439946240, + "virtualBytes": 4161150976, + "minorFaults": 90772, + "majorFaults": 0 + }, + "end": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90772, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 33.00034099999175, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018536999999999998, + "firstGuestHostCallMs": 11.161304000000001, + "firstOutputMs": 13.063033, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.191293, + "phases": [ + { + "name": "Engine", + "ms": 0.002184 + }, + { + "name": "canonicalPreopens", + "ms": 0.102631 + }, + { + "name": "moduleRead", + "ms": 5.254218 + }, + { + "name": "profileValidation", + "ms": 4.806325999999999 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009078000000000001 + }, + { + "name": "Linker", + "ms": 0.201601 + }, + { + "name": "Store", + "ms": 0.018458 + }, + { + "name": "Instance", + "ms": 0.304678 + }, + { + "name": "signalMaskInit", + "ms": 0.023146000000000003 + }, + { + "name": "entrypointLookup", + "ms": 0.003029 + }, + { + "name": "wasi.start", + "ms": 1.9627400000000002 + }, + { + "name": "Store.teardown", + "ms": 0.038074000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90772, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 436666368, + "peakRssBytes": 439091200, + "pssBytes": 439946240, + "virtualBytes": 4161150976, + "minorFaults": 90817, + "majorFaults": 0 + }, + "end": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90817, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 33.992178999993484, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.020277, + "firstGuestHostCallMs": 11.131869, + "firstOutputMs": 12.957676, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.124908, + "phases": [ + { + "name": "Engine", + "ms": 0.004965 + }, + { + "name": "canonicalPreopens", + "ms": 0.104216 + }, + { + "name": "moduleRead", + "ms": 5.090142 + }, + { + "name": "profileValidation", + "ms": 5.177059000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008893 + }, + { + "name": "Linker", + "ms": 0.198719 + }, + { + "name": "Store", + "ms": 0.016228000000000003 + }, + { + "name": "Instance", + "ms": 0.046795 + }, + { + "name": "signalMaskInit", + "ms": 0.039892 + }, + { + "name": "entrypointLookup", + "ms": 0.003228 + }, + { + "name": "wasi.start", + "ms": 1.935058 + }, + { + "name": "Store.teardown", + "ms": 0.034915 + } + ] + }, + "memory": { + "start": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90817, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439946240, + "virtualBytes": 4161150976, + "minorFaults": 90862, + "majorFaults": 0 + }, + "end": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90862, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 33.02261900001031, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.027869, + "firstGuestHostCallMs": 12.801163, + "firstOutputMs": 14.705573, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 15.805647999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.00196 + }, + { + "name": "canonicalPreopens", + "ms": 0.109577 + }, + { + "name": "moduleRead", + "ms": 5.191717000000001 + }, + { + "name": "profileValidation", + "ms": 6.725928 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009554 + }, + { + "name": "Linker", + "ms": 0.206279 + }, + { + "name": "Store", + "ms": 0.017802 + }, + { + "name": "Instance", + "ms": 0.040117 + }, + { + "name": "signalMaskInit", + "ms": 0.03537 + }, + { + "name": "entrypointLookup", + "ms": 0.004043 + }, + { + "name": "wasi.start", + "ms": 1.976416 + }, + { + "name": "Store.teardown", + "ms": 1.0038969999999998 + } + ] + }, + "memory": { + "start": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90862, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 438710272, + "peakRssBytes": 439091200, + "pssBytes": 439946240, + "virtualBytes": 8525594624, + "minorFaults": 90907, + "majorFaults": 0 + }, + "end": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90907, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3647.5878419999935, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015866, + "firstGuestHostCallMs": 3622.488811, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3629.467285, + "phases": [ + { + "name": "Engine", + "ms": 0.004451 + }, + { + "name": "canonicalPreopens", + "ms": 0.107824 + }, + { + "name": "moduleRead", + "ms": 11.050740999999999 + }, + { + "name": "profileValidation", + "ms": 14.622522 + }, + { + "name": "moduleCompile", + "ms": 3594.918841 + }, + { + "name": "importValidation", + "ms": 0.013186 + }, + { + "name": "Linker", + "ms": 0.18970099999999998 + }, + { + "name": "Store", + "ms": 0.014793 + }, + { + "name": "Instance", + "ms": 0.159961 + }, + { + "name": "signalMaskInit", + "ms": 0.064465 + }, + { + "name": "entrypointLookup", + "ms": 0.003279 + }, + { + "name": "wasi.start", + "ms": 6.795909 + }, + { + "name": "Store.teardown", + "ms": 0.158414 + } + ] + }, + "memory": { + "start": { + "rssBytes": 436613120, + "peakRssBytes": 439091200, + "pssBytes": 439892992, + "virtualBytes": 4161138688, + "minorFaults": 90907, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455843840, + "peakRssBytes": 455942144, + "pssBytes": 459156480, + "virtualBytes": 8542011392, + "minorFaults": 93521, + "majorFaults": 0 + }, + "end": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93521, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7608236, + "wasmtimeProcessRetainedRssBytes": 436613120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 63.55266300000949, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.037561000000000004, + "firstGuestHostCallMs": 29.050732999999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 41.628166, + "phases": [ + { + "name": "Engine", + "ms": 0.0016899999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.106958 + }, + { + "name": "moduleRead", + "ms": 11.157531 + }, + { + "name": "profileValidation", + "ms": 14.655652 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015677 + }, + { + "name": "Linker", + "ms": 0.23128600000000002 + }, + { + "name": "Store", + "ms": 0.018602 + }, + { + "name": "Instance", + "ms": 1.4082540000000001 + }, + { + "name": "signalMaskInit", + "ms": 0.061319 + }, + { + "name": "entrypointLookup", + "ms": 0.003139 + }, + { + "name": "wasi.start", + "ms": 12.547958999999999 + }, + { + "name": "Store.teardown", + "ms": 0.043213 + } + ] + }, + "memory": { + "start": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93521, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455761920, + "peakRssBytes": 455942144, + "pssBytes": 459131904, + "virtualBytes": 8542011392, + "minorFaults": 93613, + "majorFaults": 0 + }, + "end": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93613, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 143.09163300000364, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.027975, + "firstGuestHostCallMs": 27.515306, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 52.555591, + "phases": [ + { + "name": "Engine", + "ms": 0.001625 + }, + { + "name": "canonicalPreopens", + "ms": 0.143569 + }, + { + "name": "moduleRead", + "ms": 10.388944 + }, + { + "name": "profileValidation", + "ms": 15.2171 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013940000000000001 + }, + { + "name": "Linker", + "ms": 0.21884699999999999 + }, + { + "name": "Store", + "ms": 0.018074999999999997 + }, + { + "name": "Instance", + "ms": 0.042935999999999995 + }, + { + "name": "signalMaskInit", + "ms": 0.073663 + }, + { + "name": "entrypointLookup", + "ms": 0.003128 + }, + { + "name": "wasi.start", + "ms": 24.211902000000002 + }, + { + "name": "Store.teardown", + "ms": 0.8358009999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93613, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455761920, + "peakRssBytes": 455942144, + "pssBytes": 459118592, + "virtualBytes": 8542011392, + "minorFaults": 93705, + "majorFaults": 0 + }, + "end": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458496000, + "virtualBytes": 4177555456, + "minorFaults": 93705, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 71.08516200000304, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018185999999999997, + "firstGuestHostCallMs": 29.362253000000003, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.563281, + "phases": [ + { + "name": "Engine", + "ms": 0.002182 + }, + { + "name": "canonicalPreopens", + "ms": 0.115883 + }, + { + "name": "moduleRead", + "ms": 11.488725 + }, + { + "name": "profileValidation", + "ms": 14.865995 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015709 + }, + { + "name": "Linker", + "ms": 0.25248000000000004 + }, + { + "name": "Store", + "ms": 0.018289 + }, + { + "name": "Instance", + "ms": 1.18924 + }, + { + "name": "signalMaskInit", + "ms": 0.055434 + }, + { + "name": "entrypointLookup", + "ms": 0.003692 + }, + { + "name": "wasi.start", + "ms": 20.514255 + }, + { + "name": "Store.teardown", + "ms": 1.683402 + } + ] + }, + "memory": { + "start": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458496000, + "virtualBytes": 4177555456, + "minorFaults": 93705, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455761920, + "peakRssBytes": 455942144, + "pssBytes": 459118592, + "virtualBytes": 8542011392, + "minorFaults": 93797, + "majorFaults": 0 + }, + "end": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458496000, + "virtualBytes": 4177555456, + "minorFaults": 93797, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 68.49119200000132, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012721999999999999, + "firstGuestHostCallMs": 27.756422999999998, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.025223, + "phases": [ + { + "name": "Engine", + "ms": 0.002142 + }, + { + "name": "canonicalPreopens", + "ms": 0.11712 + }, + { + "name": "moduleRead", + "ms": 10.106698000000002 + }, + { + "name": "profileValidation", + "ms": 14.875664 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012990999999999999 + }, + { + "name": "Linker", + "ms": 0.218011 + }, + { + "name": "Store", + "ms": 0.017336 + }, + { + "name": "Instance", + "ms": 1.0072699999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.050028 + }, + { + "name": "entrypointLookup", + "ms": 0.00293 + }, + { + "name": "wasi.start", + "ms": 19.417724 + }, + { + "name": "Store.teardown", + "ms": 0.850745 + } + ] + }, + "memory": { + "start": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93797, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455761920, + "peakRssBytes": 455942144, + "pssBytes": 459119616, + "virtualBytes": 8542011392, + "minorFaults": 93889, + "majorFaults": 0 + }, + "end": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93889, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3925.991179000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014525000000000001, + "firstGuestHostCallMs": 3902.062464, + "firstOutputMs": 3903.236422, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3904.1670750000003, + "phases": [ + { + "name": "Engine", + "ms": 0.002355 + }, + { + "name": "canonicalPreopens", + "ms": 0.12074499999999999 + }, + { + "name": "moduleRead", + "ms": 12.687431 + }, + { + "name": "profileValidation", + "ms": 16.298970999999998 + }, + { + "name": "moduleCompile", + "ms": 3870.601056 + }, + { + "name": "importValidation", + "ms": 0.013365 + }, + { + "name": "Linker", + "ms": 0.185975 + }, + { + "name": "Store", + "ms": 0.017823 + }, + { + "name": "Instance", + "ms": 0.229011 + }, + { + "name": "signalMaskInit", + "ms": 0.062864 + }, + { + "name": "entrypointLookup", + "ms": 0.004655 + }, + { + "name": "wasi.start", + "ms": 1.646083 + }, + { + "name": "Store.teardown", + "ms": 0.709468 + } + ] + }, + "memory": { + "start": { + "rssBytes": 455217152, + "peakRssBytes": 455942144, + "pssBytes": 458497024, + "virtualBytes": 4177555456, + "minorFaults": 93889, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 486760448, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 8560607232, + "minorFaults": 97016, + "majorFaults": 0 + }, + "end": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487761920, + "virtualBytes": 4196151296, + "minorFaults": 97016, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11203155, + "wasmtimeProcessRetainedRssBytes": 455217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 57.64679400000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.06402999999999999, + "firstGuestHostCallMs": 32.711693, + "firstOutputMs": 33.472587999999995, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 35.362276, + "phases": [ + { + "name": "Engine", + "ms": 0.003312 + }, + { + "name": "canonicalPreopens", + "ms": 0.137772 + }, + { + "name": "moduleRead", + "ms": 13.229719 + }, + { + "name": "profileValidation", + "ms": 17.059653 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.014695999999999999 + }, + { + "name": "Linker", + "ms": 0.23535999999999999 + }, + { + "name": "Store", + "ms": 0.01627 + }, + { + "name": "Instance", + "ms": 0.052696 + }, + { + "name": "signalMaskInit", + "ms": 0.051342 + }, + { + "name": "entrypointLookup", + "ms": 0.0032389999999999997 + }, + { + "name": "wasi.start", + "ms": 1.167781 + }, + { + "name": "Store.teardown", + "ms": 1.6884370000000002 + } + ] + }, + "memory": { + "start": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487761920, + "virtualBytes": 4196151296, + "minorFaults": 97016, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 486580224, + "peakRssBytes": 487288832, + "pssBytes": 487815168, + "virtualBytes": 8560607232, + "minorFaults": 97056, + "majorFaults": 0 + }, + "end": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 4196151296, + "minorFaults": 97056, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 61.08501300000353, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.032765, + "firstGuestHostCallMs": 32.486441, + "firstOutputMs": 33.180035, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.372545, + "phases": [ + { + "name": "Engine", + "ms": 0.0036959999999999996 + }, + { + "name": "canonicalPreopens", + "ms": 0.156004 + }, + { + "name": "moduleRead", + "ms": 13.021684 + }, + { + "name": "profileValidation", + "ms": 16.714731 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016153 + }, + { + "name": "Linker", + "ms": 0.211812 + }, + { + "name": "Store", + "ms": 0.017096 + }, + { + "name": "Instance", + "ms": 0.385583 + }, + { + "name": "signalMaskInit", + "ms": 0.077122 + }, + { + "name": "entrypointLookup", + "ms": 0.004627 + }, + { + "name": "wasi.start", + "ms": 1.127248 + }, + { + "name": "Store.teardown", + "ms": 0.03583 + } + ] + }, + "memory": { + "start": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487761920, + "virtualBytes": 4196151296, + "minorFaults": 97056, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487815168, + "virtualBytes": 4196163584, + "minorFaults": 97096, + "majorFaults": 0 + }, + "end": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487761920, + "virtualBytes": 4196151296, + "minorFaults": 97096, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 53.61138899999787, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.030195000000000003, + "firstGuestHostCallMs": 31.43012, + "firstOutputMs": 32.121948999999994, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.347986000000006, + "phases": [ + { + "name": "Engine", + "ms": 0.001809 + }, + { + "name": "canonicalPreopens", + "ms": 0.11665 + }, + { + "name": "moduleRead", + "ms": 12.806255 + }, + { + "name": "profileValidation", + "ms": 16.346637 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015699 + }, + { + "name": "Linker", + "ms": 0.211484 + }, + { + "name": "Store", + "ms": 0.016656999999999998 + }, + { + "name": "Instance", + "ms": 0.057667 + }, + { + "name": "signalMaskInit", + "ms": 0.041015 + }, + { + "name": "entrypointLookup", + "ms": 0.003027 + }, + { + "name": "wasi.start", + "ms": 1.0674160000000001 + }, + { + "name": "Store.teardown", + "ms": 1.064317 + } + ] + }, + "memory": { + "start": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487761920, + "virtualBytes": 4196151296, + "minorFaults": 97096, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 486580224, + "peakRssBytes": 487288832, + "pssBytes": 487816192, + "virtualBytes": 8560607232, + "minorFaults": 97136, + "majorFaults": 0 + }, + "end": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 4196151296, + "minorFaults": 97136, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 54.31127099999867, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.020169999999999997, + "firstGuestHostCallMs": 32.826304, + "firstOutputMs": 33.518901, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.702742, + "phases": [ + { + "name": "Engine", + "ms": 0.002026 + }, + { + "name": "canonicalPreopens", + "ms": 0.116071 + }, + { + "name": "moduleRead", + "ms": 13.296837 + }, + { + "name": "profileValidation", + "ms": 16.506373 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.028931000000000002 + }, + { + "name": "Linker", + "ms": 0.221897 + }, + { + "name": "Store", + "ms": 0.021262 + }, + { + "name": "Instance", + "ms": 0.9916659999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.054323 + }, + { + "name": "entrypointLookup", + "ms": 0.004135 + }, + { + "name": "wasi.start", + "ms": 0.842975 + }, + { + "name": "Store.teardown", + "ms": 0.034011 + } + ] + }, + "memory": { + "start": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 4196151296, + "minorFaults": 97136, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487816192, + "virtualBytes": 4196163584, + "minorFaults": 97176, + "majorFaults": 0 + }, + "end": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 4196151296, + "minorFaults": 97176, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 704.4834439999977, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.029507000000000002, + "firstGuestHostCallMs": 638.4833960000001, + "firstOutputMs": 685.4837670000001, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 685.907171, + "phases": [ + { + "name": "Engine", + "ms": 0.0016330000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.104028 + }, + { + "name": "moduleRead", + "ms": 6.712718000000001 + }, + { + "name": "profileValidation", + "ms": 2.4641640000000002 + }, + { + "name": "moduleCompile", + "ms": 626.7437970000001 + }, + { + "name": "importValidation", + "ms": 0.006849999999999999 + }, + { + "name": "Linker", + "ms": 0.19795 + }, + { + "name": "Store", + "ms": 0.017674000000000002 + }, + { + "name": "Instance", + "ms": 1.416236 + }, + { + "name": "signalMaskInit", + "ms": 0.052503999999999995 + }, + { + "name": "entrypointLookup", + "ms": 0.004502 + }, + { + "name": "wasi.start", + "ms": 47.19556 + }, + { + "name": "Store.teardown", + "ms": 0.27375099999999997 + } + ] + }, + "memory": { + "start": { + "rssBytes": 484483072, + "peakRssBytes": 487288832, + "pssBytes": 487762944, + "virtualBytes": 4196151296, + "minorFaults": 97176, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488464384, + "peakRssBytes": 488845312, + "pssBytes": 492136448, + "virtualBytes": 8564469760, + "minorFaults": 97684, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491624448, + "virtualBytes": 4200013824, + "minorFaults": 97684, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15073756, + "wasmtimeProcessRetainedRssBytes": 484483072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 80.66918199999782, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.043263, + "firstGuestHostCallMs": 11.972446, + "firstOutputMs": 60.659459999999996, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.982071, + "phases": [ + { + "name": "Engine", + "ms": 0.003648 + }, + { + "name": "canonicalPreopens", + "ms": 0.10449699999999999 + }, + { + "name": "moduleRead", + "ms": 6.863761 + }, + { + "name": "profileValidation", + "ms": 2.5292790000000003 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016589 + }, + { + "name": "Linker", + "ms": 0.36858399999999997 + }, + { + "name": "Store", + "ms": 0.028926999999999998 + }, + { + "name": "Instance", + "ms": 1.184706 + }, + { + "name": "signalMaskInit", + "ms": 0.068793 + }, + { + "name": "entrypointLookup", + "ms": 0.005861 + }, + { + "name": "wasi.start", + "ms": 48.890334 + }, + { + "name": "Store.teardown", + "ms": 1.189262 + } + ] + }, + "memory": { + "start": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491624448, + "virtualBytes": 4200013824, + "minorFaults": 97684, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 492014592, + "virtualBytes": 8564469760, + "minorFaults": 97730, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97730, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 78.49901600000157, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011562000000000001, + "firstGuestHostCallMs": 10.855895, + "firstOutputMs": 56.392813, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 56.592257, + "phases": [ + { + "name": "Engine", + "ms": 0.001663 + }, + { + "name": "canonicalPreopens", + "ms": 0.104632 + }, + { + "name": "moduleRead", + "ms": 6.672148999999999 + }, + { + "name": "profileValidation", + "ms": 2.427769 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007463 + }, + { + "name": "Linker", + "ms": 0.26673600000000003 + }, + { + "name": "Store", + "ms": 0.016246999999999998 + }, + { + "name": "Instance", + "ms": 0.530946 + }, + { + "name": "signalMaskInit", + "ms": 0.074201 + }, + { + "name": "entrypointLookup", + "ms": 0.003134 + }, + { + "name": "wasi.start", + "ms": 45.748366999999995 + }, + { + "name": "Store.teardown", + "ms": 0.043767 + } + ] + }, + "memory": { + "start": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97730, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 492014592, + "virtualBytes": 8564469760, + "minorFaults": 97776, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97776, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 73.15725499999826, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013937, + "firstGuestHostCallMs": 11.149261000000001, + "firstOutputMs": 53.215142, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.382999, + "phases": [ + { + "name": "Engine", + "ms": 0.0019250000000000003 + }, + { + "name": "canonicalPreopens", + "ms": 0.106507 + }, + { + "name": "moduleRead", + "ms": 6.7321 + }, + { + "name": "profileValidation", + "ms": 2.531263 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008229 + }, + { + "name": "Linker", + "ms": 0.21441200000000002 + }, + { + "name": "Store", + "ms": 0.016336 + }, + { + "name": "Instance", + "ms": 0.717439 + }, + { + "name": "signalMaskInit", + "ms": 0.058616 + }, + { + "name": "entrypointLookup", + "ms": 0.0029620000000000002 + }, + { + "name": "wasi.start", + "ms": 42.272094 + }, + { + "name": "Store.teardown", + "ms": 0.039912 + } + ] + }, + "memory": { + "start": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97776, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 492014592, + "virtualBytes": 8564469760, + "minorFaults": 97822, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97822, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 69.68362499999057, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011182000000000001, + "firstGuestHostCallMs": 11.585134, + "firstOutputMs": 51.242568000000006, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.779971, + "phases": [ + { + "name": "Engine", + "ms": 0.0018470000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.10759700000000001 + }, + { + "name": "moduleRead", + "ms": 6.7175400000000005 + }, + { + "name": "profileValidation", + "ms": 2.458278 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008444 + }, + { + "name": "Linker", + "ms": 0.20864200000000002 + }, + { + "name": "Store", + "ms": 0.015884 + }, + { + "name": "Instance", + "ms": 1.298058 + }, + { + "name": "signalMaskInit", + "ms": 0.017858000000000002 + }, + { + "name": "entrypointLookup", + "ms": 0.003261 + }, + { + "name": "wasi.start", + "ms": 39.860061 + }, + { + "name": "Store.teardown", + "ms": 0.406957 + } + ] + }, + "memory": { + "start": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97822, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 492014592, + "virtualBytes": 8564469760, + "minorFaults": 97868, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97868, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1422.1669039999979, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013963, + "firstGuestHostCallMs": 1396.1942920000001, + "firstOutputMs": 1400.025106, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1401.644271, + "phases": [ + { + "name": "Engine", + "ms": 0.001556 + }, + { + "name": "canonicalPreopens", + "ms": 0.09814500000000001 + }, + { + "name": "moduleRead", + "ms": 6.848649 + }, + { + "name": "profileValidation", + "ms": 4.563613 + }, + { + "name": "moduleCompile", + "ms": 1383.4358399999999 + }, + { + "name": "importValidation", + "ms": 0.008155 + }, + { + "name": "Linker", + "ms": 0.190051 + }, + { + "name": "Store", + "ms": 0.01727 + }, + { + "name": "Instance", + "ms": 0.23408500000000002 + }, + { + "name": "signalMaskInit", + "ms": 0.064375 + }, + { + "name": "entrypointLookup", + "ms": 0.003682 + }, + { + "name": "wasi.start", + "ms": 5.334145 + }, + { + "name": "Store.teardown", + "ms": 0.055519 + } + ] + }, + "memory": { + "start": { + "rssBytes": 488345600, + "peakRssBytes": 488845312, + "pssBytes": 491625472, + "virtualBytes": 4200013824, + "minorFaults": 97868, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 537546752, + "peakRssBytes": 537546752, + "pssBytes": 541027328, + "virtualBytes": 8570789888, + "minorFaults": 122064, + "majorFaults": 0 + }, + "end": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507841536, + "virtualBytes": 4206333952, + "minorFaults": 122064, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15700500, + "wasmtimeProcessRetainedRssBytes": 488345600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 40.02274300000863, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023837, + "firstGuestHostCallMs": 11.511647, + "firstOutputMs": 14.995588000000001, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 16.43291, + "phases": [ + { + "name": "Engine", + "ms": 0.00217 + }, + { + "name": "canonicalPreopens", + "ms": 0.160417 + }, + { + "name": "moduleRead", + "ms": 5.730898 + }, + { + "name": "profileValidation", + "ms": 4.492091 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008741 + }, + { + "name": "Linker", + "ms": 0.20961 + }, + { + "name": "Store", + "ms": 0.020693 + }, + { + "name": "Instance", + "ms": 0.09311799999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.043485 + }, + { + "name": "entrypointLookup", + "ms": 0.003785 + }, + { + "name": "wasi.start", + "ms": 4.87642 + }, + { + "name": "Store.teardown", + "ms": 0.038126 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507841536, + "virtualBytes": 4206333952, + "minorFaults": 122064, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 508124160, + "virtualBytes": 8570789888, + "minorFaults": 122092, + "majorFaults": 0 + }, + "end": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122092, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 47.084476999996696, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023456, + "firstGuestHostCallMs": 16.895801, + "firstOutputMs": 20.360887, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.788918, + "phases": [ + { + "name": "Engine", + "ms": 0.001928 + }, + { + "name": "canonicalPreopens", + "ms": 0.11964999999999999 + }, + { + "name": "moduleRead", + "ms": 7.124592 + }, + { + "name": "profileValidation", + "ms": 6.077514 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009768 + }, + { + "name": "Linker", + "ms": 0.225197 + }, + { + "name": "Store", + "ms": 0.021579 + }, + { + "name": "Instance", + "ms": 0.04507 + }, + { + "name": "signalMaskInit", + "ms": 0.09877000000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.009953 + }, + { + "name": "wasi.start", + "ms": 7.737624 + }, + { + "name": "Store.teardown", + "ms": 0.536914 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122092, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507893760, + "virtualBytes": 8570789888, + "minorFaults": 122120, + "majorFaults": 0 + }, + "end": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122120, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 42.92211799998768, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.038838000000000004, + "firstGuestHostCallMs": 14.319132, + "firstOutputMs": 19.64978, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 23.041923999999998, + "phases": [ + { + "name": "Engine", + "ms": 0.002697 + }, + { + "name": "canonicalPreopens", + "ms": 0.12270900000000001 + }, + { + "name": "moduleRead", + "ms": 7.060201999999999 + }, + { + "name": "profileValidation", + "ms": 4.5781789999999996 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.01202 + }, + { + "name": "Linker", + "ms": 0.323221 + }, + { + "name": "Store", + "ms": 0.025039 + }, + { + "name": "Instance", + "ms": 1.282836 + }, + { + "name": "signalMaskInit", + "ms": 0.079472 + }, + { + "name": "entrypointLookup", + "ms": 0.006863 + }, + { + "name": "wasi.start", + "ms": 8.074437999999999 + }, + { + "name": "Store.teardown", + "ms": 0.664725 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122120, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 508123136, + "virtualBytes": 8570789888, + "minorFaults": 122148, + "majorFaults": 0 + }, + "end": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122148, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 42.377547999989474, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.029675, + "firstGuestHostCallMs": 11.832600000000001, + "firstOutputMs": 15.414722000000001, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.090186, + "phases": [ + { + "name": "Engine", + "ms": 0.0019290000000000002 + }, + { + "name": "canonicalPreopens", + "ms": 0.116672 + }, + { + "name": "moduleRead", + "ms": 5.7155000000000005 + }, + { + "name": "profileValidation", + "ms": 4.746901 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015896 + }, + { + "name": "Linker", + "ms": 0.269459 + }, + { + "name": "Store", + "ms": 0.022980999999999998 + }, + { + "name": "Instance", + "ms": 0.10228999999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.053946 + }, + { + "name": "entrypointLookup", + "ms": 0.0047090000000000005 + }, + { + "name": "wasi.start", + "ms": 5.213612 + }, + { + "name": "Store.teardown", + "ms": 0.04177 + } + ] + }, + "memory": { + "start": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122148, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 508127232, + "virtualBytes": 8570789888, + "minorFaults": 122176, + "majorFaults": 0 + }, + "end": { + "rssBytes": 504561664, + "peakRssBytes": 537546752, + "pssBytes": 507840512, + "virtualBytes": 4206333952, + "minorFaults": 122176, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17083936, + "wasmtimeProcessRetainedRssBytes": 504561664, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 4, + "vmSetupMs": 454.206445000018, + "fixtureSetupMs": 391.0273079999897, + "baseline": { + "rssBytes": 240775168, + "peakRssBytes": 248213504, + "pssBytes": 242141184, + "virtualBytes": 3886759936, + "minorFaults": 56178, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336338944, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 351191040, + "peakRssBytes": 461664256, + "pssBytes": 352684032, + "virtualBytes": 4051144704, + "minorFaults": 810246, + "majorFaults": 2 + }, + "retainedDelta": { + "rssBytes": 110415872, + "peakRssBytes": 213450752, + "pssBytes": 110542848, + "virtualBytes": 164384768, + "minorFaults": 754068, + "majorFaults": 2 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 65.20103199998266, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.033097 + }, + { + "name": "WebAssembly.Module", + "ms": 0.155509 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.077987 + }, + { + "name": "wasi.start", + "ms": 0.093711 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 240775168, + "peakRssBytes": 248213504, + "pssBytes": 242149376, + "virtualBytes": 3888873472, + "minorFaults": 56180, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 276140032, + "peakRssBytes": 277262336, + "pssBytes": 263567360, + "virtualBytes": 4641087488, + "minorFaults": 67146, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262316032, + "peakRssBytes": 277262336, + "pssBytes": 263567360, + "virtualBytes": 3955982336, + "minorFaults": 67146, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240775168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262316032, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 67.20107599999756, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.066299 + }, + { + "name": "WebAssembly.Module", + "ms": 0.134286 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.09563 + }, + { + "name": "wasi.start", + "ms": 0.684966 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262316032, + "peakRssBytes": 277262336, + "pssBytes": 263567360, + "virtualBytes": 3955982336, + "minorFaults": 67146, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277028864, + "peakRssBytes": 277262336, + "pssBytes": 278455296, + "virtualBytes": 4641087488, + "minorFaults": 73413, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262287360, + "peakRssBytes": 277262336, + "pssBytes": 263644160, + "virtualBytes": 3955982336, + "minorFaults": 73413, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262316032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262287360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 77.16965000002529, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 5.43878 + }, + { + "name": "WebAssembly.Module", + "ms": 0.182159 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.080989 + }, + { + "name": "wasi.start", + "ms": 0.120095 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262287360, + "peakRssBytes": 277262336, + "pssBytes": 263644160, + "virtualBytes": 3955982336, + "minorFaults": 73413, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277020672, + "peakRssBytes": 277327872, + "pssBytes": 278328320, + "virtualBytes": 4641087488, + "minorFaults": 79697, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262381568, + "peakRssBytes": 277327872, + "pssBytes": 263787520, + "virtualBytes": 3955982336, + "minorFaults": 79697, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262287360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262381568, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 65.96692499998608, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.21 + }, + { + "name": "WebAssembly.Module", + "ms": 0.146175 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.060374 + }, + { + "name": "wasi.start", + "ms": 0.089439 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262381568, + "peakRssBytes": 277327872, + "pssBytes": 263787520, + "virtualBytes": 3955982336, + "minorFaults": 79697, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277159936, + "peakRssBytes": 277393408, + "pssBytes": 278685696, + "virtualBytes": 4641611776, + "minorFaults": 85977, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262438912, + "peakRssBytes": 277393408, + "pssBytes": 263903232, + "virtualBytes": 3955982336, + "minorFaults": 85977, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262381568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262438912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 62.627100999990944, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.052332 + }, + { + "name": "WebAssembly.Module", + "ms": 0.133003 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.058051 + }, + { + "name": "wasi.start", + "ms": 0.085096 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262438912, + "peakRssBytes": 277393408, + "pssBytes": 263903232, + "virtualBytes": 3955982336, + "minorFaults": 85977, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 277172224, + "peakRssBytes": 277393408, + "pssBytes": 263952384, + "virtualBytes": 4641611776, + "minorFaults": 92238, + "majorFaults": 0 + }, + "end": { + "rssBytes": 262443008, + "peakRssBytes": 277393408, + "pssBytes": 263952384, + "virtualBytes": 3955982336, + "minorFaults": 92238, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262438912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262443008, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 198.24769999997807, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.973678 + }, + { + "name": "WebAssembly.Module", + "ms": 1.261476 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.181944 + }, + { + "name": "wasi.start", + "ms": 87.129355 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 262443008, + "peakRssBytes": 277393408, + "pssBytes": 263952384, + "virtualBytes": 3955982336, + "minorFaults": 92238, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 335695872, + "peakRssBytes": 335695872, + "pssBytes": 331053056, + "virtualBytes": 4694765568, + "minorFaults": 111439, + "majorFaults": 0 + }, + "end": { + "rssBytes": 284057600, + "peakRssBytes": 335695872, + "pssBytes": 285308928, + "virtualBytes": 3955982336, + "minorFaults": 111439, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 262443008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 284057600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 215.21729599998798, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.211752 + }, + { + "name": "WebAssembly.Module", + "ms": 1.584255 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.270556 + }, + { + "name": "wasi.start", + "ms": 97.04854 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 284057600, + "peakRssBytes": 335695872, + "pssBytes": 285308928, + "virtualBytes": 3955982336, + "minorFaults": 111439, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 341975040, + "peakRssBytes": 342515712, + "pssBytes": 339530752, + "virtualBytes": 4694884352, + "minorFaults": 126560, + "majorFaults": 0 + }, + "end": { + "rssBytes": 291663872, + "peakRssBytes": 342515712, + "pssBytes": 292734976, + "virtualBytes": 3955982336, + "minorFaults": 126560, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 284057600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291663872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 204.2548930000048, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.155081 + }, + { + "name": "WebAssembly.Module", + "ms": 0.963386 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.295561 + }, + { + "name": "wasi.start", + "ms": 82.869031 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 291663872, + "peakRssBytes": 342515712, + "pssBytes": 292734976, + "virtualBytes": 3955982336, + "minorFaults": 126560, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 351076352, + "peakRssBytes": 351887360, + "pssBytes": 349272064, + "virtualBytes": 4694622208, + "minorFaults": 142724, + "majorFaults": 0 + }, + "end": { + "rssBytes": 301088768, + "peakRssBytes": 351887360, + "pssBytes": 302381056, + "virtualBytes": 3955982336, + "minorFaults": 142724, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 291663872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 301088768, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 214.20567699999083, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.277933 + }, + { + "name": "WebAssembly.Module", + "ms": 1.495072 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.5324 + }, + { + "name": "wasi.start", + "ms": 98.425269 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 301088768, + "peakRssBytes": 351887360, + "pssBytes": 302381056, + "virtualBytes": 3955982336, + "minorFaults": 142724, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 357515264, + "peakRssBytes": 357515264, + "pssBytes": 358753280, + "virtualBytes": 4694884352, + "minorFaults": 157006, + "majorFaults": 0 + }, + "end": { + "rssBytes": 301477888, + "peakRssBytes": 357515264, + "pssBytes": 302801920, + "virtualBytes": 3955982336, + "minorFaults": 157006, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 301088768, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 301477888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 196.19557399998303, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.876788 + }, + { + "name": "WebAssembly.Module", + "ms": 1.445691 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.274983 + }, + { + "name": "wasi.start", + "ms": 81.973178 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 301477888, + "peakRssBytes": 357515264, + "pssBytes": 302801920, + "virtualBytes": 3955982336, + "minorFaults": 157006, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 360824832, + "peakRssBytes": 361365504, + "pssBytes": 356993024, + "virtualBytes": 4695146496, + "minorFaults": 174615, + "majorFaults": 0 + }, + "end": { + "rssBytes": 310734848, + "peakRssBytes": 361365504, + "pssBytes": 312080384, + "virtualBytes": 3955982336, + "minorFaults": 174615, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 301477888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 310734848, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 298.358665000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 54.504482 + }, + { + "name": "WebAssembly.Module", + "ms": 3.687999 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.210181 + }, + { + "name": "wasi.start", + "ms": 115.239284 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 310734848, + "peakRssBytes": 361365504, + "pssBytes": 312080384, + "virtualBytes": 3955982336, + "minorFaults": 174615, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 409223168, + "peakRssBytes": 410177536, + "pssBytes": 410359808, + "virtualBytes": 5472206848, + "minorFaults": 205659, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338649088, + "peakRssBytes": 410177536, + "pssBytes": 340404224, + "virtualBytes": 4030365696, + "minorFaults": 205659, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 310734848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338649088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 296.6195059999882, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 66.006519 + }, + { + "name": "WebAssembly.Module", + "ms": 3.94481 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.192619 + }, + { + "name": "wasi.start", + "ms": 93.473863 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338649088, + "peakRssBytes": 410177536, + "pssBytes": 340404224, + "virtualBytes": 4030365696, + "minorFaults": 205659, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 431505408, + "peakRssBytes": 431775744, + "pssBytes": 433330176, + "virtualBytes": 5472206848, + "minorFaults": 232122, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338440192, + "peakRssBytes": 431775744, + "pssBytes": 340510720, + "virtualBytes": 4030365696, + "minorFaults": 232122, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338649088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338440192, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 295.4741109999886, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.067201 + }, + { + "name": "WebAssembly.Module", + "ms": 2.841009 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.128616 + }, + { + "name": "wasi.start", + "ms": 96.817006 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338440192, + "peakRssBytes": 431775744, + "pssBytes": 340510720, + "virtualBytes": 4030365696, + "minorFaults": 232122, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 435150848, + "peakRssBytes": 435314688, + "pssBytes": 437143552, + "virtualBytes": 5472206848, + "minorFaults": 257421, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338788352, + "peakRssBytes": 435314688, + "pssBytes": 340645888, + "virtualBytes": 4030365696, + "minorFaults": 257421, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338440192, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338788352, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 289.81836599999224, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.29198 + }, + { + "name": "WebAssembly.Module", + "ms": 2.998876 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.490275 + }, + { + "name": "wasi.start", + "ms": 105.888752 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338788352, + "peakRssBytes": 435314688, + "pssBytes": 340645888, + "virtualBytes": 4030365696, + "minorFaults": 257421, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 435138560, + "peakRssBytes": 435314688, + "pssBytes": 437106688, + "virtualBytes": 5472468992, + "minorFaults": 284809, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338927616, + "peakRssBytes": 435314688, + "pssBytes": 340767744, + "virtualBytes": 4030365696, + "minorFaults": 284809, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338788352, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338927616, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 292.4124449999945, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.180772 + }, + { + "name": "WebAssembly.Module", + "ms": 3.001616 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.856694 + }, + { + "name": "wasi.start", + "ms": 98.476982 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338927616, + "peakRssBytes": 435314688, + "pssBytes": 340767744, + "virtualBytes": 4030365696, + "minorFaults": 284809, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 435339264, + "peakRssBytes": 435384320, + "pssBytes": 435681280, + "virtualBytes": 5472993280, + "minorFaults": 311666, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338878464, + "peakRssBytes": 435384320, + "pssBytes": 340776960, + "virtualBytes": 4030365696, + "minorFaults": 311666, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338927616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338878464, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 156.92659499999718, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.224042 + }, + { + "name": "WebAssembly.Module", + "ms": 1.337905 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.347908 + }, + { + "name": "wasi.start", + "ms": 24.279254 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338878464, + "peakRssBytes": 435384320, + "pssBytes": 340776960, + "virtualBytes": 4030365696, + "minorFaults": 311666, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 408915968, + "peakRssBytes": 435384320, + "pssBytes": 412006400, + "virtualBytes": 4787920896, + "minorFaults": 325956, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338739200, + "peakRssBytes": 435384320, + "pssBytes": 341570560, + "virtualBytes": 4030365696, + "minorFaults": 325956, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338878464, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338739200, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 168.0198579999851, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.890353 + }, + { + "name": "WebAssembly.Module", + "ms": 1.601214 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.86573 + }, + { + "name": "wasi.start", + "ms": 24.189695 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338739200, + "peakRssBytes": 435384320, + "pssBytes": 341570560, + "virtualBytes": 4030365696, + "minorFaults": 325956, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 408236032, + "peakRssBytes": 435384320, + "pssBytes": 411342848, + "virtualBytes": 4788301824, + "minorFaults": 339048, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338685952, + "peakRssBytes": 435384320, + "pssBytes": 341579776, + "virtualBytes": 4030365696, + "minorFaults": 339048, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338739200, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338685952, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 159.94001799999387, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.700574 + }, + { + "name": "WebAssembly.Module", + "ms": 2.47477 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.270001 + }, + { + "name": "wasi.start", + "ms": 31.552213 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338685952, + "peakRssBytes": 435384320, + "pssBytes": 341579776, + "virtualBytes": 4030365696, + "minorFaults": 339048, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 407130112, + "peakRssBytes": 435384320, + "pssBytes": 410257408, + "virtualBytes": 4788039680, + "minorFaults": 354432, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338497536, + "peakRssBytes": 435384320, + "pssBytes": 341587968, + "virtualBytes": 4030365696, + "minorFaults": 354432, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338685952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338497536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 163.62241599999834, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.175474 + }, + { + "name": "WebAssembly.Module", + "ms": 3.224404 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.884752 + }, + { + "name": "wasi.start", + "ms": 21.947055 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338497536, + "peakRssBytes": 435384320, + "pssBytes": 341587968, + "virtualBytes": 4030365696, + "minorFaults": 354432, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 409014272, + "peakRssBytes": 435384320, + "pssBytes": 412107776, + "virtualBytes": 4788445184, + "minorFaults": 370775, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338677760, + "peakRssBytes": 435384320, + "pssBytes": 341591040, + "virtualBytes": 4030365696, + "minorFaults": 370775, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338497536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338677760, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 166.2131170000066, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.477586 + }, + { + "name": "WebAssembly.Module", + "ms": 2.075583 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.268986 + }, + { + "name": "wasi.start", + "ms": 21.903635 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338677760, + "peakRssBytes": 435384320, + "pssBytes": 341591040, + "virtualBytes": 4030365696, + "minorFaults": 370775, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 408461312, + "peakRssBytes": 435384320, + "pssBytes": 411432960, + "virtualBytes": 4788301824, + "minorFaults": 386442, + "majorFaults": 2 + }, + "end": { + "rssBytes": 338808832, + "peakRssBytes": 435384320, + "pssBytes": 341596160, + "virtualBytes": 4030365696, + "minorFaults": 386442, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338677760, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338808832, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 121.07806999998866, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 15.779233 + }, + { + "name": "WebAssembly.Module", + "ms": 1.278627 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.127399 + }, + { + "name": "wasi.start", + "ms": 15.155171 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338808832, + "peakRssBytes": 435384320, + "pssBytes": 341596160, + "virtualBytes": 4030365696, + "minorFaults": 386442, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 386686976, + "peakRssBytes": 435384320, + "pssBytes": 384555008, + "virtualBytes": 4757618688, + "minorFaults": 403180, + "majorFaults": 2 + }, + "end": { + "rssBytes": 352972800, + "peakRssBytes": 435384320, + "pssBytes": 110075904, + "virtualBytes": 4030365696, + "minorFaults": 403180, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338808832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353243136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 149.01706400001422, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.39532 + }, + { + "name": "WebAssembly.Module", + "ms": 2.051849 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.341966 + }, + { + "name": "wasi.start", + "ms": 16.760516 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353243136, + "peakRssBytes": 435384320, + "pssBytes": 102608896, + "virtualBytes": 4030365696, + "minorFaults": 403222, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 403828736, + "peakRssBytes": 435384320, + "pssBytes": 386749440, + "virtualBytes": 4757737472, + "minorFaults": 421406, + "majorFaults": 2 + }, + "end": { + "rssBytes": 347779072, + "peakRssBytes": 435384320, + "pssBytes": 114473984, + "virtualBytes": 4030365696, + "minorFaults": 421406, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353243136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 348049408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 157.19159400000353, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 15.829264 + }, + { + "name": "WebAssembly.Module", + "ms": 2.316276 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.339272 + }, + { + "name": "wasi.start", + "ms": 16.911067 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 348049408, + "peakRssBytes": 435384320, + "pssBytes": 103709696, + "virtualBytes": 4030365696, + "minorFaults": 421461, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 396787712, + "peakRssBytes": 435384320, + "pssBytes": 397029376, + "virtualBytes": 4757737472, + "minorFaults": 444395, + "majorFaults": 2 + }, + "end": { + "rssBytes": 363462656, + "peakRssBytes": 435384320, + "pssBytes": 366434304, + "virtualBytes": 4030365696, + "minorFaults": 444395, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 348049408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363732992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 133.39156899999944, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.942979 + }, + { + "name": "WebAssembly.Module", + "ms": 2.466262 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.435186 + }, + { + "name": "wasi.start", + "ms": 14.899352 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364273664, + "peakRssBytes": 435384320, + "pssBytes": 367650816, + "virtualBytes": 4030365696, + "minorFaults": 444595, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 416227328, + "peakRssBytes": 435384320, + "pssBytes": 418091008, + "virtualBytes": 4757213184, + "minorFaults": 460637, + "majorFaults": 2 + }, + "end": { + "rssBytes": 350806016, + "peakRssBytes": 435384320, + "pssBytes": 353732608, + "virtualBytes": 4030365696, + "minorFaults": 460637, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364003328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 350806016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 126.75360600001295, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.867327 + }, + { + "name": "WebAssembly.Module", + "ms": 2.162115 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.448507 + }, + { + "name": "wasi.start", + "ms": 17.762825 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 350806016, + "peakRssBytes": 435384320, + "pssBytes": 353732608, + "virtualBytes": 4030365696, + "minorFaults": 460637, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 395182080, + "peakRssBytes": 435384320, + "pssBytes": 377226240, + "virtualBytes": 4757880832, + "minorFaults": 476017, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336642048, + "peakRssBytes": 435384320, + "pssBytes": 291802112, + "virtualBytes": 4030365696, + "minorFaults": 476017, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 350806016, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336642048, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 214.5888440000126, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 42.950191 + }, + { + "name": "WebAssembly.Module", + "ms": 3.567688 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.877053 + }, + { + "name": "wasi.start", + "ms": 22.931126 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336642048, + "peakRssBytes": 435384320, + "pssBytes": 260566016, + "virtualBytes": 4030365696, + "minorFaults": 476067, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 442273792, + "peakRssBytes": 442273792, + "pssBytes": 417857536, + "virtualBytes": 4742840320, + "minorFaults": 495230, + "majorFaults": 2 + }, + "end": { + "rssBytes": 323264512, + "peakRssBytes": 442273792, + "pssBytes": 325941248, + "virtualBytes": 4030365696, + "minorFaults": 495230, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336642048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323264512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 214.34777200000826, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 45.059566 + }, + { + "name": "WebAssembly.Module", + "ms": 3.888223 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.291383 + }, + { + "name": "wasi.start", + "ms": 35.121866 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 323264512, + "peakRssBytes": 442273792, + "pssBytes": 325941248, + "virtualBytes": 4030365696, + "minorFaults": 495230, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 416858112, + "peakRssBytes": 442273792, + "pssBytes": 419657728, + "virtualBytes": 4821323776, + "minorFaults": 510155, + "majorFaults": 2 + }, + "end": { + "rssBytes": 323080192, + "peakRssBytes": 442273792, + "pssBytes": 325944320, + "virtualBytes": 4030365696, + "minorFaults": 510155, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323264512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323080192, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 239.49872999999207, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.389274 + }, + { + "name": "WebAssembly.Module", + "ms": 4.050695 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.024154 + }, + { + "name": "wasi.start", + "ms": 45.18367 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 323080192, + "peakRssBytes": 442273792, + "pssBytes": 325944320, + "virtualBytes": 4030365696, + "minorFaults": 510155, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 417255424, + "peakRssBytes": 442273792, + "pssBytes": 419657728, + "virtualBytes": 4821323776, + "minorFaults": 524397, + "majorFaults": 2 + }, + "end": { + "rssBytes": 323489792, + "peakRssBytes": 442273792, + "pssBytes": 325949440, + "virtualBytes": 4030365696, + "minorFaults": 524397, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323080192, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323489792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 229.55114699999103, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 54.313369 + }, + { + "name": "WebAssembly.Module", + "ms": 3.968773 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.593821 + }, + { + "name": "wasi.start", + "ms": 40.185303 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 323489792, + "peakRssBytes": 442273792, + "pssBytes": 325949440, + "virtualBytes": 4030365696, + "minorFaults": 524397, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 416231424, + "peakRssBytes": 442273792, + "pssBytes": 418801664, + "virtualBytes": 4769165312, + "minorFaults": 540458, + "majorFaults": 2 + }, + "end": { + "rssBytes": 325730304, + "peakRssBytes": 442273792, + "pssBytes": 328250368, + "virtualBytes": 4030365696, + "minorFaults": 540458, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 323489792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 325730304, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 236.40987099998165, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 43.473806 + }, + { + "name": "WebAssembly.Module", + "ms": 4.526786 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.342553 + }, + { + "name": "wasi.start", + "ms": 45.603266 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 325730304, + "peakRssBytes": 442273792, + "pssBytes": 328250368, + "virtualBytes": 4030365696, + "minorFaults": 540458, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 421150720, + "peakRssBytes": 442273792, + "pssBytes": 423925760, + "virtualBytes": 4821061632, + "minorFaults": 555113, + "majorFaults": 2 + }, + "end": { + "rssBytes": 325550080, + "peakRssBytes": 442273792, + "pssBytes": 328251392, + "virtualBytes": 4030365696, + "minorFaults": 555113, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 325730304, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 325550080, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 256.18613099999493, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.722603 + }, + { + "name": "WebAssembly.Module", + "ms": 4.176855 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.411175 + }, + { + "name": "wasi.start", + "ms": 6.189217 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 325550080, + "peakRssBytes": 442273792, + "pssBytes": 328251392, + "virtualBytes": 4030365696, + "minorFaults": 555113, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 459726848, + "peakRssBytes": 459796480, + "pssBytes": 461677568, + "virtualBytes": 4810985472, + "minorFaults": 579072, + "majorFaults": 2 + }, + "end": { + "rssBytes": 329523200, + "peakRssBytes": 459796480, + "pssBytes": 331498496, + "virtualBytes": 4030947328, + "minorFaults": 579072, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 325550080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329523200, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 245.2724370000069, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.715033 + }, + { + "name": "WebAssembly.Module", + "ms": 3.320696 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.134469 + }, + { + "name": "wasi.start", + "ms": 5.083013 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329523200, + "peakRssBytes": 459796480, + "pssBytes": 331498496, + "virtualBytes": 4030947328, + "minorFaults": 579072, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 461582336, + "peakRssBytes": 461664256, + "pssBytes": 463493120, + "virtualBytes": 4812140544, + "minorFaults": 601736, + "majorFaults": 2 + }, + "end": { + "rssBytes": 329596928, + "peakRssBytes": 461664256, + "pssBytes": 331520000, + "virtualBytes": 4031959040, + "minorFaults": 601736, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329523200, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329596928, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 250.14351300001726, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 76.620721 + }, + { + "name": "WebAssembly.Module", + "ms": 4.536233 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.131658 + }, + { + "name": "wasi.start", + "ms": 5.893024 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329596928, + "peakRssBytes": 461664256, + "pssBytes": 331520000, + "virtualBytes": 4031959040, + "minorFaults": 601736, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 461283328, + "peakRssBytes": 461664256, + "pssBytes": 463619072, + "virtualBytes": 4811735040, + "minorFaults": 625438, + "majorFaults": 2 + }, + "end": { + "rssBytes": 329240576, + "peakRssBytes": 461664256, + "pssBytes": 331613184, + "virtualBytes": 4031959040, + "minorFaults": 625438, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329596928, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329240576, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 232.30195599998115, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.432158 + }, + { + "name": "WebAssembly.Module", + "ms": 3.574336 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.482583 + }, + { + "name": "wasi.start", + "ms": 5.23836 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329240576, + "peakRssBytes": 461664256, + "pssBytes": 331613184, + "virtualBytes": 4031959040, + "minorFaults": 625438, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 461193216, + "peakRssBytes": 461664256, + "pssBytes": 463607808, + "virtualBytes": 4811735040, + "minorFaults": 647583, + "majorFaults": 2 + }, + "end": { + "rssBytes": 329601024, + "peakRssBytes": 461664256, + "pssBytes": 331614208, + "virtualBytes": 4031959040, + "minorFaults": 647583, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329240576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329601024, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 263.31257700000424, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 76.177884 + }, + { + "name": "WebAssembly.Module", + "ms": 6.073202 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.911973 + }, + { + "name": "wasi.start", + "ms": 7.522311 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329601024, + "peakRssBytes": 461664256, + "pssBytes": 331614208, + "virtualBytes": 4031959040, + "minorFaults": 647583, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 461312000, + "peakRssBytes": 461664256, + "pssBytes": 463586304, + "virtualBytes": 4811997184, + "minorFaults": 667103, + "majorFaults": 2 + }, + "end": { + "rssBytes": 329474048, + "peakRssBytes": 461664256, + "pssBytes": 331614208, + "virtualBytes": 4031959040, + "minorFaults": 667103, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329601024, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329474048, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 189.41482900001574, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.038964 + }, + { + "name": "WebAssembly.Module", + "ms": 0.96623 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.534165 + }, + { + "name": "wasi.start", + "ms": 73.601111 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329474048, + "peakRssBytes": 461664256, + "pssBytes": 331614208, + "virtualBytes": 4031959040, + "minorFaults": 667103, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 385671168, + "peakRssBytes": 461664256, + "pssBytes": 383322112, + "virtualBytes": 4769255424, + "minorFaults": 680843, + "majorFaults": 2 + }, + "end": { + "rssBytes": 333893632, + "peakRssBytes": 461664256, + "pssBytes": 335915008, + "virtualBytes": 4032192512, + "minorFaults": 680843, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329474048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333893632, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 197.99462299997685, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.256939 + }, + { + "name": "WebAssembly.Module", + "ms": 2.218687 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.380348 + }, + { + "name": "wasi.start", + "ms": 78.479483 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333893632, + "peakRssBytes": 461664256, + "pssBytes": 335915008, + "virtualBytes": 4032192512, + "minorFaults": 680843, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 385568768, + "peakRssBytes": 461664256, + "pssBytes": 388023296, + "virtualBytes": 4770041856, + "minorFaults": 694555, + "majorFaults": 2 + }, + "end": { + "rssBytes": 333733888, + "peakRssBytes": 461664256, + "pssBytes": 335918080, + "virtualBytes": 4032192512, + "minorFaults": 694555, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333893632, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333733888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 186.17764599999646, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 20.716362 + }, + { + "name": "WebAssembly.Module", + "ms": 0.925174 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.366315 + }, + { + "name": "wasi.start", + "ms": 68.098945 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333733888, + "peakRssBytes": 461664256, + "pssBytes": 335918080, + "virtualBytes": 4032192512, + "minorFaults": 694555, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 386797568, + "peakRssBytes": 461664256, + "pssBytes": 389023744, + "virtualBytes": 4770041856, + "minorFaults": 708000, + "majorFaults": 2 + }, + "end": { + "rssBytes": 334942208, + "peakRssBytes": 461664256, + "pssBytes": 336918528, + "virtualBytes": 4032192512, + "minorFaults": 708000, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333733888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334942208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 191.30907800002024, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.300577 + }, + { + "name": "WebAssembly.Module", + "ms": 1.084523 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.399416 + }, + { + "name": "wasi.start", + "ms": 74.888461 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 334942208, + "peakRssBytes": 461664256, + "pssBytes": 336918528, + "virtualBytes": 4032192512, + "minorFaults": 708000, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 388018176, + "peakRssBytes": 461664256, + "pssBytes": 390001664, + "virtualBytes": 4769779712, + "minorFaults": 722485, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336240640, + "peakRssBytes": 461664256, + "pssBytes": 337994752, + "virtualBytes": 4032192512, + "minorFaults": 722485, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 334942208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336240640, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 203.0590599999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.169566 + }, + { + "name": "WebAssembly.Module", + "ms": 1.69276 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.456621 + }, + { + "name": "wasi.start", + "ms": 78.667029 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336240640, + "peakRssBytes": 461664256, + "pssBytes": 337994752, + "virtualBytes": 4032192512, + "minorFaults": 722485, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 388018176, + "peakRssBytes": 461664256, + "pssBytes": 390092800, + "virtualBytes": 4769255424, + "minorFaults": 736194, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336105472, + "peakRssBytes": 461664256, + "pssBytes": 337995776, + "virtualBytes": 4032192512, + "minorFaults": 736194, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336240640, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336105472, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 144.8575950000086, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.709196 + }, + { + "name": "WebAssembly.Module", + "ms": 2.670084 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.377653 + }, + { + "name": "wasi.start", + "ms": 20.09891 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336105472, + "peakRssBytes": 461664256, + "pssBytes": 337995776, + "virtualBytes": 4032192512, + "minorFaults": 736194, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 405635072, + "peakRssBytes": 461664256, + "pssBytes": 407873536, + "virtualBytes": 4789829632, + "minorFaults": 750850, + "majorFaults": 2 + }, + "end": { + "rssBytes": 335966208, + "peakRssBytes": 461664256, + "pssBytes": 338032640, + "virtualBytes": 4033699840, + "minorFaults": 750850, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336105472, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335966208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 145.8278500000015, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.006256 + }, + { + "name": "WebAssembly.Module", + "ms": 1.521096 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.353569 + }, + { + "name": "wasi.start", + "ms": 15.609444 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335966208, + "peakRssBytes": 461664256, + "pssBytes": 338032640, + "virtualBytes": 4033699840, + "minorFaults": 750850, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 406007808, + "peakRssBytes": 461664256, + "pssBytes": 406049792, + "virtualBytes": 4790358016, + "minorFaults": 765569, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336056320, + "peakRssBytes": 461664256, + "pssBytes": 338035712, + "virtualBytes": 4033966080, + "minorFaults": 765569, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335966208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336056320, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 167.34580100001767, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.533875 + }, + { + "name": "WebAssembly.Module", + "ms": 2.956083 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.345706 + }, + { + "name": "wasi.start", + "ms": 17.535838 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336056320, + "peakRssBytes": 461664256, + "pssBytes": 338035712, + "virtualBytes": 4033966080, + "minorFaults": 765569, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 403845120, + "peakRssBytes": 461664256, + "pssBytes": 406079488, + "virtualBytes": 4790620160, + "minorFaults": 779776, + "majorFaults": 2 + }, + "end": { + "rssBytes": 335962112, + "peakRssBytes": 461664256, + "pssBytes": 338036736, + "virtualBytes": 4033966080, + "minorFaults": 779776, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336056320, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335962112, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 149.4516650000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.272236 + }, + { + "name": "WebAssembly.Module", + "ms": 1.301142 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.36405 + }, + { + "name": "wasi.start", + "ms": 15.345836 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335962112, + "peakRssBytes": 461664256, + "pssBytes": 338036736, + "virtualBytes": 4033966080, + "minorFaults": 779776, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 404172800, + "peakRssBytes": 461664256, + "pssBytes": 406079488, + "virtualBytes": 4790358016, + "minorFaults": 793984, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336171008, + "peakRssBytes": 461664256, + "pssBytes": 338036736, + "virtualBytes": 4033966080, + "minorFaults": 793984, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335962112, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336171008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 154.60512500000186, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784616393624760665/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.317143 + }, + { + "name": "WebAssembly.Module", + "ms": 2.14326 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.377222 + }, + { + "name": "wasi.start", + "ms": 15.020275 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336171008, + "peakRssBytes": 461664256, + "pssBytes": 338036736, + "virtualBytes": 4033966080, + "minorFaults": 793984, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 404160512, + "peakRssBytes": 461664256, + "pssBytes": 406062080, + "virtualBytes": 4789833728, + "minorFaults": 808700, + "majorFaults": 2 + }, + "end": { + "rssBytes": 336338944, + "peakRssBytes": 461664256, + "pssBytes": 338035712, + "virtualBytes": 4033966080, + "minorFaults": 808700, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336171008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336338944, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 4, + "vmSetupMs": 443.08333200000925, + "fixtureSetupMs": 393.16497199999867, + "baseline": { + "rssBytes": 237268992, + "peakRssBytes": 242765824, + "pssBytes": 239399936, + "virtualBytes": 3886272512, + "minorFaults": 55119, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 501338112, + "peakRssBytes": 507736064, + "pssBytes": 504264704, + "virtualBytes": 4202930176, + "minorFaults": 112521, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 264069120, + "peakRssBytes": 264970240, + "pssBytes": 264864768, + "virtualBytes": 316657664, + "minorFaults": 57402, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 64.24876500002574, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.06837900000000001, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 48.585128, + "phases": [ + { + "name": "Engine", + "ms": 0.06252100000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.158243 + }, + { + "name": "moduleRead", + "ms": 2.3098170000000002 + }, + { + "name": "profileValidation", + "ms": 0.297675 + }, + { + "name": "moduleCompile", + "ms": 45.291551 + }, + { + "name": "importValidation", + "ms": 0.003086 + }, + { + "name": "Linker", + "ms": 0.185502 + }, + { + "name": "Store", + "ms": 0.016204 + }, + { + "name": "Instance", + "ms": 0.04638 + }, + { + "name": "signalMaskInit", + "ms": 0.093684 + }, + { + "name": "entrypointLookup", + "ms": 0.002875 + }, + { + "name": "wasi.start", + "ms": 0.024534 + }, + { + "name": "Store.teardown", + "ms": 0.020381999999999997 + } + ] + }, + "memory": { + "start": { + "rssBytes": 237268992, + "peakRssBytes": 242765824, + "pssBytes": 239408128, + "virtualBytes": 3888386048, + "minorFaults": 55121, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56261, + "majorFaults": 0 + }, + "end": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56261, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 237268992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 20.447126000013668, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008792, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 5.060998, + "phases": [ + { + "name": "Engine", + "ms": 0.00183 + }, + { + "name": "canonicalPreopens", + "ms": 0.24828699999999998 + }, + { + "name": "moduleRead", + "ms": 3.443079 + }, + { + "name": "profileValidation", + "ms": 0.215977 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002978 + }, + { + "name": "Linker", + "ms": 0.191315 + }, + { + "name": "Store", + "ms": 0.01422 + }, + { + "name": "Instance", + "ms": 0.035338 + }, + { + "name": "signalMaskInit", + "ms": 0.5620860000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.007063 + }, + { + "name": "wasi.start", + "ms": 0.241776 + }, + { + "name": "Store.teardown", + "ms": 0.017539 + } + ] + }, + "memory": { + "start": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56261, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 8319873024, + "minorFaults": 56278, + "majorFaults": 0 + }, + "end": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56278, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 21.027724999992643, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010169000000000001, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3.685434, + "phases": [ + { + "name": "Engine", + "ms": 0.001464 + }, + { + "name": "canonicalPreopens", + "ms": 0.141735 + }, + { + "name": "moduleRead", + "ms": 2.318123 + }, + { + "name": "profileValidation", + "ms": 0.219855 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.002246 + }, + { + "name": "Linker", + "ms": 0.18101 + }, + { + "name": "Store", + "ms": 0.012384999999999998 + }, + { + "name": "Instance", + "ms": 0.028597 + }, + { + "name": "signalMaskInit", + "ms": 0.6519940000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.005632 + }, + { + "name": "wasi.start", + "ms": 0.036809 + }, + { + "name": "Store.teardown", + "ms": 0.016398000000000003 + } + ] + }, + "memory": { + "start": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56278, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56295, + "majorFaults": 0 + }, + "end": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56295, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 20.851196000003256, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008309, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 2.6157310000000003, + "phases": [ + { + "name": "Engine", + "ms": 0.001703 + }, + { + "name": "canonicalPreopens", + "ms": 0.13572 + }, + { + "name": "moduleRead", + "ms": 1.2522309999999999 + }, + { + "name": "profileValidation", + "ms": 0.22119699999999998 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.0027930000000000003 + }, + { + "name": "Linker", + "ms": 0.190474 + }, + { + "name": "Store", + "ms": 0.012789 + }, + { + "name": "Instance", + "ms": 0.029648 + }, + { + "name": "signalMaskInit", + "ms": 0.6402829999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.006593 + }, + { + "name": "wasi.start", + "ms": 0.035908 + }, + { + "name": "Store.teardown", + "ms": 0.017036000000000003 + } + ] + }, + "memory": { + "start": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56295, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56312, + "majorFaults": 0 + }, + "end": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56312, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 20.360107000014978, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28203, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009082999999999999, + "firstGuestHostCallMs": null, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3.734811, + "phases": [ + { + "name": "Engine", + "ms": 0.002023 + }, + { + "name": "canonicalPreopens", + "ms": 0.138125 + }, + { + "name": "moduleRead", + "ms": 2.379289 + }, + { + "name": "profileValidation", + "ms": 0.226258 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.003725 + }, + { + "name": "Linker", + "ms": 0.19150999999999999 + }, + { + "name": "Store", + "ms": 0.013262 + }, + { + "name": "Instance", + "ms": 0.028561999999999997 + }, + { + "name": "signalMaskInit", + "ms": 0.6191359999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.006186 + }, + { + "name": "wasi.start", + "ms": 0.039497 + }, + { + "name": "Store.teardown", + "ms": 0.016408 + } + ] + }, + "memory": { + "start": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56312, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56329, + "majorFaults": 0 + }, + "end": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56329, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1124.4417949999915, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010835999999999998, + "firstGuestHostCallMs": 1054.79901, + "firstOutputMs": 1106.168316, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1106.794445, + "phases": [ + { + "name": "Engine", + "ms": 0.001552 + }, + { + "name": "canonicalPreopens", + "ms": 0.102798 + }, + { + "name": "moduleRead", + "ms": 5.410698 + }, + { + "name": "profileValidation", + "ms": 3.8367 + }, + { + "name": "moduleCompile", + "ms": 1044.254918 + }, + { + "name": "importValidation", + "ms": 0.007297 + }, + { + "name": "Linker", + "ms": 0.180718 + }, + { + "name": "Store", + "ms": 0.016697999999999998 + }, + { + "name": "Instance", + "ms": 0.17186400000000002 + }, + { + "name": "signalMaskInit", + "ms": 0.07601200000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.003145 + }, + { + "name": "wasi.start", + "ms": 51.652649000000004 + }, + { + "name": "Store.teardown", + "ms": 0.452328 + } + ] + }, + "memory": { + "start": { + "rssBytes": 247709696, + "peakRssBytes": 247894016, + "pssBytes": 249907200, + "virtualBytes": 3957784576, + "minorFaults": 56329, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 285876224, + "peakRssBytes": 286138368, + "pssBytes": 288347136, + "virtualBytes": 8327688192, + "minorFaults": 65580, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287675392, + "virtualBytes": 3963232256, + "minorFaults": 65580, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 45291, + "wasmtimeProcessRetainedRssBytes": 247709696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 77.46428200000082, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010641, + "firstGuestHostCallMs": 12.40008, + "firstOutputMs": 60.037757, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.536733000000005, + "phases": [ + { + "name": "Engine", + "ms": 0.003023 + }, + { + "name": "canonicalPreopens", + "ms": 0.150015 + }, + { + "name": "moduleRead", + "ms": 7.3502719999999995 + }, + { + "name": "profileValidation", + "ms": 3.841849 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007003 + }, + { + "name": "Linker", + "ms": 0.19223800000000002 + }, + { + "name": "Store", + "ms": 0.015739 + }, + { + "name": "Instance", + "ms": 0.047462 + }, + { + "name": "signalMaskInit", + "ms": 0.03389 + }, + { + "name": "entrypointLookup", + "ms": 0.0025220000000000004 + }, + { + "name": "wasi.start", + "ms": 47.879482 + }, + { + "name": "Store.teardown", + "ms": 0.388557 + } + ] + }, + "memory": { + "start": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287675392, + "virtualBytes": 3963232256, + "minorFaults": 65580, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286011392, + "peakRssBytes": 286138368, + "pssBytes": 288290816, + "virtualBytes": 8327688192, + "minorFaults": 65649, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65649, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 76.16672899998957, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.02104, + "firstGuestHostCallMs": 11.548918, + "firstOutputMs": 58.620862, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 58.779586, + "phases": [ + { + "name": "Engine", + "ms": 0.005324000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.095099 + }, + { + "name": "moduleRead", + "ms": 6.49807 + }, + { + "name": "profileValidation", + "ms": 3.888867 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006647 + }, + { + "name": "Linker", + "ms": 0.19270400000000001 + }, + { + "name": "Store", + "ms": 0.015941 + }, + { + "name": "Instance", + "ms": 0.04312 + }, + { + "name": "signalMaskInit", + "ms": 0.048024 + }, + { + "name": "entrypointLookup", + "ms": 0.0027700000000000003 + }, + { + "name": "wasi.start", + "ms": 47.323714 + }, + { + "name": "Store.teardown", + "ms": 0.048086 + } + ] + }, + "memory": { + "start": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65649, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286011392, + "peakRssBytes": 286138368, + "pssBytes": 288290816, + "virtualBytes": 8327688192, + "minorFaults": 65718, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65718, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 77.72045700001763, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018678999999999998, + "firstGuestHostCallMs": 11.603302, + "firstOutputMs": 59.700741, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 60.492731, + "phases": [ + { + "name": "Engine", + "ms": 0.001815 + }, + { + "name": "canonicalPreopens", + "ms": 0.104139 + }, + { + "name": "moduleRead", + "ms": 6.574948 + }, + { + "name": "profileValidation", + "ms": 3.849707 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006573 + }, + { + "name": "Linker", + "ms": 0.194869 + }, + { + "name": "Store", + "ms": 0.016113 + }, + { + "name": "Instance", + "ms": 0.07167499999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.024913 + }, + { + "name": "entrypointLookup", + "ms": 0.0028109999999999997 + }, + { + "name": "wasi.start", + "ms": 48.348917 + }, + { + "name": "Store.teardown", + "ms": 0.681063 + } + ] + }, + "memory": { + "start": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65718, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286011392, + "peakRssBytes": 286138368, + "pssBytes": 288290816, + "virtualBytes": 8327688192, + "minorFaults": 65787, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65787, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 78.68227100002696, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208565, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013001, + "firstGuestHostCallMs": 11.329677, + "firstOutputMs": 61.148463, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 61.298384, + "phases": [ + { + "name": "Engine", + "ms": 0.001588 + }, + { + "name": "canonicalPreopens", + "ms": 0.15511100000000003 + }, + { + "name": "moduleRead", + "ms": 5.43875 + }, + { + "name": "profileValidation", + "ms": 3.874952 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007873000000000002 + }, + { + "name": "Linker", + "ms": 0.203181 + }, + { + "name": "Store", + "ms": 0.01599 + }, + { + "name": "Instance", + "ms": 0.7918 + }, + { + "name": "signalMaskInit", + "ms": 0.049187 + }, + { + "name": "entrypointLookup", + "ms": 0.0029289999999999997 + }, + { + "name": "wasi.start", + "ms": 50.101597 + }, + { + "name": "Store.teardown", + "ms": 0.051046 + } + ] + }, + "memory": { + "start": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287676416, + "virtualBytes": 3963232256, + "minorFaults": 65787, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286011392, + "peakRssBytes": 286138368, + "pssBytes": 288290816, + "virtualBytes": 8327688192, + "minorFaults": 65856, + "majorFaults": 0 + }, + "end": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287675392, + "virtualBytes": 3963232256, + "minorFaults": 65856, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3791.293307999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009932000000000002, + "firstGuestHostCallMs": 3379.2718609999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3769.332258, + "phases": [ + { + "name": "Engine", + "ms": 0.00195 + }, + { + "name": "canonicalPreopens", + "ms": 0.106764 + }, + { + "name": "moduleRead", + "ms": 11.438514 + }, + { + "name": "profileValidation", + "ms": 13.892355 + }, + { + "name": "moduleCompile", + "ms": 3350.9139870000004 + }, + { + "name": "importValidation", + "ms": 0.008671 + }, + { + "name": "Linker", + "ms": 0.180109 + }, + { + "name": "Store", + "ms": 0.017528 + }, + { + "name": "Instance", + "ms": 1.172873 + }, + { + "name": "signalMaskInit", + "ms": 0.062753 + }, + { + "name": "entrypointLookup", + "ms": 0.004161 + }, + { + "name": "wasi.start", + "ms": 390.043271 + }, + { + "name": "Store.teardown", + "ms": 0.053059 + } + ] + }, + "memory": { + "start": { + "rssBytes": 285413376, + "peakRssBytes": 286138368, + "pssBytes": 287675392, + "virtualBytes": 3963232256, + "minorFaults": 65856, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 415354880, + "peakRssBytes": 415367168, + "pssBytes": 418681856, + "virtualBytes": 12846845952, + "minorFaults": 83820, + "majorFaults": 0 + }, + "end": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418083840, + "virtualBytes": 4117934080, + "minorFaults": 83820, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1089546, + "wasmtimeProcessRetainedRssBytes": 285413376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 78.35819800000172, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013604999999999999, + "firstGuestHostCallMs": 29.414507, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 59.501162, + "phases": [ + { + "name": "Engine", + "ms": 0.0019060000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.11083900000000001 + }, + { + "name": "moduleRead", + "ms": 13.070020999999999 + }, + { + "name": "profileValidation", + "ms": 12.509084 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.01562 + }, + { + "name": "Linker", + "ms": 0.251231 + }, + { + "name": "Store", + "ms": 0.019662 + }, + { + "name": "Instance", + "ms": 1.880393 + }, + { + "name": "signalMaskInit", + "ms": 0.07496399999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.0038550000000000004 + }, + { + "name": "wasi.start", + "ms": 30.042053 + }, + { + "name": "Store.teardown", + "ms": 0.038856 + } + ] + }, + "memory": { + "start": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418083840, + "virtualBytes": 4117934080, + "minorFaults": 83820, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418887680, + "virtualBytes": 12846845952, + "minorFaults": 83900, + "majorFaults": 0 + }, + "end": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 83900, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 73.06782500000554, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021205, + "firstGuestHostCallMs": 26.963721, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.663374, + "phases": [ + { + "name": "Engine", + "ms": 0.0017519999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.105567 + }, + { + "name": "moduleRead", + "ms": 11.448981999999999 + }, + { + "name": "profileValidation", + "ms": 13.612665 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011171 + }, + { + "name": "Linker", + "ms": 0.201573 + }, + { + "name": "Store", + "ms": 0.021892 + }, + { + "name": "Instance", + "ms": 0.060321 + }, + { + "name": "signalMaskInit", + "ms": 0.050442 + }, + { + "name": "entrypointLookup", + "ms": 0.003993 + }, + { + "name": "wasi.start", + "ms": 26.663677 + }, + { + "name": "Store.teardown", + "ms": 0.04208 + } + ] + }, + "memory": { + "start": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 83900, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418887680, + "virtualBytes": 12846845952, + "minorFaults": 83980, + "majorFaults": 0 + }, + "end": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 83980, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 73.66523700000835, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014439, + "firstGuestHostCallMs": 25.824084, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 56.062383, + "phases": [ + { + "name": "Engine", + "ms": 0.002363 + }, + { + "name": "canonicalPreopens", + "ms": 0.190601 + }, + { + "name": "moduleRead", + "ms": 11.540806 + }, + { + "name": "profileValidation", + "ms": 12.31732 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010117 + }, + { + "name": "Linker", + "ms": 0.206347 + }, + { + "name": "Store", + "ms": 0.016564 + }, + { + "name": "Instance", + "ms": 0.047651 + }, + { + "name": "signalMaskInit", + "ms": 0.067289 + }, + { + "name": "entrypointLookup", + "ms": 0.0028320000000000003 + }, + { + "name": "wasi.start", + "ms": 30.195545 + }, + { + "name": "Store.teardown", + "ms": 0.039474 + } + ] + }, + "memory": { + "start": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 83980, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418887680, + "virtualBytes": 12846845952, + "minorFaults": 84060, + "majorFaults": 0 + }, + "end": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 84060, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 75.40493999997852, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082692, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012763, + "firstGuestHostCallMs": 25.827199, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 55.345707000000004, + "phases": [ + { + "name": "Engine", + "ms": 0.002258 + }, + { + "name": "canonicalPreopens", + "ms": 0.115523 + }, + { + "name": "moduleRead", + "ms": 11.390741 + }, + { + "name": "profileValidation", + "ms": 12.368044 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011751 + }, + { + "name": "Linker", + "ms": 0.22076400000000002 + }, + { + "name": "Store", + "ms": 0.017915 + }, + { + "name": "Instance", + "ms": 0.211428 + }, + { + "name": "signalMaskInit", + "ms": 0.047372 + }, + { + "name": "entrypointLookup", + "ms": 0.0037459999999999998 + }, + { + "name": "wasi.start", + "ms": 29.093255 + }, + { + "name": "Store.teardown", + "ms": 0.437345 + } + ] + }, + "memory": { + "start": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 84060, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414687232, + "peakRssBytes": 415367168, + "pssBytes": 418887680, + "virtualBytes": 12846845952, + "minorFaults": 84140, + "majorFaults": 0 + }, + "end": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 84140, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1570.2526739999885, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.010848, + "firstGuestHostCallMs": 1541.908805, + "firstOutputMs": 1549.081157, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1549.322259, + "phases": [ + { + "name": "Engine", + "ms": 0.001709 + }, + { + "name": "canonicalPreopens", + "ms": 0.115411 + }, + { + "name": "moduleRead", + "ms": 6.753947 + }, + { + "name": "profileValidation", + "ms": 6.493571 + }, + { + "name": "moduleCompile", + "ms": 1525.015651 + }, + { + "name": "importValidation", + "ms": 0.010308 + }, + { + "name": "Linker", + "ms": 0.19342299999999998 + }, + { + "name": "Store", + "ms": 0.017693 + }, + { + "name": "Instance", + "ms": 2.3853519999999997 + }, + { + "name": "signalMaskInit", + "ms": 0.08135300000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.0051719999999999995 + }, + { + "name": "wasi.start", + "ms": 7.422869 + }, + { + "name": "Store.teardown", + "ms": 0.040707 + } + ] + }, + "memory": { + "start": { + "rssBytes": 414633984, + "peakRssBytes": 415367168, + "pssBytes": 418084864, + "virtualBytes": 4117934080, + "minorFaults": 84140, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422375424, + "peakRssBytes": 422776832, + "pssBytes": 426485760, + "virtualBytes": 8489787392, + "minorFaults": 85048, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426485760, + "virtualBytes": 4125331456, + "minorFaults": 85048, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4797959, + "wasmtimeProcessRetainedRssBytes": 414633984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 42.24373799999012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.015212999999999999, + "firstGuestHostCallMs": 14.74586, + "firstOutputMs": 22.616659000000002, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.822080000000003, + "phases": [ + { + "name": "Engine", + "ms": 0.004579 + }, + { + "name": "canonicalPreopens", + "ms": 0.107276 + }, + { + "name": "moduleRead", + "ms": 6.898312 + }, + { + "name": "profileValidation", + "ms": 6.32436 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010801 + }, + { + "name": "Linker", + "ms": 0.20184000000000002 + }, + { + "name": "Store", + "ms": 0.016722 + }, + { + "name": "Instance", + "ms": 0.356775 + }, + { + "name": "signalMaskInit", + "ms": 0.059290999999999996 + }, + { + "name": "entrypointLookup", + "ms": 0.0028959999999999997 + }, + { + "name": "wasi.start", + "ms": 8.040364 + }, + { + "name": "Store.teardown", + "ms": 0.037870999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426485760, + "virtualBytes": 4125331456, + "minorFaults": 85048, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426539008, + "virtualBytes": 8489787392, + "minorFaults": 85098, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426510336, + "virtualBytes": 4125331456, + "minorFaults": 85098, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 45.58245999997598, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014263999999999999, + "firstGuestHostCallMs": 15.634167000000001, + "firstOutputMs": 22.146836999999998, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.381189, + "phases": [ + { + "name": "Engine", + "ms": 0.001809 + }, + { + "name": "canonicalPreopens", + "ms": 0.111935 + }, + { + "name": "moduleRead", + "ms": 7.046701 + }, + { + "name": "profileValidation", + "ms": 6.297478 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011708999999999999 + }, + { + "name": "Linker", + "ms": 0.216836 + }, + { + "name": "Store", + "ms": 0.016388999999999997 + }, + { + "name": "Instance", + "ms": 1.122963 + }, + { + "name": "signalMaskInit", + "ms": 0.05171 + }, + { + "name": "entrypointLookup", + "ms": 0.003805 + }, + { + "name": "wasi.start", + "ms": 6.676832 + }, + { + "name": "Store.teardown", + "ms": 0.048918 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426510336, + "virtualBytes": 4125331456, + "minorFaults": 85098, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 427030528, + "virtualBytes": 8489787392, + "minorFaults": 85148, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426534912, + "virtualBytes": 4125331456, + "minorFaults": 85148, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 43.62858699998469, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.012612, + "firstGuestHostCallMs": 15.813227999999999, + "firstOutputMs": 22.257853, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 22.466984, + "phases": [ + { + "name": "Engine", + "ms": 0.001683 + }, + { + "name": "canonicalPreopens", + "ms": 0.115232 + }, + { + "name": "moduleRead", + "ms": 7.66235 + }, + { + "name": "profileValidation", + "ms": 6.912556 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.011422 + }, + { + "name": "Linker", + "ms": 0.22384 + }, + { + "name": "Store", + "ms": 0.0188 + }, + { + "name": "Instance", + "ms": 0.048533 + }, + { + "name": "signalMaskInit", + "ms": 0.058268 + }, + { + "name": "entrypointLookup", + "ms": 0.003176 + }, + { + "name": "wasi.start", + "ms": 6.60741 + }, + { + "name": "Store.teardown", + "ms": 0.037423 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426534912, + "virtualBytes": 4125331456, + "minorFaults": 85148, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 427042816, + "virtualBytes": 8489787392, + "minorFaults": 85193, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426539008, + "virtualBytes": 4125331456, + "minorFaults": 85193, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 45.1337019999919, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561500, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013689, + "firstGuestHostCallMs": 16.308597000000002, + "firstOutputMs": 23.613753000000003, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 23.931911, + "phases": [ + { + "name": "Engine", + "ms": 0.001595 + }, + { + "name": "canonicalPreopens", + "ms": 0.104879 + }, + { + "name": "moduleRead", + "ms": 7.647221 + }, + { + "name": "profileValidation", + "ms": 6.312564 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010043 + }, + { + "name": "Linker", + "ms": 0.205392 + }, + { + "name": "Store", + "ms": 0.016218999999999997 + }, + { + "name": "Instance", + "ms": 1.179303 + }, + { + "name": "signalMaskInit", + "ms": 0.06821999999999999 + }, + { + "name": "entrypointLookup", + "ms": 0.003208 + }, + { + "name": "wasi.start", + "ms": 7.525157999999999 + }, + { + "name": "Store.teardown", + "ms": 0.055779999999999996 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426539008, + "virtualBytes": 4125331456, + "minorFaults": 85193, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426948608, + "virtualBytes": 8489787392, + "minorFaults": 85237, + "majorFaults": 0 + }, + "end": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426539008, + "virtualBytes": 4125331456, + "minorFaults": 85237, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1259.1288490000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011203, + "firstGuestHostCallMs": 1238.7059840000002, + "firstOutputMs": 1240.624453, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1241.550632, + "phases": [ + { + "name": "Engine", + "ms": 0.001776 + }, + { + "name": "canonicalPreopens", + "ms": 0.158883 + }, + { + "name": "moduleRead", + "ms": 5.203392 + }, + { + "name": "profileValidation", + "ms": 4.821524 + }, + { + "name": "moduleCompile", + "ms": 1227.7348729999999 + }, + { + "name": "importValidation", + "ms": 0.007781000000000001 + }, + { + "name": "Linker", + "ms": 0.181529 + }, + { + "name": "Store", + "ms": 0.016628999999999998 + }, + { + "name": "Instance", + "ms": 0.074669 + }, + { + "name": "signalMaskInit", + "ms": 0.070982 + }, + { + "name": "entrypointLookup", + "ms": 0.0034850000000000003 + }, + { + "name": "wasi.start", + "ms": 1.9804570000000001 + }, + { + "name": "Store.teardown", + "ms": 0.828782 + } + ] + }, + "memory": { + "start": { + "rssBytes": 422146048, + "peakRssBytes": 422776832, + "pssBytes": 426539008, + "virtualBytes": 4125331456, + "minorFaults": 85237, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429776896, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 8495255552, + "minorFaults": 85625, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85625, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6322975, + "wasmtimeProcessRetainedRssBytes": 422146048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 33.024099000002025, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.009089999999999999, + "firstGuestHostCallMs": 10.999939, + "firstOutputMs": 12.956692, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.086228, + "phases": [ + { + "name": "Engine", + "ms": 0.001675 + }, + { + "name": "canonicalPreopens", + "ms": 0.09900099999999999 + }, + { + "name": "moduleRead", + "ms": 5.108301 + }, + { + "name": "profileValidation", + "ms": 4.847591 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010909 + }, + { + "name": "Linker", + "ms": 0.220623 + }, + { + "name": "Store", + "ms": 0.017188 + }, + { + "name": "Instance", + "ms": 0.216406 + }, + { + "name": "signalMaskInit", + "ms": 0.043725 + }, + { + "name": "entrypointLookup", + "ms": 0.00238 + }, + { + "name": "wasi.start", + "ms": 2.018096 + }, + { + "name": "Store.teardown", + "ms": 0.038414000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85625, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432060416, + "virtualBytes": 4130811904, + "minorFaults": 85670, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85670, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 33.83038099997793, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013183, + "firstGuestHostCallMs": 10.970049000000001, + "firstOutputMs": 12.999521999999999, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.390524, + "phases": [ + { + "name": "Engine", + "ms": 0.001597 + }, + { + "name": "canonicalPreopens", + "ms": 0.118508 + }, + { + "name": "moduleRead", + "ms": 4.085114 + }, + { + "name": "profileValidation", + "ms": 4.856927 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008427 + }, + { + "name": "Linker", + "ms": 0.19635599999999998 + }, + { + "name": "Store", + "ms": 0.014981 + }, + { + "name": "Instance", + "ms": 1.19216 + }, + { + "name": "signalMaskInit", + "ms": 0.058085 + }, + { + "name": "entrypointLookup", + "ms": 0.003292 + }, + { + "name": "wasi.start", + "ms": 2.110346 + }, + { + "name": "Store.teardown", + "ms": 0.26869 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85670, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429711360, + "peakRssBytes": 430092288, + "pssBytes": 432060416, + "virtualBytes": 8495255552, + "minorFaults": 85715, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85715, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 32.021424000005936, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016703, + "firstGuestHostCallMs": 11.420711, + "firstOutputMs": 13.238486, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.357275999999999, + "phases": [ + { + "name": "Engine", + "ms": 0.001616 + }, + { + "name": "canonicalPreopens", + "ms": 0.109275 + }, + { + "name": "moduleRead", + "ms": 5.235398 + }, + { + "name": "profileValidation", + "ms": 5.327833 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009786000000000001 + }, + { + "name": "Linker", + "ms": 0.20997100000000002 + }, + { + "name": "Store", + "ms": 0.015556000000000002 + }, + { + "name": "Instance", + "ms": 0.045544999999999995 + }, + { + "name": "signalMaskInit", + "ms": 0.021259 + }, + { + "name": "entrypointLookup", + "ms": 0.003477 + }, + { + "name": "wasi.start", + "ms": 1.8782379999999999 + }, + { + "name": "Store.teardown", + "ms": 0.032345000000000006 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85715, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427667456, + "peakRssBytes": 430092288, + "pssBytes": 432060416, + "virtualBytes": 4131078144, + "minorFaults": 85760, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85760, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 33.98194599998533, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878882, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014039000000000001, + "firstGuestHostCallMs": 11.418622999999998, + "firstOutputMs": 13.236602999999999, + "guestLinearMemoryBytes": 16777216, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 13.39066, + "phases": [ + { + "name": "Engine", + "ms": 0.002492 + }, + { + "name": "canonicalPreopens", + "ms": 0.10851000000000001 + }, + { + "name": "moduleRead", + "ms": 5.192408 + }, + { + "name": "profileValidation", + "ms": 4.907607 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009275 + }, + { + "name": "Linker", + "ms": 0.208765 + }, + { + "name": "Store", + "ms": 0.017054 + }, + { + "name": "Instance", + "ms": 0.512982 + }, + { + "name": "signalMaskInit", + "ms": 0.022425999999999998 + }, + { + "name": "entrypointLookup", + "ms": 0.0027719999999999997 + }, + { + "name": "wasi.start", + "ms": 1.878507 + }, + { + "name": "Store.teardown", + "ms": 0.065273 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85760, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432060416, + "virtualBytes": 4130811904, + "minorFaults": 85805, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85805, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3636.640886999987, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.011603, + "firstGuestHostCallMs": 3610.4902700000002, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3617.884401, + "phases": [ + { + "name": "Engine", + "ms": 0.0019420000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.102418 + }, + { + "name": "moduleRead", + "ms": 11.124261 + }, + { + "name": "profileValidation", + "ms": 14.590883 + }, + { + "name": "moduleCompile", + "ms": 3582.815358 + }, + { + "name": "importValidation", + "ms": 0.012298 + }, + { + "name": "Linker", + "ms": 0.199411 + }, + { + "name": "Store", + "ms": 0.016967 + }, + { + "name": "Instance", + "ms": 0.186264 + }, + { + "name": "signalMaskInit", + "ms": 0.08916600000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.005045 + }, + { + "name": "wasi.start", + "ms": 7.33411 + }, + { + "name": "Store.teardown", + "ms": 0.050187 + } + ] + }, + "memory": { + "start": { + "rssBytes": 427614208, + "peakRssBytes": 430092288, + "pssBytes": 432007168, + "virtualBytes": 4130799616, + "minorFaults": 85805, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444788736, + "peakRssBytes": 444866560, + "pssBytes": 449189888, + "virtualBytes": 8511672320, + "minorFaults": 88934, + "majorFaults": 0 + }, + "end": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 88934, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7550710, + "wasmtimeProcessRetainedRssBytes": 427614208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 60.86270200001309, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.028714, + "firstGuestHostCallMs": 28.150219, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 41.508343, + "phases": [ + { + "name": "Engine", + "ms": 0.001809 + }, + { + "name": "canonicalPreopens", + "ms": 0.110405 + }, + { + "name": "moduleRead", + "ms": 11.303586 + }, + { + "name": "profileValidation", + "ms": 14.782922000000001 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.013566 + }, + { + "name": "Linker", + "ms": 0.225399 + }, + { + "name": "Store", + "ms": 0.021127 + }, + { + "name": "Instance", + "ms": 0.20966600000000002 + }, + { + "name": "signalMaskInit", + "ms": 0.08667 + }, + { + "name": "entrypointLookup", + "ms": 0.0036950000000000004 + }, + { + "name": "wasi.start", + "ms": 13.177176000000001 + }, + { + "name": "Store.teardown", + "ms": 0.220332 + } + ] + }, + "memory": { + "start": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 88934, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444686336, + "peakRssBytes": 444866560, + "pssBytes": 449157120, + "virtualBytes": 8511672320, + "minorFaults": 89026, + "majorFaults": 0 + }, + "end": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89026, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 67.83799399997224, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018575, + "firstGuestHostCallMs": 28.409589, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 46.896248, + "phases": [ + { + "name": "Engine", + "ms": 0.0041080000000000005 + }, + { + "name": "canonicalPreopens", + "ms": 0.108304 + }, + { + "name": "moduleRead", + "ms": 11.313541 + }, + { + "name": "profileValidation", + "ms": 14.761448 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.01267 + }, + { + "name": "Linker", + "ms": 0.207462 + }, + { + "name": "Store", + "ms": 0.017898 + }, + { + "name": "Instance", + "ms": 0.573712 + }, + { + "name": "signalMaskInit", + "ms": 0.044836 + }, + { + "name": "entrypointLookup", + "ms": 0.00432 + }, + { + "name": "wasi.start", + "ms": 18.451756 + }, + { + "name": "Store.teardown", + "ms": 0.049454 + } + ] + }, + "memory": { + "start": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89026, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444686336, + "peakRssBytes": 444866560, + "pssBytes": 449173504, + "virtualBytes": 8511672320, + "minorFaults": 89118, + "majorFaults": 0 + }, + "end": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89118, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 66.90239800000563, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017249999999999998, + "firstGuestHostCallMs": 27.663196, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 46.274854, + "phases": [ + { + "name": "Engine", + "ms": 0.001767 + }, + { + "name": "canonicalPreopens", + "ms": 0.120366 + }, + { + "name": "moduleRead", + "ms": 11.176051 + }, + { + "name": "profileValidation", + "ms": 14.618861 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.012596 + }, + { + "name": "Linker", + "ms": 0.214103 + }, + { + "name": "Store", + "ms": 0.017697 + }, + { + "name": "Instance", + "ms": 0.044763 + }, + { + "name": "signalMaskInit", + "ms": 0.065399 + }, + { + "name": "entrypointLookup", + "ms": 0.003972 + }, + { + "name": "wasi.start", + "ms": 18.612437999999997 + }, + { + "name": "Store.teardown", + "ms": 0.043469 + } + ] + }, + "memory": { + "start": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89118, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444686336, + "peakRssBytes": 444866560, + "pssBytes": 449173504, + "virtualBytes": 8511672320, + "minorFaults": 89210, + "majorFaults": 0 + }, + "end": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89210, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 75.88679899999988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854951, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.008438, + "firstGuestHostCallMs": 30.14234, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 51.207273, + "phases": [ + { + "name": "Engine", + "ms": 0.0017620000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.11649000000000001 + }, + { + "name": "moduleRead", + "ms": 11.859987 + }, + { + "name": "profileValidation", + "ms": 15.101972 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.020557 + }, + { + "name": "Linker", + "ms": 0.23624299999999998 + }, + { + "name": "Store", + "ms": 0.018431 + }, + { + "name": "Instance", + "ms": 1.3629149999999999 + }, + { + "name": "signalMaskInit", + "ms": 0.054646 + }, + { + "name": "entrypointLookup", + "ms": 0.0038890000000000005 + }, + { + "name": "wasi.start", + "ms": 20.305653 + }, + { + "name": "Store.teardown", + "ms": 0.7665299999999999 + } + ] + }, + "memory": { + "start": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448534528, + "virtualBytes": 4147216384, + "minorFaults": 89210, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 444686336, + "peakRssBytes": 444866560, + "pssBytes": 449156096, + "virtualBytes": 8511672320, + "minorFaults": 89302, + "majorFaults": 0 + }, + "end": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448533504, + "virtualBytes": 4147216384, + "minorFaults": 89302, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3906.815888000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017791, + "firstGuestHostCallMs": 3884.993384, + "firstOutputMs": 3885.784048, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 3886.407627, + "phases": [ + { + "name": "Engine", + "ms": 0.001564 + }, + { + "name": "canonicalPreopens", + "ms": 0.131622 + }, + { + "name": "moduleRead", + "ms": 12.755644 + }, + { + "name": "profileValidation", + "ms": 17.026891 + }, + { + "name": "moduleCompile", + "ms": 3852.7662330000003 + }, + { + "name": "importValidation", + "ms": 0.013481 + }, + { + "name": "Linker", + "ms": 0.182552 + }, + { + "name": "Store", + "ms": 0.016599000000000003 + }, + { + "name": "Instance", + "ms": 0.187656 + }, + { + "name": "signalMaskInit", + "ms": 0.06708700000000001 + }, + { + "name": "entrypointLookup", + "ms": 0.004030000000000001 + }, + { + "name": "wasi.start", + "ms": 1.204474 + }, + { + "name": "Store.teardown", + "ms": 0.45783 + } + ] + }, + "memory": { + "start": { + "rssBytes": 444141568, + "peakRssBytes": 444866560, + "pssBytes": 448533504, + "virtualBytes": 4147216384, + "minorFaults": 89302, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 464871424, + "peakRssBytes": 465543168, + "pssBytes": 467130368, + "virtualBytes": 8530268160, + "minorFaults": 89824, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467130368, + "virtualBytes": 4165812224, + "minorFaults": 89824, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11133525, + "wasmtimeProcessRetainedRssBytes": 444141568, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 55.45800800001598, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.044169, + "firstGuestHostCallMs": 33.125649, + "firstOutputMs": 33.854665, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 34.048014, + "phases": [ + { + "name": "Engine", + "ms": 0.001641 + }, + { + "name": "canonicalPreopens", + "ms": 0.108373 + }, + { + "name": "moduleRead", + "ms": 12.860762 + }, + { + "name": "profileValidation", + "ms": 17.216004 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015489000000000001 + }, + { + "name": "Linker", + "ms": 0.211541 + }, + { + "name": "Store", + "ms": 0.017254000000000002 + }, + { + "name": "Instance", + "ms": 0.050784 + }, + { + "name": "signalMaskInit", + "ms": 0.081829 + }, + { + "name": "entrypointLookup", + "ms": 0.004323 + }, + { + "name": "wasi.start", + "ms": 1.8321539999999998 + }, + { + "name": "Store.teardown", + "ms": 0.03956 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467130368, + "virtualBytes": 4165812224, + "minorFaults": 89824, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467183616, + "virtualBytes": 4165824512, + "minorFaults": 89864, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89864, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 61.35085399998934, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.044953, + "firstGuestHostCallMs": 35.937135000000005, + "firstOutputMs": 36.686956, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 37.302002, + "phases": [ + { + "name": "Engine", + "ms": 0.002707 + }, + { + "name": "canonicalPreopens", + "ms": 0.15156499999999998 + }, + { + "name": "moduleRead", + "ms": 12.988605999999999 + }, + { + "name": "profileValidation", + "ms": 20.493225 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.016354999999999998 + }, + { + "name": "Linker", + "ms": 0.212301 + }, + { + "name": "Store", + "ms": 0.017989 + }, + { + "name": "Instance", + "ms": 0.053593 + }, + { + "name": "signalMaskInit", + "ms": 0.088018 + }, + { + "name": "entrypointLookup", + "ms": 0.011969 + }, + { + "name": "wasi.start", + "ms": 1.1920279999999999 + }, + { + "name": "Store.teardown", + "ms": 0.454362 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89864, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 464834560, + "peakRssBytes": 465543168, + "pssBytes": 467182592, + "virtualBytes": 8530268160, + "minorFaults": 89904, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89904, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 60.34373699998832, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.021227, + "firstGuestHostCallMs": 34.128831999999996, + "firstOutputMs": 34.887896000000005, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 35.148629, + "phases": [ + { + "name": "Engine", + "ms": 0.003084 + }, + { + "name": "canonicalPreopens", + "ms": 0.125165 + }, + { + "name": "moduleRead", + "ms": 12.822566 + }, + { + "name": "profileValidation", + "ms": 16.995883 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.015369 + }, + { + "name": "Linker", + "ms": 0.21037399999999998 + }, + { + "name": "Store", + "ms": 0.018616 + }, + { + "name": "Instance", + "ms": 0.181513 + }, + { + "name": "signalMaskInit", + "ms": 0.053884 + }, + { + "name": "entrypointLookup", + "ms": 0.003637 + }, + { + "name": "wasi.start", + "ms": 3.059654 + }, + { + "name": "Store.teardown", + "ms": 0.034176 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89904, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467182592, + "virtualBytes": 4165824512, + "minorFaults": 89944, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89944, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 54.73701599999913, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397393, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014501, + "firstGuestHostCallMs": 31.469129, + "firstOutputMs": 32.182409, + "guestLinearMemoryBytes": 9371648, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 33.936497, + "phases": [ + { + "name": "Engine", + "ms": 0.0020889999999999997 + }, + { + "name": "canonicalPreopens", + "ms": 0.12312600000000001 + }, + { + "name": "moduleRead", + "ms": 12.915382 + }, + { + "name": "profileValidation", + "ms": 16.213687 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.017809 + }, + { + "name": "Linker", + "ms": 0.216529 + }, + { + "name": "Store", + "ms": 0.018940000000000002 + }, + { + "name": "Instance", + "ms": 0.051012 + }, + { + "name": "signalMaskInit", + "ms": 0.075878 + }, + { + "name": "entrypointLookup", + "ms": 0.003859 + }, + { + "name": "wasi.start", + "ms": 1.117524 + }, + { + "name": "Store.teardown", + "ms": 1.592268 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467129344, + "virtualBytes": 4165812224, + "minorFaults": 89944, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 464834560, + "peakRssBytes": 465543168, + "pssBytes": 467183616, + "virtualBytes": 8530268160, + "minorFaults": 89984, + "majorFaults": 0 + }, + "end": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467130368, + "virtualBytes": 4165812224, + "minorFaults": 89984, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 688.8527680000116, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.023496, + "firstGuestHostCallMs": 630.554535, + "firstOutputMs": 670.392428, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 670.712094, + "phases": [ + { + "name": "Engine", + "ms": 0.001788 + }, + { + "name": "canonicalPreopens", + "ms": 0.116274 + }, + { + "name": "moduleRead", + "ms": 5.641091 + }, + { + "name": "profileValidation", + "ms": 2.476821 + }, + { + "name": "moduleCompile", + "ms": 621.036764 + }, + { + "name": "importValidation", + "ms": 0.0068920000000000006 + }, + { + "name": "Linker", + "ms": 0.183949 + }, + { + "name": "Store", + "ms": 0.01682 + }, + { + "name": "Instance", + "ms": 0.274614 + }, + { + "name": "signalMaskInit", + "ms": 0.052009 + }, + { + "name": "entrypointLookup", + "ms": 0.003685 + }, + { + "name": "wasi.start", + "ms": 40.022054000000004 + }, + { + "name": "Store.teardown", + "ms": 0.168289 + } + ] + }, + "memory": { + "start": { + "rssBytes": 462737408, + "peakRssBytes": 465543168, + "pssBytes": 467130368, + "virtualBytes": 4165812224, + "minorFaults": 89984, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 466718720, + "peakRssBytes": 467099648, + "pssBytes": 471504896, + "virtualBytes": 8534130688, + "minorFaults": 90492, + "majorFaults": 0 + }, + "end": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90492, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 14986291, + "wasmtimeProcessRetainedRssBytes": 462737408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 74.57392500000424, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.025473000000000003, + "firstGuestHostCallMs": 10.644153, + "firstOutputMs": 53.964612, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 55.736456, + "phases": [ + { + "name": "Engine", + "ms": 0.001827 + }, + { + "name": "canonicalPreopens", + "ms": 0.10517900000000001 + }, + { + "name": "moduleRead", + "ms": 6.508475 + }, + { + "name": "profileValidation", + "ms": 2.4692999999999996 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.006945000000000001 + }, + { + "name": "Linker", + "ms": 0.219507 + }, + { + "name": "Store", + "ms": 0.016457 + }, + { + "name": "Instance", + "ms": 0.44620899999999997 + }, + { + "name": "signalMaskInit", + "ms": 0.044520000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.003178 + }, + { + "name": "wasi.start", + "ms": 43.577859 + }, + { + "name": "Store.teardown", + "ms": 1.6430520000000002 + } + ] + }, + "memory": { + "start": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90492, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 471380992, + "virtualBytes": 8534130688, + "minorFaults": 90538, + "majorFaults": 0 + }, + "end": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470991872, + "virtualBytes": 4169674752, + "minorFaults": 90538, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 78.51414400001522, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018435, + "firstGuestHostCallMs": 11.090931, + "firstOutputMs": 55.023637, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 56.307446999999996, + "phases": [ + { + "name": "Engine", + "ms": 0.002034 + }, + { + "name": "canonicalPreopens", + "ms": 0.12164799999999999 + }, + { + "name": "moduleRead", + "ms": 5.546685 + }, + { + "name": "profileValidation", + "ms": 2.7726 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008038 + }, + { + "name": "Linker", + "ms": 0.217311 + }, + { + "name": "Store", + "ms": 0.01782 + }, + { + "name": "Instance", + "ms": 1.56898 + }, + { + "name": "signalMaskInit", + "ms": 0.047639999999999995 + }, + { + "name": "entrypointLookup", + "ms": 0.003718 + }, + { + "name": "wasi.start", + "ms": 44.18916299999999 + }, + { + "name": "Store.teardown", + "ms": 1.081929 + } + ] + }, + "memory": { + "start": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470991872, + "virtualBytes": 4169674752, + "minorFaults": 90538, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 471380992, + "virtualBytes": 8534130688, + "minorFaults": 90584, + "majorFaults": 0 + }, + "end": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470991872, + "virtualBytes": 4169674752, + "minorFaults": 90584, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 91.73133199999575, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.026741, + "firstGuestHostCallMs": 11.974489, + "firstOutputMs": 67.743268, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 68.558386, + "phases": [ + { + "name": "Engine", + "ms": 0.004705 + }, + { + "name": "canonicalPreopens", + "ms": 0.201199 + }, + { + "name": "moduleRead", + "ms": 7.366397999999999 + }, + { + "name": "profileValidation", + "ms": 2.636985 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.010835000000000001 + }, + { + "name": "Linker", + "ms": 0.242083 + }, + { + "name": "Store", + "ms": 0.023087 + }, + { + "name": "Instance", + "ms": 0.590109 + }, + { + "name": "signalMaskInit", + "ms": 0.082266 + }, + { + "name": "entrypointLookup", + "ms": 0.005364 + }, + { + "name": "wasi.start", + "ms": 56.026066 + }, + { + "name": "Store.teardown", + "ms": 0.63884 + } + ] + }, + "memory": { + "start": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470991872, + "virtualBytes": 4169674752, + "minorFaults": 90584, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 471382016, + "virtualBytes": 8534130688, + "minorFaults": 90630, + "majorFaults": 0 + }, + "end": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90630, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 73.8295849999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349812, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.055105, + "firstGuestHostCallMs": 12.343774999999999, + "firstOutputMs": 53.574783000000004, + "guestLinearMemoryBytes": 2097152, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 53.729751, + "phases": [ + { + "name": "Engine", + "ms": 0.004065999999999999 + }, + { + "name": "canonicalPreopens", + "ms": 0.13842 + }, + { + "name": "moduleRead", + "ms": 6.8428960000000005 + }, + { + "name": "profileValidation", + "ms": 3.109351 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.007503999999999999 + }, + { + "name": "Linker", + "ms": 0.201103 + }, + { + "name": "Store", + "ms": 0.016886 + }, + { + "name": "Instance", + "ms": 1.171214 + }, + { + "name": "signalMaskInit", + "ms": 0.062185000000000004 + }, + { + "name": "entrypointLookup", + "ms": 0.003111 + }, + { + "name": "wasi.start", + "ms": 41.425333 + }, + { + "name": "Store.teardown", + "ms": 0.034702000000000004 + } + ] + }, + "memory": { + "start": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90630, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 471382016, + "virtualBytes": 8534130688, + "minorFaults": 90676, + "majorFaults": 0 + }, + "end": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90676, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1417.5353950000135, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": false, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.013141, + "firstGuestHostCallMs": 1386.816061, + "firstOutputMs": 1390.236051, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 1392.296679, + "phases": [ + { + "name": "Engine", + "ms": 0.001911 + }, + { + "name": "canonicalPreopens", + "ms": 0.103042 + }, + { + "name": "moduleRead", + "ms": 6.753045 + }, + { + "name": "profileValidation", + "ms": 4.454835 + }, + { + "name": "moduleCompile", + "ms": 1374.2719029999998 + }, + { + "name": "importValidation", + "ms": 0.008457999999999999 + }, + { + "name": "Linker", + "ms": 0.192888 + }, + { + "name": "Store", + "ms": 0.017107999999999998 + }, + { + "name": "Instance", + "ms": 0.22589299999999998 + }, + { + "name": "signalMaskInit", + "ms": 0.054401000000000005 + }, + { + "name": "entrypointLookup", + "ms": 0.003751 + }, + { + "name": "wasi.start", + "ms": 5.235204 + }, + { + "name": "Store.teardown", + "ms": 0.181625 + } + ] + }, + "memory": { + "start": { + "rssBytes": 466599936, + "peakRssBytes": 467099648, + "pssBytes": 470992896, + "virtualBytes": 4169674752, + "minorFaults": 90676, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 507736064, + "peakRssBytes": 507736064, + "pssBytes": 512329728, + "virtualBytes": 8567566336, + "minorFaults": 112407, + "majorFaults": 0 + }, + "end": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112407, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15607328, + "wasmtimeProcessRetainedRssBytes": 466599936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 38.16244099999312, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.017723999999999997, + "firstGuestHostCallMs": 12.456597, + "firstOutputMs": 15.840107999999999, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.72293, + "phases": [ + { + "name": "Engine", + "ms": 0.002105 + }, + { + "name": "canonicalPreopens", + "ms": 0.113011 + }, + { + "name": "moduleRead", + "ms": 6.834194 + }, + { + "name": "profileValidation", + "ms": 4.443263 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008239 + }, + { + "name": "Linker", + "ms": 0.212104 + }, + { + "name": "Store", + "ms": 0.015987 + }, + { + "name": "Instance", + "ms": 0.045827 + }, + { + "name": "signalMaskInit", + "ms": 0.028954 + }, + { + "name": "entrypointLookup", + "ms": 0.0038260000000000004 + }, + { + "name": "wasi.start", + "ms": 5.206017999999999 + }, + { + "name": "Store.teardown", + "ms": 0.042445 + } + ] + }, + "memory": { + "start": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112407, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 500326400, + "peakRssBytes": 507736064, + "pssBytes": 504600576, + "virtualBytes": 8567566336, + "minorFaults": 112435, + "majorFaults": 0 + }, + "end": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112435, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 37.73678700000164, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.018974, + "firstGuestHostCallMs": 12.678223000000001, + "firstOutputMs": 15.965380000000001, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.357092, + "phases": [ + { + "name": "Engine", + "ms": 0.001733 + }, + { + "name": "canonicalPreopens", + "ms": 0.107246 + }, + { + "name": "moduleRead", + "ms": 7.0751859999999995 + }, + { + "name": "profileValidation", + "ms": 4.41939 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009038000000000001 + }, + { + "name": "Linker", + "ms": 0.20599299999999998 + }, + { + "name": "Store", + "ms": 0.015579 + }, + { + "name": "Instance", + "ms": 0.043646000000000004 + }, + { + "name": "signalMaskInit", + "ms": 0.050344 + }, + { + "name": "entrypointLookup", + "ms": 0.003366 + }, + { + "name": "wasi.start", + "ms": 4.640709 + }, + { + "name": "Store.teardown", + "ms": 0.031730999999999995 + } + ] + }, + "memory": { + "start": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112435, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504596480, + "virtualBytes": 8567566336, + "minorFaults": 112463, + "majorFaults": 0 + }, + "end": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112463, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 39.31024500000058, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.016141, + "firstGuestHostCallMs": 12.388059, + "firstOutputMs": 15.698117000000002, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 17.150774000000002, + "phases": [ + { + "name": "Engine", + "ms": 0.0018160000000000001 + }, + { + "name": "canonicalPreopens", + "ms": 0.108314 + }, + { + "name": "moduleRead", + "ms": 6.811485 + }, + { + "name": "profileValidation", + "ms": 4.3972940000000005 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.009109 + }, + { + "name": "Linker", + "ms": 0.212675 + }, + { + "name": "Store", + "ms": 0.016441 + }, + { + "name": "Instance", + "ms": 0.046435 + }, + { + "name": "signalMaskInit", + "ms": 0.045625 + }, + { + "name": "entrypointLookup", + "ms": 0.003125 + }, + { + "name": "wasi.start", + "ms": 4.683491 + }, + { + "name": "Store.teardown", + "ms": 0.070769 + } + ] + }, + "memory": { + "start": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112463, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504530944, + "virtualBytes": 8567566336, + "minorFaults": 112491, + "majorFaults": 0 + }, + "end": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112491, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 39.05209700000705, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "completed", + "backend": "wasmtime", + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509578, + "moduleCacheHit": true, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "firstHostCallMs": 0.014704, + "firstGuestHostCallMs": 13.287343, + "firstOutputMs": 16.72493, + "guestLinearMemoryBytes": 1835008, + "asyncStackBytes": 2097152, + "reservedStoreBytes": 144314880, + "totalMs": 18.463971, + "phases": [ + { + "name": "Engine", + "ms": 0.001739 + }, + { + "name": "canonicalPreopens", + "ms": 0.10867 + }, + { + "name": "moduleRead", + "ms": 7.518398 + }, + { + "name": "profileValidation", + "ms": 4.593427 + }, + { + "name": "moduleCompile", + "ms": 0 + }, + { + "name": "importValidation", + "ms": 0.008775 + }, + { + "name": "Linker", + "ms": 0.208244 + }, + { + "name": "Store", + "ms": 0.016527 + }, + { + "name": "Instance", + "ms": 0.04021 + }, + { + "name": "signalMaskInit", + "ms": 0.045045 + }, + { + "name": "entrypointLookup", + "ms": 0.003387 + }, + { + "name": "wasi.start", + "ms": 4.832514000000001 + }, + { + "name": "Store.teardown", + "ms": 0.33850800000000003 + } + ] + }, + "memory": { + "start": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504313856, + "virtualBytes": 4203110400, + "minorFaults": 112491, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504595456, + "virtualBytes": 8567566336, + "minorFaults": 112519, + "majorFaults": 0 + }, + "end": { + "rssBytes": 499920896, + "peakRssBytes": 507736064, + "pssBytes": 504312832, + "virtualBytes": 4203110400, + "minorFaults": 112519, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 16981600, + "wasmtimeProcessRetainedRssBytes": 499920896, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + } + ], + "concurrency": [ + { + "backend": "v8", + "levels": [ + { + "level": 1, + "mode": "repeated", + "durationMs": 70.43615299998783, + "throughputPerSecond": 14.197254639959864, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 259633152, + "peakRssBytes": 357371904, + "pssBytes": 260363264, + "virtualBytes": 3888386048, + "minorFaults": 151185, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 285327360, + "peakRssBytes": 357371904, + "pssBytes": 272262144, + "virtualBytes": 4640600064, + "minorFaults": 159824, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271503360, + "peakRssBytes": 357371904, + "pssBytes": 272262144, + "virtualBytes": 3955494912, + "minorFaults": 159824, + "majorFaults": 0 + } + }, + "drainMs": 24.54157000000123 + }, + { + "level": 1, + "mode": "diverse", + "durationMs": 66.32381699999678, + "throughputPerSecond": 15.077539943155692, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 271503360, + "peakRssBytes": 357371904, + "pssBytes": 272262144, + "virtualBytes": 3955494912, + "minorFaults": 159824, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286228480, + "peakRssBytes": 357371904, + "pssBytes": 287081472, + "virtualBytes": 4640600064, + "minorFaults": 166094, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271568896, + "peakRssBytes": 357371904, + "pssBytes": 272364544, + "virtualBytes": 3955494912, + "minorFaults": 166094, + "majorFaults": 0 + } + }, + "drainMs": 25.530501000001095 + }, + { + "level": 10, + "mode": "repeated", + "durationMs": 411.9216210000159, + "throughputPerSecond": 24.27646302158928, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 271568896, + "peakRssBytes": 357371904, + "pssBytes": 272364544, + "virtualBytes": 3955494912, + "minorFaults": 166094, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 581492736, + "peakRssBytes": 583520256, + "pssBytes": 436757504, + "virtualBytes": 11360030720, + "minorFaults": 266463, + "majorFaults": 0 + }, + "end": { + "rssBytes": 426852352, + "peakRssBytes": 583520256, + "pssBytes": 428052480, + "virtualBytes": 4619661312, + "minorFaults": 266463, + "majorFaults": 0 + } + }, + "drainMs": 25.41490899998462 + }, + { + "level": 10, + "mode": "diverse", + "durationMs": 771.5169840000162, + "throughputPerSecond": 12.961477462432363, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 426852352, + "peakRssBytes": 583520256, + "pssBytes": 428052480, + "virtualBytes": 4619661312, + "minorFaults": 266463, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 795979776, + "peakRssBytes": 796250112, + "pssBytes": 662057984, + "virtualBytes": 11118653440, + "minorFaults": 431615, + "majorFaults": 0 + }, + "end": { + "rssBytes": 509149184, + "peakRssBytes": 796250112, + "pssBytes": 510145536, + "virtualBytes": 4716515328, + "minorFaults": 431615, + "majorFaults": 0 + } + }, + "drainMs": 25.513349000015296 + }, + { + "level": 50, + "mode": "repeated", + "durationMs": 1815.7994269999908, + "throughputPerSecond": 11.014432377613092, + "fulfilled": 50, + "successful": 20, + "failedExitCodes": 30, + "failureExamples": [ + "sidecar rejected request 928: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 929: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 930: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 509149184, + "peakRssBytes": 796250112, + "pssBytes": 510145536, + "virtualBytes": 4716515328, + "minorFaults": 431615, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1006063616, + "peakRssBytes": 1006735360, + "pssBytes": 830010368, + "virtualBytes": 18986061824, + "minorFaults": 637557, + "majorFaults": 0 + }, + "end": { + "rssBytes": 857862144, + "peakRssBytes": 1006735360, + "pssBytes": 618415104, + "virtualBytes": 14785413120, + "minorFaults": 637557, + "majorFaults": 0 + } + }, + "drainMs": 25.503911999985576 + }, + { + "level": 50, + "mode": "diverse", + "durationMs": 3651.4833389999985, + "throughputPerSecond": 5.203364834523214, + "fulfilled": 50, + "successful": 19, + "failedExitCodes": 31, + "failureExamples": [ + "sidecar rejected request 1023: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1024: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1025: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 668491776, + "peakRssBytes": 1006735360, + "pssBytes": 669619200, + "virtualBytes": 5331824640, + "minorFaults": 637557, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1497767936, + "peakRssBytes": 1498017792, + "pssBytes": 1489821696, + "virtualBytes": 18692263936, + "minorFaults": 1108823, + "majorFaults": 0 + }, + "end": { + "rssBytes": 876015616, + "peakRssBytes": 1498017792, + "pssBytes": 489856000, + "virtualBytes": 6704611328, + "minorFaults": 1108823, + "majorFaults": 0 + } + }, + "drainMs": 25.440368000010494 + }, + { + "level": 100, + "mode": "repeated", + "durationMs": 2848.995160999999, + "throughputPerSecond": 7.020018943443901, + "fulfilled": 100, + "successful": 20, + "failedExitCodes": 80, + "failureExamples": [ + "sidecar rejected request 1139: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1140: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1141: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 802099200, + "peakRssBytes": 1498017792, + "pssBytes": 803169280, + "virtualBytes": 5427015680, + "minorFaults": 1108823, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1093419008, + "peakRssBytes": 1498017792, + "pssBytes": 1093092352, + "virtualBytes": 18725466112, + "minorFaults": 1288182, + "majorFaults": 0 + }, + "end": { + "rssBytes": 1049399296, + "peakRssBytes": 1498017792, + "pssBytes": 743975936, + "virtualBytes": 16938565632, + "minorFaults": 1288182, + "majorFaults": 0 + } + }, + "drainMs": 25.58588400000008 + }, + { + "level": 100, + "mode": "diverse", + "durationMs": 5590.820376999996, + "throughputPerSecond": 3.3984279083913793, + "fulfilled": 100, + "successful": 19, + "failedExitCodes": 81, + "failureExamples": [ + "sidecar rejected request 1282: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1283: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1284: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 799526912, + "peakRssBytes": 1498017792, + "pssBytes": 800855040, + "virtualBytes": 5427015680, + "minorFaults": 1288182, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1534472192, + "peakRssBytes": 1534472192, + "pssBytes": 1523490816, + "virtualBytes": 18689671168, + "minorFaults": 1946730, + "majorFaults": 0 + }, + "end": { + "rssBytes": 863576064, + "peakRssBytes": 1534472192, + "pssBytes": 831947776, + "virtualBytes": 5965746176, + "minorFaults": 1946730, + "majorFaults": 0 + } + }, + "drainMs": 25.50715799999307 + }, + { + "level": 200, + "mode": "repeated", + "durationMs": 3901.6233540000103, + "throughputPerSecond": 0.25630357142874466, + "fulfilled": 200, + "successful": 1, + "failedExitCodes": 199, + "failureExamples": [ + "sidecar rejected request 1632: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1634: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1636: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 833302528, + "peakRssBytes": 1534472192, + "pssBytes": 834196480, + "virtualBytes": 5427552256, + "minorFaults": 1946730, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1137184768, + "peakRssBytes": 1534472192, + "pssBytes": 1130217472, + "virtualBytes": 18988662784, + "minorFaults": 2151516, + "majorFaults": 0 + }, + "end": { + "rssBytes": 1104330752, + "peakRssBytes": 1534472192, + "pssBytes": 729844736, + "virtualBytes": 17620807680, + "minorFaults": 2151516, + "majorFaults": 0 + } + }, + "drainMs": 25.458535999991 + }, + { + "level": 200, + "mode": "diverse", + "durationMs": 6909.159589999996, + "throughputPerSecond": 0.14473540334013338, + "fulfilled": 200, + "successful": 1, + "failedExitCodes": 199, + "failureExamples": [ + "sidecar rejected request 1875: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1877: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1879: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 867524608, + "peakRssBytes": 1534472192, + "pssBytes": 868295680, + "virtualBytes": 5427552256, + "minorFaults": 2151516, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 1536536576, + "peakRssBytes": 1536757760, + "pssBytes": 1519494144, + "virtualBytes": 18833715200, + "minorFaults": 2969695, + "majorFaults": 0 + }, + "end": { + "rssBytes": 1214058496, + "peakRssBytes": 1536757760, + "pssBytes": 665839616, + "virtualBytes": 13119102976, + "minorFaults": 2969695, + "majorFaults": 0 + } + }, + "drainMs": 46.378146000002744 + } + ] + }, + { + "backend": "wasmtime", + "levels": [ + { + "level": 1, + "mode": "repeated", + "durationMs": 23.498747000005096, + "throughputPerSecond": 42.55546051028947, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 3980144640, + "minorFaults": 86791, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 8342233088, + "minorFaults": 86808, + "majorFaults": 1 + }, + "end": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 3980144640, + "minorFaults": 86808, + "majorFaults": 1 + } + }, + "drainMs": 25.43736500001978 + }, + { + "level": 1, + "mode": "diverse", + "durationMs": 22.041999000008218, + "throughputPerSecond": 45.367936002520786, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 3980144640, + "minorFaults": 86808, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 8342233088, + "minorFaults": 86825, + "majorFaults": 1 + }, + "end": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 3980144640, + "minorFaults": 86825, + "majorFaults": 1 + } + }, + "drainMs": 25.368098000006285 + }, + { + "level": 10, + "mode": "repeated", + "durationMs": 28.997982000000775, + "throughputPerSecond": 344.85158312049896, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 319397888, + "peakRssBytes": 383946752, + "pssBytes": 320428032, + "virtualBytes": 3980144640, + "minorFaults": 86825, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 321110016, + "peakRssBytes": 383946752, + "pssBytes": 322242560, + "virtualBytes": 4637765632, + "minorFaults": 87438, + "majorFaults": 1 + }, + "end": { + "rssBytes": 321110016, + "peakRssBytes": 383946752, + "pssBytes": 322242560, + "virtualBytes": 4637642752, + "minorFaults": 87438, + "majorFaults": 1 + } + }, + "drainMs": 24.57775799999945 + }, + { + "level": 10, + "mode": "diverse", + "durationMs": 57.92543099998147, + "throughputPerSecond": 172.6357461199244, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 321110016, + "peakRssBytes": 383946752, + "pssBytes": 322242560, + "virtualBytes": 4637642752, + "minorFaults": 87438, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 345812992, + "peakRssBytes": 383946752, + "pssBytes": 346765312, + "virtualBytes": 30835007488, + "minorFaults": 92268, + "majorFaults": 1 + }, + "end": { + "rssBytes": 345812992, + "peakRssBytes": 383946752, + "pssBytes": 346765312, + "virtualBytes": 4648247296, + "minorFaults": 92268, + "majorFaults": 1 + } + }, + "drainMs": 26.002676000003703 + }, + { + "level": 50, + "mode": "repeated", + "durationMs": 55.16756999999052, + "throughputPerSecond": 362.5318280287393, + "fulfilled": 50, + "successful": 20, + "failedExitCodes": 30, + "failureExamples": [ + "sidecar rejected request 992: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 993: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 994: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 345812992, + "peakRssBytes": 383946752, + "pssBytes": 346764288, + "virtualBytes": 4648247296, + "minorFaults": 92268, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 346087424, + "peakRssBytes": 383946752, + "pssBytes": 347915264, + "virtualBytes": 66455187456, + "minorFaults": 93131, + "majorFaults": 1 + }, + "end": { + "rssBytes": 324976640, + "peakRssBytes": 383946752, + "pssBytes": 325743616, + "virtualBytes": 5286043648, + "minorFaults": 93131, + "majorFaults": 1 + } + }, + "drainMs": 25.731099000026006 + }, + { + "level": 50, + "mode": "diverse", + "durationMs": 122.12226299999747, + "throughputPerSecond": 163.77030288081392, + "fulfilled": 50, + "successful": 20, + "failedExitCodes": 30, + "failureExamples": [ + "sidecar rejected request 1085: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1086: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1087: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 324976640, + "peakRssBytes": 383946752, + "pssBytes": 325743616, + "virtualBytes": 5286043648, + "minorFaults": 93131, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 370663424, + "peakRssBytes": 383946752, + "pssBytes": 368121856, + "virtualBytes": 66503868416, + "minorFaults": 102175, + "majorFaults": 1 + }, + "end": { + "rssBytes": 367353856, + "peakRssBytes": 383946752, + "pssBytes": 368121856, + "virtualBytes": 5399359488, + "minorFaults": 102175, + "majorFaults": 1 + } + }, + "drainMs": 25.493407999980263 + }, + { + "level": 100, + "mode": "repeated", + "durationMs": 54.68940199998906, + "throughputPerSecond": 365.7015668228371, + "fulfilled": 100, + "successful": 20, + "failedExitCodes": 80, + "failureExamples": [ + "sidecar rejected request 1199: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1200: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1201: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 367353856, + "peakRssBytes": 383946752, + "pssBytes": 368121856, + "virtualBytes": 5399359488, + "minorFaults": 102175, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 367476736, + "peakRssBytes": 383946752, + "pssBytes": 368883712, + "virtualBytes": 22855012352, + "minorFaults": 102522, + "majorFaults": 1 + }, + "end": { + "rssBytes": 367357952, + "peakRssBytes": 383946752, + "pssBytes": 368125952, + "virtualBytes": 5399359488, + "minorFaults": 102522, + "majorFaults": 1 + } + }, + "drainMs": 25.52823900000658 + }, + { + "level": 100, + "mode": "diverse", + "durationMs": 145.853535000002, + "throughputPerSecond": 137.12386196193137, + "fulfilled": 100, + "successful": 20, + "failedExitCodes": 80, + "failureExamples": [ + "sidecar rejected request 1342: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1343: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1344: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 367357952, + "peakRssBytes": 383946752, + "pssBytes": 368125952, + "virtualBytes": 5399359488, + "minorFaults": 102522, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 381054976, + "peakRssBytes": 383946752, + "pssBytes": 382830592, + "virtualBytes": 66504409088, + "minorFaults": 105552, + "majorFaults": 1 + }, + "end": { + "rssBytes": 378736640, + "peakRssBytes": 383946752, + "pssBytes": 379504640, + "virtualBytes": 5399887872, + "minorFaults": 105552, + "majorFaults": 1 + } + }, + "drainMs": 25.424352000001818 + }, + { + "level": 200, + "mode": "repeated", + "durationMs": 66.78456699999515, + "throughputPerSecond": 14.973519256328675, + "fulfilled": 200, + "successful": 1, + "failedExitCodes": 199, + "failureExamples": [ + "sidecar rejected request 1687: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1688: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1690: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 378736640, + "peakRssBytes": 383946752, + "pssBytes": 379504640, + "virtualBytes": 5399887872, + "minorFaults": 105552, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 378753024, + "peakRssBytes": 383946752, + "pssBytes": 380270592, + "virtualBytes": 22852640768, + "minorFaults": 105899, + "majorFaults": 1 + }, + "end": { + "rssBytes": 378736640, + "peakRssBytes": 383946752, + "pssBytes": 379508736, + "virtualBytes": 5399887872, + "minorFaults": 105899, + "majorFaults": 1 + } + }, + "drainMs": 25.370991000003414 + }, + { + "level": 200, + "mode": "diverse", + "durationMs": 148.0591939999722, + "throughputPerSecond": 6.754055408407719, + "fulfilled": 200, + "successful": 1, + "failedExitCodes": 199, + "failureExamples": [ + "sidecar rejected request 1932: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1933: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1938: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 378736640, + "peakRssBytes": 383946752, + "pssBytes": 379508736, + "virtualBytes": 5399887872, + "minorFaults": 105899, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 382115840, + "peakRssBytes": 383946752, + "pssBytes": 380962816, + "virtualBytes": 66504409088, + "minorFaults": 106717, + "majorFaults": 1 + }, + "end": { + "rssBytes": 379637760, + "peakRssBytes": 383946752, + "pssBytes": 248033280, + "virtualBytes": 27224268800, + "minorFaults": 106717, + "majorFaults": 1 + } + }, + "drainMs": 37.61723999999231 + } + ] + } + ], + "paths": [ + { + "backend": "v8", + "denial": { + "exitCode": 7, + "stderr": "curl: (7) getsockname() failed with errno 28: Invalid argument", + "passed": true + }, + "cancellation": { + "rejected": true, + "name": "AbortError", + "message": "AbortError: This operation was aborted", + "durationMs": 108.64180800001486, + "passed": true + }, + "resourceLimit": { + "exitCode": 137, + "durationMs": 5012.1312409999955, + "stderr": "", + "passed": true + } + }, + { + "backend": "wasmtime", + "denial": { + "exitCode": 7, + "stderr": "curl: (7) getsockname() failed with errno 28: Invalid argument", + "passed": true + }, + "cancellation": { + "rejected": true, + "name": "AbortError", + "message": "AbortError: This operation was aborted", + "durationMs": 36.09543099999428, + "passed": true + }, + "resourceLimit": { + "exitCode": 137, + "durationMs": 5031.358445999998, + "stderr": "ECANCELED: Wasmtime execution was canceled", + "passed": true + } + } + ], + "status": "complete", + "summary": { + "workloads": [ + { + "name": "trivial", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 59.57136900000478, + "p50": 62.627100999990944, + "p95": 71.90598459999978, + "max": 77.16965000002529 + }, + "wasmtime": { + "count": 25, + "min": 18.68338100000983, + "p50": 20.858960999990813, + "p95": 1319.5600934, + "max": 1586.4033530000015 + }, + "cold": { + "v8": { + "count": 5, + "min": 61.38473900000099, + "p50": 65.20103199998266, + "p95": 71.84695399999978, + "max": 72.01046599999972 + }, + "wasmtime": { + "count": 5, + "min": 64.24876500002574, + "p50": 65.33928299999388, + "p95": 67.10293859999948, + "max": 67.37656699999934 + }, + "p50Ratio": 1.0021203805487504 + }, + "warm": { + "v8": { + "count": 20, + "min": 59.57136900000478, + "p50": 62.576996499992674, + "p95": 71.77213855000129, + "max": 77.16965000002529 + }, + "wasmtime": { + "count": 20, + "min": 18.68338100000983, + "p50": 20.779751499998383, + "p95": 1349.1210018, + "max": 1586.4033530000015 + }, + "p50Ratio": 0.33206693613044874 + }, + "p50Ratio": 0.3330660475565336, + "p95Ratio": 18.351185937310756 + }, + { + "name": "coreutils", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 188.18507199999294, + "p50": 203.66028599999845, + "p95": 220.10674799999904, + "max": 311.08343899999454 + }, + "wasmtime": { + "count": 25, + "min": 76.16672899998957, + "p50": 79.63946199999191, + "p95": 1127.7341364000029, + "max": 1132.9555429999891 + }, + "cold": { + "v8": { + "count": 5, + "min": 193.73045200000342, + "p50": 202.68641900000512, + "p95": 217.71261219999943, + "max": 221.05488400000013 + }, + "wasmtime": { + "count": 5, + "min": 1122.9707440000057, + "p50": 1124.7428459999974, + "p95": 1132.060826199992, + "max": 1132.9555429999891 + }, + "p50Ratio": 5.549177155278317 + }, + "warm": { + "v8": { + "count": 20, + "min": 188.18507199999294, + "p50": 203.95758950000163, + "p95": 221.05266574999484, + "max": 311.08343899999454 + }, + "wasmtime": { + "count": 20, + "min": 76.16672899998957, + "p50": 78.95193850000214, + "p95": 104.69860659999526, + "max": 404.8369820000007 + }, + "p50Ratio": 0.3870997823300001 + }, + "p50Ratio": 0.3910407058938949, + "p95Ratio": 5.123578203063578 + }, + { + "name": "shell", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 275.38358199999493, + "p50": 289.89052000000083, + "p95": 308.5851572000003, + "max": 608.1495689999997 + }, + "wasmtime": { + "count": 25, + "min": 69.20789499999955, + "p50": 78.35819800000172, + "p95": 3803.2653519999963, + "max": 3833.2702419999987 + }, + "cold": { + "v8": { + "count": 5, + "min": 280.2332799999999, + "p50": 288.921451000002, + "p95": 297.04752180000503, + "max": 298.358665000007 + }, + "wasmtime": { + "count": 5, + "min": 3791.293307999993, + "p50": 3801.143683999995, + "p95": 3827.375347399998, + "max": 3833.2702419999987 + }, + "p50Ratio": 13.156322145149302 + }, + "warm": { + "v8": { + "count": 20, + "min": 275.38358199999493, + "p50": 290.0262905000018, + "p95": 324.9707370500003, + "max": 608.1495689999997 + }, + "wasmtime": { + "count": 20, + "min": 69.20789499999955, + "p50": 76.37429499998689, + "p95": 86.83488075000152, + "max": 104.91694800000187 + }, + "p50Ratio": 0.26333576472780634 + }, + "p50Ratio": 0.2703027266983463, + "p95Ratio": 12.324848630146597 + }, + { + "name": "curl", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 145.81927399999404, + "p50": 157.13079000000016, + "p95": 177.95054079999971, + "max": 178.96058100000664 + }, + "wasmtime": { + "count": 25, + "min": 37.231541000001016, + "p50": 44.221361000003526, + "p95": 1575.4350507999975, + "max": 1578.7898149999965 + }, + "cold": { + "v8": { + "count": 5, + "min": 149.62543899999582, + "p50": 156.92659499999718, + "p95": 174.21882519999946, + "max": 178.32801499999914 + }, + "wasmtime": { + "count": 5, + "min": 1560.877677000004, + "p50": 1570.2526739999885, + "p95": 1578.3779809999971, + "max": 1578.7898149999965 + }, + "p50Ratio": 10.006287806091866 + }, + "warm": { + "v8": { + "count": 20, + "min": 145.81927399999404, + "p50": 157.23243750000483, + "p95": 176.5666408500023, + "max": 178.96058100000664 + }, + "wasmtime": { + "count": 20, + "min": 37.231541000001016, + "p50": 43.574276499992266, + "p95": 49.7168186999778, + "max": 128.26963400001114 + }, + "p50Ratio": 0.2771328689729874 + }, + "p50Ratio": 0.28143027219556066, + "p95Ratio": 8.853218673668678 + }, + { + "name": "sqlite", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 119.87707899999805, + "p50": 128.89805299999716, + "p95": 148.01418700001085, + "max": 157.19159400000353 + }, + "wasmtime": { + "count": 25, + "min": 30.554088000000775, + "p50": 33.786996999999246, + "p95": 1260.899436999997, + "max": 1272.3886730000013 + }, + "cold": { + "v8": { + "count": 5, + "min": 119.87707899999805, + "p50": 121.07806999998866, + "p95": 133.30939419999996, + "max": 135.88163299999997 + }, + "wasmtime": { + "count": 5, + "min": 1259.1288490000006, + "p50": 1260.7793850000016, + "p95": 1270.0968284000003, + "max": 1272.3886730000013 + }, + "p50Ratio": 10.412945837343788 + }, + "warm": { + "v8": { + "count": 20, + "min": 124.31854400000157, + "p50": 132.73753699999952, + "p95": 149.42579050001368, + "max": 157.19159400000353 + }, + "wasmtime": { + "count": 20, + "min": 30.554088000000775, + "p50": 33.19739750000008, + "p95": 34.505042249993494, + "max": 37.06238999999914 + }, + "p50Ratio": 0.2500980374526627 + }, + "p50Ratio": 0.2621218568755262, + "p95Ratio": 8.518774196961976 + }, + { + "name": "vim", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 211.28133700000762, + "p50": 228.71948300000076, + "p95": 245.7815776000003, + "max": 249.97187999999733 + }, + "wasmtime": { + "count": 25, + "min": 60.86270200001309, + "p50": 68.49119200000132, + "p95": 3646.7097611999952, + "max": 3657.8598960000018 + }, + "cold": { + "v8": { + "count": 5, + "min": 211.28133700000762, + "p50": 215.65722700000333, + "p95": 231.86205279999905, + "max": 234.06816999999864 + }, + "wasmtime": { + "count": 5, + "min": 3629.959167000001, + "p50": 3643.197438000003, + "p95": 3655.8054852, + "max": 3657.8598960000018 + }, + "p50Ratio": 16.893463245727194 + }, + "warm": { + "v8": { + "count": 20, + "min": 214.34777200000826, + "p50": 229.27213599999232, + "p95": 246.71831904999982, + "max": 249.97187999999733 + }, + "wasmtime": { + "count": 20, + "min": 60.86270200001309, + "p50": 67.82249449998926, + "p95": 80.24472784999712, + "max": 143.09163300000364 + }, + "p50Ratio": 0.29581655967121767 + }, + "p50Ratio": 0.29945499658199687, + "p95Ratio": 14.837197306686955 + }, + { + "name": "large-module", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 232.23686199999065, + "p50": 254.20395999999982, + "p95": 276.5952762000015, + "max": 299.49480400000175 + }, + "wasmtime": { + "count": 25, + "min": 51.291408000004594, + "p50": 58.004785999997694, + "p95": 3921.148156799993, + "max": 3925.991179000004 + }, + "cold": { + "v8": { + "count": 5, + "min": 245.6948569999995, + "p50": 256.9399199999898, + "p95": 262.74797320000073, + "max": 263.1384560000006 + }, + "wasmtime": { + "count": 5, + "min": 3906.815888000012, + "p50": 3915.2825840000005, + "p95": 3925.3158532000016, + "max": 3925.991179000004 + }, + "p50Ratio": 15.23812486592257 + }, + "warm": { + "v8": { + "count": 20, + "min": 232.23686199999065, + "p50": 252.65428149999207, + "p95": 280.3325449500022, + "max": 299.49480400000175 + }, + "wasmtime": { + "count": 20, + "min": 51.291408000004594, + "p50": 55.097512000007555, + "p95": 61.38697389998997, + "max": 62.07325200000196 + }, + "p50Ratio": 0.21807472120756158 + }, + "p50Ratio": 0.22818207080644115, + "p95Ratio": 14.176482731992413 + }, + { + "name": "compute-heavy", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 177.93755200000305, + "p50": 192.66382500000327, + "p95": 335.75826499999926, + "max": 959.1709859999992 + }, + "wasmtime": { + "count": 25, + "min": 68.29262800000288, + "p50": 75.6135890000005, + "p95": 704.3596711999984, + "max": 708.9819379999972 + }, + "cold": { + "v8": { + "count": 5, + "min": 183.8470639999996, + "p50": 192.66382500000327, + "p95": 217.84665839999798, + "max": 221.2920529999974 + }, + "wasmtime": { + "count": 5, + "min": 688.8527680000116, + "p50": 703.8645800000013, + "p95": 708.0822391999973, + "max": 708.9819379999972 + }, + "p50Ratio": 3.6533302502428224 + }, + "warm": { + "v8": { + "count": 20, + "min": 177.93755200000305, + "p50": 193.4362325000002, + "p95": 394.1146264000006, + "max": 959.1709859999992 + }, + "wasmtime": { + "count": 20, + "min": 68.29262800000288, + "p50": 74.20175500000187, + "p95": 81.22228949999773, + "max": 91.73133199999575 + }, + "p50Ratio": 0.3835980159508214 + }, + "p50Ratio": 0.39246386289693574, + "p95Ratio": 2.0978178190192875 + }, + { + "name": "host-call-heavy", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 138.22382399999879, + "p50": 154.60512500000186, + "p95": 986.9136583999996, + "max": 1351.7398759999996 + }, + "wasmtime": { + "count": 25, + "min": 37.25239200000942, + "p50": 40.201015999991796, + "p95": 1422.0893457999978, + "max": 1422.7365940000018 + }, + "cold": { + "v8": { + "count": 5, + "min": 138.22382399999879, + "p50": 147.65516300000309, + "p95": 166.6182134000017, + "max": 167.4548300000024 + }, + "wasmtime": { + "count": 5, + "min": 1412.0978959999993, + "p50": 1421.7791129999969, + "p95": 1422.622656000001, + "max": 1422.7365940000018 + }, + "p50Ratio": 9.629051122309669 + }, + "warm": { + "v8": { + "count": 20, + "min": 141.74686500000098, + "p50": 154.7221264999971, + "p95": 1034.4257324000002, + "max": 1351.7398759999996 + }, + "wasmtime": { + "count": 20, + "min": 37.25239200000942, + "p50": 39.60486599999422, + "p95": 44.707426899993884, + "max": 47.084476999996696 + }, + "p50Ratio": 0.25597415764573894 + }, + "p50Ratio": 0.26002382521272377, + "p95Ratio": 1.4409460581440448 + } + ], + "geometricMeanP50Ratio": 0.29723524174118376, + "throughput": [ + { + "level": 1, + "mode": "repeated", + "v8": 14.197254639959864, + "wasmtime": 42.55546051028947, + "ratio": 2.9974429274876893 + }, + { + "level": 1, + "mode": "diverse", + "v8": 15.077539943155692, + "wasmtime": 45.367936002520786, + "ratio": 3.008974685098754 + }, + { + "level": 10, + "mode": "repeated", + "v8": 24.27646302158928, + "wasmtime": 344.85158312049896, + "ratio": 14.205182312341766 + }, + { + "level": 10, + "mode": "diverse", + "v8": 12.961477462432363, + "wasmtime": 172.6357461199244, + "ratio": 13.319141017703657 + }, + { + "level": 50, + "mode": "repeated", + "v8": 11.014432377613092, + "wasmtime": 362.5318280287393, + "ratio": 32.9142542801922 + }, + { + "level": 50, + "mode": "diverse", + "v8": 5.203364834523214, + "wasmtime": 163.77030288081392, + "ratio": 31.473922757488182 + }, + { + "level": 100, + "mode": "repeated", + "v8": 7.020018943443901, + "wasmtime": 365.7015668228371, + "ratio": 52.09409971241903 + }, + { + "level": 100, + "mode": "diverse", + "v8": 3.3984279083913793, + "wasmtime": 137.12386196193137, + "ratio": 40.34920429630003 + }, + { + "level": 200, + "mode": "repeated", + "v8": 0.25630357142874466, + "wasmtime": 14.973519256328675, + "ratio": 58.421032422060826 + }, + { + "level": 200, + "mode": "diverse", + "v8": 0.14473540334013338, + "wasmtime": 6.754055408407719, + "ratio": 46.664846696391535 + } + ], + "retained": { + "v8RssBytes": 127930368, + "wasmtimeRssBytes": 264069120, + "v8PssBytes": 129094656, + "wasmtimePssBytes": 264836096 + }, + "gates": { + "correctness": true, + "geometricMeanP50": true, + "individualP95": false, + "throughput": true, + "retainedRss": false, + "retainedPss": false + }, + "preferredBackend": "v8", + "omissionBehavior": "v8", + "rollbackBackend": "v8" + }, + "completedAt": "2026-07-21T06:47:53.193Z" +} diff --git a/packages/runtime-benchmarks/src/focused/wasm-backend-comparison.bench.ts b/packages/runtime-benchmarks/src/focused/wasm-backend-comparison.bench.ts new file mode 100644 index 0000000000..83a3c908c5 --- /dev/null +++ b/packages/runtime-benchmarks/src/focused/wasm-backend-comparison.bench.ts @@ -0,0 +1,954 @@ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync, statSync, writeFileSync } from "node:fs"; +import http from "node:http"; +import { cpus, hostname, totalmem } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; +import { + type ProcessMemorySnapshot, + readProcessMemorySnapshot, +} from "../lib/memory.js"; +import { + type BenchVm, + createBenchSidecar, + createBenchVm, + formatSidecarProvenance, + resolveBenchCommandsDir, + resolveBenchSidecarProvenance, +} from "../lib/vm.js"; + +type Backend = "v8" | "wasmtime"; + +interface Workload { + name: string; + command: string; + args: (context: WorkloadContext) => string[]; + stdin?: string; + validate: (result: CommandResult, context: WorkloadContext) => void; +} + +interface WorkloadContext { + port: number; +} + +interface CommandResult { + stdout: string; + stderr: string; + exitCode: number; +} + +interface PhaseDiagnostic { + backend?: string; + sourceModuleBytes?: number | null; + moduleCacheHit?: boolean | null; + moduleBytes?: number | null; + firstHostCallMs?: number | null; + firstGuestHostCallMs?: number | null; + firstOutputMs?: number | null; + guestLinearMemoryBytes?: number; + asyncStackBytes?: number; + reservedStoreBytes?: number; + totalMs?: number; + phases?: Array<{ name: string; ms: number }>; + [key: string]: unknown; +} + +interface TimedMemory { + start: ProcessMemorySnapshot; + peak: ProcessMemorySnapshot; + end: ProcessMemorySnapshot; +} + +const PHASE_PREFIX = "__AGENTOS_WASM_PHASE_METRICS__:"; +const freshProcesses = integerEnv("AGENTOS_WASM_BENCH_FRESH_PROCESSES", 5); +const samplesPerProcess = integerEnv("AGENTOS_WASM_BENCH_SAMPLES", 5); +const concurrencyLevels = listEnv( + "AGENTOS_WASM_BENCH_CONCURRENCY", + [1, 10, 50, 100, 200], +); +const retainedSettleMs = integerEnv( + "AGENTOS_WASM_BENCH_RETAINED_SETTLE_MS", + 250, +); +const outputPath = resolve( + process.env.AGENTOS_WASM_BENCH_OUTPUT ?? + join( + dirname(fileURLToPath(import.meta.url)), + "../../results/wasm-backend-comparison.json", + ), +); +const commandsDir = resolveBenchCommandsDir( + process.env.AGENTOS_WASM_COMMANDS_DIR, +); +const sidecarProvenance = resolveBenchSidecarProvenance(); + +const allWorkloads: Workload[] = [ + { + name: "trivial", + command: "true", + args: () => [], + validate: expectExitZero, + }, + { + name: "coreutils", + command: "ls", + args: () => ["-la", "/tmp/wasmtime-bench-tree"], + validate: (result) => { + expectExitZero(result); + if (!result.stdout.includes("file-063")) + throw new Error("ls output missing fixture"); + }, + }, + { + name: "shell", + command: "sh", + args: () => ["-c", "printf 'alpha\\nbeta\\n' | /opt/agentos/bin/grep beta"], + validate: (result) => { + expectExitZero(result); + if (result.stdout.trim() !== "beta") + throw new Error("shell pipeline output mismatch"); + }, + }, + { + name: "curl", + command: "curl", + args: ({ port }) => ["-fsS", `http://127.0.0.1:${port}/payload`], + validate: (result) => { + expectExitZero(result); + if (result.stdout !== "wasmtime-benchmark-loopback") { + throw new Error("curl body mismatch"); + } + }, + }, + { + name: "sqlite", + command: "sqlite3", + args: () => [ + ":memory:", + "select sum(value) from generate_series(1, 1000);", + ], + validate: (result) => { + expectExitZero(result); + if (result.stdout.trim() !== "500500") + throw new Error("sqlite result mismatch"); + }, + }, + { + name: "vim", + command: "vim", + args: () => ["-u", "NONE", "-N", "-n", "-es", "-c", "q"], + validate: expectExitZero, + }, + { + name: "large-module", + command: "git", + args: () => ["--version"], + validate: (result) => { + expectExitZero(result); + if (!result.stdout.startsWith("git version")) + throw new Error("git version mismatch"); + }, + }, + { + name: "compute-heavy", + command: "sha256sum", + args: () => ["/tmp/wasmtime-bench-compute.bin"], + validate: (result) => { + expectExitZero(result); + if (!/^[0-9a-f]{64}\s/u.test(result.stdout)) + throw new Error("sha256 output mismatch"); + }, + }, + { + name: "host-call-heavy", + command: "find", + args: () => ["/tmp/wasmtime-bench-tree", "-type", "f", "-print"], + validate: (result) => { + expectExitZero(result); + if (!result.stdout.includes("file-063")) + throw new Error("find output missing fixture"); + }, + }, +]; +const workloadFilter = process.env.AGENTOS_WASM_BENCH_WORKLOADS?.split(",") + .map((entry) => entry.trim()) + .filter(Boolean); +const workloads = workloadFilter + ? allWorkloads.filter((workload) => workloadFilter.includes(workload.name)) + : allWorkloads; +if (workloads.length === 0) + throw new Error("workload filter selected no workloads"); + +const diverseConcurrency = [ + ["true", []], + ["printf", ["x"]], + ["pwd", []], + ["uname", []], + ["id", []], + ["date", ["+%s"]], + ["dirname", ["/a/b"]], + ["basename", ["/a/b"]], +] as const; + +async function main(): Promise { + if (sidecarProvenance.profile !== "release") { + throw new Error( + `Wasmtime backend comparison requires a release sidecar, got ${formatSidecarProvenance(sidecarProvenance)}`, + ); + } + const loopback = await listenLoopback(); + try { + const startedAt = new Date().toISOString(); + const result: Record = { + metadata: { + startedAt, + hostname: hostname(), + platform: process.platform, + arch: process.arch, + cpuModel: cpus()[0]?.model ?? "unknown", + logicalCpus: cpus().length, + totalMemoryBytes: totalmem(), + kernel: execFileSync("uname", ["-srvm"], { + encoding: "utf8", + }).trim(), + node: process.version, + sidecar: sidecarProvenance, + commandsDir, + freshProcesses, + samplesPerProcess, + concurrencyLevels, + memoryAllocation: "on-demand", + memoryInitCow: true, + pooling: false, + aot: false, + wizer: false, + liveSnapshots: false, + }, + modules: moduleInventory(), + fresh: [], + concurrency: [], + paths: [], + status: "running", + }; + for (let processIndex = 0; processIndex < freshProcesses; processIndex++) { + for (const backend of ["v8", "wasmtime"] as const) { + console.error( + `fresh process ${processIndex + 1}/${freshProcesses} ${backend}`, + ); + (result.fresh as unknown[]).push( + await runFreshProcess(backend, processIndex, loopback.port), + ); + writeCheckpoint(result); + } + } + for (const backend of ["v8", "wasmtime"] as const) { + console.error(`concurrency ${backend}`); + (result.concurrency as unknown[]).push( + await runConcurrency(backend, loopback.port), + ); + writeCheckpoint(result); + console.error(`safety/control paths ${backend}`); + (result.paths as unknown[]).push( + await runControlPaths(backend, loopback.port), + ); + writeCheckpoint(result); + } + result.summary = summarize(result); + result.completedAt = new Date().toISOString(); + result.status = "complete"; + writeCheckpoint(result); + console.log(JSON.stringify(result.summary, null, 2)); + console.error(`raw results: ${outputPath}`); + } finally { + await closeServer(loopback.server); + } +} + +function writeCheckpoint(result: Record): void { + writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`); +} + +async function runFreshProcess( + backend: Backend, + processIndex: number, + port: number, +) { + const sidecar = createBenchSidecar(); + let vm: BenchVm | undefined; + try { + const vmStarted = performance.now(); + vm = await createBenchVm({ sidecar, loopbackExemptPorts: [port] }); + const activeVm = vm; + const vmSetupMs = performance.now() - vmStarted; + const fixtureStarted = performance.now(); + await prepareFixtures(activeVm); + const fixtureSetupMs = performance.now() - fixtureStarted; + const pid = requiredSidecarPid(activeVm); + const baseline = readProcessMemorySnapshot(pid); + const workloadResults = []; + for (const workload of workloads) { + console.error(` ${backend} ${workload.name}`); + const samples = []; + for ( + let sampleIndex = 0; + sampleIndex < samplesPerProcess; + sampleIndex++ + ) { + const before = await activeVm.getResourceSnapshot(); + const measured = await measureCommandMemory(pid, () => + activeVm.execArgv(workload.command, workload.args({ port }), { + wasmBackend: backend, + stdin: workload.stdin, + env: { AGENTOS_WASM_WARMUP_DEBUG: "1" }, + }), + ); + let validationError: string | null = null; + try { + workload.validate(measured.value, { port }); + } catch (error) { + validationError = String(error); + } + const phase = parsePhaseDiagnostic(measured.value.stderr); + const expectedSourceBytes = statSync( + join(commandsDir, workload.command), + ).size; + if (phase?.sourceModuleBytes !== expectedSourceBytes) { + validationError ??= `${backend} ${workload.name} executed ${String(phase?.sourceModuleBytes)} source bytes; expected ${expectedSourceBytes}`; + } + const after = await activeVm.getResourceSnapshot(); + samples.push({ + index: sampleIndex, + cacheState: sampleIndex === 0 ? "fresh" : "warm", + durationMs: measured.durationMs, + exitCode: measured.value.exitCode, + passed: validationError === null, + validationError, + stdoutBytes: Buffer.byteLength(measured.value.stdout), + stderrBytes: Buffer.byteLength( + stripDiagnostics(measured.value.stderr), + ), + phase, + memory: measured.memory, + resourceBefore: projectResources(before), + resourceAfter: projectResources(after), + }); + } + workloadResults.push({ + name: workload.name, + command: workload.command, + samples, + }); + } + const beforeDispose = await activeVm.getResourceSnapshot(); + await activeVm.dispose(); + vm = undefined; + await delay(retainedSettleMs); + const retained = readProcessMemorySnapshot(pid); + return { + backend, + processIndex, + vmSetupMs, + fixtureSetupMs, + baseline, + beforeDispose: projectResources(beforeDispose), + retained, + retainedDelta: memoryDelta(retained, baseline), + workloads: workloadResults, + }; + } finally { + if (vm) await vm.dispose().catch(() => undefined); + await sidecar.dispose(); + } +} + +async function runConcurrency(backend: Backend, port: number) { + const sidecar = createBenchSidecar(); + let vm: BenchVm | undefined; + try { + vm = await createBenchVm({ sidecar, loopbackExemptPorts: [port] }); + const activeVm = vm; + await prepareFixtures(activeVm); + for (const [command, args] of diverseConcurrency) { + const warm = await activeVm.execArgv(command, [...args], { + wasmBackend: backend, + }); + if (warm.exitCode !== 0) + throw new Error(`${backend} concurrency warmup ${command} failed`); + } + await waitForRuntimeDrain(activeVm); + const pid = requiredSidecarPid(activeVm); + const levels = []; + for (const level of concurrencyLevels) { + for (const mode of ["repeated", "diverse"] as const) { + const measured = await measureCommandMemory(pid, async () => { + const settled = await Promise.allSettled( + Array.from({ length: level }, (_, index) => { + const [command, args] = + mode === "repeated" + ? (["true", []] as const) + : diverseConcurrency[index % diverseConcurrency.length]; + return activeVm.execArgv(command, [...args], { + wasmBackend: backend, + }); + }), + ); + return settled; + }); + const fulfilled = measured.value.filter( + (entry): entry is PromiseFulfilledResult => + entry.status === "fulfilled", + ); + const failedExitCodes = fulfilled.filter( + (entry) => entry.value.exitCode !== 0, + ); + const successful = fulfilled.length - failedExitCodes.length; + const rejected = measured.value + .filter( + (entry): entry is PromiseRejectedResult => + entry.status === "rejected", + ) + .map((entry) => String(entry.reason)); + const failureExamples = [ + ...new Set( + failedExitCodes.map((entry) => + stripDiagnostics(entry.value.stderr).slice(0, 1_000), + ), + ), + ].slice(0, 3); + const drainMs = await waitForRuntimeDrain(activeVm); + levels.push({ + level, + mode, + durationMs: measured.durationMs, + throughputPerSecond: (successful * 1_000) / measured.durationMs, + fulfilled: fulfilled.length, + successful, + failedExitCodes: failedExitCodes.length, + failureExamples, + rejectedCount: rejected.length, + rejectionExamples: [...new Set(rejected)].slice(0, 3), + memory: measured.memory, + drainMs, + }); + } + } + return { backend, levels }; + } finally { + if (vm) await vm.dispose().catch(() => undefined); + await sidecar.dispose(); + } +} + +async function runControlPaths(backend: Backend, port: number) { + const allowedSidecar = createBenchSidecar(); + const deniedSidecar = createBenchSidecar(); + let allowed: BenchVm | undefined; + let denied: BenchVm | undefined; + try { + allowed = await createBenchVm({ + sidecar: allowedSidecar, + loopbackExemptPorts: [port], + }); + await prepareFixtures(allowed); + denied = await createBenchVm({ + sidecar: deniedSidecar, + loopbackExemptPorts: [port], + permissions: { network: "deny" }, + }); + const denial = await denied.execArgv( + "curl", + ["-fsS", `http://127.0.0.1:${port}/payload`], + { + wasmBackend: backend, + timeout: 5_000, + }, + ); + + const cancellationController = new AbortController(); + const cancellationStarted = performance.now(); + const cancellationPromise = allowed.execArgv( + "sh", + ["-c", "while :; do :; done"], + { + wasmBackend: backend, + signal: cancellationController.signal, + }, + ); + setTimeout(() => cancellationController.abort(), 25); + let cancellation: Record; + try { + const value = await cancellationPromise; + cancellation = { rejected: false, value }; + } catch (error) { + cancellation = { + rejected: true, + name: error instanceof Error ? error.name : "unknown", + message: String(error), + }; + } + cancellation.durationMs = performance.now() - cancellationStarted; + + const resourceStarted = performance.now(); + const resource = await allowed.execArgv( + "sh", + ["-c", "while :; do :; done"], + { + wasmBackend: backend, + cpuTimeLimitMs: 25, + timeout: 5_000, + }, + ); + return { + backend, + denial: { + exitCode: denial.exitCode, + stderr: stripDiagnostics(denial.stderr).slice(0, 1_000), + passed: denial.exitCode !== 0, + }, + cancellation: { + ...cancellation, + passed: cancellation.rejected === true, + }, + resourceLimit: { + exitCode: resource.exitCode, + durationMs: performance.now() - resourceStarted, + stderr: stripDiagnostics(resource.stderr).slice(0, 1_000), + passed: resource.exitCode !== 0, + }, + }; + } finally { + if (denied) await denied.dispose().catch(() => undefined); + if (allowed) await allowed.dispose().catch(() => undefined); + await deniedSidecar.dispose(); + await allowedSidecar.dispose(); + } +} + +async function prepareFixtures(vm: BenchVm): Promise { + await vm.mkdir("/tmp/wasmtime-bench-tree", { recursive: true }); + await Promise.all( + Array.from({ length: 64 }, (_, index) => + vm.writeFile( + `/tmp/wasmtime-bench-tree/file-${index.toString().padStart(3, "0")}`, + `fixture-${index}\n`, + ), + ), + ); + const compute = new Uint8Array(4 * 1024 * 1024); + for (let index = 0; index < compute.length; index++) + compute[index] = index & 0xff; + await vm.writeFile("/tmp/wasmtime-bench-compute.bin", compute); +} + +async function measureCommandMemory(pid: number, run: () => Promise) { + const start = readProcessMemorySnapshot(pid); + let peak = start; + const sample = () => { + try { + peak = maxMemory(peak, readProcessMemorySnapshot(pid)); + } catch { + // The sidecar exiting is reported by the command itself. + } + }; + const poll = setInterval(sample, 5); + const started = performance.now(); + try { + const value = await run(); + sample(); + const end = readProcessMemorySnapshot(pid); + return { + value, + durationMs: performance.now() - started, + memory: { start, peak: maxMemory(peak, end), end } satisfies TimedMemory, + }; + } finally { + clearInterval(poll); + } +} + +async function waitForRuntimeDrain(vm: BenchVm): Promise { + const started = performance.now(); + const timeoutMs = 5_000; + for (;;) { + const snapshot = await vm.getResourceSnapshot(); + if ( + snapshot.runningProcesses === 0 && + snapshot.wasmReservedMemoryBytes === 0 + ) { + // Exit delivery precedes the executor worker's final permit drop by a + // very small interval. Require one quiet scheduler turn so the next + // level measures its own admission capacity rather than prior teardown. + await delay(25); + return performance.now() - started; + } + if (performance.now() - started >= timeoutMs) { + throw new Error( + `runtime did not drain within ${timeoutMs} ms (runningProcesses=${snapshot.runningProcesses}, wasmReservedMemoryBytes=${snapshot.wasmReservedMemoryBytes})`, + ); + } + await delay(10); + } +} + +function summarize(result: Record) { + const fresh = result.fresh as Array<{ + backend: Backend; + retainedDelta: ProcessMemorySnapshot; + workloads: Array<{ + name: string; + samples: Array<{ + durationMs: number; + cacheState: "fresh" | "warm"; + passed: boolean; + }>; + }>; + }>; + const workloadRows = workloads.map((workload) => { + const samples = (backend: Backend, cacheState?: "fresh" | "warm") => + fresh + .filter((entry) => entry.backend === backend) + .flatMap( + (entry) => + entry.workloads.find( + (candidate) => candidate.name === workload.name, + )?.samples ?? [], + ) + .filter( + (sample) => + cacheState === undefined || sample.cacheState === cacheState, + ) + .map((sample) => sample.durationMs); + const v8 = samples("v8"); + const wasmtime = samples("wasmtime"); + const v8Cold = samples("v8", "fresh"); + const wasmtimeCold = samples("wasmtime", "fresh"); + const v8Warm = samples("v8", "warm"); + const wasmtimeWarm = samples("wasmtime", "warm"); + return { + name: workload.name, + correctness: { + v8Failures: fresh + .filter((entry) => entry.backend === "v8") + .flatMap( + (entry) => + entry.workloads.find( + (candidate) => candidate.name === workload.name, + )?.samples ?? [], + ) + .filter((sample) => !sample.passed).length, + wasmtimeFailures: fresh + .filter((entry) => entry.backend === "wasmtime") + .flatMap( + (entry) => + entry.workloads.find( + (candidate) => candidate.name === workload.name, + )?.samples ?? [], + ) + .filter((sample) => !sample.passed).length, + }, + v8: stats(v8), + wasmtime: stats(wasmtime), + cold: { + v8: stats(v8Cold), + wasmtime: stats(wasmtimeCold), + p50Ratio: ratio(quantile(wasmtimeCold, 0.5), quantile(v8Cold, 0.5)), + }, + warm: { + v8: stats(v8Warm), + wasmtime: stats(wasmtimeWarm), + p50Ratio: ratio(quantile(wasmtimeWarm, 0.5), quantile(v8Warm, 0.5)), + }, + p50Ratio: quantile(wasmtime, 0.5) / quantile(v8, 0.5), + p95Ratio: quantile(wasmtime, 0.95) / quantile(v8, 0.95), + }; + }); + const geometricMeanP50Ratio = Math.exp( + workloadRows.reduce((sum, row) => sum + Math.log(row.p50Ratio), 0) / + workloadRows.length, + ); + const concurrency = result.concurrency as Array<{ + backend: Backend; + levels: Array<{ + level: number; + mode: string; + throughputPerSecond: number; + failedExitCodes: number; + rejectedCount: number; + }>; + }>; + const throughputRows = + concurrency + .find((entry) => entry.backend === "v8") + ?.levels.map((v8) => { + const wasmtime = concurrency + .find((entry) => entry.backend === "wasmtime") + ?.levels.find( + (candidate) => + candidate.level === v8.level && candidate.mode === v8.mode, + ); + return { + level: v8.level, + mode: v8.mode, + v8: v8.throughputPerSecond, + wasmtime: wasmtime?.throughputPerSecond ?? 0, + ratio: (wasmtime?.throughputPerSecond ?? 0) / v8.throughputPerSecond, + }; + }) ?? []; + const retainedMedian = (backend: Backend, key: "rssBytes" | "pssBytes") => + quantile( + fresh + .filter((entry) => entry.backend === backend) + .map((entry) => entry.retainedDelta[key]), + 0.5, + ); + const retained = { + v8RssBytes: retainedMedian("v8", "rssBytes"), + wasmtimeRssBytes: retainedMedian("wasmtime", "rssBytes"), + v8PssBytes: retainedMedian("v8", "pssBytes"), + wasmtimePssBytes: retainedMedian("wasmtime", "pssBytes"), + }; + const retainedAllowance = (baseline: number) => + Math.max(baseline * 0.1, 4 * 1024 * 1024); + const paths = result.paths as Array<{ + denial: { passed: boolean }; + cancellation: { passed: boolean }; + resourceLimit: { passed: boolean }; + }>; + const gates = { + correctness: + workloadRows.every( + (row) => + row.correctness.v8Failures === 0 && + row.correctness.wasmtimeFailures === 0, + ) && + paths.every( + (entry) => + entry.denial.passed && + entry.cancellation.passed && + entry.resourceLimit.passed, + ) && + concurrency.every((entry) => + entry.levels + .filter((level) => level.level <= 10) + .every( + (level) => level.failedExitCodes === 0 && level.rejectedCount === 0, + ), + ), + geometricMeanP50: geometricMeanP50Ratio <= 1.1, + individualP95: workloadRows.every((row) => row.p95Ratio <= 1.2), + throughput: throughputRows.every((row) => + row.v8 === 0 ? row.wasmtime >= row.v8 : row.ratio >= 0.9, + ), + retainedRss: + retained.wasmtimeRssBytes <= + retained.v8RssBytes + retainedAllowance(retained.v8RssBytes), + retainedPss: + retained.wasmtimePssBytes <= + retained.v8PssBytes + retainedAllowance(retained.v8PssBytes), + }; + const preferredBackend = Object.values(gates).every(Boolean) + ? "wasmtime" + : "v8"; + return { + workloads: workloadRows, + geometricMeanP50Ratio, + throughput: throughputRows, + retained, + gates, + preferredBackend, + omissionBehavior: preferredBackend, + rollbackBackend: "v8", + }; +} + +function moduleInventory() { + return [ + ...new Set([ + ...workloads.map((workload) => workload.command), + ...diverseConcurrency.map(([c]) => c), + ]), + ] + .sort() + .map((command) => { + const path = join(commandsDir, command); + const bytes = readFileSync(path); + return { + command, + path, + bytes: statSync(path).size, + sha256: createHash("sha256").update(bytes).digest("hex"), + }; + }); +} + +function projectResources( + resource: Awaited>, +) { + return { + runningProcesses: resource.runningProcesses, + openFds: resource.openFds, + pipes: resource.pipes, + pipeBufferedBytes: resource.pipeBufferedBytes, + ptys: resource.ptys, + ptyBufferedInputBytes: resource.ptyBufferedInputBytes, + ptyBufferedOutputBytes: resource.ptyBufferedOutputBytes, + sockets: resource.sockets, + socketBufferedBytes: resource.socketBufferedBytes, + socketDatagramQueueLen: resource.socketDatagramQueueLen, + wasmReservedMemoryBytes: resource.wasmReservedMemoryBytes, + wasmtimeEngineProfiles: resource.wasmtimeEngineProfiles, + wasmtimeModuleEntries: resource.wasmtimeModuleEntries, + wasmtimeModuleCacheHits: resource.wasmtimeModuleCacheHits, + wasmtimeModuleCacheMisses: resource.wasmtimeModuleCacheMisses, + wasmtimeModuleCacheEvictions: resource.wasmtimeModuleCacheEvictions, + wasmtimeCompiledSourceBytes: resource.wasmtimeCompiledSourceBytes, + wasmtimeChargedModuleBytes: resource.wasmtimeChargedModuleBytes, + wasmtimeCompileTimeMicros: resource.wasmtimeCompileTimeMicros, + wasmtimeProcessRetainedRssBytes: resource.wasmtimeProcessRetainedRssBytes, + kernelBufferedBytes: + resource.pipeBufferedBytes + + resource.ptyBufferedInputBytes + + resource.ptyBufferedOutputBytes + + resource.socketBufferedBytes, + }; +} + +function parsePhaseDiagnostic(stderr: string): PhaseDiagnostic | null { + for (const line of stderr.split(/\r?\n/u).reverse()) { + if (!line.startsWith(PHASE_PREFIX)) continue; + try { + return JSON.parse(line.slice(PHASE_PREFIX.length)) as PhaseDiagnostic; + } catch { + return null; + } + } + return null; +} + +function stripDiagnostics(stderr: string): string { + return stderr + .split(/\r?\n/u) + .filter((line) => !line.startsWith("__AGENTOS_WASM_")) + .join("\n") + .trim(); +} + +function expectExitZero(result: CommandResult): void { + if (result.exitCode !== 0) { + throw new Error( + `command exited ${result.exitCode}: ${stripDiagnostics(result.stderr)}`, + ); + } +} + +function maxMemory( + left: ProcessMemorySnapshot, + right: ProcessMemorySnapshot, +): ProcessMemorySnapshot { + return { + rssBytes: Math.max(left.rssBytes, right.rssBytes), + peakRssBytes: Math.max(left.peakRssBytes, right.peakRssBytes), + pssBytes: Math.max(left.pssBytes, right.pssBytes), + virtualBytes: Math.max(left.virtualBytes, right.virtualBytes), + minorFaults: Math.max(left.minorFaults, right.minorFaults), + majorFaults: Math.max(left.majorFaults, right.majorFaults), + }; +} + +function memoryDelta( + after: ProcessMemorySnapshot, + before: ProcessMemorySnapshot, +) { + return { + rssBytes: after.rssBytes - before.rssBytes, + peakRssBytes: after.peakRssBytes - before.peakRssBytes, + pssBytes: after.pssBytes - before.pssBytes, + virtualBytes: after.virtualBytes - before.virtualBytes, + minorFaults: after.minorFaults - before.minorFaults, + majorFaults: after.majorFaults - before.majorFaults, + }; +} + +function stats(values: number[]) { + if (values.length === 0) { + return { count: 0, min: null, p50: null, p95: null, max: null }; + } + return { + count: values.length, + min: Math.min(...values), + p50: quantile(values, 0.5), + p95: quantile(values, 0.95), + max: Math.max(...values), + }; +} + +function quantile(values: number[], q: number): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const index = (sorted.length - 1) * q; + const lower = Math.floor(index); + const fraction = index - lower; + return sorted[lower] + (sorted[lower + 1] - sorted[lower] || 0) * fraction; +} + +function ratio(numerator: number, denominator: number): number | null { + return denominator === 0 ? null : numerator / denominator; +} + +function requiredSidecarPid(vm: BenchVm): number { + const pid = vm.sidecarPid(); + if (pid === null) + throw new Error("benchmark could not resolve the sidecar pid"); + return pid; +} + +function integerEnv(name: string, fallback: number): number { + const value = process.env[name]; + if (!value) return fallback; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) + throw new Error(`${name} must be positive`); + return parsed; +} + +function listEnv(name: string, fallback: number[]): number[] { + const value = process.env[name]; + return value + ? value.split(",").map((entry) => Number(entry.trim())) + : fallback; +} + +function delay(ms: number): Promise { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +async function listenLoopback(): Promise<{ + port: number; + server: http.Server; +}> { + const server = http.createServer((_request, response) => { + response.setHeader("connection", "close"); + response.end("wasmtime-benchmark-loopback"); + }); + await new Promise((resolveListen, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolveListen); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("loopback listener has no port"); + return { port: address.port, server }; +} + +async function closeServer(server: http.Server): Promise { + await new Promise((resolveClose, reject) => { + server.close((error) => (error ? reject(error) : resolveClose())); + }); +} + +void main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/runtime-benchmarks/src/lib/memory.ts b/packages/runtime-benchmarks/src/lib/memory.ts index baeb7b6aa2..74a31fabc8 100644 --- a/packages/runtime-benchmarks/src/lib/memory.ts +++ b/packages/runtime-benchmarks/src/lib/memory.ts @@ -46,6 +46,50 @@ export function readRssBytes(pid: number | null): number { } } +export interface ProcessMemorySnapshot { + rssBytes: number; + peakRssBytes: number; + pssBytes: number; + virtualBytes: number; + minorFaults: number; + majorFaults: number; +} + +/** Read orthogonal Linux process-memory counters without conflating VIRT/RSS/PSS. */ +export function readProcessMemorySnapshot(pid: number): ProcessMemorySnapshot { + const status = readFileSync(`/proc/${pid}/status`, "utf8"); + const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); + let pssBytes = 0; + try { + const rollup = readFileSync(`/proc/${pid}/smaps_rollup`, "utf8"); + pssBytes = readKibibytes(rollup, "Pss"); + } catch { + // Some hardened Linux hosts deny smaps_rollup. Preserve an explicit zero. + } + const closingParen = stat.lastIndexOf(") "); + if (closingParen < 0) { + throw new Error(`could not parse /proc/${pid}/stat`); + } + const fields = stat + .slice(closingParen + 2) + .trim() + .split(/\s+/); + return { + rssBytes: readKibibytes(status, "VmRSS"), + peakRssBytes: readKibibytes(status, "VmHWM"), + pssBytes, + virtualBytes: readKibibytes(status, "VmSize"), + // `fields[0]` is field 3 (`state`); minflt/majflt are fields 10/12. + minorFaults: Number(fields[7] ?? 0), + majorFaults: Number(fields[9] ?? 0), + }; +} + +function readKibibytes(contents: string, field: string): number { + const match = contents.match(new RegExp(`^${field}:\\s+(\\d+)\\s+kB`, "m")); + return match ? Number(match[1]) * 1024 : 0; +} + export interface LaneMemory { memBytes: number; memProvenance: string; @@ -69,7 +113,10 @@ export function procPeakMemorySupportReason(): string | undefined { if (process.platform !== "linux") { return "Linux /proc clear_refs/VmHWM memory measurement is unavailable on this platform"; } - if (!existsSync("/proc/self/status") || !existsSync("/proc/self/clear_refs")) { + if ( + !existsSync("/proc/self/status") || + !existsSync("/proc/self/clear_refs") + ) { return "Linux /proc status/clear_refs memory measurement is unavailable"; } return undefined; @@ -146,25 +193,34 @@ export function runCommandWithMaxRss( const sample = () => { if (child.pid === undefined) return; try { - maxRssBytes = Math.max(maxRssBytes, readStatusBytes(child.pid, "VmHWM")); + maxRssBytes = Math.max( + maxRssBytes, + readStatusBytes(child.pid, "VmHWM"), + ); } catch { try { - maxRssBytes = Math.max(maxRssBytes, readStatusBytes(child.pid, "VmRSS")); + maxRssBytes = Math.max( + maxRssBytes, + readStatusBytes(child.pid, "VmRSS"), + ); } catch { // The child may have exited between polls. } } }; - const collect = (chunks: Buffer[], kind: "stdout" | "stderr") => (chunk: Buffer) => { - if (kind === "stdout") stdoutBytes += chunk.length; - else stderrBytes += chunk.length; - if (stdoutBytes + stderrBytes > maxBuffer) { - child.kill("SIGKILL"); - reject(new Error(`${command} output exceeded maxBuffer ${maxBuffer}`)); - return; - } - chunks.push(chunk); - }; + const collect = + (chunks: Buffer[], kind: "stdout" | "stderr") => (chunk: Buffer) => { + if (kind === "stdout") stdoutBytes += chunk.length; + else stderrBytes += chunk.length; + if (stdoutBytes + stderrBytes > maxBuffer) { + child.kill("SIGKILL"); + reject( + new Error(`${command} output exceeded maxBuffer ${maxBuffer}`), + ); + return; + } + chunks.push(chunk); + }; child.stdout.on("data", collect(stdout, "stdout")); child.stderr.on("data", collect(stderr, "stderr")); @@ -204,7 +260,9 @@ export class SidecarPeakMemorySampler { static forVm(vm: BenchVm): SidecarPeakMemorySampler | undefined { if (procPeakMemorySupportReason()) return undefined; const pid = vm.sidecarPid(); - return typeof pid === "number" ? new SidecarPeakMemorySampler(pid) : undefined; + return typeof pid === "number" + ? new SidecarPeakMemorySampler(pid) + : undefined; } async measure(fn: () => Promise | T): Promise> { @@ -248,7 +306,10 @@ function readStatusBytes(pid: number, field: "VmRSS" | "VmHWM"): number { return Number(match[1]) * 1024; } -export async function sampleMemory(vm: BenchVm, cycle: number): Promise { +export async function sampleMemory( + vm: BenchVm, + cycle: number, +): Promise { forceGC(); const resource = await vm.getResourceSnapshot(); const guestHeapRss = await sampleGuestHeap(vm); @@ -268,7 +329,10 @@ export async function sampleMemory(vm: BenchVm, cycle: number): Promise, key: string): number { const n = samples.length; const sx = samples.reduce((sum, sample) => sum + sample.cycle, 0); - const sy = samples.reduce((sum, sample) => sum + Number((sample as any)[key]), 0); + const sy = samples.reduce( + (sum, sample) => sum + Number((sample as any)[key]), + 0, + ); const sxy = samples.reduce( (sum, sample) => sum + sample.cycle * Number((sample as any)[key]), 0, diff --git a/packages/runtime-benchmarks/src/lib/vm.ts b/packages/runtime-benchmarks/src/lib/vm.ts index 629f7863cf..ca302e6ab7 100644 --- a/packages/runtime-benchmarks/src/lib/vm.ts +++ b/packages/runtime-benchmarks/src/lib/vm.ts @@ -1,19 +1,19 @@ import { statSync } from "node:fs"; import { - NodeRuntime, - resolveNodeRuntimeSidecarBinary, - resolveNodeRuntimeCommandsDir, - SidecarProcess, type HostDirectoryMount, + NodeRuntime, type NodeRuntimeCreateOptions, type NodeRuntimeProcess, type NodeRuntimeResourceSnapshot, + resolveNodeRuntimeCommandsDir, + resolveNodeRuntimeSidecarBinary, + SidecarProcess, type SidecarSpawnOptions, type VirtualDirEntry, } from "@rivet-dev/agentos-runtime-core"; import { createInMemoryFileSystem } from "@rivet-dev/agentos-runtime-core/test-runtime"; -import { hasNativeBaselineWasm, supportsWasmLayer } from "./layers.js"; import type { BenchmarkOp, CommandBenchmarkOp } from "./layers.js"; +import { hasNativeBaselineWasm, supportsWasmLayer } from "./layers.js"; const NATIVE_BASELINE_WASM_COMMAND = "native-baseline"; const NATIVE_BASELINE_WASM_PREWARM_DIR = "/tmp/native-baseline-wasm"; @@ -29,9 +29,22 @@ export interface BenchVmOptions { export interface BenchVmProcess { pid: number; + kill(signal?: NodeJS.Signals | number): void; wait(): Promise; } +export interface BenchVmExecOptions { + wasmBackend?: "v8" | "wasmtime"; + env?: Record; + cwd?: string; + stdin?: string | Uint8Array; + timeout?: number; + cpuTimeLimitMs?: number; + signal?: AbortSignal; + onStdout?: (data: Uint8Array) => void; + onStderr?: (data: Uint8Array) => void; +} + export interface BenchVm { writeFile(path: string, content: string | Uint8Array): Promise; mkdir(path: string, options?: { recursive?: boolean }): Promise; @@ -42,24 +55,12 @@ export interface BenchVm { readDirWithTypes(path: string): Promise; exec( commandLine: string, - options?: { - env?: Record; - cwd?: string; - stdin?: string | Uint8Array; - onStdout?: (data: Uint8Array) => void; - onStderr?: (data: Uint8Array) => void; - }, + options?: BenchVmExecOptions, ): Promise<{ stdout: string; stderr: string; exitCode: number }>; execArgv( command: string, args: string[], - options?: { - env?: Record; - cwd?: string; - stdin?: string | Uint8Array; - onStdout?: (data: Uint8Array) => void; - onStderr?: (data: Uint8Array) => void; - }, + options?: BenchVmExecOptions, ): Promise<{ stdout: string; stderr: string; exitCode: number }>; spawnNodeCapture( argsOrProgramPath: string[] | string, @@ -72,24 +73,13 @@ export interface BenchVm { spawn( command: string, args: string[], - options?: { - env?: Record; - cwd?: string; - onStdout?: (data: Uint8Array) => void; - onStderr?: (data: Uint8Array) => void; - }, + options?: BenchVmExecOptions, ): BenchVmProcess; waitProcess(pid: number): Promise; execWasmCommand( cmd: string, args: string[], - options?: { - env?: Record; - cwd?: string; - stdin?: string | Uint8Array; - onStdout?: (data: Uint8Array) => void; - onStderr?: (data: Uint8Array) => void; - }, + options?: BenchVmExecOptions, ): Promise<{ stdout: string; stderr: string; exitCode: number }>; getResourceSnapshot(): Promise; dispose(): Promise; @@ -104,7 +94,9 @@ export interface SidecarBinaryProvenance { sizeBytes: number; } -export async function createBenchVm(options: BenchVmOptions = {}): Promise { +export async function createBenchVm( + options: BenchVmOptions = {}, +): Promise { const runtime = await NodeRuntime.create({ filesystem: createInMemoryFileSystem(), permissions: { @@ -135,14 +127,18 @@ export async function createBenchVm(options: BenchVmOptions = {}): Promise proc.kill(signal), wait: async () => { try { return await proc.wait(); @@ -232,7 +230,9 @@ export async function prewarmBenchVm( ): Promise { const nodeResult = await vm.spawnNodeCapture(["-e", ""]); if (nodeResult.exitCode !== 0) { - throw new Error(`guest node prewarm exited ${nodeResult.exitCode}\n${nodeResult.stderr}`); + throw new Error( + `guest node prewarm exited ${nodeResult.exitCode}\n${nodeResult.stderr}`, + ); } if ( @@ -265,7 +265,9 @@ export async function prewarmBenchVm( } } -export function createBenchSidecar(options: SidecarSpawnOptions = {}): SidecarProcess { +export function createBenchSidecar( + options: SidecarSpawnOptions = {}, +): SidecarProcess { return SidecarProcess.spawn({ ...options, command: options.command ?? resolveNodeRuntimeSidecarBinary(), @@ -295,17 +297,19 @@ export function formatSidecarProvenance( } function sidecarPidFromRuntime(runtime: NodeRuntime): number | null { - const kernel = (runtime as unknown as { - kernel?: { - client?: { - child?: { pid?: number }; - protocolClient?: { + const kernel = ( + runtime as unknown as { + kernel?: { + client?: { child?: { pid?: number }; - sidecarProcess?: { child?: { pid?: number } }; + protocolClient?: { + child?: { pid?: number }; + sidecarProcess?: { child?: { pid?: number } }; + }; }; }; - }; - }).kernel; + } + ).kernel; const pid = kernel?.client?.child?.pid ?? kernel?.client?.protocolClient?.child?.pid ?? diff --git a/packages/runtime-core/src/generated-protocol.ts b/packages/runtime-core/src/generated-protocol.ts index 95965393ec..abeae46ce5 100644 --- a/packages/runtime-core/src/generated-protocol.ts +++ b/packages/runtime-core/src/generated-protocol.ts @@ -3366,6 +3366,16 @@ export type ResourceSnapshotResponse = { readonly socketConnections: u64 readonly socketBufferedBytes: u64 readonly socketDatagramQueueLen: u64 + readonly wasmReservedMemoryBytes: u64 + readonly wasmtimeEngineProfiles: u64 + readonly wasmtimeModuleEntries: u64 + readonly wasmtimeModuleCacheHits: u64 + readonly wasmtimeModuleCacheMisses: u64 + readonly wasmtimeModuleCacheEvictions: u64 + readonly wasmtimeCompiledSourceBytes: u64 + readonly wasmtimeChargedModuleBytes: u64 + readonly wasmtimeCompileTimeMicros: u64 + readonly wasmtimeProcessRetainedRssBytes: u64 | null readonly queueSnapshots: readonly QueueSnapshotEntry[] } @@ -3386,6 +3396,16 @@ export function readResourceSnapshotResponse(bc: bare.ByteCursor): ResourceSnaps socketConnections: bare.readU64(bc), socketBufferedBytes: bare.readU64(bc), socketDatagramQueueLen: bare.readU64(bc), + wasmReservedMemoryBytes: bare.readU64(bc), + wasmtimeEngineProfiles: bare.readU64(bc), + wasmtimeModuleEntries: bare.readU64(bc), + wasmtimeModuleCacheHits: bare.readU64(bc), + wasmtimeModuleCacheMisses: bare.readU64(bc), + wasmtimeModuleCacheEvictions: bare.readU64(bc), + wasmtimeCompiledSourceBytes: bare.readU64(bc), + wasmtimeChargedModuleBytes: bare.readU64(bc), + wasmtimeCompileTimeMicros: bare.readU64(bc), + wasmtimeProcessRetainedRssBytes: read21(bc), queueSnapshots: read35(bc), } } @@ -3406,6 +3426,16 @@ export function writeResourceSnapshotResponse(bc: bare.ByteCursor, x: ResourceSn bare.writeU64(bc, x.socketConnections) bare.writeU64(bc, x.socketBufferedBytes) bare.writeU64(bc, x.socketDatagramQueueLen) + bare.writeU64(bc, x.wasmReservedMemoryBytes) + bare.writeU64(bc, x.wasmtimeEngineProfiles) + bare.writeU64(bc, x.wasmtimeModuleEntries) + bare.writeU64(bc, x.wasmtimeModuleCacheHits) + bare.writeU64(bc, x.wasmtimeModuleCacheMisses) + bare.writeU64(bc, x.wasmtimeModuleCacheEvictions) + bare.writeU64(bc, x.wasmtimeCompiledSourceBytes) + bare.writeU64(bc, x.wasmtimeChargedModuleBytes) + bare.writeU64(bc, x.wasmtimeCompileTimeMicros) + write21(bc, x.wasmtimeProcessRetainedRssBytes) write35(bc, x.queueSnapshots) } diff --git a/packages/runtime-core/src/node-runtime.ts b/packages/runtime-core/src/node-runtime.ts index 0bdf252eea..a7144e7a3f 100644 --- a/packages/runtime-core/src/node-runtime.ts +++ b/packages/runtime-core/src/node-runtime.ts @@ -21,25 +21,25 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; +import type { VmUserConfig } from "./generated/VmUserConfig.js"; +import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js"; +import type { SidecarProcess } from "./sidecar-process.js"; import type { - ExecResult, BindingDefinition, + ExecResult, Kernel, KernelBootTiming, Permissions, VirtualDirEntry, VirtualFileSystem, } from "./test-runtime.js"; -import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; -import type { VmUserConfig } from "./generated/VmUserConfig.js"; -import type { SidecarProcess } from "./sidecar-process.js"; import { createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, } from "./test-runtime.js"; -import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js"; export type { BindingDefinition, @@ -348,6 +348,11 @@ export interface NodeRuntimeExecResult { /** Options for a single {@link NodeRuntime.exec} call. */ export interface NodeRuntimeExecOptions { + /** + * Select the engine for a standalone WebAssembly command. JavaScript and + * Python commands ignore this option. Omission preserves the runtime default. + */ + wasmBackend?: "v8" | "wasmtime"; /** Extra environment variables for this run, merged over the VM env. */ env?: Record; /** Working directory for this run. */ @@ -356,6 +361,8 @@ export interface NodeRuntimeExecOptions { stdin?: string | Uint8Array; /** Abort the run after this many milliseconds. */ timeout?: number; + /** Bound active guest CPU time independently of elapsed wall time. */ + cpuTimeLimitMs?: number; /** * Cancel the run when this signal aborts. On abort the guest process is * killed inside the VM (the kernel delivers `SIGTERM`) and the call rejects @@ -509,6 +516,16 @@ export interface NodeRuntimeResourceSnapshot { socketConnections: number; socketBufferedBytes: number; socketDatagramQueueLen: number; + wasmReservedMemoryBytes: number; + wasmtimeEngineProfiles: number; + wasmtimeModuleEntries: number; + wasmtimeModuleCacheHits: number; + wasmtimeModuleCacheMisses: number; + wasmtimeModuleCacheEvictions: number; + wasmtimeCompiledSourceBytes: number; + wasmtimeChargedModuleBytes: number; + wasmtimeCompileTimeMicros: number; + wasmtimeProcessRetainedRssBytes?: number; queueSnapshots: Array<{ name: string; category: string; @@ -588,9 +605,7 @@ export class NodeRuntime { * session, creates the VM with a bootstrapped root filesystem, mounts the * shell and Node runtimes, and waits for the VM to report ready. */ - static async create( - options: NodeRuntimeCreateOptions, - ): Promise { + static async create(options: NodeRuntimeCreateOptions): Promise { options = parseNodeRuntimeCreateOptions(options); const commandsDir = resolveNodeRuntimeCommandsDir(options.commandsDir); @@ -871,8 +886,10 @@ export class NodeRuntime { options: NodeRuntimeSpawnOptions = {}, ): NodeRuntimeProcess { const proc = this.kernel.spawn(command, args, { + wasmBackend: options.wasmBackend, env: options.env, cwd: options.cwd, + cpuTimeLimitMs: options.cpuTimeLimitMs, onStdout: options.onStdout, onStderr: options.onStderr, streamStdin: true, @@ -909,8 +926,10 @@ export class NodeRuntime { const stdoutChunks: Uint8Array[] = []; const stderrChunks: Uint8Array[] = []; const proc = this.spawnCommand(command, args, { + wasmBackend: options.wasmBackend, env: options.env, cwd: options.cwd, + cpuTimeLimitMs: options.cpuTimeLimitMs, onStdout: (chunk) => { stdoutChunks.push(chunk); options.onStdout?.(chunk); @@ -926,11 +945,25 @@ export class NodeRuntime { proc.closeStdin(); let timer: ReturnType | undefined; + let aborted = options.signal?.aborted ?? false; + const onAbort = () => { + aborted = true; + proc.kill("SIGTERM"); + }; if (options.timeout !== undefined) { timer = setTimeout(() => proc.kill("SIGKILL"), options.timeout); } + if (options.signal) { + options.signal.addEventListener("abort", onAbort, { once: true }); + if (aborted) { + onAbort(); + } + } try { const exitCode = await proc.wait(); + if (aborted && options.signal) { + throw toAbortError(options.signal); + } return { stdout: decodeChunks(stdoutChunks), stderr: decodeChunks(stderrChunks), @@ -940,6 +973,7 @@ export class NodeRuntime { if (timer !== undefined) { clearTimeout(timer); } + options.signal?.removeEventListener("abort", onAbort); } } diff --git a/packages/runtime-core/src/response-payloads.ts b/packages/runtime-core/src/response-payloads.ts index 216ced2222..01571d93c1 100644 --- a/packages/runtime-core/src/response-payloads.ts +++ b/packages/runtime-core/src/response-payloads.ts @@ -64,6 +64,16 @@ export interface LiveResourceSnapshot { socket_connections: number; socket_buffered_bytes: number; socket_datagram_queue_len: number; + wasm_reserved_memory_bytes: number; + wasmtime_engine_profiles: number; + wasmtime_module_entries: number; + wasmtime_module_cache_hits: number; + wasmtime_module_cache_misses: number; + wasmtime_module_cache_evictions: number; + wasmtime_compiled_source_bytes: number; + wasmtime_charged_module_bytes: number; + wasmtime_compile_time_micros: number; + wasmtime_process_retained_rss_bytes?: number; queue_snapshots: LiveQueueSnapshotEntry[]; } @@ -483,6 +493,50 @@ export function fromGeneratedResponsePayload( payload.val.socketDatagramQueueLen, "resource_snapshot.socket_datagram_queue_len", ), + wasm_reserved_memory_bytes: bigIntToSafeNumber( + payload.val.wasmReservedMemoryBytes, + "resource_snapshot.wasm_reserved_memory_bytes", + ), + wasmtime_engine_profiles: bigIntToSafeNumber( + payload.val.wasmtimeEngineProfiles, + "resource_snapshot.wasmtime_engine_profiles", + ), + wasmtime_module_entries: bigIntToSafeNumber( + payload.val.wasmtimeModuleEntries, + "resource_snapshot.wasmtime_module_entries", + ), + wasmtime_module_cache_hits: bigIntToSafeNumber( + payload.val.wasmtimeModuleCacheHits, + "resource_snapshot.wasmtime_module_cache_hits", + ), + wasmtime_module_cache_misses: bigIntToSafeNumber( + payload.val.wasmtimeModuleCacheMisses, + "resource_snapshot.wasmtime_module_cache_misses", + ), + wasmtime_module_cache_evictions: bigIntToSafeNumber( + payload.val.wasmtimeModuleCacheEvictions, + "resource_snapshot.wasmtime_module_cache_evictions", + ), + wasmtime_compiled_source_bytes: bigIntToSafeNumber( + payload.val.wasmtimeCompiledSourceBytes, + "resource_snapshot.wasmtime_compiled_source_bytes", + ), + wasmtime_charged_module_bytes: bigIntToSafeNumber( + payload.val.wasmtimeChargedModuleBytes, + "resource_snapshot.wasmtime_charged_module_bytes", + ), + wasmtime_compile_time_micros: bigIntToSafeNumber( + payload.val.wasmtimeCompileTimeMicros, + "resource_snapshot.wasmtime_compile_time_micros", + ), + ...(payload.val.wasmtimeProcessRetainedRssBytes !== null + ? { + wasmtime_process_retained_rss_bytes: bigIntToSafeNumber( + payload.val.wasmtimeProcessRetainedRssBytes, + "resource_snapshot.wasmtime_process_retained_rss_bytes", + ), + } + : {}), queue_snapshots: payload.val.queueSnapshots.map((queue) => ({ name: queue.name, category: queue.category, diff --git a/packages/runtime-core/src/sidecar-process.ts b/packages/runtime-core/src/sidecar-process.ts index 6e0ced16f4..52074b2bb8 100644 --- a/packages/runtime-core/src/sidecar-process.ts +++ b/packages/runtime-core/src/sidecar-process.ts @@ -1,9 +1,9 @@ -import { - type LiveSidecarRequestPayload, - type LiveSidecarResponsePayload, +import type { + LiveSidecarRequestPayload, + LiveSidecarResponsePayload, } from "./callbacks.js"; import type { MountConfigJsonObject } from "./descriptors.js"; -import { type LiveSidecarEventSelector } from "./event-buffer.js"; +import type { LiveSidecarEventSelector } from "./event-buffer.js"; import { decodeGuestFilesystemContent, encodeGuestFilesystemContent, @@ -12,44 +12,45 @@ import { type LiveRootFilesystemLowerDescriptor, } from "./filesystem.js"; import type { CreateVmConfig } from "./generated/CreateVmConfig.js"; -import type { SidecarProcessTransport } from "./sidecar-client.js"; -import { type LiveOwnershipScope } from "./ownership.js"; -import { - type LiveFsPermissionRule, - type LivePatternPermissionRule, - type LivePermissionMode, - type LivePermissionScope, - type LivePermissionsPolicy, - type LiveRulePermissions, +import type { LiveOwnershipScope } from "./ownership.js"; +import type { + LiveFsPermissionRule, + LivePatternPermissionRule, + LivePermissionMode, + LivePermissionScope, + LivePermissionsPolicy, + LiveRulePermissions, } from "./permissions.js"; -import { SIDECAR_PROTOCOL_SCHEMA } from "./protocol-schema.js"; +import type { + LiveEventFrame, + LiveRequestFrame, + LiveResponseFrame, + LiveSidecarRequestFrame, + LiveSidecarRequestHandler, + LiveSidecarResponseFrame, + ProtocolFramePayloadCodec, +} from "./protocol-frames.js"; import type { LiveFilesystemOperation, LiveGuestRuntimeKind, LiveWasmPermissionTier, } from "./protocol-maps.js"; -import { - type LiveEventFrame, - type LiveSidecarRequestHandler, - type LiveRequestFrame, - type LiveResponseFrame, - type LiveSidecarRequestFrame, - type LiveSidecarResponseFrame, - type ProtocolFramePayloadCodec, -} from "./protocol-frames.js"; -import { type LiveRequestPayload } from "./request-payloads.js"; +import { SIDECAR_PROTOCOL_SCHEMA } from "./protocol-schema.js"; +import type { LiveRequestPayload } from "./request-payloads.js"; import type { LiveGuestDirEntry } from "./response-payloads.js"; -import { - type LiveGuestFilesystemStat, - type LiveProcessSnapshotEntry, - type LiveSocketStateEntry, +import type { SidecarProcessTransport } from "./sidecar-client.js"; +import type { + LiveGuestFilesystemStat, + LiveProcessSnapshotEntry, + LiveSocketStateEntry, } from "./state.js"; + +export { SidecarEventBufferOverflow } from "./event-buffer.js"; export { SidecarProcessError, SidecarProcessExited, SidecarSilenceTimeout, } from "./sidecar-errors.js"; -export { SidecarEventBufferOverflow } from "./event-buffer.js"; // `Sidecar` is the public name for the native sidecar process client. The class // is `SidecarProcess` internally; consumers import it as `Sidecar` via the // `@rivet-dev/agentos-runtime-core/sidecar-client` subpath and the package root. @@ -147,6 +148,16 @@ export interface SidecarResourceSnapshot { socketConnections: number; socketBufferedBytes: number; socketDatagramQueueLen: number; + wasmReservedMemoryBytes: number; + wasmtimeEngineProfiles: number; + wasmtimeModuleEntries: number; + wasmtimeModuleCacheHits: number; + wasmtimeModuleCacheMisses: number; + wasmtimeModuleCacheEvictions: number; + wasmtimeCompiledSourceBytes: number; + wasmtimeChargedModuleBytes: number; + wasmtimeCompileTimeMicros: number; + wasmtimeProcessRetainedRssBytes?: number; queueSnapshots: SidecarQueueSnapshotEntry[]; } @@ -868,7 +879,9 @@ export class SidecarProcess { payload: { type: "list_mounts" }, }); if (response.payload.type !== "mounts_listed") { - throw new Error(`unexpected list_mounts response: ${response.payload.type}`); + throw new Error( + `unexpected list_mounts response: ${response.payload.type}`, + ); } return response.payload.mounts.map((mount) => ({ path: mount.path, @@ -1255,9 +1268,7 @@ export class SidecarProcess { ...(options.wasmPermissionTier ? { wasm_permission_tier: options.wasmPermissionTier } : {}), - ...(options.wasmBackend - ? { wasm_backend: options.wasmBackend } - : {}), + ...(options.wasmBackend ? { wasm_backend: options.wasmBackend } : {}), }, }); if (response.payload.type !== "process_started") { @@ -1461,6 +1472,24 @@ export class SidecarProcess { socketConnections: response.payload.socket_connections, socketBufferedBytes: response.payload.socket_buffered_bytes, socketDatagramQueueLen: response.payload.socket_datagram_queue_len, + wasmReservedMemoryBytes: response.payload.wasm_reserved_memory_bytes, + wasmtimeEngineProfiles: response.payload.wasmtime_engine_profiles, + wasmtimeModuleEntries: response.payload.wasmtime_module_entries, + wasmtimeModuleCacheHits: response.payload.wasmtime_module_cache_hits, + wasmtimeModuleCacheMisses: response.payload.wasmtime_module_cache_misses, + wasmtimeModuleCacheEvictions: + response.payload.wasmtime_module_cache_evictions, + wasmtimeCompiledSourceBytes: + response.payload.wasmtime_compiled_source_bytes, + wasmtimeChargedModuleBytes: + response.payload.wasmtime_charged_module_bytes, + wasmtimeCompileTimeMicros: response.payload.wasmtime_compile_time_micros, + ...(response.payload.wasmtime_process_retained_rss_bytes !== undefined + ? { + wasmtimeProcessRetainedRssBytes: + response.payload.wasmtime_process_retained_rss_bytes, + } + : {}), queueSnapshots: response.payload.queue_snapshots.map((queue) => ({ name: queue.name, category: queue.category, diff --git a/packages/runtime-core/src/test-runtime.ts b/packages/runtime-core/src/test-runtime.ts index 27ab90c229..a14762e88b 100644 --- a/packages/runtime-core/src/test-runtime.ts +++ b/packages/runtime-core/src/test-runtime.ts @@ -531,6 +531,16 @@ export interface Kernel extends KernelInterface { socketConnections: number; socketBufferedBytes: number; socketDatagramQueueLen: number; + wasmReservedMemoryBytes: number; + wasmtimeEngineProfiles: number; + wasmtimeModuleEntries: number; + wasmtimeModuleCacheHits: number; + wasmtimeModuleCacheMisses: number; + wasmtimeModuleCacheEvictions: number; + wasmtimeCompiledSourceBytes: number; + wasmtimeChargedModuleBytes: number; + wasmtimeCompileTimeMicros: number; + wasmtimeProcessRetainedRssBytes?: number; queueSnapshots: Array<{ name: string; category: string; From 57aaa37ac3a4a9858ffef37f01c731450e182196 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 21 Jul 2026 00:09:22 -0700 Subject: [PATCH 5/6] feat: add threaded Wasmtime execution --- .github/workflows/ci-nightly.yml | 56 + .github/workflows/ci.yml | 102 +- .github/workflows/publish.yaml | 74 +- .gitignore | 4 + Cargo.lock | 11 + Cargo.toml | 3 + crates/client/src/agent_os.rs | 35 + crates/client/src/config.rs | 19 + crates/client/src/process.rs | 6 +- crates/client/src/shell.rs | 8 +- crates/client/tests/common/mod.rs | 14 + crates/client/tests/wasm_command_mount_e2e.rs | 43 +- crates/execution/Cargo.toml | 5 +- .../execution/assets/runners/wasm-runner.mjs | 152 +- crates/execution/src/backend/lifecycle.rs | 14 + crates/execution/src/backend/reply.rs | 61 +- crates/execution/src/host/mod.rs | 2 +- crates/execution/src/host/network.rs | 4 + crates/execution/src/host/process.rs | 31 + crates/execution/src/host/signal.rs | 40 +- crates/execution/src/javascript.rs | 3 +- crates/execution/src/lib.rs | 7 +- crates/execution/src/python.rs | 1 + crates/execution/src/wasm.rs | 80 +- crates/execution/src/wasm/profile.rs | 50 +- crates/execution/src/wasm/wasmtime/engine.rs | 40 +- .../execution/src/wasm/wasmtime/lifecycle.rs | 535 +- crates/execution/src/wasm/wasmtime/limits.rs | 25 + .../execution/src/wasm/wasmtime/linker/mod.rs | 84 +- .../src/wasm/wasmtime/linker/network.rs | 424 +- .../src/wasm/wasmtime/linker/preview1.rs | 3 + .../src/wasm/wasmtime/linker/process.rs | 329 +- crates/execution/src/wasm/wasmtime/memory.rs | 67 +- crates/execution/src/wasm/wasmtime/mod.rs | 7 + crates/execution/src/wasm/wasmtime/module.rs | 11 +- crates/execution/src/wasm/wasmtime/store.rs | 163 +- crates/execution/src/wasm/wasmtime/threads.rs | 406 + crates/execution/src/wasm/wasmtime/worker.rs | 1027 + crates/execution/tests/wasm.rs | 3 +- .../tests/wasm_host_fs_errno_contract.rs | 19 + crates/kernel/src/fd_table.rs | 39 + crates/kernel/src/kernel.rs | 557 +- crates/kernel/src/process_table.rs | 515 +- crates/kernel/src/socket_table.rs | 138 +- crates/kernel/tests/dns_resolution.rs | 11 +- crates/kernel/tests/loopback_routing.rs | 55 +- crates/kernel/tests/resource_accounting.rs | 116 + crates/native-sidecar-core/src/limits.rs | 35 + .../src/execution/child_process.rs | 230 +- .../src/execution/coordinator.rs | 188 +- .../src/execution/host_dispatch/filesystem.rs | 194 +- .../src/execution/host_dispatch/mod.rs | 184 +- .../src/execution/host_dispatch/network.rs | 1 + .../execution/host_dispatch/network_compat.rs | 260 +- .../src/execution/host_dispatch/process.rs | 64 +- .../src/execution/host_dispatch/signal.rs | 119 +- .../src/execution/javascript/http.rs | 346 +- .../src/execution/javascript/rpc.rs | 76 +- crates/native-sidecar/src/execution/launch.rs | 44 +- crates/native-sidecar/src/execution/mod.rs | 71 +- .../native-sidecar/src/execution/process.rs | 68 +- .../src/execution/process_events.rs | 13 +- crates/native-sidecar/src/main.rs | 9 + crates/native-sidecar/src/service.rs | 13 +- crates/native-sidecar/src/state.rs | 10 + crates/native-sidecar/src/stdio.rs | 25 +- crates/native-sidecar/src/vm.rs | 79 +- .../tests/architecture_guards.rs | 196 +- .../tests/fixtures/limits-inventory.json | 51 + crates/native-sidecar/tests/guest_identity.rs | 12 +- crates/native-sidecar/tests/limits.rs | 22 + crates/native-sidecar/tests/limits_audit.rs | 8 +- crates/native-sidecar/tests/service.rs | 279 +- crates/native-sidecar/tests/support/mod.rs | 60 +- .../tests/wasm_software_parity.rs | 46 +- .../native-sidecar/tests/wasmtime_safety.rs | 943 +- .../tests/xfstests_correctness.rs | 103 +- crates/resource/src/lib.rs | 5 +- crates/runtime/src/accounting.rs | 1 + crates/runtime/src/lib.rs | 8 + .../protocol/agentos_sidecar_v1.bare | 1 + crates/sidecar-protocol/src/wire.rs | 4 + crates/v8-runtime/src/host_call.rs | 99 +- crates/v8-runtime/src/isolate.rs | 6 + .../src/local/sqlite_metadata_store.rs | 20 + crates/vfs-store/tests/local.rs | 93 +- crates/vm-config/src/lib.rs | 48 + docs/design/wasmtime-executor.md | 85 +- docs/design/wasmtime-phase-0.md | 7 +- docs/wasmvm/executors.md | 93 +- justfile | 1 + packages/agentos/src/actor.ts | 1 + .../tests/fixtures/actor-runtime-server.mjs | 5 + .../build-tools/bridge-src/builtins/http.ts | 30 +- packages/core/src/agent-os.ts | 62 +- .../core/src/generated/WasmLimitsConfig.ts | 12 +- packages/core/src/options-schema.ts | 3 + packages/core/src/runtime-compat.ts | 7 +- packages/core/src/runtime.ts | 4 +- packages/core/src/sidecar/rpc-client.ts | 26 +- .../tests/helpers/default-vm-permissions.ts | 12 +- packages/core/tests/options-schema.test.ts | 26 + .../tests/wasm-backend-selectors.e2e.test.ts | 37 + packages/runtime-benchmarks/package.json | 3 + .../wasm-backend-comparison-phase4.json | 61444 ++++++++++++++++ .../results/wasm-mixed-soak.json | 6938 ++ .../results/wasmtime-threads.json | 1024 + .../src/focused/wasm-mixed-soak.ts | 566 + .../src/focused/wasmtime-threads.bench.ts | 535 + packages/runtime-benchmarks/src/lib/memory.ts | 69 + packages/runtime-benchmarks/src/lib/vm.ts | 6 +- .../scripts/copy-wasm-commands.mjs | 9 +- .../runtime-core/src/generated-protocol.ts | 7 + .../src/generated/CreateVmConfig.ts | 3 +- .../src/generated/StandaloneWasmBackend.ts | 9 + .../src/generated/WasmLimitsConfig.ts | 2 +- packages/runtime-core/src/kernel-proxy.ts | 168 +- .../src/node-runtime-options-schema.ts | 7 + packages/runtime-core/src/node-runtime.ts | 9 +- packages/runtime-core/src/protocol-maps.ts | 4 +- packages/runtime-core/src/sidecar-process.ts | 2 +- packages/runtime-core/src/test-runtime.ts | 26 +- packages/runtime-core/src/vm-config.ts | 9 +- .../tests/copy-wasm-commands.test.ts | 21 +- .../bridge-child-process.nightly.test.ts | 104 + .../cross-runtime-network.nightly.test.ts | 850 +- .../integration/wasi-http.nightly.test.ts | 723 +- .../tests/request-payloads.test.ts | 23 + .../tests/response-payloads.test.ts | 20 + .../npm/darwin-arm64/package.json | 3 + .../npm/darwin-x64/package.json | 3 + .../npm/linux-arm64-gnu/package.json | 3 + .../npm/linux-x64-gnu/package.json | 3 + .../npm/darwin-arm64/package.json | 3 + .../npm/darwin-x64/package.json | 3 + .../npm/linux-arm64-gnu/package.json | 3 + .../npm/linux-x64-gnu/package.json | 3 + packages/vm-test-harness/src/index.ts | 32 +- scripts/ci/smoke-packed-wasm-backends.mjs | 183 + tests/xfstests/Makefile | 20 +- tests/xfstests/README.md | 12 +- toolchain/Makefile | 27 +- toolchain/c/Makefile | 60 +- toolchain/c/scripts/build-duckdb.sh | 20 +- toolchain/conformance/c-parity.test.ts | 42 +- toolchain/crates/libs/wasi-http/src/lib.rs | 4 +- toolchain/crates/wasi-ext/src/lib.rs | 4 +- toolchain/scripts/ensure-wasm-opt.sh | 63 + toolchain/scripts/patch-wasi-libc.sh | 69 +- .../0012-wasi-hidden-preopen-path-alias.patch | 17 +- .../0001-agentos-system-identity.patch | 21 + .../crates/platform-info/copy.manifest | 1 + .../std-patches/crates/platform-info/wasi.rs | 79 + .../crates/uu_chmod/0001-wasi-compat.patch | 30 +- .../0001-wasi-host-fs-mode-display.patch | 7 +- .../uu_stat/0001-wasi-metadata-compat.patch | 21 +- .../0048-cooperative-pthread-cancel.patch | 121 + toolchain/test-programs/pthread_benchmark.c | 112 + toolchain/test-programs/pthread_conformance.c | 106 + turbo.json | 1 + .../docs/custom-software/building-wasm.md | 4 +- 161 files changed, 81359 insertions(+), 2146 deletions(-) create mode 100644 crates/execution/src/wasm/wasmtime/threads.rs create mode 100644 crates/execution/src/wasm/wasmtime/worker.rs create mode 100644 packages/core/tests/wasm-backend-selectors.e2e.test.ts create mode 100644 packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json create mode 100644 packages/runtime-benchmarks/results/wasm-mixed-soak.json create mode 100644 packages/runtime-benchmarks/results/wasmtime-threads.json create mode 100644 packages/runtime-benchmarks/src/focused/wasm-mixed-soak.ts create mode 100644 packages/runtime-benchmarks/src/focused/wasmtime-threads.bench.ts create mode 100644 packages/runtime-core/src/generated/StandaloneWasmBackend.ts create mode 100644 scripts/ci/smoke-packed-wasm-backends.mjs create mode 100755 toolchain/scripts/ensure-wasm-opt.sh create mode 100644 toolchain/std-patches/crates/platform-info/0001-agentos-system-identity.patch create mode 100644 toolchain/std-patches/crates/platform-info/copy.manifest create mode 100644 toolchain/std-patches/crates/platform-info/wasi.rs create mode 100644 toolchain/std-patches/wasi-libc/0048-cooperative-pthread-cancel.patch create mode 100644 toolchain/test-programs/pthread_benchmark.c create mode 100644 toolchain/test-programs/pthread_conformance.c diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 4110a8df22..638a4f8af7 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -6,6 +6,51 @@ on: - cron: "41 10 * * *" jobs: + xfstests: + name: "xfstests (${{ matrix.wasm_backend }}, ${{ matrix.storage_backend }})" + runs-on: [self-hosted, agentos-builder] + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + wasm_backend: [v8, wasmtime] + storage_backend: [chunked_local, memory, chunked_s3] + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - run: pnpm install --frozen-lockfile + - name: Run the pinned filesystem conformance corpus + run: make -C tests/xfstests run + env: + XFSTESTS_WASM_BACKENDS: ${{ matrix.wasm_backend }} + XFSTESTS_BACKENDS: ${{ matrix.storage_backend }} + XFSTESTS_CONCURRENCY: '2' + - name: Run the 1,000-file multi-process directory stress gate + if: matrix.storage_backend == 'chunked_local' + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_dirstress_process_matrix -- \ + --ignored --exact --nocapture --test-threads=1 + env: + AGENTOS_TEST_WASM_BACKEND: ${{ matrix.wasm_backend }} + XFSTESTS_ROOT: ${{ github.workspace }}/tests/xfstests/.work/xfstests + AGENTOS_V8_BRIDGE_PREBUILT_DIR: ${{ github.workspace }}/tests/xfstests/.cache/v8-bridge + CARGO_TARGET_DIR: ${{ github.workspace }}/tests/xfstests/.cache/cargo-target + CARGO_BUILD_JOBS: '1' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: xfstests-${{ matrix.wasm_backend }}-${{ matrix.storage_backend }} + path: tests/xfstests/report + if-no-files-found: warn + nightly: name: Nightly runtime validation runs-on: [self-hosted, agentos-builder] @@ -44,6 +89,7 @@ jobs: - run: make -C toolchain commands - run: make -C toolchain cmd/duckdb - run: make -C toolchain codex + - run: make -C toolchain/c pthread-conformance-wasm pthread-benchmark-wasm - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - name: Use stable Rust for repository validation run: echo "RUSTUP_TOOLCHAIN=stable" >> "$GITHUB_ENV" @@ -84,6 +130,16 @@ jobs: - run: cargo build --release -p agentos-native-sidecar - run: cargo build --release -p agentos-native-baseline - run: cargo build --release --target wasm32-wasip1 -p agentos-native-baseline + - name: Long mixed V8-JavaScript and Wasmtime leak/plateau gate + run: pnpm --dir packages/runtime-benchmarks test:wasm-mixed-soak + env: + AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-native-sidecar + AGENTOS_WASMTIME_WORKER_PATH: ${{ github.workspace }}/target/release/agentos-native-sidecar + - name: Wasmtime Threads startup, throughput, memory, concurrency, and termination + run: pnpm --dir packages/runtime-benchmarks bench:wasm-threads + env: + AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-native-sidecar + AGENTOS_WASMTIME_WORKER_PATH: ${{ github.workspace }}/target/release/agentos-native-sidecar - run: pnpm --dir packages/runtime-benchmarks bench:matrix env: AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-native-sidecar diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b412b54bb0..5fdfddf8d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,14 +123,36 @@ jobs: workspaces: toolchain -> target key: wasm-commands-${{ hashFiles('toolchain/Cargo.lock') }} - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' - - run: make -C toolchain pr-commands - - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require-coreutils + - run: make -C toolchain commands + # DuckDB and Vim are intentionally outside the default command set because + # they are heavy builds, but the runtime/parity suites require their real + # command artifacts. + - name: Build required heavy integration commands + run: make -C toolchain cmd/duckdb cmd/vim + - name: Build mandatory threaded-WASM conformance fixtures + run: make -C toolchain/c pthread-conformance-wasm pthread-benchmark-wasm + - name: Stage native/WASM C parity fixtures + run: make -C toolchain/c conformance-artifacts + - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - uses: actions/upload-artifact@v4 with: name: wasm-commands path: packages/runtime-core/commands retention-days: 1 if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: wasm-thread-fixtures + path: | + toolchain/c/build/pthread_conformance.wasm + toolchain/c/build/pthread_benchmark.wasm + toolchain/c/build/exec_variants + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: wasm-c-parity-fixtures + path: toolchain/c/build/conformance + if-no-files-found: error rust: name: Rust @@ -333,3 +355,79 @@ jobs: exit 1 fi done + + wasm-backend-matrix: + name: "WASM backend (${{ matrix.backend }})" + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + needs: [wasm-commands] + runs-on: [self-hosted, agentos-builder] + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + backend: [v8, wasmtime] + env: + AGENTOS_TEST_WASM_BACKEND: ${{ matrix.backend }} + AGENTOS_E2E_NETWORK: '1' + AGENT_OS_CLIENT_ALLOW_E2E_SKIPS: '0' + AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-native-sidecar + AGENTOS_WASMTIME_WORKER_PATH: ${{ github.workspace }}/target/release/agentos-native-sidecar + AGENTOS_WASM_COMMANDS_DIR: ${{ github.workspace }}/packages/runtime-core/commands + AGENTOS_C_WASM_COMMANDS_DIR: ${{ github.workspace }}/toolchain/c/build/conformance + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: wasm-backend-${{ matrix.backend }} + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + name: wasm-commands + path: packages/runtime-core/commands + - uses: actions/download-artifact@v4 + with: + name: wasm-thread-fixtures + path: toolchain/c/build + - uses: actions/download-artifact@v4 + with: + name: wasm-c-parity-fixtures + path: toolchain/c/build/conformance + - name: Restore and package the validated command corpus + run: | + mkdir -p toolchain/target/wasm32-wasip1/release/commands + cp -a packages/runtime-core/commands/. toolchain/target/wasm32-wasip1/release/commands/ + node packages/runtime-core/scripts/copy-wasm-commands.mjs --require + pnpm --filter @agentos-software/coreutils build:runtime + pnpm --filter '@agentos-software/common...' build + - name: Build the release sidecar and public test clients + run: | + cargo build --release -p agentos-native-sidecar + pnpm --filter '@rivet-dev/agentos-runtime-core...' build + pnpm --filter @rivet-dev/agentos-vm-test-harness build + pnpm --filter @rivet-dev/agentos-core build + pnpm --filter @rivet-dev/agentos build + - name: Run all native sidecar tests through the selected VM backend + run: cargo test --release -p agentos-native-sidecar --tests -- --test-threads=1 + - name: Run owned pthread libc conformance + if: matrix.backend == 'wasmtime' + run: cargo test --release -p agentos-native-sidecar --test wasmtime_safety owned_pthread_libc_mutex_cond_tls_join_detach_and_cancel_conform -- --ignored --exact --nocapture --test-threads=1 + - name: Run native-vs-WASM C conformance through the selected VM backend + run: pnpm --dir packages/core exec vitest run --root ../.. toolchain/conformance/c-parity.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1 + - name: Run runtime-core WASM and cross-runtime integration serially + run: pnpm --dir packages/runtime-core exec vitest run --reporter=verbose --maxWorkers=1 --minWorkers=1 + - name: Run the public TypeScript client suite serially + run: pnpm --dir packages/core exec vitest run --reporter=verbose --maxWorkers=1 --minWorkers=1 + - name: Run the public Rust client without artifact skips + run: cargo test -p agentos-client -- --test-threads=1 + - name: Run every registry software suite through the selected WASM backend + run: pnpm exec turbo test --concurrency=1 --filter='@agentos-software/*' + - name: Run deterministic actor, ACP, and agent-tool conformance + run: pnpm --filter @rivet-dev/agentos test:e2e:run + - name: Run mixed V8-JavaScript and Wasmtime resource-plateau smoke + if: matrix.backend == 'wasmtime' + run: pnpm --dir packages/runtime-benchmarks test:wasm-mixed-smoke diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index b0d7a39cc6..10ea334bc3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -73,12 +73,25 @@ jobs: key: wasm-commands-${{ hashFiles('toolchain/Cargo.lock') }} - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' - run: make -C toolchain commands + # These real upstream commands are intentionally outside the fast default + # toolchain target, but are part of the published and parity-tested corpus. + - name: Build required heavy release commands + run: make -C toolchain cmd/duckdb cmd/vim + - run: make -C toolchain/c pthread-conformance-wasm + - run: make -C toolchain/c build/exec_variants - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - uses: actions/upload-artifact@v4 with: name: wasm-commands path: packages/runtime-core/commands if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: wasm-thread-fixtures + path: | + toolchain/c/build/pthread_conformance.wasm + toolchain/c/build/exec_variants + if-no-files-found: error codex-wasm: name: Codex WASI @@ -272,10 +285,63 @@ jobs: path: target/sidecar-artifacts/${{ matrix.platform }} if-no-files-found: error + smoke-sidecar-artifacts: + needs: [wasm-commands, build-sidecar, build-sidecar-darwin] + name: "Packaged WASM backends (${{ matrix.platform }})" + strategy: + fail-fast: false + matrix: + include: + - platform: linux-x64-gnu + runner: [self-hosted, agentos-builder] + - platform: linux-arm64-gnu + runner: [self-hosted, agentos-builder-arm64] + - platform: darwin-x64 + runner: macos-15-intel + - platform: darwin-arm64 + runner: macos-15 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' + - uses: actions/download-artifact@v4 + with: + name: wasm-commands + path: packages/runtime-core/commands + - uses: actions/download-artifact@v4 + with: + name: wasm-thread-fixtures + path: target/wasm-thread-fixtures + - uses: actions/download-artifact@v4 + with: + name: sidecar-${{ matrix.platform }} + path: target/smoke-sidecar + - name: Stage the actual release binary in its npm platform package + run: | + set -euo pipefail + package_dir="packages/runtime-sidecar/npm/${{ matrix.platform }}" + cp target/smoke-sidecar/agentos-native-sidecar "$package_dir/agentos-native-sidecar" + chmod +x "$package_dir/agentos-native-sidecar" + - run: pnpm --filter @rivet-dev/agentos-runtime-core build + - name: Install tarballs and exercise every shipped WASM selector + run: | + node scripts/ci/smoke-packed-wasm-backends.mjs \ + --platform-package "packages/runtime-sidecar/npm/${{ matrix.platform }}" \ + --thread-fixture target/wasm-thread-fixtures/pthread_conformance.wasm + publish-npm: - needs: [context, wasm-commands, codex-wasm, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin] + needs: [context, wasm-commands, codex-wasm, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin, smoke-sidecar-artifacts] name: Publish npm - if: ${{ !cancelled() && needs.wasm-commands.result == 'success' && needs.codex-wasm.result == 'success' && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} + if: ${{ !cancelled() && needs.wasm-commands.result == 'success' && needs.codex-wasm.result == 'success' && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' && needs.smoke-sidecar-artifacts.result == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -357,9 +423,9 @@ jobs: ${{ needs.context.outputs.trigger == 'release' && '--release-mode' || '' }} release-assets: - needs: [context, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin] + needs: [context, wasmtime-darwin-smoke, build-sidecar, build-sidecar-darwin, smoke-sidecar-artifacts] name: Release assets - if: ${{ !cancelled() && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' }} + if: ${{ !cancelled() && needs.wasmtime-darwin-smoke.result == 'success' && needs.build-sidecar.result == 'success' && needs.build-sidecar-darwin.result == 'success' && needs.smoke-sidecar-artifacts.result == 'success' }} runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index 02596ff942..7b070be287 100644 --- a/.gitignore +++ b/.gitignore @@ -43,11 +43,14 @@ secrets/**/* # Generated WASM toolchain and software artifacts. These are rebuilt for tests # and releases and must never be committed. packages/runtime-core/commands/ +packages/runtime-sidecar/npm/*/agentos-native-sidecar +packages/sidecar-binary/npm/*/agentos-sidecar toolchain/vendor/ toolchain/c/build/ toolchain/c/vendor/ toolchain/c/libs/ toolchain/c/sysroot/ +toolchain/c/sysroot-threads/ toolchain/c/.cache/ toolchain/std-patches/wasi-libc-overrides/*.o software/*/bin/ @@ -86,6 +89,7 @@ scripts/ralph/.codex-last-msg-* # Local caches and stray outputs .agentos/ +.cache/ crates/execution/assets/v8-bridge.js crates/execution/assets/v8-bridge-zlib.js crates/execution/.agentos-pyodide-cache/ diff --git a/Cargo.lock b/Cargo.lock index 73515a230e..340ba049bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ dependencies = [ "getrandom 0.2.17", "nix 0.29.0", "serde", + "serde_bytes", "serde_json", "sha2 0.10.9", "tempfile", @@ -4012,6 +4013,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index baa2e2ae44..908f2f626f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,10 @@ vbare-compiler = { package = "rivet-vbare-compiler", version = "0.0.5" } wasmtime = { version = "=46.0.0", default-features = false, features = [ "async", "cranelift", + "gc", + "gc-drc", "runtime", "std", + "threads", ] } wasmparser = "=0.251.0" diff --git a/crates/client/src/agent_os.rs b/crates/client/src/agent_os.rs index 842edd23e1..3b2ee68fa6 100644 --- a/crates/client/src/agent_os.rs +++ b/crates/client/src/agent_os.rs @@ -1034,6 +1034,15 @@ fn serialize_create_vm_config_for_sidecar( database: config.database.clone(), cwd: None, env: BTreeMap::new(), + wasm_backend: config.wasm_backend.map(|backend| match backend { + crate::process::StandaloneWasmBackend::V8 => vm_config::StandaloneWasmBackend::V8, + crate::process::StandaloneWasmBackend::Wasmtime => { + vm_config::StandaloneWasmBackend::Wasmtime + } + crate::process::StandaloneWasmBackend::WasmtimeThreads => { + vm_config::StandaloneWasmBackend::WasmtimeThreads + } + }), user: config.user.clone(), root_filesystem, permissions: Some(permissions_policy_config(config)), @@ -3749,6 +3758,7 @@ mod tests { DirEntryType, FilesystemEntry, FilesystemEntryEncoding, FilesystemSnapshotEntries, FilesystemSnapshotExport, RootSnapshotExport, SnapshotExportKind, }; + use crate::process::StandaloneWasmBackend; use agentos_sidecar_client::wire::{ FsPermissionScope, PatternPermissionScope, PermissionMode as WirePermissionMode, }; @@ -4009,6 +4019,31 @@ mod tests { assert!(native_root.read_only); } + #[test] + fn create_vm_config_preserves_standalone_wasm_backend() { + for (client_backend, config_backend) in [ + ( + StandaloneWasmBackend::V8, + agentos_vm_config::StandaloneWasmBackend::V8, + ), + ( + StandaloneWasmBackend::Wasmtime, + agentos_vm_config::StandaloneWasmBackend::Wasmtime, + ), + ( + StandaloneWasmBackend::WasmtimeThreads, + agentos_vm_config::StandaloneWasmBackend::WasmtimeThreads, + ), + ] { + let config = serialize_create_vm_config_for_sidecar(&AgentOsConfig { + wasm_backend: Some(client_backend), + ..Default::default() + }) + .expect("serialize create VM config"); + assert_eq!(config.wasm_backend, Some(config_backend)); + } + } + #[test] fn create_vm_config_preserves_typed_limits() { let config = serialize_create_vm_config_for_sidecar(&AgentOsConfig { diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 91e0e96131..c91d7c1ef8 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -40,6 +40,8 @@ pub struct AgentOsConfig { pub loopback_exempt_ports: Vec, /// Allowed Node.js builtins. Default: the hardened native-bridge set. pub allowed_node_builtins: Option>, + /// VM-wide default for standalone WASM commands. JavaScript remains on V8. + pub wasm_backend: Option, /// Root filesystem configuration. Default: overlay + bundled base snapshot. pub root_filesystem: RootFilesystemConfig, /// Additional mounts. @@ -101,6 +103,11 @@ impl AgentOsConfigBuilder { self } + pub fn wasm_backend(mut self, backend: crate::process::StandaloneWasmBackend) -> Self { + self.config.wasm_backend = Some(backend); + self + } + pub fn user(mut self, user: VmUserConfig) -> Self { self.config.user = Some(user); self @@ -726,6 +733,18 @@ pub struct WasmLimits { skip_serializing_if = "Option::is_none" )] pub deterministic_fuel: Option, + #[serde( + default, + rename = "maxThreads", + skip_serializing_if = "Option::is_none" + )] + pub max_threads: Option, + #[serde( + default, + rename = "maxConcurrentThreads", + skip_serializing_if = "Option::is_none" + )] + pub max_concurrent_threads: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/client/src/process.rs b/crates/client/src/process.rs index 50d2ac33c2..163eaec8ec 100644 --- a/crates/client/src/process.rs +++ b/crates/client/src/process.rs @@ -58,10 +58,13 @@ pub enum TimingMitigation { /// Engine override for standalone WebAssembly commands. JavaScript and its /// `WebAssembly.*` APIs always remain on V8. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] pub enum StandaloneWasmBackend { + #[serde(rename = "v8")] V8, + #[serde(rename = "wasmtime")] Wasmtime, + #[serde(rename = "wasmtime-threads")] + WasmtimeThreads, } impl From for wire::StandaloneWasmBackend { @@ -69,6 +72,7 @@ impl From for wire::StandaloneWasmBackend { match value { StandaloneWasmBackend::V8 => Self::V8, StandaloneWasmBackend::Wasmtime => Self::Wasmtime, + StandaloneWasmBackend::WasmtimeThreads => Self::WasmtimeThreads, } } } diff --git a/crates/client/src/shell.rs b/crates/client/src/shell.rs index 985d231877..374c699ab4 100644 --- a/crates/client/src/shell.rs +++ b/crates/client/src/shell.rs @@ -50,6 +50,8 @@ pub struct OpenShellOptions { pub cwd: Option, pub cols: Option, pub rows: Option, + /// Engine affinity inherited by standalone WASM commands launched by the shell. + pub wasm_backend: Option, } /// Options for `connect_terminal` (extends [`OpenShellOptions`]). @@ -289,7 +291,7 @@ impl AgentOs { env: options.env.clone().into_iter().collect(), cwd: options.cwd.clone(), wasm_permission_tier: None, - wasm_backend: None, + wasm_backend: options.wasm_backend.map(Into::into), }; // Background: subscribe to events first (so no output is missed), issue the spawn, fan @@ -441,7 +443,7 @@ impl AgentOs { env: options.env.clone().into_iter().collect(), cwd: options.cwd.clone(), wasm_permission_tier: None, - wasm_backend: None, + wasm_backend: options.wasm_backend.map(Into::into), }; let agent = self.clone(); @@ -598,7 +600,7 @@ impl AgentOs { env: base.env.clone().into_iter().collect(), cwd: base.cwd.clone(), wasm_permission_tier: None, - wasm_backend: None, + wasm_backend: base.wasm_backend.map(Into::into), }; // Subscribe before issuing the spawn so no output is missed. diff --git a/crates/client/tests/common/mod.rs b/crates/client/tests/common/mod.rs index fcebfbf55b..88136c20b2 100644 --- a/crates/client/tests/common/mod.rs +++ b/crates/client/tests/common/mod.rs @@ -17,6 +17,17 @@ use agentos_client::AgentOs; static INIT: Once = Once::new(); +fn test_wasm_backend() -> Option { + match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { + Ok("v8") => Some(agentos_client::StandaloneWasmBackend::V8), + Ok("wasmtime") => Some(agentos_client::StandaloneWasmBackend::Wasmtime), + Ok(value) => { + panic!("AGENTOS_TEST_WASM_BACKEND must be \"v8\" or \"wasmtime\", got {value:?}") + } + Err(_) => None, + } +} + fn test_node_modules_dir() -> PathBuf { std::env::var_os("AGENTOS_TEST_NODE_MODULES_DIR") .map(PathBuf::from) @@ -93,6 +104,7 @@ pub async fn new_vm_with_sidecar_pool(pool: impl Into) -> AgentOs { sidecar: Some(AgentOsSidecarConfig::Shared { pool: Some(pool.into()), }), + wasm_backend: test_wasm_backend(), ..Default::default() }) .await @@ -131,6 +143,7 @@ async fn new_vm_with_config( loopback_exempt_ports, mounts: all_mounts, permissions, + wasm_backend: test_wasm_backend(), ..Default::default() }) .await @@ -230,6 +243,7 @@ pub async fn new_vm_with_commands() -> Option { packages: vec![PackageRef { path: package_dir.to_string_lossy().into_owned(), }], + wasm_backend: test_wasm_backend(), ..Default::default() }; Some( diff --git a/crates/client/tests/wasm_command_mount_e2e.rs b/crates/client/tests/wasm_command_mount_e2e.rs index 2e76534922..e3d1a444d3 100644 --- a/crates/client/tests/wasm_command_mount_e2e.rs +++ b/crates/client/tests/wasm_command_mount_e2e.rs @@ -11,7 +11,8 @@ mod common; -use agentos_client::ExecOptions; +use agentos_client::config::{AgentOsConfig, PackageRef}; +use agentos_client::{AgentOs, ExecOptions, StandaloneWasmBackend}; #[tokio::test] async fn wasm_command_software_mounts_into_vm() { @@ -42,3 +43,43 @@ async fn wasm_command_software_mounts_into_vm() { os.shutdown().await.expect("shutdown"); } + +#[tokio::test] +async fn every_public_wasm_backend_selector_executes_projected_commands() { + if !common::require_sidecar("every_public_wasm_backend_selector_executes_projected_commands") { + return; + } + let Some(package_dir) = common::coreutils_package_dir() else { + eprintln!( + "skipping every_public_wasm_backend_selector_executes_projected_commands: coreutils package artifacts absent" + ); + return; + }; + + for backend in [ + StandaloneWasmBackend::V8, + StandaloneWasmBackend::Wasmtime, + StandaloneWasmBackend::WasmtimeThreads, + ] { + let os = AgentOs::create(AgentOsConfig { + packages: vec![PackageRef { + path: package_dir.to_string_lossy().into_owned(), + }], + wasm_backend: Some(backend), + ..Default::default() + }) + .await + .expect("create VM for explicit WASM backend"); + let result = os + .exec("printf selector | tr a-z A-Z", ExecOptions::default()) + .await + .expect("execute projected command through selected backend"); + assert_eq!( + result.exit_code, 0, + "backend {backend:?} failed: stderr={:?}", + result.stderr + ); + assert_eq!(result.stdout, "SELECTOR"); + os.shutdown().await.expect("shutdown selector VM"); + } +} diff --git a/crates/execution/Cargo.toml b/crates/execution/Cargo.toml index d9da05b02e..f2fcf02f98 100644 --- a/crates/execution/Cargo.toml +++ b/crates/execution/Cargo.toml @@ -29,11 +29,12 @@ base64 = "0.22" ciborium = "0.2" getrandom = "0.2" flume = "0.11" -nix = { version = "0.29", features = ["fs", "time"] } +nix = { version = "0.29", features = ["fs", "process", "signal", "time"] } serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11" serde_json = "1" sha2 = "0.10" -tokio = { version = "1", features = ["rt", "sync", "time"] } +tokio = { version = "1", features = ["io-util", "process", "rt", "sync", "time"] } tracing = "0.1" wasmtime = { workspace = true } wasmparser = { workspace = true } diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 4e431192d0..e70b813227 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -47,6 +47,7 @@ const WASI_ERRNO_NAMETOOLONG = 37; const WASI_ERRNO_NOBUFS = 42; const WASI_ERRNO_NOENT = 44; const WASI_ERRNO_NOEXEC = 45; +const WASI_ERRNO_NOMEM = 48; const WASI_ERRNO_NOSPC = 51; const WASI_ERRNO_NOSYS = 52; const WASI_ERRNO_HOSTUNREACH = 23; @@ -980,6 +981,21 @@ function parseLinuxShebang(bytes) { }; } +function compileResolvedExecImage(bytes, subject, argv) { + try { + const binary = enforceMemoryLimit(bytes, maxMemoryPages); + return { module: new WebAssembly.Module(binary), argv }; + } catch (error) { + if ( + error instanceof WebAssembly.CompileError || + error?.message === 'module is not a valid WebAssembly binary' + ) { + throw execError('ENOEXEC', `${subject} is not a supported WebAssembly executable image`); + } + throw error; + } +} + function compileExecImage(bytes, subject, argv, interpreterDepth = 0) { const shebang = parseLinuxShebang(bytes); if (shebang) { @@ -999,21 +1015,57 @@ function compileExecImage(bytes, subject, argv, interpreterDepth = 0) { ); } - try { - const binary = enforceMemoryLimit(bytes, maxMemoryPages); - return { module: new WebAssembly.Module(binary), argv }; - } catch (error) { - if ( - error instanceof WebAssembly.CompileError || - error?.message === 'module is not a valid WebAssembly binary' - ) { - throw execError('ENOEXEC', `${subject} is not a supported WebAssembly executable image`); - } - throw error; - } + return compileResolvedExecImage(bytes, subject, argv); } function loadExecImageFromPath(command, argv, interpreterDepth = 0) { + if (SIDECAR_MANAGED_PROCESS) { + const subject = String(command); + const image = callSyncRpc('process.exec_image_open', [subject, argv]); + const handle = String(image?.handle ?? ''); + let bytes; + try { + const size = Number(image?.size); + if (!Number.isSafeInteger(size) || size < 0) { + throw execError('EFBIG', `${subject} has an invalid executable image size`); + } + if (size > maxModuleFileBytes) { + throw execError( + 'EFBIG', + `${subject} is ${size} bytes, exceeding limits.wasm.maxModuleFileBytes (${maxModuleFileBytes}); raise limits.wasm.maxModuleFileBytes if needed`, + ); + } + bytes = Buffer.alloc(size); + let offset = 0; + while (offset < size) { + const requested = boundedWasmSyncRpcReadLength(size - offset); + const chunk = Buffer.from(callSyncRpc('process.exec_image_read', [ + handle, + String(offset), + requested, + ]) ?? []); + if (chunk.byteLength === 0 || chunk.byteLength > requested) { + throw execError('EIO', `${subject} changed while its executable image was read`); + } + chunk.copy(bytes, offset); + offset += chunk.byteLength; + } + } finally { + if (handle.length > 0) { + callSyncRpc('process.exec_image_close', [handle]); + } + } + if (!Array.isArray(image?.argv) || image.argv.some((value) => typeof value !== 'string')) { + throw execError('EIO', `${subject} resolution did not return a valid argv`); + } + traceHostProcess('exec-image-bytes', { + command, + byteLength: bytes.byteLength, + magic: Array.from(bytes.subarray(0, Math.min(16, bytes.byteLength))), + }); + return compileResolvedExecImage(bytes, subject, image.argv); + } + let bytes = readExecutablePathBytes(command); traceHostProcess('exec-image-bytes', { command, @@ -1053,14 +1105,12 @@ function loadExecImageFromFd(fd, argv, closeFds) { if (SIDECAR_MANAGED_PROCESS) { const image = callSyncRpc('process.exec_image_open_fd', [ canonicalKernelFdForSpawnAction(descriptor), + argv, + kernelCloexecFdsForCommit(closeFds), ]); const imageHandle = String(image?.handle ?? ''); let bytes; try { - const projectedExecutable = isProjectedCommandGuestPath(image?.canonicalPath ?? ''); - if (!projectedExecutable && (Number(image?.mode) & 0o111) === 0) { - throw execError('EACCES', `${scriptRef} does not have an executable mode bit`); - } const size = Number(image?.size); if (!Number.isSafeInteger(size) || size < 0) { throw execError('EFBIG', `${scriptRef} has an invalid executable image size`); @@ -1091,11 +1141,11 @@ function loadExecImageFromFd(fd, argv, closeFds) { callSyncRpc('process.exec_image_close', [imageHandle]); } } - if (parseLinuxShebang(bytes) && closeFds.includes(descriptor)) { - throw execError('ENOENT', `${scriptRef} will be closed before its interpreter opens it`); + if (!Array.isArray(image?.argv) || image.argv.some((value) => typeof value !== 'string')) { + throw execError('EIO', `${scriptRef} resolution did not return a valid argv`); } return { - ...compileExecImage(bytes, scriptRef, argv), + ...compileExecImage(bytes, scriptRef, image.argv), scriptRef, }; } @@ -2902,6 +2952,8 @@ function mapSyntheticFsError(error) { return WASI_ERRNO_NOTEMPTY; case 'ENOEXEC': return WASI_ERRNO_NOEXEC; + case 'ENOMEM': + return WASI_ERRNO_NOMEM; case 'ENOSPC': return WASI_ERRNO_NOSPC; case 'ENOSYS': @@ -2985,6 +3037,8 @@ function mapHostProcessError(error) { return WASI_ERRNO_NOENT; case 'ENOEXEC': return WASI_ERRNO_NOEXEC; + case 'ENOMEM': + return WASI_ERRNO_NOMEM; case 'ENOTDIR': return WASI_ERRNO_NOTDIR; case 'ENOTEMPTY': @@ -4411,13 +4465,21 @@ function finalizeChildExit(record, exitCode, signal, coreDumped = false) { return status; } -function takeManagedWaitTransition(selector, options, _blocking) { +function takeManagedWaitTransition(selector, options, blocking) { const numericSelector = Number(selector) | 0; const normalizedOptions = Number(options) >>> 0; for (;;) { try { - const transition = callSyncRpc('process.waitpid', [numericSelector, normalizedOptions]); + // A POSIX blocking wait can legitimately outlive the bridge operation + // deadline. Ask the kernel for one bounded event-driven slice at a time + // and re-probe without polling or holding a sidecar worker. + const transition = callSyncRpc('process.waitpid', [ + numericSelector, + normalizedOptions, + KERNEL_WAIT_SLICE_MS, + ]); dispatchPendingWasmSignals(); + if (transition == null && blocking) continue; return transition; } catch (error) { const mustInterrupt = dispatchPendingWasmSignals(true); @@ -7901,14 +7963,15 @@ const hostNetImport = { try { const servername = readGuestString(hostnamePtr, hostnameLen); if (socket.managed === true) { - if ((Number(flags) & 1) === 1 || guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') { - return WASI_ERRNO_NOTSUP; - } + const rejectUnauthorized = !( + (Number(flags) & 1) === 1 || guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0' + ); callSyncRpc('process.hostnet_tls_connect', [ managedHostNetTargetFd(socket), servername, [], unixConnectTimeoutMs, + rejectUnauthorized, ]); return WASI_ERRNO_SUCCESS; } @@ -11405,8 +11468,15 @@ const hostFsImport = { Number(followSymlinks) !== 0, ]); return Number(stat?.mode) >>> 0; - } catch { - traceHostProcess('host-fs-path-mode-fault', {}); + } catch (error) { + traceHostProcess('host-fs-path-mode-fault', { + fd: Number(fd) >>> 0, + path: (() => { + try { return readGuestString(pathPtr, pathLen); } catch { return null; } + })(), + code: error?.code, + message: error?.message, + }); return 0; } }, @@ -11776,13 +11846,23 @@ if (delegatePathOpen) { } const openedFd = registerKernelDelegateFd(kernelFd); const writeResult = writeGuestUint32(openedFdPtr, openedFd); + traceHostProcess('path-open-managed', { + inputFd: Number(fd) >>> 0, + kernelDirFd: Number(passthroughDirHandle.targetFd) >>> 0, + path: managedOpenPath, + kernelFd, + openedFd, + writeResult, + }); if (writeResult !== WASI_ERRNO_SUCCESS) { wasiImport.fd_close(openedFd); } return writeResult; } catch (error) { - traceHostProcess('path-open-error', { + traceHostProcess('path-open-managed-error', { + inputFd: Number(fd) >>> 0, guestPath, + path: managedOpenPath, oflags: Number(oflags) >>> 0, rightsBase: String(rightsBase), fdflags: Number(fdflags) >>> 0, @@ -11905,6 +11985,16 @@ function delegatePathDirFd(fd) { } function kernelPathOperand(fd, pathPtr, pathLen) { + const numericFd = Number(fd) >>> 0; + if (numericFd === NODE_CWD_FD) { + return { + // AgentOS extension imports use this sentinel for an ordinary pathname + // resolved from the process cwd. WASI's private tagged preopens still go + // through lookupFdHandle so they retain their capability-root identity. + dirFd: numericFd, + path: readGuestString(pathPtr, pathLen), + }; + } const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { return { @@ -12285,6 +12375,12 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { const written = writeBytesToGuestIovs(iovs, iovsLen, bytes, iovRequest); return writeGuestUint32(nreadPtr, written); } catch (error) { + traceHostProcess('fd-read-managed-error', { + guestFd: numericFd, + kernelFd: Number(handle.targetFd) >>> 0, + code: error?.code, + message: error?.message, + }); return mapHostProcessError(error); } } @@ -12450,9 +12546,11 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } if (rejectClosedPassthroughFd(numericFd)) { + traceHostProcess('fd-read-closed', { guestFd: numericFd, handleKind: handle?.kind ?? null }); return WASI_ERRNO_BADF; } + traceHostProcess('fd-read-delegate', { guestFd: numericFd, handleKind: handle?.kind ?? null }); return delegateManagedFdRead ? __agentOSWasiMeasurePhase('fd_read', 'delegate_call', () => delegateManagedFdRead(numericFd, iovs, iovsLen, nreadPtr) diff --git a/crates/execution/src/backend/lifecycle.rs b/crates/execution/src/backend/lifecycle.rs index 22a581ac9c..1871ccfa72 100644 --- a/crates/execution/src/backend/lifecycle.rs +++ b/crates/execution/src/backend/lifecycle.rs @@ -81,6 +81,7 @@ pub struct PublishedSignalCheckpoint { pub signal: i32, pub delivery_token: u64, pub flags: u32, + pub thread_id: u32, } /// Sidecar-facing lifecycle shared by thread-affine and Send backends. @@ -142,6 +143,7 @@ pub trait ExecutionBackend { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result; /// Takes one delivery already claimed by the kernel control plane and @@ -153,6 +155,18 @@ pub trait ExecutionBackend { Ok(None) } + fn take_signal_checkpoint_for_thread( + &self, + identity: ExecutionWakeIdentity, + thread_id: u32, + ) -> Result, HostServiceError> { + if thread_id == 0 { + self.take_signal_checkpoint(identity) + } else { + Ok(None) + } + } + /// Drops checkpoints claimed by the replaced image after a successful /// kernel exec commit. The kernel has already cleared those delivery /// scopes, so the replacement must never report their stale tokens. diff --git a/crates/execution/src/backend/reply.rs b/crates/execution/src/backend/reply.rs index bb01d923ec..b49b9a0cbe 100644 --- a/crates/execution/src/backend/reply.rs +++ b/crates/execution/src/backend/reply.rs @@ -20,11 +20,11 @@ pub struct HostCallIdentity { pub call_id: u64, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum HostCallReply { Empty, Json(Value), - Raw(Vec), + Raw(#[serde(with = "serde_bytes")] Vec), } type DirectHostReplyResult = Result; @@ -58,9 +58,17 @@ impl DirectHostReplyTarget for DirectHostReplyWaiterTarget { .ok_or_else(|| { HostServiceError::new("EALREADY", "direct host-reply waiter is already settled") })?; - sender - .send(result) - .map_err(|_| HostServiceError::new("EPIPE", "direct host-reply receiver was canceled")) + if sender.send(result).is_err() { + // Cancellation can race a claimed destructive call. The response + // is terminal and must not be replayed; report the lost consumer + // without escalating an expected guest teardown into a sidecar + // process failure. + eprintln!( + "WARN_AGENTOS_DIRECT_HOST_REPLY_RECEIVER_CANCELED: generation={} pid={} callId={}", + self.identity.generation, self.identity.pid, self.identity.call_id + ); + } + Ok(()) } fn dismiss_claimed(&self, call_id: u64) -> Result<(), HostServiceError> { @@ -73,12 +81,19 @@ impl DirectHostReplyTarget for DirectHostReplyWaiterTarget { .ok_or_else(|| { HostServiceError::new("EALREADY", "direct host-reply waiter is already settled") })?; - sender + if sender .send(Err(HostServiceError::new( "ERR_AGENTOS_EXEC_REPLACED", "the kernel committed a replacement process image", ))) - .map_err(|_| HostServiceError::new("EPIPE", "direct host-reply receiver was canceled")) + .is_err() + { + eprintln!( + "WARN_AGENTOS_DIRECT_HOST_REPLY_RECEIVER_CANCELED: generation={} pid={} callId={} execReplacement=true", + self.identity.generation, self.identity.pid, self.identity.call_id + ); + } + Ok(()) } } @@ -787,6 +802,38 @@ mod tests { ); } + #[test] + fn cancellation_after_claim_does_not_escalate_terminal_reply_delivery() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 27, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + assert!(reply.claim().expect("claim before cancellation")); + drop(receiver); + reply + .succeed(HostCallReply::Empty) + .expect("claimed side effect remains terminal after waiter cancellation"); + assert!(!reply.delivery_failed()); + } + + #[test] + fn cancellation_after_exec_claim_does_not_escalate_dismissal() { + let identity = HostCallIdentity { + generation: 9, + pid: 17, + call_id: 28, + }; + let (reply, receiver) = direct_host_reply_channel(identity, 1024).expect("direct channel"); + assert!(reply.claim().expect("claim exec before cancellation")); + drop(receiver); + reply + .dismiss_claimed() + .expect("exec dismissal remains terminal after waiter cancellation"); + assert!(!reply.delivery_failed()); + } + #[test] fn dropping_native_reply_settles_waiter_as_canceled() { let identity = HostCallIdentity { diff --git a/crates/execution/src/host/mod.rs b/crates/execution/src/host/mod.rs index f64a91ba83..51d1c7a3db 100644 --- a/crates/execution/src/host/mod.rs +++ b/crates/execution/src/host/mod.rs @@ -31,7 +31,7 @@ use std::sync::Arc; /// Authority identifying the already-registered process issuing a host call. /// Permission tier and resource rights are looked up from kernel state; they /// are intentionally not caller-selectable fields. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct HostProcessContext { pub generation: u64, pub pid: u32, diff --git a/crates/execution/src/host/network.rs b/crates/execution/src/host/network.rs index 13cf6e6340..04b293e7f9 100644 --- a/crates/execution/src/host/network.rs +++ b/crates/execution/src/host/network.rs @@ -334,12 +334,16 @@ pub enum NetworkOperation { /// `None` is poll(2)'s indefinite wait; `Some(0)` is a probe. timeout_ms: Option, signal_mask: Option, + /// Kernel signal-thread record that owns the temporary ppoll mask. + /// `None` preserves the single-thread/main-thread compatibility path. + signal_thread_id: Option, }, TlsConnect { fd: u32, server_name: BoundedString, alpn: BoundedVec, deadline_ms: Option, + reject_unauthorized: bool, }, TlsRead { session_id: u64, diff --git a/crates/execution/src/host/process.rs b/crates/execution/src/host/process.rs index f4be935085..3de979a145 100644 --- a/crates/execution/src/host/process.rs +++ b/crates/execution/src/host/process.rs @@ -221,6 +221,34 @@ pub enum ExecutableImageSource { Descriptor(u32), } +/// Linux process-image context required when an executable snapshot may be a +/// shebang script. This is admitted before it can enter the host-operation +/// queue; the kernel owns interpreter resolution and argv rewriting. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutableImageResolutionRequest { + pub argv: Vec, + #[serde(default)] + pub close_on_exec_fds: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoundedExecutableImageResolutionRequest(ExecutableImageResolutionRequest); + +impl BoundedExecutableImageResolutionRequest { + pub fn try_new( + request: ExecutableImageResolutionRequest, + limit: &PayloadLimit, + ) -> Result { + limit.admit_json(&request)?; + Ok(Self(request)) + } + + pub fn as_request(&self) -> &ExecutableImageResolutionRequest { + &self.0 + } +} + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum ProcessOperation { @@ -241,6 +269,9 @@ pub enum ProcessOperation { /// VM's WASM module-file limit and returns an opaque generation handle. OpenExecutableImage { source: ExecutableImageSource, + /// Present for exec/fexec snapshots, absent for an already-resolved + /// trusted initial module. + resolution: Option, }, ReadExecutableImage { handle: u64, diff --git a/crates/execution/src/host/signal.rs b/crates/execution/src/host/signal.rs index 7afd3621cf..41d377f316 100644 --- a/crates/execution/src/host/signal.rs +++ b/crates/execution/src/host/signal.rs @@ -1,30 +1,37 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub struct SignalSetValue(pub u64); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SignalDispositionValue { Default, Ignore, User, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SignalActionValue { pub disposition: SignalDispositionValue, pub flags: u32, pub mask: SignalSetValue, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SignalMaskHow { Block, Unblock, Set, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[non_exhaustive] pub enum SignalOperation { + RegisterThread { + thread_id: u32, + inherit_from: u32, + }, + UnregisterThread { + thread_id: u32, + }, GetAction { signal: i32, }, @@ -36,16 +43,39 @@ pub enum SignalOperation { how: SignalMaskHow, set: SignalSetValue, }, + UpdateMaskForThread { + thread_id: u32, + how: SignalMaskHow, + set: SignalSetValue, + }, Pending, BeginDelivery, + BeginDeliveryForThread { + thread_id: u32, + }, TakePublishedDelivery, + TakePublishedDeliveryForThread { + thread_id: u32, + }, EndDelivery { token: u64, }, + EndDeliveryForThread { + thread_id: u32, + token: u64, + }, BeginTemporaryMask { mask: SignalSetValue, }, EndTemporaryMask { token: u64, }, + BeginTemporaryMaskForThread { + thread_id: u32, + mask: SignalSetValue, + }, + EndTemporaryMaskForThread { + thread_id: u32, + token: u64, + }, } diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index 9527c4c99e..c4a0489877 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -517,7 +517,7 @@ pub struct JavascriptExecutionLimits { /// guest's virtual identity no longer rides the ambient env channel. `None` /// keeps the guest-runtime default. See the env-vs-wire rule in /// `crates/sidecar/CLAUDE.md`. -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GuestRuntimeConfig { /// Virtual `process.pid`. pub virtual_pid: Option, @@ -2624,6 +2624,7 @@ impl ExecutionBackend for JavascriptExecution { signal: i32, delivery_token: u64, _flags: u32, + _thread_id: u32, ) -> Result { let Some(wake) = self.wake_handle(identity) else { return Ok(if let Some(process_id) = self.native_process_id() { diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index 39603d75a2..b54aa447ab 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -1,4 +1,4 @@ -#![forbid(unsafe_code)] +#![deny(unsafe_code)] //! Native execution plane scaffold for the secure-exec runtime migration. @@ -38,7 +38,10 @@ pub use python::{ PythonVfsRpcStat, StartPythonExecutionRequest, }; pub use signal::{ExecutionSignalDispositionAction, ExecutionSignalHandlerRegistration}; -pub use wasm::wasmtime::TRUSTED_INITIAL_MODULE_PREFIX; +pub use wasm::wasmtime::{ + run_worker_entry as run_wasmtime_thread_worker, TRUSTED_INITIAL_MODULE_PREFIX, + WORKER_MODE_ARGUMENT as WASMTIME_THREAD_WORKER_ARGUMENT, +}; pub use wasm::{ CreateWasmContextRequest, NativeBinaryFormat, StandaloneWasmBackend, StartWasmExecutionRequest, WasmContext, WasmExecution, WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, diff --git a/crates/execution/src/python.rs b/crates/execution/src/python.rs index b890db6971..84c35b84d4 100644 --- a/crates/execution/src/python.rs +++ b/crates/execution/src/python.rs @@ -1236,6 +1236,7 @@ impl ExecutionBackend for PythonExecution { signal: i32, delivery_token: u64, _flags: u32, + _thread_id: u32, ) -> Result { let Some(wake) = self.wake_handle(identity) else { return Ok(if let Some(process_id) = self.native_process_id() { diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index 0b6d8f7a79..737fc80e3c 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -187,6 +187,9 @@ pub enum StandaloneWasmBackend { #[default] V8, Wasmtime, + /// Explicit Wasmtime pthread/shared-memory profile. Ordinary Wasmtime + /// executions retain the single-thread feature surface. + WasmtimeThreads, } impl WasmPermissionTier { @@ -218,7 +221,7 @@ pub struct WasmContext { /// kernel `ResourceLimits` (originating from `CreateVmConfig` on the BARE wire); /// `None` selects "unlimited / engine default". See the env-vs-wire rule in /// `crates/sidecar/CLAUDE.md`. -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WasmExecutionLimits { /// Active CPU-time budget in milliseconds. The V8 compatibility backend /// maps this to the trusted runner isolate's CPU watchdog. @@ -265,9 +268,12 @@ pub struct WasmExecutionLimits { /// Maximum aggregate bytes retained by compatibility-adapter event queues. /// Sidecar VMs supply limits.process.pendingEventBytes. pub pending_event_bytes: Option, + /// Maximum native guest threads in an explicitly threaded Wasmtime group, + /// including the initial thread. + pub max_threads: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct StartWasmExecutionRequest { pub vm_id: String, pub context_id: String, @@ -1546,6 +1552,7 @@ impl ExecutionBackend for V8WasmExecution { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result { let Some(wake) = self.wake_handle(identity) else { return Ok(if let Some(process_id) = self.native_process_id() { @@ -1560,6 +1567,7 @@ impl ExecutionBackend for V8WasmExecution { signal, delivery_token, flags, + thread_id, }, )?; if let Err(error) = wake.publish_signal(signal, delivery_token) { @@ -1588,6 +1596,9 @@ impl WasmExecution { pub fn standalone_backend(&self) -> StandaloneWasmBackend { match &self.backend { WasmExecutionBackend::V8(_) => StandaloneWasmBackend::V8, + WasmExecutionBackend::Wasmtime(execution) if execution.is_threaded() => { + StandaloneWasmBackend::WasmtimeThreads + } WasmExecutionBackend::Wasmtime(_) => StandaloneWasmBackend::Wasmtime, } } @@ -1942,13 +1953,24 @@ impl ExecutionBackend for WasmExecution { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result { match &self.backend { - WasmExecutionBackend::V8(execution) => { - execution.deliver_signal_checkpoint(identity, signal, delivery_token, flags) - } + WasmExecutionBackend::V8(execution) => execution.deliver_signal_checkpoint( + identity, + signal, + delivery_token, + flags, + thread_id, + ), WasmExecutionBackend::Wasmtime(execution) => { - execution.deliver_signal_checkpoint(identity, signal, delivery_token, flags)?; + execution.deliver_signal_checkpoint( + identity, + signal, + delivery_token, + flags, + thread_id, + )?; Ok(SignalCheckpointOutcome::Published) } } @@ -1964,6 +1986,22 @@ impl ExecutionBackend for WasmExecution { } } + fn take_signal_checkpoint_for_thread( + &self, + identity: ExecutionWakeIdentity, + thread_id: u32, + ) -> Result, HostServiceError> { + match &self.backend { + WasmExecutionBackend::V8(execution) if thread_id == 0 => { + execution.take_signal_checkpoint(identity) + } + WasmExecutionBackend::V8(_) => Ok(None), + WasmExecutionBackend::Wasmtime(execution) => { + execution.take_signal_checkpoint_for_thread(identity, thread_id) + } + } + } + fn discard_signal_checkpoints( &self, identity: ExecutionWakeIdentity, @@ -2189,7 +2227,10 @@ impl WasmExecutionEngine { self.create_execution_with_runtime(request, runtime, defer_execute) } StandaloneWasmBackend::Wasmtime => { - self.create_wasmtime_execution(request, runtime, defer_execute) + self.create_wasmtime_execution(request, runtime, defer_execute, false) + } + StandaloneWasmBackend::WasmtimeThreads => { + self.create_wasmtime_execution(request, runtime, defer_execute, true) } } } @@ -2199,6 +2240,7 @@ impl WasmExecutionEngine { request: StartWasmExecutionRequest, runtime: RuntimeContext, defer_execute: bool, + threaded: bool, ) -> Result { let context = self .contexts @@ -2223,6 +2265,7 @@ impl WasmExecutionEngine { runtime, self.event_notify.clone(), defer_execute, + threaded, )?; Ok(WasmExecution { backend: WasmExecutionBackend::Wasmtime(execution), @@ -2252,6 +2295,11 @@ impl WasmExecutionEngine { let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; + // Enforce bounded structural parsing before the complete feature + // validator. Oversized section counts and pathological varuints must + // fail at AgentOS's explicit parser limits rather than being obscured + // by a later engine/validator EOF diagnostic. + validate_module_limits(&resolved_module, &request)?; validate_module_profile(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; let javascript_context_id = self @@ -2266,7 +2314,6 @@ impl WasmExecutionEngine { .map_err(WasmExecutionError::PrepareWarmPath)?; } let frozen_time_ms = frozen_time_ms(); - validate_module_limits(&resolved_module, &request)?; // Fail closed when a stack byte budget is configured. The V8 runner does // not yet expose a per-module stack lever, so accepting the value would // claim to enforce a policy that the runtime actually ignores. @@ -2331,6 +2378,7 @@ impl WasmExecutionEngine { let resolved_module = resolve_wasm_module(&context, &request)?; verify_wasm_module_header(&resolved_module)?; + validate_module_limits(&resolved_module, &request)?; validate_module_profile(&resolved_module)?; let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?; let javascript_context_id = self @@ -2346,7 +2394,6 @@ impl WasmExecutionEngine { .map_err(WasmExecutionError::PrepareWarmPath)?; } let frozen_time_ms = frozen_time_ms(); - validate_module_limits(&resolved_module, &request)?; wasm_stack_limit_bytes(&request)?; let execution_timeout = resolve_wasm_wall_clock_limit(&request)?; let import_cache = self @@ -2397,7 +2444,10 @@ impl WasmExecutionEngine { .await } StandaloneWasmBackend::Wasmtime => { - self.create_wasmtime_execution(request, runtime, false) + self.create_wasmtime_execution(request, runtime, false, false) + } + StandaloneWasmBackend::WasmtimeThreads => { + self.create_wasmtime_execution(request, runtime, false, true) } } } @@ -6183,6 +6233,7 @@ mod tests { signal: 15, delivery_token: 99, flags: 0x8000_0000, + thread_id: 0, }; inbox @@ -6883,6 +6934,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, }; StartWasmExecutionRequest { limits, @@ -6992,6 +7044,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, }); assert_eq!( @@ -7074,6 +7127,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, }); let resolved_module = ResolvedWasmModule { specifier: String::from("./guest.wasm"), @@ -7129,6 +7183,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, }); request .env @@ -7169,6 +7224,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, }); request .env @@ -8060,7 +8116,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet } #[test] - fn managed_wait_uses_one_kernel_authoritative_direct_reply() { + fn managed_wait_uses_bounded_kernel_authoritative_direct_replies() { let runner = include_str!("../assets/runners/wasm-runner.mjs"); let start = runner .find("function takeManagedWaitTransition(") @@ -8071,6 +8127,8 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet .expect("managed wait helper end"); let helper = &runner[start..end]; assert_eq!(helper.matches("callSyncRpc('process.waitpid'").count(), 1); + assert!(helper.contains("KERNEL_WAIT_SLICE_MS")); + assert!(helper.contains("transition == null && blocking")); assert!(!helper.contains("process.waitpid_transition")); assert!(!helper.contains("pumpSpawnedChildren")); assert!(!helper.contains("Atomics.wait")); diff --git a/crates/execution/src/wasm/profile.rs b/crates/execution/src/wasm/profile.rs index df1e4953eb..b7775788bf 100644 --- a/crates/execution/src/wasm/profile.rs +++ b/crates/execution/src/wasm/profile.rs @@ -20,11 +20,39 @@ pub fn locked_wasm_features() -> WasmFeatures { features.set(WasmFeatures::MULTI_VALUE, true); features.set(WasmFeatures::BULK_MEMORY, true); features.set(WasmFeatures::SIMD, true); + // C++ commands use finalized WebAssembly exception tags and exnref-based + // instructions. LLVM 19 still emits the Phase-3 encoding, so the owned + // toolchain translates it with Binaryen before staging the artifact. + // Wasmtime intentionally cannot compile the legacy encoding. + features.set(WasmFeatures::EXCEPTIONS, true); + features.set(WasmFeatures::LEGACY_EXCEPTIONS, false); + features +} + +/// The explicitly selected pthread profile extends the ordinary AgentOS +/// surface with only core shared-memory threads/atomics. All unrelated +/// proposals remain locked to the same values as the single-thread profile. +pub fn locked_threaded_wasm_features() -> WasmFeatures { + let mut features = locked_wasm_features(); + features.set(WasmFeatures::THREADS, true); features } pub fn validate_locked_profile(bytes: &[u8]) -> Result<(), HostServiceError> { - Validator::new_with_features(locked_wasm_features()) + validate_profile(bytes, false) +} + +pub fn validate_locked_threaded_profile(bytes: &[u8]) -> Result<(), HostServiceError> { + validate_profile(bytes, true) +} + +fn validate_profile(bytes: &[u8], threaded: bool) -> Result<(), HostServiceError> { + let features = if threaded { + locked_threaded_wasm_features() + } else { + locked_wasm_features() + }; + Validator::new_with_features(features) .validate_all(bytes) .map(|_| ()) .map_err(|error| { @@ -41,19 +69,37 @@ mod tests { use super::*; #[test] - fn locked_profile_accepts_simd_and_rejects_threads_and_memory64() { + fn locked_profile_accepts_simd_finalized_exceptions_and_rejects_legacy_threads_and_memory64() { + let features = locked_wasm_features(); + assert!(features.contains(WasmFeatures::EXCEPTIONS)); + assert!(!features.contains(WasmFeatures::LEGACY_EXCEPTIONS)); validate_locked_profile(&wat::parse_str("(module (func (drop (f64.const 1))))").unwrap()) .expect("MVP floating point is enabled"); validate_locked_profile( &wat::parse_str("(module (func (drop (v128.const i32x4 1 2 3 4))))").unwrap(), ) .expect("SIMD128 is enabled"); + validate_locked_profile(&wat::parse_str("(module (tag (param i32)))").unwrap()) + .expect("exception tags required by the canonical DuckDB artifact are enabled"); let threads = wat::parse_str("(module (memory 1 1 shared))").unwrap(); assert!(validate_locked_profile(&threads).is_err()); let memory64 = wat::parse_str("(module (memory i64 1))").unwrap(); assert!(validate_locked_profile(&memory64).is_err()); } + #[test] + fn threaded_profile_accepts_shared_memory_without_widening_other_proposals() { + let threads = wat::parse_str("(module (memory 1 2 shared))").unwrap(); + validate_locked_threaded_profile(&threads).expect("core threads are enabled"); + assert!(validate_locked_profile(&threads).is_err()); + + let memory64 = wat::parse_str("(module (memory i64 1))").unwrap(); + assert!(validate_locked_threaded_profile(&memory64).is_err()); + let tail_call = + wat::parse_str("(module (func $callee) (func (return_call $callee)))").unwrap(); + assert!(validate_locked_threaded_profile(&tail_call).is_err()); + } + #[test] fn locked_profile_rejects_multi_memory_relaxed_simd_and_tail_calls() { let multi_memory = wat::parse_str("(module (memory 1) (memory 1))").unwrap(); diff --git a/crates/execution/src/wasm/wasmtime/engine.rs b/crates/execution/src/wasm/wasmtime/engine.rs index 0eb0d6cf7d..05c30de2d3 100644 --- a/crates/execution/src/wasm/wasmtime/engine.rs +++ b/crates/execution/src/wasm/wasmtime/engine.rs @@ -34,6 +34,9 @@ pub enum WasmtimeFeatureProfile { /// AgentOS-owned Preview1/POSIX ABI with the proposal switches configured /// in `build_engine`; changing any switch requires a new keyed variant. AgentOsOwnedWasiV1, + /// AgentOS-owned Preview1/POSIX ABI plus the core WebAssembly threads + /// proposal. This profile is selected explicitly and never inferred. + AgentOsOwnedWasiV1Threads, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -44,6 +47,20 @@ pub struct WasmtimeEngineProfile { impl WasmtimeEngineProfile { pub fn new(wasm_stack_bytes: Option) -> Result { + Self::with_feature_profile(wasm_stack_bytes, WasmtimeFeatureProfile::AgentOsOwnedWasiV1) + } + + pub fn new_threaded(wasm_stack_bytes: Option) -> Result { + Self::with_feature_profile( + wasm_stack_bytes, + WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads, + ) + } + + fn with_feature_profile( + wasm_stack_bytes: Option, + feature_profile: WasmtimeFeatureProfile, + ) -> Result { let wasm_stack_bytes = wasm_stack_bytes .map(usize::try_from) .transpose() @@ -56,7 +73,7 @@ impl WasmtimeEngineProfile { .checked_add(HOST_CALL_STACK_HEADROOM_BYTES) .ok_or_else(|| invalid_stack("WASM plus host-call stack reservation overflows"))?; Ok(Self { - feature_profile: WasmtimeFeatureProfile::AgentOsOwnedWasiV1, + feature_profile, wasm_stack_bytes, }) } @@ -269,6 +286,10 @@ fn process_retained_rss_bytes() -> Option { } fn build_engine(profile: WasmtimeEngineProfile) -> Result { + let threaded = matches!( + profile.feature_profile, + WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads + ); let mut config = Config::new(); config .epoch_interruption(true) @@ -278,9 +299,10 @@ fn build_engine(profile: WasmtimeEngineProfile) -> Result Result, paused: Arc, pause_notify: Arc, - cancel_notify: Arc, + pub(super) cancel_notify: Arc, started: AtomicBool, start_notify: Notify, engine: Mutex>>, signal_checkpoints: Mutex>, max_signal_checkpoints: usize, signal_pending: Arc, + pub(super) worker_pid: AtomicU32, + pub(super) teardown_timeout: Duration, + worker_input: Mutex>>, } impl Control { - fn new(started: bool, max_signal_checkpoints: usize) -> Self { + fn new(started: bool, max_signal_checkpoints: usize, teardown_timeout: Duration) -> Self { Self { cancelled: Arc::new(AtomicBool::new(false)), paused: Arc::new(AtomicBool::new(false)), @@ -107,6 +111,9 @@ impl Control { signal_checkpoints: Mutex::new(VecDeque::new()), max_signal_checkpoints: max_signal_checkpoints.max(1), signal_pending: Arc::new(AtomicBool::new(false)), + worker_pid: AtomicU32::new(0), + teardown_timeout, + worker_input: Mutex::new(None), } } @@ -140,12 +147,26 @@ impl Control { } checkpoints.push_back((identity, delivery)); self.signal_pending.store(true, Ordering::Release); + if let Ok(input) = self.worker_input.lock() { + if let Some(input) = input.as_ref() { + if let Err(error) = input.try_send(super::worker::ParentFrame::SignalWake) { + checkpoints.pop_back(); + self.signal_pending + .store(!checkpoints.is_empty(), Ordering::Release); + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_CONTROL_LIMIT", + format!("thread-worker control queue rejected signal wake: {error}"), + )); + } + } + } Ok(()) } fn take_signal( &self, identity: ExecutionWakeIdentity, + thread_id: Option, ) -> Result, HostServiceError> { let mut checkpoints = self.signal_checkpoints.lock().map_err(|_| { HostServiceError::new( @@ -153,16 +174,13 @@ impl Control { "ERR_AGENTOS_WASMTIME_SIGNAL_INBOX_POISONED: signal checkpoint state is poisoned", ) })?; - let Some((pending_identity, _)) = checkpoints.front() else { + let Some(position) = checkpoints.iter().position(|(pending_identity, delivery)| { + *pending_identity == identity + && thread_id.is_none_or(|thread_id| delivery.thread_id == thread_id) + }) else { return Ok(None); }; - if *pending_identity != identity { - return Err(HostServiceError::new( - "ESTALE", - "published signal delivery identity does not match the active Wasmtime execution", - )); - } - let delivery = checkpoints.pop_front().map(|(_, delivery)| delivery); + let delivery = checkpoints.remove(position).map(|(_, delivery)| delivery); self.signal_pending .store(!checkpoints.is_empty(), Ordering::Release); Ok(delivery) @@ -192,6 +210,42 @@ impl Control { engine.engine().increment_epoch(); } } + self.signal_worker(nix::sys::signal::Signal::SIGKILL); + } + + fn signal_worker(&self, signal: nix::sys::signal::Signal) { + let pid = self.worker_pid.load(Ordering::Acquire); + if pid == 0 { + return; + } + if let Ok(pid) = i32::try_from(pid) { + match nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), signal) { + Ok(()) | Err(nix::errno::Errno::ESRCH) => {} + Err(error) => eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_SIGNAL: failed to send {signal:?} to worker {pid}: {error}" + ), + } + } + } + + pub(super) fn set_worker_input( + &self, + sender: tokio::sync::mpsc::Sender, + ) -> Result<(), HostServiceError> { + let mut input = self.worker_input.lock().map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_CONTROL_POISONED", + "thread-worker control state is poisoned", + ) + })?; + *input = Some(sender); + Ok(()) + } + + pub(super) fn clear_worker_input(&self) { + if let Ok(mut input) = self.worker_input.lock() { + *input = None; + } } } @@ -204,6 +258,7 @@ pub struct WasmtimeExecution { worker: Mutex>>, teardown_timeout: Duration, prepared: bool, + threaded: bool, } impl std::fmt::Debug for WasmtimeExecution { @@ -225,6 +280,7 @@ impl WasmtimeExecution { runtime: RuntimeContext, event_notify: Option>, defer_execute: bool, + threaded: bool, ) -> Result { let permit = runtime .vm_executor_admission() @@ -238,6 +294,48 @@ impl WasmtimeExecution { })), ) })?; + let thread_group_reservations = if threaded { + let maximum = request.limits.max_threads.unwrap_or(16).max(1); + let threads = runtime + .resources() + .reserve(ResourceClass::WasmThreads, maximum) + .map_err(|error| { + WasmExecutionError::Host( + HostServiceError::new("ERR_AGENTOS_WASM_THREAD_LIMIT", error.to_string()) + .with_details(serde_json::json!({ + "limitName": "limits.wasm.maxThreads", + "observed": maximum, + })), + ) + })?; + let profile = WasmtimeEngineProfile::new_threaded(request.limits.max_stack_bytes) + .map_err(WasmExecutionError::Host)?; + let async_stack_bytes = profile + .async_stack_bytes() + .map_err(WasmExecutionError::Host)?; + let memory_bytes = + limits::threaded_group_memory_bytes(&request.limits, async_stack_bytes) + .map_err(WasmExecutionError::Host)?; + let memory = runtime + .resources() + .reserve(ResourceClass::WasmMemoryBytes, memory_bytes) + .map_err(|error| { + WasmExecutionError::Host( + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_GROUP_MEMORY_LIMIT", + error.to_string(), + ) + .with_details(serde_json::json!({ + "limitName": "runtime.resources.maxWasmMemoryBytes", + "observed": memory_bytes, + "maxThreads": maximum, + })), + ) + })?; + Some((threads, memory)) + } else { + None + }; let pending_count = request.limits.pending_event_count.unwrap_or(64).max(1); let (event_sender, events) = flume::bounded(pending_count); let (done_sender, worker_done) = flume::bounded(1); @@ -245,6 +343,7 @@ impl WasmtimeExecution { let control = Arc::new(Control::new( !defer_execute, request.limits.pending_event_count.unwrap_or(64), + runtime.vm_executor_teardown_timeout(), )); let worker_host = Arc::clone(&host); let worker_control = Arc::clone(&control); @@ -254,6 +353,10 @@ impl WasmtimeExecution { .name(format!("agentos-wasmtime-{execution_id}")) .spawn(move || { let _permit = permit; + // Admission is transactional for the whole potential group: + // no guest code starts unless both the VM and its process + // parent can reserve the configured maximum thread count. + let _thread_group_reservations = thread_group_reservations; let event_resources = Arc::clone(worker_runtime.resources()); let result = worker_runtime.handle().block_on(run_execution( module_path, @@ -263,6 +366,7 @@ impl WasmtimeExecution { Arc::clone(&worker_control), event_sender.clone(), event_notify.clone(), + threaded, )); publish_worker_result( &event_sender, @@ -286,6 +390,7 @@ impl WasmtimeExecution { worker: Mutex::new(Some(worker)), teardown_timeout: runtime.vm_executor_teardown_timeout(), prepared: defer_execute, + threaded, }) } @@ -293,6 +398,10 @@ impl WasmtimeExecution { &self.execution_id } + pub fn is_threaded(&self) -> bool { + self.threaded + } + pub fn configure_host_services(&self, host: ProcessHostCapabilitySet) { match self.host.value.lock() { Ok(mut slot) if slot.is_none() => { @@ -345,6 +454,11 @@ impl WasmtimeExecution { engine.engine().increment_epoch(); } } + self.control.signal_worker(if paused { + nix::sys::signal::Signal::SIGSTOP + } else { + nix::sys::signal::Signal::SIGCONT + }); } pub fn deliver_signal_checkpoint( @@ -353,6 +467,7 @@ impl WasmtimeExecution { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result<(), HostServiceError> { self.control.publish_signal( identity, @@ -360,6 +475,7 @@ impl WasmtimeExecution { signal, delivery_token, flags, + thread_id, }, ) } @@ -368,7 +484,15 @@ impl WasmtimeExecution { &self, identity: ExecutionWakeIdentity, ) -> Result, HostServiceError> { - self.control.take_signal(identity) + self.control.take_signal(identity, None) + } + + pub fn take_signal_checkpoint_for_thread( + &self, + identity: ExecutionWakeIdentity, + thread_id: u32, + ) -> Result, HostServiceError> { + self.control.take_signal(identity, Some(thread_id)) } pub fn discard_signal_checkpoints( @@ -459,6 +583,7 @@ impl WasmtimeExecutionEngine { } } +#[allow(clippy::too_many_arguments)] async fn run_execution( module_path: String, request: StartWasmExecutionRequest, @@ -467,6 +592,7 @@ async fn run_execution( control: Arc, event_sender: Sender, event_notify: Option>, + threaded: bool, ) -> Result { let diagnostics = Arc::new(ExecutionDiagnostics::new( request @@ -487,8 +613,21 @@ async fn run_execution( event_notify, ) .with_diagnostics(Arc::clone(&diagnostics)); + if threaded && !cfg!(test) { + host.submit( + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens), + 0, + ) + .await?; + let bytes = load_module(&host, &module_path, &request).await?; + return super::worker::run_worker_process(bytes, request, host, control).await; + } let engine_started = Instant::now(); - let profile = WasmtimeEngineProfile::new(request.limits.max_stack_bytes)?; + let profile = if threaded { + WasmtimeEngineProfile::new_threaded(request.limits.max_stack_bytes)? + } else { + WasmtimeEngineProfile::new(request.limits.max_stack_bytes)? + }; let engine = WasmtimeEngineRegistry::process().get_or_create(profile)?; diagnostics.phase("Engine", engine_started.elapsed()); control @@ -574,6 +713,34 @@ async fn run_loaded_module( let module_read_started = Instant::now(); let bytes = load_module(&host, &module_path, &request).await?; diagnostics.phase("moduleRead", module_read_started.elapsed()); + run_loaded_module_bytes( + module_path, + request, + bytes, + runtime, + host, + engine, + profile, + paused, + pause_notify, + diagnostics, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +async fn run_loaded_module_bytes( + _module_path: String, + request: StartWasmExecutionRequest, + bytes: Vec, + runtime: RuntimeContext, + host: WasmtimeHostClient, + engine: Arc, + profile: WasmtimeEngineProfile, + paused: Arc, + pause_notify: Arc, + diagnostics: Arc, +) -> Result { let compiled = super::module::compile_module(&engine, &bytes)?; diagnostics.phase("profileValidation", compiled.profile_validation); diagnostics.phase("moduleCompile", compiled.compilation); @@ -581,22 +748,41 @@ async fn run_loaded_module( let mut module = compiled.module; let mut request = request; let import_validation_started = Instant::now(); - linker::validate_module_imports(&module, request.permission_tier)?; + let threaded = matches!( + profile.feature_profile, + super::engine::WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads + ); + linker::validate_module_imports(&module, request.permission_tier, threaded)?; diagnostics.phase("importValidation", import_validation_started.elapsed()); - let linker_started = Instant::now(); - let linker = - linker::build_linker(engine.engine(), request.permission_tier).map_err(|error| { + // One process image may replace itself repeatedly with fexecve. Preserve + // the same active-CPU origin across Stores so exec cannot reset its budget. + let active_cpu_started_ns = store::thread_cpu_time_ns(); + loop { + let module_uses_threads = threaded && module_uses_thread_runtime(&module); + let thread_group = if module_uses_threads { + Some(ThreadGroup::new( + Arc::clone(&engine), + Arc::clone(&module), + runtime.clone(), + host.clone(), + request.clone(), + profile, + Arc::clone(&paused), + Arc::clone(&pause_notify), + )?) + } else { + None + }; + let linker_started = Instant::now(); + let mut linker = linker::build_linker(engine.engine(), request.permission_tier, threaded) + .map_err(|error| { eprintln!("ERR_AGENTOS_WASMTIME_LINKER: private linker diagnostic: {error:#}"); HostServiceError::new( "ERR_AGENTOS_WASMTIME_LINKER", "failed to build the AgentOS WebAssembly host linker", ) })?; - diagnostics.phase("Linker", linker_started.elapsed()); - // One process image may replace itself repeatedly with fexecve. Preserve - // the same active-CPU origin across Stores so exec cannot reset its budget. - let active_cpu_started_ns = store::thread_cpu_time_ns(); - loop { + diagnostics.phase("Linker", linker_started.elapsed()); let store_started = Instant::now(); let mut store = store::create_store( Arc::clone(&engine), @@ -607,7 +793,26 @@ async fn run_loaded_module( active_cpu_started_ns, Arc::clone(&paused), Arc::clone(&pause_notify), + matches!( + profile.feature_profile, + super::engine::WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads + ), + thread_group.clone(), + 0, )?; + if let Some(group) = thread_group.as_ref() { + linker + .define(&store, "env", "memory", group.memory().clone()) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASM_THREAD_MEMORY_LINK: private linker diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_MEMORY_LINK", + "failed to link the threaded WebAssembly shared memory", + ) + })?; + } diagnostics.phase("Store", store_started.elapsed()); let async_stack_bytes = profile.async_stack_bytes()?; let reserved_store_bytes = async_stack_bytes @@ -642,7 +847,18 @@ async fn run_loaded_module( reserved_store_bytes, ); let call_started = Instant::now(); - let call_result = start.call_async(&mut store, ()).await; + let call_result = if let Some(group) = thread_group.as_ref() { + tokio::select! { + result = start.call_async(&mut store, ()) => result, + failure = group.wait_for_failure() => return Err(failure), + } + } else { + start.call_async(&mut store, ()).await + }; + thread_group + .as_ref() + .map(|group| group.settle_main()) + .transpose()?; diagnostics.phase("wasi.start", call_started.elapsed()); diagnostics.store_memory( guest_linear_memory_bytes(&instance, &mut store), @@ -670,7 +886,7 @@ async fn run_loaded_module( request.argv = replacement.argv; request.env = replacement.env; module = replacement.module; - linker::validate_module_imports(&module, request.permission_tier)?; + linker::validate_module_imports(&module, request.permission_tier, threaded)?; let teardown_started = Instant::now(); drop(start); drop(store); @@ -702,13 +918,64 @@ async fn run_loaded_module( } } +fn module_uses_thread_runtime(module: &wasmtime::Module) -> bool { + module + .imports() + .any(|import| match (import.module(), import.name()) { + ("wasi", "thread-spawn") => true, + ("env", "memory") => matches!( + import.ty(), + wasmtime::ExternType::Memory(memory) if memory.is_shared() + ), + _ => false, + }) +} + +pub(super) async fn run_worker_loaded_module( + request: StartWasmExecutionRequest, + bytes: Vec, + runtime: RuntimeContext, + worker: super::worker::WorkerIpcClient, +) -> Result { + let cancelled = Arc::new(AtomicBool::new(false)); + let cancel_notify = Arc::new(Notify::new()); + let signal_pending = Arc::new(AtomicBool::new(false)); + let (events, _event_receiver) = flume::bounded(1); + let host = WasmtimeHostClient::new_worker( + worker, + store::max_host_reply_bytes(&request)?, + cancelled, + cancel_notify, + signal_pending, + Arc::clone(runtime.resources()), + events, + ); + let profile = WasmtimeEngineProfile::new_threaded(request.limits.max_stack_bytes)?; + let engine = WasmtimeEngineRegistry::process().get_or_create(profile)?; + run_loaded_module_bytes( + String::from(""), + request, + bytes, + runtime, + host, + engine, + profile, + Arc::new(AtomicBool::new(false)), + Arc::new(Notify::new()), + Arc::new(ExecutionDiagnostics::new(false)), + ) + .await +} + fn guest_linear_memory_bytes( instance: &wasmtime::Instance, store: &mut wasmtime::Store, ) -> usize { - instance - .get_memory(&mut *store, "memory") - .map_or(0, |memory| memory.data_size(&*store)) + match instance.get_export(&mut *store, "memory") { + Some(wasmtime::Extern::Memory(memory)) => memory.data_size(&*store), + Some(wasmtime::Extern::SharedMemory(memory)) => memory.data_size(), + _ => 0, + } } async fn wait_until_started(control: &Control) -> Result<(), HostServiceError> { @@ -794,11 +1061,23 @@ pub(super) async fn load_executable_image( }; let open = host .submit( - HostOperation::Process(ProcessOperation::OpenExecutableImage { source }), + HostOperation::Process(ProcessOperation::OpenExecutableImage { + source, + resolution: None, + }), retained_request_bytes, ) .await?; - let (handle, size) = decode_open_image(open)?; + let (bytes, _) = read_open_executable_image(host, open, maximum).await?; + Ok(bytes) +} + +pub(super) async fn read_open_executable_image( + host: &WasmtimeHostClient, + open: HostCallReply, + maximum: usize, +) -> Result<(Vec, Option>), HostServiceError> { + let (handle, size, argv) = decode_open_image(open)?; let result = read_module_image(host, handle, size, maximum).await; let close = host .submit( @@ -807,7 +1086,7 @@ pub(super) async fn load_executable_image( ) .await; match (result, close) { - (Ok(bytes), Ok(_)) => Ok(bytes), + (Ok(bytes), Ok(_)) => Ok((bytes, argv)), (Err(error), Ok(_)) => Err(error), (Ok(_), Err(error)) => Err(error), (Err(error), Err(close_error)) => { @@ -820,7 +1099,9 @@ pub(super) async fn load_executable_image( } } -fn decode_open_image(reply: HostCallReply) -> Result<(u64, usize), HostServiceError> { +fn decode_open_image( + reply: HostCallReply, +) -> Result<(u64, usize, Option>), HostServiceError> { let HostCallReply::Json(value) = reply else { return Err(HostServiceError::new( "ERR_AGENTOS_WASMTIME_IMAGE_REPLY", @@ -838,7 +1119,16 @@ fn decode_open_image(reply: HostCallReply) -> Result<(u64, usize), HostServiceEr .and_then(Value::as_u64) .and_then(|size| usize::try_from(size).ok()) .ok_or_else(|| HostServiceError::new("EFBIG", "image size does not fit this platform"))?; - Ok((handle, size)) + let argv = value + .get("argv") + .filter(|value| !value.is_null()) + .map(|value| { + serde_json::from_value::>(value.clone()).map_err(|error| { + HostServiceError::new("EIO", format!("executable-image argv is invalid: {error}")) + }) + }) + .transpose()?; + Ok((handle, size, argv)) } async fn read_module_image( @@ -858,8 +1148,14 @@ async fn read_module_image( "observed": size, }))); } - let read_limit = - PayloadLimit::new("limits.wasm.moduleReadChunkBytes", MODULE_READ_CHUNK_BYTES)?; + // This bound describes the fixed chunk size selected by the runtime, not + // guest consumption of a configurable resource. Using the normal warning + // hook here would warn on every full-sized internal transfer. + let read_limit = PayloadLimit::with_warning_hook( + "limits.wasm.moduleReadChunkBytes", + MODULE_READ_CHUNK_BYTES, + None, + )?; let mut bytes = Vec::with_capacity(size); while bytes.len() < size { let requested = (size - bytes.len()).min(MODULE_READ_CHUNK_BYTES); @@ -927,7 +1223,17 @@ fn publish_worker_result( Ok(WasmExecutionEvent::Exited(code)), 0, ), - Err(error) if error.code == "ERR_AGENTOS_EXEC_REPLACED" => {} + Err(error) + if matches!( + error.code.as_str(), + "ERR_AGENTOS_EXEC_REPLACED" | "ECANCELED" + ) => + { + // Exec replacement and sidecar-directed cancellation have their + // authoritative lifecycle event published by the controller. + // They are not guest program failures and must not leak synthetic + // stderr or a competing exit status from the worker. + } Err(error) => { let message = format!("{}: {}\n", error.code, error.message); let message_bytes = message.into_bytes(); @@ -1003,6 +1309,31 @@ mod tests { assert_eq!(resources.usage(ResourceClass::AsyncCompletionBytes).used, 0); } + #[test] + fn controller_cancellation_does_not_publish_guest_failure_events() { + let resources = Arc::new(ResourceLedger::root( + "wasmtime-cancel-test", + [( + ResourceClass::AsyncCompletionBytes, + ResourceLimit::new(1024, "limits.reactor.maxAsyncCompletionBytes"), + )], + )); + let (sender, receiver) = flume::bounded(2); + publish_worker_result( + &sender, + None, + &resources, + Err(HostServiceError::new( + "ECANCELED", + "controller requested teardown", + )), + ); + assert!(matches!( + receiver.try_recv(), + Err(flume::TryRecvError::Empty) + )); + } + #[test] fn executes_kernel_supplied_module_without_v8_or_ambient_wasi() { let runtime = SidecarRuntime::process(&RuntimeConfig::default()) @@ -1088,6 +1419,7 @@ mod tests { runtime, None, false, + false, ) .expect("spawn executor"); execution @@ -1101,6 +1433,133 @@ mod tests { host_worker.join().expect("host worker"); } + #[test] + fn threaded_profile_spawns_a_store_sharing_atomic_memory() { + let runtime = SidecarRuntime::process(&RuntimeConfig::default()) + .expect("test runtime") + .context(); + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (i32.atomic.rmw.add (i32.const 0) (i32.const 1))) + (drop (memory.atomic.notify (i32.const 4) (i32.const 1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 99)) (i32.const 1)) + (then unreachable)) + (loop $wait + (if (i32.lt_u (i32.atomic.load (i32.const 0)) (i32.const 1)) + (then + (drop (memory.atomic.wait32 + (i32.const 4) (i32.const 0) (i64.const -1))) + (br $wait))))))"#, + ) + .expect("threaded test module"); + let request = StartWasmExecutionRequest { + vm_id: String::from("vm-thread-test"), + context_id: String::from("ctx-thread-test"), + managed_kernel_host: true, + argv: vec![String::from("/thread-test.wasm")], + env: BTreeMap::new(), + cwd: PathBuf::from("/"), + permission_tier: WasmPermissionTier::Full, + limits: WasmExecutionLimits { + max_threads: Some(2), + ..WasmExecutionLimits::default() + }, + guest_runtime: GuestRuntimeConfig::default(), + }; + let process = crate::host::HostProcessContext { + generation: 9, + pid: 44, + }; + let (submission, host_events) = bounded_execution_event_channel( + process, + 16, + PayloadLimit::new("limits.process.pendingEventBytes", 1024 * 1024) + .expect("event byte limit"), + Arc::new(|| {}), + ) + .expect("host event channel"); + let module_for_host = module.clone(); + let host_worker = std::thread::spawn(move || { + let mut completed = 0; + while completed < 8 { + let Some(event) = host_events.try_recv().expect("host event poll") else { + std::thread::yield_now(); + continue; + }; + let ExecutionEvent::HostCall { operation, reply } = event else { + panic!("unexpected non-host event"); + }; + match operation { + HostOperation::Filesystem(FilesystemOperation::CanonicalPreopens) => reply + .succeed_json(Value::Null) + .expect("canonical-preopens reply"), + HostOperation::Process(ProcessOperation::OpenExecutableImage { .. }) => reply + .succeed_json(serde_json::json!({ + "handle": "1", + "size": module_for_host.len(), + })) + .expect("open reply"), + HostOperation::Process(ProcessOperation::ReadExecutableImage { + offset, + max_bytes, + .. + }) => { + let start = offset as usize; + let end = start + .saturating_add(max_bytes.get()) + .min(module_for_host.len()); + reply + .succeed_raw(module_for_host[start..end].to_vec()) + .expect("read reply"); + } + HostOperation::Process(ProcessOperation::CloseExecutableImage { .. }) => { + reply.succeed_json(Value::Null).expect("close reply"); + } + HostOperation::Signal( + crate::host::SignalOperation::UpdateMask { .. } + | crate::host::SignalOperation::UpdateMaskForThread { .. }, + ) => { + reply + .succeed_json(serde_json::json!({ "signals": [] })) + .expect("signal-mask reply"); + } + HostOperation::Signal( + crate::host::SignalOperation::RegisterThread { .. } + | crate::host::SignalOperation::UnregisterThread { .. }, + ) => reply + .succeed_json(Value::Null) + .expect("signal-thread lifecycle reply"), + operation => panic!("unexpected host operation: {operation:?}"), + } + completed += 1; + } + }); + let execution = WasmtimeExecution::spawn( + String::from("exec-thread-test"), + String::from("/thread-test.wasm"), + request, + runtime, + None, + false, + true, + ) + .expect("spawn threaded executor"); + execution + .configure_host_services(ProcessHostCapabilitySet::from_event_submission(submission)); + assert_eq!( + execution + .poll_event_blocking(Duration::from_secs(10)) + .expect("threaded execution event"), + Some(WasmExecutionEvent::Exited(0)) + ); + host_worker.join().expect("host worker"); + } + #[test] fn native_preview1_import_uses_owned_direct_waiter_event() { let runtime = SidecarRuntime::process(&RuntimeConfig::default()) @@ -1196,6 +1655,7 @@ mod tests { runtime, None, false, + false, ) .expect("spawn executor"); execution @@ -1315,7 +1775,7 @@ mod tests { std::thread::yield_now(); }; execution - .deliver_signal_checkpoint(identity, 10, 99, 0) + .deliver_signal_checkpoint(identity, 10, 99, 0, 0) .expect("publish signal"); reply .succeed_json(serde_json::json!({ "signals": [] })) @@ -1353,6 +1813,7 @@ mod tests { runtime, None, false, + false, ) .expect("spawn executor"), ); diff --git a/crates/execution/src/wasm/wasmtime/limits.rs b/crates/execution/src/wasm/wasmtime/limits.rs index 3fa9c350ca..d4e33f12b7 100644 --- a/crates/execution/src/wasm/wasmtime/limits.rs +++ b/crates/execution/src/wasm/wasmtime/limits.rs @@ -41,11 +41,36 @@ pub fn max_memory_bytes(limits: &WasmExecutionLimits) -> Result Result { + // Finalized WebAssembly exceptions allocate exnref objects in Wasmtime's + // internal GC heap. StoreLimits applies the same per-memory byte cap to + // both that heap and the guest's linear memory, so admission must reserve + // both worst-case regions even though the guest GC proposal stays disabled. max_memory_bytes(limits)? + .checked_mul(2) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes"))? .checked_add(DEFAULT_TABLE_ACCOUNTING_BYTES) .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes")) } +pub fn threaded_group_memory_bytes( + limits: &WasmExecutionLimits, + async_stack_bytes: usize, +) -> Result { + let maximum_threads = limits.max_threads.unwrap_or(16).max(1); + let per_store = async_stack_bytes + .checked_add(max_memory_bytes(limits)?) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes"))? + .checked_add(DEFAULT_TABLE_ACCOUNTING_BYTES) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes"))?; + max_memory_bytes(limits)? + .checked_add( + per_store + .checked_mul(maximum_threads) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes"))?, + ) + .ok_or_else(|| limit_overflow("limits.resources.maxWasmMemoryBytes")) +} + fn limit_overflow(name: &'static str) -> HostServiceError { HostServiceError::new( "ERR_AGENTOS_WASMTIME_LIMIT_CONFIG", diff --git a/crates/execution/src/wasm/wasmtime/linker/mod.rs b/crates/execution/src/wasm/wasmtime/linker/mod.rs index 0819d4c59e..cbeb80f52d 100644 --- a/crates/execution/src/wasm/wasmtime/linker/mod.rs +++ b/crates/execution/src/wasm/wasmtime/linker/mod.rs @@ -29,6 +29,7 @@ const MAX_SIGNALS_PER_SAFE_POINT: usize = 64; pub fn build_linker( engine: &Engine, tier: WasmPermissionTier, + threaded: bool, ) -> wasmtime::Result> { let mut linker = Linker::new(engine); for abi in ABI_BINDINGS { @@ -42,6 +43,19 @@ pub fn build_linker( link_binding(&mut linker, engine, abi, alias.alias_module)?; } } + if threaded { + linker.func_wrap( + "wasi", + "thread-spawn", + |caller: Caller<'_, WasmtimeStoreState>, start_arg: i32| -> i32 { + caller + .data() + .thread_group + .as_ref() + .map_or(-1, |group| group.spawn(start_arg)) + }, + )?; + } Ok(linker) } @@ -52,9 +66,12 @@ pub fn build_linker( pub fn validate_module_imports( module: &Module, tier: WasmPermissionTier, + threaded: bool, ) -> Result<(), HostServiceError> { for import in module.imports() { - if import_permitted(import.module(), import.name(), tier) { + if import_permitted(import.module(), import.name(), tier) + || (threaded && is_thread_runtime_import(import.module(), import.name())) + { continue; } return Err(HostServiceError::new( @@ -73,6 +90,10 @@ pub fn validate_module_imports( Ok(()) } +fn is_thread_runtime_import(module: &str, name: &str) -> bool { + matches!((module, name), ("env", "memory") | ("wasi", "thread-spawn")) +} + fn import_permitted(module: &str, name: &str, tier: WasmPermissionTier) -> bool { ABI_BINDINGS .iter() @@ -135,11 +156,15 @@ async fn dispatch( "ERR_AGENTOS_WASMTIME_CANCELED: execution canceled" )); } - drain_signal_checkpoints(caller).await?; + drain_signal_checkpoints(caller, false).await?; loop { dispatch_once(caller, abi, params, results).await?; - let signals = drain_signal_checkpoints(caller).await?; let interrupted = matches!(results, [Val::I32(value)] if *value == WASI_ERRNO_INTR); + // EINTR is itself an authoritative signal wake. In the threaded worker + // topology its reply can become runnable just before the coalesced + // SignalWake frame updates the local fast-path counter, so probe the + // parent-owned checkpoint once unconditionally. + let signals = drain_signal_checkpoints(caller, interrupted).await?; if interrupted && abi.restartability == Restartability::SignalRestartable && signals.delivered @@ -266,6 +291,7 @@ struct SignalDispatch { async fn drain_signal_checkpoints( caller: &mut Caller<'_, WasmtimeStoreState>, + mut force_probe: bool, ) -> wasmtime::Result { let mut outcome = SignalDispatch { delivered: false, @@ -273,14 +299,20 @@ async fn drain_signal_checkpoints( }; for _ in 0..MAX_SIGNALS_PER_SAFE_POINT { let host = caller.data().host.clone(); - if !host.signal_pending() { + if !force_probe && !host.signal_pending() { return Ok(outcome); } + force_probe = false; + let thread_id = u32::try_from(caller.data().thread_id).map_err(|_| { + wasmtime::format_err!("ERR_AGENTOS_WASMTIME_SIGNAL_THREAD_ID: invalid thread id") + })?; + let take = if caller.data().thread_group.is_some() { + SignalOperation::TakePublishedDeliveryForThread { thread_id } + } else { + SignalOperation::TakePublishedDelivery + }; let reply = host - .submit( - HostOperation::Signal(SignalOperation::TakePublishedDelivery), - 0, - ) + .submit(HostOperation::Signal(take), std::mem::size_of::()) .await .map_err(wasmtime_host_error)?; let value = host_json(reply, "process.take_signal")?; @@ -319,13 +351,18 @@ async fn drain_signal_checkpoints( ) })?; let handler_result = trampoline.call_async(&mut *caller, signal).await; + let end = if caller.data().thread_group.is_some() { + SignalOperation::EndDeliveryForThread { thread_id, token } + } else { + SignalOperation::EndDelivery { token } + }; let end_result = caller .data() .host .clone() .submit( - HostOperation::Signal(SignalOperation::EndDelivery { token }), - std::mem::size_of::(), + HostOperation::Signal(end), + std::mem::size_of::() + std::mem::size_of::(), ) .await; if let Err(error) = handler_result { @@ -347,15 +384,30 @@ pub async fn initialize_inherited_signal_mask( store: &mut wasmtime::Store, instance: &wasmtime::Instance, ) -> Result<(), HostServiceError> { + let thread_id = u32::try_from(store.data().thread_id).map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_SIGNAL_THREAD_ID", + "invalid Wasmtime signal thread id", + ) + })?; + let update = if store.data().thread_group.is_some() { + SignalOperation::UpdateMaskForThread { + thread_id, + how: SignalMaskHow::Block, + set: SignalSetValue::default(), + } + } else { + SignalOperation::UpdateMask { + how: SignalMaskHow::Block, + set: SignalSetValue::default(), + } + }; let reply = store .data() .host .clone() .submit( - HostOperation::Signal(SignalOperation::UpdateMask { - how: SignalMaskHow::Block, - set: SignalSetValue::default(), - }), + HostOperation::Signal(update), std::mem::size_of::(), ) .await?; @@ -552,7 +604,7 @@ mod tests { .expect("allowed import module"), ) .expect("compile allowed import module"); - validate_module_imports(&module, WasmPermissionTier::Isolated) + validate_module_imports(&module, WasmPermissionTier::Isolated, false) .expect("generated registry permits import"); let hostile = Module::new( @@ -561,7 +613,7 @@ mod tests { .expect("hostile import module"), ) .expect("compile hostile import module"); - let error = validate_module_imports(&hostile, WasmPermissionTier::Full) + let error = validate_module_imports(&hostile, WasmPermissionTier::Full, false) .expect_err("unknown import must fail before linker diagnostics"); assert_eq!(error.code, "ERR_AGENTOS_WASM_UNSUPPORTED_IMPORT"); assert_eq!( diff --git a/crates/execution/src/wasm/wasmtime/linker/network.rs b/crates/execution/src/wasm/wasmtime/linker/network.rs index 888d550787..5b43c50385 100644 --- a/crates/execution/src/wasm/wasmtime/linker/network.rs +++ b/crates/execution/src/wasm/wasmtime/linker/network.rs @@ -15,6 +15,7 @@ use crate::wasm::wasmtime::{memory, store::WasmtimeStoreState}; use base64::Engine as _; use serde_json::{json, Value}; use std::collections::HashMap; +use std::time::Instant; use wasmtime::{Caller, Val}; const ERRNO_AGAIN: i32 = 6; @@ -31,7 +32,10 @@ const SOCK_STREAM: u32 = 6; const SOCK_CLOEXEC: u32 = 0x2000; const SOCK_NONBLOCK: u32 = 0x4000; const KERNEL_O_NONBLOCK: u32 = 0x800; +const MSG_DONTWAIT: u32 = 0x40; const MSG_TRUNC: u32 = 0x20; +const POLLIN: u32 = 0x001; +const POLLRDNORM: u32 = 0x040; pub async fn dispatch( caller: &mut Caller<'_, WasmtimeStoreState>, @@ -458,49 +462,161 @@ async fn accept(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> { return ERRNO_FAULT; } - match call( - caller, - "process.hostnet_accept", - vec![json!(fd), json!(false), json!(false), Value::Null], - HashMap::new(), - ) - .await - { - Ok(reply) => { - let Ok(value) = json_reply(reply) else { + let nonblocking = match fd_is_nonblocking(caller, fd).await { + Ok(nonblocking) => nonblocking, + Err(error) => return error, + }; + let started = Instant::now(); + let mut warned_near_limit = false; + loop { + match call( + caller, + "process.hostnet_accept", + vec![json!(fd), json!(false), json!(false), Value::Null], + HashMap::new(), + ) + .await + { + Ok(reply) => { + let Ok(value) = json_reply(reply) else { + return ERRNO_IO; + }; + if value.is_null() + || value.get("kind").and_then(Value::as_str) == Some("wouldBlock") + { + if nonblocking { + return ERRNO_AGAIN; + } + let wait = wait_for_socket_readable( + caller, + fd, + "blocking socket accept", + started, + &mut warned_near_limit, + ) + .await; + if wait != SUCCESS { + return wait; + } + continue; + } + let Some(accepted_fd) = value + .get("fd") + .and_then(value_u64) + .and_then(|value| u32::try_from(value).ok()) + else { + return ERRNO_IO; + }; + let address = encode_address(value.get("info").unwrap_or(&value), true); + let copied = if commit(caller, fd_output, &accepted_fd.to_le_bytes()) == SUCCESS { + publish_bytes( + caller, + address_output, + capacity, + length_output, + address.as_bytes(), + false, + ) + } else { + ERRNO_FAULT + }; + if copied == SUCCESS { + return SUCCESS; + } else { + log_fd_rollback(caller, accepted_fd, "accept output commit").await; + return copied; + } + } + Err(error) => return errno(&error), + } + } +} + +pub(super) async fn fd_is_nonblocking( + caller: &mut Caller<'_, WasmtimeStoreState>, + fd: u32, +) -> Result { + match call(caller, "process.fd_stat", vec![json!(fd)], HashMap::new()).await { + Ok(reply) => json_reply(reply) + .ok() + .and_then(|value| value.get("flags").and_then(value_u64)) + .map(|flags| flags & u64::from(KERNEL_O_NONBLOCK) != 0) + .ok_or(ERRNO_IO), + Err(error) => Err(errno(&error)), + } +} + +pub(super) async fn wait_for_socket_readable( + caller: &mut Caller<'_, WasmtimeStoreState>, + fd: u32, + operation: &str, + started: Instant, + warned_near_limit: &mut bool, +) -> i32 { + let limit_ms = caller.data().max_blocking_read_ms; + loop { + let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX); + let warning_ms = limit_ms.saturating_mul(4) / 5; + if !*warned_near_limit && elapsed_ms >= warning_ms { + let warning = format!( + "[agentos] {operation} is nearing limits.resources.maxBlockingReadMs ({limit_ms} ms)\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { return ERRNO_IO; - }; - if value.is_null() || value.get("kind").and_then(Value::as_str) == Some("wouldBlock") { - return ERRNO_AGAIN; } - let Some(accepted_fd) = value - .get("fd") - .and_then(value_u64) - .and_then(|value| u32::try_from(value).ok()) - else { + *warned_near_limit = true; + } + if elapsed_ms >= limit_ms { + let warning = format!( + "[agentos] {operation} exceeded limits.resources.maxBlockingReadMs ({limit_ms} ms); raise limits.resources.maxBlockingReadMs if needed\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { return ERRNO_IO; - }; - let address = encode_address(value.get("info").unwrap_or(&value), true); - let copied = if commit(caller, fd_output, &accepted_fd.to_le_bytes()) == SUCCESS { - publish_bytes( - caller, - address_output, - capacity, - length_output, - address.as_bytes(), - false, - ) - } else { - ERRNO_FAULT - }; - if copied == SUCCESS { - SUCCESS - } else { - log_fd_rollback(caller, accepted_fd, "accept output commit").await; - copied } + return ERRNO_TIMEDOUT; + } + let checkpoint_ms = if *warned_near_limit { + limit_ms + } else { + warning_ms.max(1) + }; + let wait_ms = checkpoint_ms.saturating_sub(elapsed_ms).max(1); + let reply = call( + caller, + "process.posix_poll", + vec![ + json!([{"fd": fd, "events": POLLIN | POLLRDNORM}]), + json!(wait_ms), + Value::Null, + ], + HashMap::new(), + ) + .await; + match reply { + Ok(reply) => { + let ready = json_reply(reply) + .ok() + .and_then(|value| value.get("readyCount").and_then(value_u64)) + .unwrap_or_default() + > 0; + if ready { + return SUCCESS; + } + } + Err(error) => return errno(&error), } - Err(error) => errno(&error), } } @@ -618,15 +734,38 @@ async fn send(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], to: b Value::Null }; let mut raw = HashMap::new(); - raw.insert(1, bytes); - match call( + raw.insert(1, bytes.clone()); + let host_reply = call( caller, "process.hostnet_send", vec![json!(fd), Value::Null, json!(flags), address, Value::Null], raw, ) - .await - { + .await; + let reply = match host_reply { + reply @ Ok(_) => reply, + // The kernel owns AF_UNIX socketpair data and message boundaries. + // V8 routes non-host-network descriptors through this same operation; + // keep the native linker as a codec rather than a second socket stack. + Err(error) if !to && error.code == "ENOTSOCK" => { + let mut raw = HashMap::new(); + raw.insert(1, bytes); + call( + caller, + "process.fd_sendmsg_rights", + vec![ + json!(fd), + Value::Null, + Value::Array(Vec::new()), + json!(flags), + ], + raw, + ) + .await + } + reply => reply, + }; + match reply { Ok(reply) => { let written = match reply { HostCallReply::Json(value) => { @@ -675,76 +814,140 @@ async fn receive(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], fr { return ERRNO_FAULT; } - match call( - caller, - "process.hostnet_recv", - vec![json!(fd), json!(capacity), json!(flags), Value::Null], - HashMap::new(), - ) - .await - { - Ok(reply) => { - if matches!(&reply, HostCallReply::Json(Value::Null)) { - return commit(caller, length_output, &0u32.to_le_bytes()); + let nonblocking = match fd_is_nonblocking(caller, fd).await { + Ok(nonblocking) => nonblocking || flags & MSG_DONTWAIT != 0, + Err(error) => return error, + }; + let started = Instant::now(); + let mut warned_near_limit = false; + loop { + let host_reply = call( + caller, + "process.hostnet_recv", + vec![json!(fd), json!(capacity), json!(flags), Value::Null], + HashMap::new(), + ) + .await; + let reply = match host_reply { + reply @ Ok(_) => reply, + Err(error) if !from && error.code == "ENOTSOCK" => { + call( + caller, + "process.fd_recvmsg_rights", + vec![ + json!(fd), + json!(capacity), + json!(0), + json!(false), + json!(flags & 0x2 != 0), + json!(flags & MSG_DONTWAIT != 0), + json!(flags & 0x100 != 0), + ], + HashMap::new(), + ) + .await } - let (bytes, address) = match reply { - HostCallReply::Json(value) - if value.get("kind").and_then(Value::as_str) == Some("wouldBlock") => - { - return ERRNO_AGAIN; + reply => reply, + }; + match reply { + Ok(reply) => { + if matches!(&reply, HostCallReply::Json(Value::Null)) { + return commit(caller, length_output, &0u32.to_le_bytes()); } - HostCallReply::Json(value) - if value.get("type").and_then(Value::as_str) == Some("message") => - { - let bytes = value - .get("data") - .cloned() - .map(HostCallReply::Json) - .and_then(|value| reply_bytes(value).ok()) - .ok_or(ERRNO_IO); - let address = encode_address(&value, true); - match bytes { - Ok(bytes) => (bytes, Some(address)), - Err(error) => return error, + let full_length = match &reply { + HostCallReply::Json(value) => value + .get("fullLength") + .and_then(value_u64) + .and_then(|value| usize::try_from(value).ok()), + _ => None, + }; + let (bytes, address) = match reply { + HostCallReply::Json(value) + if value.get("kind").and_then(Value::as_str) == Some("wouldBlock") => + { + if nonblocking { + return ERRNO_AGAIN; + } + let wait = wait_for_socket_readable( + caller, + fd, + "blocking socket receive", + started, + &mut warned_near_limit, + ) + .await; + if wait != SUCCESS { + return wait; + } + continue; + } + HostCallReply::Json(value) + if value.get("type").and_then(Value::as_str) == Some("message") => + { + let bytes = value + .get("data") + .cloned() + .map(HostCallReply::Json) + .and_then(|value| reply_bytes(value).ok()) + .ok_or(ERRNO_IO); + let address = encode_address(&value, true); + match bytes { + Ok(bytes) => (bytes, Some(address)), + Err(error) => return error, + } } + HostCallReply::Json(value) if value.get("data").is_some() => { + let bytes = value + .get("data") + .cloned() + .map(HostCallReply::Json) + .and_then(|value| reply_bytes(value).ok()) + .ok_or(ERRNO_IO); + match bytes { + Ok(bytes) => (bytes, None), + Err(error) => return error, + } + } + reply => match reply_bytes(reply) { + Ok(bytes) => (bytes, None), + Err(error) => return error, + }, + }; + let written = bytes.len().min(capacity as usize); + if commit(caller, output, &bytes[..written]) != SUCCESS { + return ERRNO_FAULT; } - reply => match reply_bytes(reply) { - Ok(bytes) => (bytes, None), - Err(error) => return error, - }, - }; - let written = bytes.len().min(capacity as usize); - if commit(caller, output, &bytes[..written]) != SUCCESS { - return ERRNO_FAULT; - } - let reported = if flags & MSG_TRUNC != 0 { - bytes.len() - } else { - written - }; - let Ok(reported) = u32::try_from(reported) else { - return ERRNO_2BIG; - }; - if commit(caller, length_output, &reported.to_le_bytes()) != SUCCESS { - return ERRNO_FAULT; - } - if let Some((address_output, address_capacity, address_length_output)) = address_outputs - { - let Some(address) = address else { - return ERRNO_IO; + let full_length = full_length.unwrap_or(bytes.len()); + let reported = if flags & MSG_TRUNC != 0 { + full_length + } else { + written }; - return publish_bytes( - caller, - address_output, - address_capacity, - address_length_output, - address.as_bytes(), - false, - ); + let Ok(reported) = u32::try_from(reported) else { + return ERRNO_2BIG; + }; + if commit(caller, length_output, &reported.to_le_bytes()) != SUCCESS { + return ERRNO_FAULT; + } + if let Some((address_output, address_capacity, address_length_output)) = + address_outputs + { + let Some(address) = address else { + return ERRNO_IO; + }; + return publish_bytes( + caller, + address_output, + address_capacity, + address_length_output, + address.as_bytes(), + false, + ); + } + return SUCCESS; } - SUCCESS + Err(error) => return errno(&error), } - Err(error) => errno(&error), } } @@ -874,10 +1077,19 @@ async fn tls_connect(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val] let Ok(hostname) = memory::read_string(caller, pointer, length as usize) else { return ERRNO_FAULT; }; + let reject_unauthorized = !caller.data().env.iter().any(|entry| { + entry.strip_suffix(&[0]).unwrap_or(entry.as_slice()) == b"NODE_TLS_REJECT_UNAUTHORIZED=0" + }); simple_call( caller, "process.hostnet_tls_connect", - vec![json!(fd), json!(hostname), json!([]), Value::Null], + vec![ + json!(fd), + json!(hostname), + json!([]), + Value::Null, + json!(reject_unauthorized), + ], ) .await } diff --git a/crates/execution/src/wasm/wasmtime/linker/preview1.rs b/crates/execution/src/wasm/wasmtime/linker/preview1.rs index 3f6b92fdc6..f332ed9021 100644 --- a/crates/execution/src/wasm/wasmtime/linker/preview1.rs +++ b/crates/execution/src/wasm/wasmtime/linker/preview1.rs @@ -49,6 +49,7 @@ const ERRNO_NFILE: i32 = 41; const ERRNO_NOBUFS: i32 = 42; const ERRNO_NOENT: i32 = 44; const ERRNO_NOEXEC: i32 = 45; +const ERRNO_NOMEM: i32 = 48; const ERRNO_NOSPC: i32 = 51; const ERRNO_NOSYS: i32 = 52; const ERRNO_NOTCONN: i32 = 53; @@ -216,6 +217,7 @@ pub(super) fn errno(error: &HostServiceError) -> i32 { "ENODATA" => ERRNO_NODATA, "ENOENT" => ERRNO_NOENT, "ENOEXEC" => ERRNO_NOEXEC, + "ENOMEM" => ERRNO_NOMEM, "ENOSPC" => ERRNO_NOSPC, "ENOSYS" => ERRNO_NOSYS, "ENOTCONN" => ERRNO_NOTCONN, @@ -1351,6 +1353,7 @@ mod tests { fn stable_errno_mapping_matches_preview1() { assert_eq!(errno(&HostServiceError::new("EBADF", "bad fd")), 8); assert_eq!(errno(&HostServiceError::new("EWOULDBLOCK", "wait")), 6); + assert_eq!(errno(&HostServiceError::new("ENOMEM", "limit")), 48); assert_eq!(errno(&HostServiceError::new("unknown", "fault")), 29); } } diff --git a/crates/execution/src/wasm/wasmtime/linker/process.rs b/crates/execution/src/wasm/wasmtime/linker/process.rs index 7d2487d634..05a041fce4 100644 --- a/crates/execution/src/wasm/wasmtime/linker/process.rs +++ b/crates/execution/src/wasm/wasmtime/linker/process.rs @@ -11,21 +11,25 @@ use super::preview1::{ use super::{i32_arg, set_i32_result}; use crate::abi::{AbiBinding, ImportId}; use crate::backend::HostCallReply; -use crate::host::ExecutableImageSource; +use crate::host::{HostOperation, SignalMaskHow, SignalOperation, SignalSetValue}; use crate::wasm::wasmtime::{ lifecycle, memory, module, store::{PendingExecReplacement, WasmtimeStoreState}, }; use serde_json::{json, Value}; use std::collections::{BTreeMap, HashMap}; +use std::time::Instant; use wasmtime::{Caller, Extern, Val, ValType}; +const ERRNO_AGAIN: i32 = 6; const ERRNO_BADF: i32 = 8; +const ERRNO_INTR: i32 = 27; const ERRNO_NOENT: i32 = 44; const ERRNO_NOEXEC: i32 = 45; const ERRNO_NOTSUP: i32 = 58; const ERRNO_PERM: i32 = 63; const ERRNO_SRCH: i32 = 71; +const ERRNO_TIMEDOUT: i32 = 73; const MAX_FDS: usize = 1 << 20; const MAX_RIGHTS: usize = 253; const SUPPORTED_SPAWN_FLAGS: u32 = 0xff; @@ -548,7 +552,11 @@ fn spawn_fd_state(snapshot: &[Value]) -> (Vec, Vec) { continue; }; mappings.push(json!([fd, fd])); - if entry.get("kind").and_then(Value::as_str) == Some("socket") { + // Kernel-owned AF_UNIX socketpairs and managed host-network sockets + // deliberately share the Linux descriptor kind. The sidecar registry + // annotates the one-shot snapshot with its authoritative ownership; + // do not invent a host-network description for an ordinary socket. + if entry.get("managedHostNet").and_then(Value::as_bool) == Some(true) { host_net.push(json!({ "guestFd": fd, "descriptionId": entry.get("descriptionId").cloned().unwrap_or(Value::Null), @@ -593,12 +601,13 @@ async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd else { return ERRNO_FAULT; }; - let Ok(argv) = memory::read_bytes(caller, argv_pointer, argv_length as usize) + let Ok(mut argv) = memory::read_bytes(caller, argv_pointer, argv_length as usize) .map_err(|_| ERRNO_FAULT) .and_then(nul_strings) else { return ERRNO_FAULT; }; + let original_argv = argv.clone(); let Ok(env) = memory::read_bytes(caller, env_pointer, env_length as usize) .map_err(|_| ERRNO_FAULT) .and_then(serialized_env) @@ -624,32 +633,66 @@ async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd } else { None }; - let prepared_replacement = if let Some(fd) = executable_fd { - let host = caller.data().host.clone(); - let engine = caller.data().engine.clone(); - let maximum = caller.data().max_module_file_bytes; - let bytes = match lifecycle::load_executable_image( - &host, - ExecutableImageSource::Descriptor(fd), - maximum, + let command = command.unwrap_or_else(|| format!("/proc/self/fd/{}", executable_fd.unwrap())); + let host = caller.data().host.clone(); + let engine = caller.data().engine.clone(); + let maximum = caller.data().max_module_file_bytes; + let (open_method, open_args) = if let Some(fd) = executable_fd { + ( + "process.exec_image_open_fd", + vec![json!(fd), json!(argv), json!(close_fds)], ) - .await - { - Ok(bytes) => bytes, - Err(error) => return errno(&error), + } else { + ("process.exec_image_open", vec![json!(command), json!(argv)]) + }; + let open = match call(caller, open_method, open_args, HashMap::new()).await { + Ok(reply) => reply, + Err(error) => return errno(&error), + }; + let prepared_replacement = { + let (bytes, resolved_argv) = + match lifecycle::read_open_executable_image(&host, open, maximum).await { + Ok(image) => image, + Err(error) => return errno(&error), + }; + let Some(resolved_argv) = resolved_argv else { + return ERRNO_IO; }; + argv = resolved_argv; let compiled = match module::compile_module(&engine, &bytes) { Ok(compiled) => compiled.module, + Err(error) if error.code == "ERR_AGENTOS_WASM_INVALID_MODULE" && !by_fd => { + let request = json!({ + "command": command, + "args": original_argv.iter().skip(1).cloned().collect::>(), + "options": { + "argv0": original_argv.first().cloned().unwrap_or_else(|| command.clone()), + "env": env, + "shell": false, + "cloexecFds": close_fds, + "localReplacement": false, + "internalBootstrapEnv": {}, + } + }); + return match call(caller, "process.exec", vec![request], HashMap::new()).await { + Ok(_) => { + caller.data_mut().exec_replaced = true; + ERRNO_IO + } + Err(error) if error.code == "ERR_AGENTOS_EXEC_REPLACED" => { + caller.data_mut().exec_replaced = true; + ERRNO_IO + } + Err(error) => errno(&error), + }; + } Err(error) if error.code == "ERR_AGENTOS_WASM_INVALID_MODULE" => { return ERRNO_NOEXEC; } Err(error) => return errno(&error), }; Some(compiled) - } else { - None }; - let command = command.unwrap_or_else(|| format!("/proc/self/fd/{}", executable_fd.unwrap())); let request = json!({ "command": command, "args": argv.iter().skip(1).cloned().collect::>(), @@ -658,7 +701,7 @@ async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd "env": env, "shell": false, "cloexecFds": close_fds, - "localReplacement": by_fd, + "localReplacement": true, "executableFd": executable_fd, "internalBootstrapEnv": {}, } @@ -669,21 +712,26 @@ async fn exec(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val], by_fd "process.exec" }; match call(caller, method, vec![request], HashMap::new()).await { - Ok(_) if by_fd => { + Ok(_) => { caller.data_mut().pending_exec_replacement = Some(PendingExecReplacement { - module: prepared_replacement.expect("fexec replacement was precompiled"), + module: prepared_replacement.expect("exec replacement was precompiled"), argv, env, }); caller.data_mut().exec_replaced = true; ERRNO_IO } - Ok(_) => ERRNO_IO, Err(error) if error.code == "ERR_AGENTOS_EXEC_REPLACED" => { caller.data_mut().exec_replaced = true; ERRNO_IO } - Err(error) => errno(&error), + Err(error) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_EXEC_COMMIT: method={method} command={command:?} code={} message={}", + error.code, error.message + ); + errno(&error) + } } } @@ -1240,20 +1288,75 @@ async fn record_lock(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val] } } } - match call( - caller, - "process.fd_record_lock", - vec![ - json!(fd), - json!(command), - json!(kind), - json!(start.to_string()), - json!(length.to_string()), - ], - HashMap::new(), - ) - .await - { + let arguments = vec![ + json!(fd), + json!(command), + json!(kind), + json!(start.to_string()), + json!(length.to_string()), + ]; + let started = Instant::now(); + let limit_ms = caller.data().max_blocking_read_ms; + let warning_ms = limit_ms.saturating_mul(4) / 5; + let mut warned_near_limit = false; + let reply = loop { + let reply = call( + caller, + "process.fd_record_lock", + arguments.clone(), + HashMap::new(), + ) + .await; + let Err(error) = &reply else { + break reply; + }; + if command != 14 || errno(error) != ERRNO_AGAIN { + break reply; + } + + // F_SETLKW registration and conflict/deadlock ownership remain in the + // kernel. Yield here so another process can release the lock, then ask + // the kernel to atomically retry the registered request. + let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX); + if !warned_near_limit && elapsed_ms >= warning_ms { + let warning = format!( + "[agentos] F_SETLKW is nearing limits.resources.maxBlockingReadMs ({limit_ms} ms)\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { + cancel_record_lock_wait(caller).await; + return ERRNO_IO; + } + warned_near_limit = true; + } + if elapsed_ms >= limit_ms { + cancel_record_lock_wait(caller).await; + let warning = format!( + "[agentos] F_SETLKW exceeded limits.resources.maxBlockingReadMs ({limit_ms} ms); raise limits.resources.maxBlockingReadMs if needed\n" + ); + if caller + .data() + .host + .publish_stderr(warning.into_bytes()) + .await + .is_err() + { + return ERRNO_IO; + } + return ERRNO_TIMEDOUT; + } + let wait_status = simple_call(caller, "process.sleep", vec![json!(1)]).await; + if wait_status != SUCCESS { + cancel_record_lock_wait(caller).await; + return wait_status; + } + }; + match reply { Ok(_reply) if command != 12 => SUCCESS, Ok(reply) => { let Ok(value) = json_reply(reply) else { @@ -1285,6 +1388,15 @@ async fn record_lock(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val] } } +async fn cancel_record_lock_wait(caller: &mut Caller<'_, WasmtimeStoreState>) { + let status = simple_call(caller, "process.fd_record_lock_cancel", Vec::new()).await; + if status != SUCCESS && status != ERRNO_INTR { + eprintln!( + "ERR_AGENTOS_WASMTIME_RECORD_LOCK_CANCEL: failed to cancel a blocking record-lock wait: errno={status}" + ); + } +} + async fn closefrom(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i32 { let Ok(minimum) = i32_arg(params, 0) else { return ERRNO_INVAL; @@ -1331,10 +1443,35 @@ async fn send_rights(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val] if memory::validate_range(caller, output, 4).is_err() { return ERRNO_FAULT; } - let rights = rights_bytes + let right_fds = rights_bytes .chunks_exact(4) - .map(|bytes| json!(u32::from_le_bytes(bytes.try_into().unwrap()))) + .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) .collect::>(); + let snapshot = match fd_snapshot(caller).await { + Ok(snapshot) => snapshot, + Err(error) => return error, + }; + let mut rights = Vec::with_capacity(right_fds.len()); + for fd in right_fds { + let Some(entry) = snapshot + .iter() + .find(|entry| entry.get("fd").and_then(value_u64) == Some(u64::from(fd))) + else { + return ERRNO_BADF; + }; + if entry.get("managedHostNet").and_then(Value::as_bool) == Some(true) { + let Some(description_id) = entry.get("descriptionId").and_then(Value::as_str) else { + return ERRNO_IO; + }; + rights.push(json!({ + "kind": "hostNet", + "fd": fd, + "descriptionId": description_id, + })); + } else { + rights.push(json!(fd)); + } + } let mut raw = HashMap::new(); raw.insert(1, bytes); match call( @@ -1385,22 +1522,50 @@ async fn receive_rights(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[V return ERRNO_FAULT; } let close_on_exec = *flags & 0x4000_0000 != 0; - match call( - caller, - "process.fd_recvmsg_rights", - vec![ - json!(fd), - json!(data_capacity), - json!(rights_capacity), - json!(close_on_exec), - json!(*flags & 0x2 != 0), - json!(*flags & 0x40 != 0), - json!(*flags & 0x100 != 0), - ], - HashMap::new(), - ) - .await - { + let nonblocking = match super::network::fd_is_nonblocking(caller, *fd).await { + Ok(nonblocking) => nonblocking || *flags & 0x40 != 0, + Err(error) => return error, + }; + let started = Instant::now(); + let mut warned_near_limit = false; + let reply = loop { + let reply = call( + caller, + "process.fd_recvmsg_rights", + vec![ + json!(fd), + json!(data_capacity), + json!(rights_capacity), + json!(close_on_exec), + json!(*flags & 0x2 != 0), + // Never block the sidecar actor inside the kernel recvmsg. + // A child sharing this socket may need that same actor to run + // its sendmsg host call. The readiness wait below is deferred + // by the shared reactor and therefore preserves progress. + json!(true), + json!(*flags & 0x100 != 0), + ], + HashMap::new(), + ) + .await; + match reply { + Err(error) if errno(&error) == ERRNO_AGAIN && !nonblocking => { + let wait = super::network::wait_for_socket_readable( + caller, + *fd, + "blocking socket receive", + started, + &mut warned_near_limit, + ) + .await; + if wait != SUCCESS { + return wait; + } + } + reply => break reply, + } + }; + match reply { Ok(reply) => { let Ok(value) = json_reply(reply) else { return ERRNO_IO; @@ -1567,17 +1732,39 @@ async fn signal_mask(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val] { return ERRNO_FAULT; } - let requested = signal_set(low, high) - .into_iter() - .filter(|signal| !matches!(signal, 9 | 19)) - .collect::>(); - match call( - caller, - "process.signal_mask", - vec![json!(how), json!(requested)], - HashMap::new(), - ) - .await + let mut set = (low as u64) | ((high as u64) << 32); + set &= !(1u64 << (9 - 1)); + set &= !(1u64 << (19 - 1)); + let how = match how { + 0 => SignalMaskHow::Block, + 1 => SignalMaskHow::Unblock, + 2 => SignalMaskHow::Set, + 3 if set == 0 => SignalMaskHow::Block, + _ => return ERRNO_INVAL, + }; + let thread_id = match u32::try_from(caller.data().thread_id) { + Ok(thread_id) => thread_id, + Err(_) => return ERRNO_INVAL, + }; + let operation = if caller.data().thread_group.is_some() { + SignalOperation::UpdateMaskForThread { + thread_id, + how, + set: SignalSetValue(set), + } + } else { + SignalOperation::UpdateMask { + how, + set: SignalSetValue(set), + } + }; + let host = caller.data().host.clone(); + match host + .submit( + HostOperation::Signal(operation), + std::mem::size_of::(), + ) + .await { Ok(reply) => { let Ok(value) = json_reply(reply) else { @@ -1672,10 +1859,18 @@ async fn ppoll(caller: &mut Caller<'_, WasmtimeStoreState>, params: &[Val]) -> i } else { Value::Null }; + let signal_thread_id = if caller.data().thread_group.is_some() { + match u32::try_from(caller.data().thread_id) { + Ok(thread_id) => json!(thread_id), + Err(_) => return ERRNO_INVAL, + } + } else { + Value::Null + }; match call( caller, "process.posix_poll", - vec![Value::Array(entries), timeout, mask], + vec![Value::Array(entries), timeout, mask, signal_thread_id], HashMap::new(), ) .await diff --git a/crates/execution/src/wasm/wasmtime/memory.rs b/crates/execution/src/wasm/wasmtime/memory.rs index ef0ab5722b..f22c0a6992 100644 --- a/crates/execution/src/wasm/wasmtime/memory.rs +++ b/crates/execution/src/wasm/wasmtime/memory.rs @@ -2,7 +2,8 @@ use super::store::WasmtimeStoreState; use std::ops::Range; -use wasmtime::{Caller, Extern, Memory}; +use std::sync::atomic::{AtomicU8, Ordering}; +use wasmtime::{Caller, Extern, Memory, SharedMemory}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GuestMemoryError { @@ -25,11 +26,63 @@ impl std::fmt::Display for GuestMemoryError { impl std::error::Error for GuestMemoryError {} +#[derive(Clone)] +pub enum GuestMemory { + Local(Memory), + Shared(SharedMemory), +} + +impl GuestMemory { + fn data_size(&self, caller: &Caller<'_, WasmtimeStoreState>) -> usize { + match self { + Self::Local(memory) => memory.data_size(caller), + Self::Shared(memory) => memory.data_size(), + } + } + + fn read(&self, caller: &Caller<'_, WasmtimeStoreState>, range: Range) -> Vec { + match self { + Self::Local(memory) => memory.data(caller)[range].to_vec(), + Self::Shared(memory) => memory.data()[range] + .iter() + .map(|byte| { + // SAFETY: `AtomicU8` has alignment one and the Wasmtime + // shared-memory API guarantees the backing allocation + // remains valid. Atomic access is required because guest + // threads may concurrently mutate these bytes. + unsafe { &*byte.get().cast::() }.load(Ordering::SeqCst) + }) + .collect(), + } + } + + fn write( + &self, + caller: &mut Caller<'_, WasmtimeStoreState>, + range: Range, + bytes: &[u8], + ) { + match self { + Self::Local(memory) => memory.data_mut(caller)[range].copy_from_slice(bytes), + Self::Shared(memory) => { + for (destination, source) in memory.data()[range].iter().zip(bytes) { + // SAFETY: see the corresponding shared-memory read. Each + // byte is stored atomically so Rust never races with a + // guest load/store on another native worker. + unsafe { &*destination.get().cast::() } + .store(*source, Ordering::SeqCst); + } + } + } + } +} + pub fn exported_memory( caller: &mut Caller<'_, WasmtimeStoreState>, -) -> Result { +) -> Result { match caller.get_export("memory") { - Some(Extern::Memory(memory)) => Ok(memory), + Some(Extern::Memory(memory)) => Ok(GuestMemory::Local(memory)), + Some(Extern::SharedMemory(memory)) => Ok(GuestMemory::Shared(memory)), _ => Err(GuestMemoryError::MissingMemory), } } @@ -38,13 +91,13 @@ pub fn validate_range( caller: &mut Caller<'_, WasmtimeStoreState>, pointer: u32, length: usize, -) -> Result<(Memory, Range), GuestMemoryError> { +) -> Result<(GuestMemory, Range), GuestMemoryError> { let memory = exported_memory(caller)?; let start = usize::try_from(pointer).map_err(|_| GuestMemoryError::AddressOverflow)?; let end = start .checked_add(length) .ok_or(GuestMemoryError::AddressOverflow)?; - if end > memory.data_size(&mut *caller) { + if end > memory.data_size(caller) { return Err(GuestMemoryError::OutOfBounds); } Ok((memory, start..end)) @@ -56,7 +109,7 @@ pub fn read_bytes( length: usize, ) -> Result, GuestMemoryError> { let (memory, range) = validate_range(caller, pointer, length)?; - Ok(memory.data(&mut *caller)[range].to_vec()) + Ok(memory.read(caller, range)) } pub fn read_string( @@ -74,7 +127,7 @@ pub fn write_bytes( bytes: &[u8], ) -> Result<(), GuestMemoryError> { let (memory, range) = validate_range(caller, pointer, bytes.len())?; - memory.data_mut(&mut *caller)[range].copy_from_slice(bytes); + memory.write(caller, range, bytes); Ok(()) } diff --git a/crates/execution/src/wasm/wasmtime/mod.rs b/crates/execution/src/wasm/wasmtime/mod.rs index 270738e11a..b43cbb36f3 100644 --- a/crates/execution/src/wasm/wasmtime/mod.rs +++ b/crates/execution/src/wasm/wasmtime/mod.rs @@ -12,9 +12,15 @@ mod error; mod lifecycle; mod limits; mod linker; +// Wasmtime exposes shared memory as `UnsafeCell` and requires host access +// through atomics. Keep the necessary pointer cast isolated to this audited +// codec module; unsafe code remains denied everywhere else in execution. +#[allow(unsafe_code)] mod memory; mod module; mod store; +mod threads; +mod worker; pub use engine::{ WasmtimeEngineHandle, WasmtimeEngineProfile, WasmtimeEngineRegistry, WasmtimeFeatureProfile, @@ -22,6 +28,7 @@ pub use engine::{ }; pub use lifecycle::{WasmtimeExecution, WasmtimeExecutionEngine}; pub use limits::DEFAULT_TABLE_ACCOUNTING_BYTES; +pub use worker::{run_worker_entry, WORKER_MODE_ARGUMENT}; pub const PINNED_WASMTIME_VERSION: &str = "46.0.0"; pub const TRUSTED_INITIAL_MODULE_PREFIX: &str = "agentos-trusted-initial:"; diff --git a/crates/execution/src/wasm/wasmtime/module.rs b/crates/execution/src/wasm/wasmtime/module.rs index ca3837b067..abaee7b4b7 100644 --- a/crates/execution/src/wasm/wasmtime/module.rs +++ b/crates/execution/src/wasm/wasmtime/module.rs @@ -1,6 +1,6 @@ //! Shared-profile module compilation. -use super::super::profile::validate_locked_profile; +use super::super::profile::{validate_locked_profile, validate_locked_threaded_profile}; use super::engine::WasmtimeEngineHandle; use crate::backend::HostServiceError; use std::sync::Arc; @@ -19,7 +19,14 @@ pub fn compile_module( bytes: &[u8], ) -> Result { let validation_started = Instant::now(); - validate_locked_profile(bytes)?; + match engine.profile().feature_profile { + super::engine::WasmtimeFeatureProfile::AgentOsOwnedWasiV1 => { + validate_locked_profile(bytes)?; + } + super::engine::WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads => { + validate_locked_threaded_profile(bytes)?; + } + } let profile_validation = validation_started.elapsed(); let mut modules = engine.modules().lock().map_err(|_| { HostServiceError::new( diff --git a/crates/execution/src/wasm/wasmtime/store.rs b/crates/execution/src/wasm/wasmtime/store.rs index 023cde2b15..014d26a927 100644 --- a/crates/execution/src/wasm/wasmtime/store.rs +++ b/crates/execution/src/wasm/wasmtime/store.rs @@ -5,6 +5,8 @@ use super::diagnostics::ExecutionDiagnostics; use super::engine::{WasmtimeEngineHandle, WasmtimeEngineProfile}; use super::lifecycle::QueuedWasmtimeEvent; use super::limits; +use super::threads::ThreadGroup; +use super::worker::WorkerIpcClient; use crate::backend::{ direct_host_reply_channel, HostCallIdentity, HostCallReply, HostServiceError, }; @@ -31,7 +33,9 @@ pub struct PendingExecReplacement { /// every import issued by a Store. It owns no sidecar or kernel state. #[derive(Clone)] pub struct WasmtimeHostClient { - host: ProcessHostCapabilitySet, + process: HostProcessContext, + host: Option, + worker: Option, next_call_id: Arc, max_host_reply_bytes: usize, cancelled: Arc, @@ -56,7 +60,9 @@ impl WasmtimeHostClient { event_notify: Option>, ) -> Self { Self { - host, + process: host.process(), + host: Some(host), + worker: None, next_call_id: Arc::new(AtomicU64::new(1)), max_host_reply_bytes, cancelled, @@ -69,13 +75,38 @@ impl WasmtimeHostClient { } } + pub(super) fn new_worker( + worker: WorkerIpcClient, + max_host_reply_bytes: usize, + cancelled: Arc, + cancel_notify: Arc, + signal_pending: Arc, + resources: Arc, + events: Sender, + ) -> Self { + Self { + process: worker.process(), + host: None, + worker: Some(worker), + next_call_id: Arc::new(AtomicU64::new(1)), + max_host_reply_bytes, + cancelled, + cancel_notify, + signal_pending, + resources, + events, + event_notify: None, + diagnostics: None, + } + } + pub fn with_diagnostics(mut self, diagnostics: Arc) -> Self { self.diagnostics = Some(diagnostics); self } pub fn process(&self) -> HostProcessContext { - self.host.process() + self.process } pub fn canceled(&self) -> bool { @@ -83,7 +114,10 @@ impl WasmtimeHostClient { } pub fn signal_pending(&self) -> bool { - self.signal_pending.load(Ordering::Acquire) + self.worker + .as_ref() + .map(WorkerIpcClient::signal_pending) + .unwrap_or_else(|| self.signal_pending.load(Ordering::Acquire)) } pub async fn submit( @@ -100,6 +134,9 @@ impl WasmtimeHostClient { "Wasmtime execution was canceled before host-call admission", )); } + if let Some(worker) = self.worker.as_ref() { + return worker.submit(operation).await; + } let call_id = self .next_call_id .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { @@ -111,15 +148,18 @@ impl WasmtimeHostClient { "Wasmtime host-call identity space is exhausted", ) })?; - let process = self.host.process(); + let process = self.process; let identity = HostCallIdentity { generation: process.generation, pid: process.pid, call_id, }; - let admission = self.host.admit_request(retained_request_bytes)?; + let host = self.host.as_ref().ok_or_else(|| { + HostServiceError::new("EIO", "direct Wasmtime host capability is unavailable") + })?; + let admission = host.admit_request(retained_request_bytes)?; let (reply, receiver) = direct_host_reply_channel(identity, self.max_host_reply_bytes)?; - self.host.submit(operation, reply, admission)?; + host.submit(operation, reply, admission)?; tokio::select! { reply = receiver => reply, () = self.cancel_notify.notified() => Err(HostServiceError::new( @@ -157,6 +197,11 @@ impl WasmtimeHostClient { "Wasmtime execution was canceled before adapter-call admission", )); } + if let Some(worker) = self.worker.as_ref() { + return worker + .submit_adapter_call(method, args, raw_bytes_args) + .await; + } let call_id = self .next_call_id .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { @@ -168,7 +213,7 @@ impl WasmtimeHostClient { "Wasmtime host-call identity space is exhausted", ) })?; - let process = self.host.process(); + let process = self.process; let identity = HostCallIdentity { generation: process.generation, pid: process.pid, @@ -234,6 +279,9 @@ impl WasmtimeHostClient { } pub async fn publish_stderr(&self, bytes: Vec) -> Result<(), HostServiceError> { + if let Some(worker) = self.worker.as_ref() { + return worker.publish_stderr(bytes).await; + } let retained_bytes = bytes.len(); let event = QueuedWasmtimeEvent::new( &self.resources, @@ -257,6 +305,17 @@ impl WasmtimeHostClient { } Ok(()) } + + pub(super) fn report_thread_group_failure(&self, error: HostServiceError) { + if let Some(worker) = self.worker.as_ref() { + if let Err(report_error) = worker.report_group_failure(error) { + eprintln!( + "{}: failed to report pthread group failure to parent: {}", + report_error.code, report_error.message + ); + } + } + } } pub struct WasmtimeStoreState { @@ -266,6 +325,8 @@ pub struct WasmtimeStoreState { pub env: Vec>, pub virtual_pid: u32, pub virtual_ppid: u32, + pub thread_id: i32, + pub thread_group: Option>, pub limits: StoreLimits, pub exit_code: Option, pub exec_replaced: bool, @@ -281,8 +342,8 @@ pub struct WasmtimeStoreState { active_cpu_started_ns: u64, paused: Arc, pause_notify: Arc, - _async_stack_reservation: Reservation, - _guest_memory_reservation: Reservation, + _async_stack_reservation: Option, + _guest_memory_reservation: Option, } impl std::fmt::Debug for WasmtimeStoreState { @@ -308,36 +369,55 @@ impl WasmtimeStoreState { active_cpu_started_ns: u64, paused: Arc, pause_notify: Arc, + group_memory_pre_reserved: bool, + thread_group: Option>, + thread_id: i32, ) -> Result { let async_stack_bytes = profile.async_stack_bytes()?; - let async_stack_reservation = runtime - .resources() - .reserve(ResourceClass::WasmMemoryBytes, async_stack_bytes) - .map_err(|error| { - HostServiceError::new("ERR_AGENTOS_WASMTIME_ASYNC_STACK_LIMIT", error.to_string()) - .with_details(serde_json::json!({ - "limitName": "limits.resources.maxWasmStackBytes", - "observed": async_stack_bytes, - })) - })?; + let async_stack_reservation = if group_memory_pre_reserved { + None + } else { + Some( + runtime + .resources() + .reserve(ResourceClass::WasmMemoryBytes, async_stack_bytes) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_ASYNC_STACK_LIMIT", + error.to_string(), + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmStackBytes", + "observed": async_stack_bytes, + })) + })?, + ) + }; let linear_memory_bytes = limits::max_memory_bytes(&request.limits)?; let aggregate_memory_bytes = limits::aggregate_store_memory_bytes(&request.limits)?; - let guest_memory_reservation = runtime - .resources() - .reserve(ResourceClass::WasmMemoryBytes, aggregate_memory_bytes) - .map_err(|error| { - HostServiceError::new( - "ERR_AGENTOS_WASMTIME_AGGREGATE_MEMORY_LIMIT", - error.to_string(), - ) - .with_details(serde_json::json!({ - "limitName": "limits.resources.maxWasmMemoryBytes", - "observed": aggregate_memory_bytes, - "linearMemoryBytes": linear_memory_bytes, - "tableAccountingBytes": limits::DEFAULT_TABLE_ACCOUNTING_BYTES, - "resource": "wasmtimeGuestMemory", - })) - })?; + let guest_memory_reservation = if group_memory_pre_reserved { + None + } else { + Some( + runtime + .resources() + .reserve(ResourceClass::WasmMemoryBytes, aggregate_memory_bytes) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_AGGREGATE_MEMORY_LIMIT", + error.to_string(), + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmMemoryBytes", + "observed": aggregate_memory_bytes, + "linearMemoryBytes": linear_memory_bytes, + "exceptionGcHeapBytes": linear_memory_bytes, + "tableAccountingBytes": limits::DEFAULT_TABLE_ACCOUNTING_BYTES, + "resource": "wasmtimeGuestMemory", + })) + })?, + ) + }; let argv = nul_terminated_strings(request.argv.iter().map(String::as_str), "argv")?; let guest_env = guest_visible_wasm_env(&request.env); let env = nul_terminated_strings( @@ -363,6 +443,8 @@ impl WasmtimeStoreState { env, virtual_pid, virtual_ppid, + thread_id, + thread_group, limits: limits::store_limits(&request.limits)?, exit_code: None, exec_replaced: false, @@ -460,6 +542,9 @@ pub fn create_store( active_cpu_started_ns: u64, paused: Arc, pause_notify: Arc, + group_memory_pre_reserved: bool, + thread_group: Option>, + thread_id: i32, ) -> Result, HostServiceError> { let deterministic_fuel = request.limits.deterministic_fuel; let mut store = Store::new( @@ -473,6 +558,9 @@ pub fn create_store( active_cpu_started_ns, paused, pause_notify, + group_memory_pre_reserved, + thread_group, + thread_id, )?, ); store.limiter(|state| &mut state.limits); @@ -645,6 +733,9 @@ mod tests { thread_cpu_time_ns(), Arc::new(AtomicBool::new(false)), Arc::new(Notify::new()), + false, + None, + 0, ) .expect("store state"); assert_eq!( diff --git a/crates/execution/src/wasm/wasmtime/threads.rs b/crates/execution/src/wasm/wasmtime/threads.rs new file mode 100644 index 0000000000..1be7236514 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/threads.rs @@ -0,0 +1,406 @@ +//! Explicit WASI-threads group for the `wasmtime-threads` backend. +//! +//! Linux/POSIX semantics remain in the kernel and owned libc. This module +//! only owns engine objects: one imported shared memory and one Store/Instance +//! per native guest thread. + +use super::super::StartWasmExecutionRequest; +use super::engine::{WasmtimeEngineHandle, WasmtimeEngineProfile}; +use super::linker; +use super::store::{self, WasmtimeHostClient}; +use crate::backend::HostServiceError; +use agentos_runtime::RuntimeContext; +use std::sync::{Arc, Mutex}; +use tokio::sync::Notify; +use wasmtime::{ExternType, Module, SharedMemory}; + +const MAX_WASI_THREAD_ID: i32 = 0x1fff_ffff; + +#[derive(Debug)] +struct ThreadGroupState { + next_tid: i32, + active: usize, + shutting_down: bool, + first_failure: Option, + handles: Vec>, +} + +pub struct ThreadGroup { + engine: Arc, + module: Arc, + runtime: RuntimeContext, + host: WasmtimeHostClient, + request: StartWasmExecutionRequest, + profile: WasmtimeEngineProfile, + paused: Arc, + pause_notify: Arc, + memory: SharedMemory, + maximum_threads: usize, + debug: bool, + state: Mutex, + failure_notify: Notify, +} + +impl std::fmt::Debug for ThreadGroup { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ThreadGroup") + .field("process", &self.host.process()) + .field("maximum_threads", &self.maximum_threads) + .field("memory_pages", &self.memory.size()) + .finish_non_exhaustive() + } +} + +impl ThreadGroup { + #[allow(clippy::too_many_arguments)] + pub fn new( + engine: Arc, + module: Arc, + runtime: RuntimeContext, + host: WasmtimeHostClient, + request: StartWasmExecutionRequest, + profile: WasmtimeEngineProfile, + paused: Arc, + pause_notify: Arc, + ) -> Result, HostServiceError> { + let memory_type = module + .imports() + .find_map(|import| { + (import.module() == "env" && import.name() == "memory").then(|| import.ty()) + }) + .and_then(|ty| match ty { + ExternType::Memory(memory) => Some(memory), + _ => None, + }) + .ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_IMPORT", + "threaded WebAssembly must import shared memory as env.memory", + ) + })?; + if !memory_type.is_shared() { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_NOT_SHARED", + "threaded WebAssembly env.memory must use the shared-memory type", + )); + } + let maximum_pages = memory_type.maximum().ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_UNBOUNDED", + "threaded WebAssembly shared memory must declare a maximum", + ) + })?; + let maximum_bytes = maximum_pages + .checked_mul(memory_type.page_size()) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_LIMIT", + "threaded WebAssembly shared-memory maximum does not fit this platform", + ) + })?; + let configured_maximum = super::limits::max_memory_bytes(&request.limits)?; + if maximum_bytes > configured_maximum { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_LIMIT", + "threaded WebAssembly shared-memory maximum exceeds the configured limit", + ) + .with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmMemoryBytes", + "limit": configured_maximum, + "observed": maximum_bytes, + }))); + } + let memory = SharedMemory::new(engine.engine(), memory_type).map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASM_THREADS_MEMORY_CREATE: private shared-memory diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_THREADS_MEMORY_CREATE", + "failed to allocate the threaded WebAssembly shared memory", + ) + })?; + Ok(Arc::new(Self { + engine, + module, + runtime, + host, + maximum_threads: request.limits.max_threads.unwrap_or(16).max(1), + debug: request + .env + .get("AGENTOS_WASM_THREAD_DEBUG") + .is_some_and(|value| value == "1"), + request, + profile, + paused, + pause_notify, + memory, + state: Mutex::new(ThreadGroupState { + next_tid: 1, + active: 1, + shutting_down: false, + first_failure: None, + handles: Vec::new(), + }), + failure_notify: Notify::new(), + })) + } + + pub fn memory(&self) -> &SharedMemory { + &self.memory + } + + /// WASI threads returns a positive TID on success and a negative value on + /// failure. wasi-libc translates every negative result to `EAGAIN`. + pub fn spawn(self: &Arc, start_arg: i32) -> i32 { + let tid = { + let mut state = match self.state.lock() { + Ok(state) => state, + Err(_) => { + eprintln!( + "ERR_AGENTOS_WASM_THREAD_GROUP_POISONED: cannot admit another pthread" + ); + return -1; + } + }; + if state.shutting_down || state.active >= self.maximum_threads { + return -1; + } + let tid = state.next_tid; + if !(1..=MAX_WASI_THREAD_ID).contains(&tid) { + return -1; + } + state.next_tid = tid.saturating_add(1); + state.active += 1; + tid + }; + + let group = Arc::clone(self); + if self.debug { + eprintln!("AGENTOS_WASM_THREAD_DEBUG spawn tid={tid} arg={start_arg}"); + } + // AGENTOS_THREAD_SITE: admitted-threaded-wasmtime-guest + let handle = match std::thread::Builder::new() + .name(format!("agentos-wasm-pthread-{tid}")) + .spawn(move || { + let result = group + .runtime + .handle() + .block_on(group.run_secondary(tid, start_arg)); + group.finish_secondary(result); + }) { + Ok(handle) => handle, + Err(error) => { + eprintln!("ERR_AGENTOS_WASM_THREAD_SPAWN: native worker spawn failed: {error}"); + match self.state.lock() { + Ok(mut state) => state.active = state.active.saturating_sub(1), + Err(_) => eprintln!( + "ERR_AGENTOS_WASM_THREAD_GROUP_POISONED: native spawn rollback failed" + ), + } + return -1; + } + }; + match self.state.lock() { + Ok(mut state) => state.handles.push(handle), + Err(_) => { + // The spawned thread still owns its complete execution state; + // dropping the handle detaches it, so mark the process group + // failed and rely on the outer killable worker boundary. + eprintln!("ERR_AGENTOS_WASM_THREAD_GROUP_POISONED: lost pthread join handle"); + } + } + tid + } + + async fn run_secondary( + self: &Arc, + tid: i32, + start_arg: i32, + ) -> Result<(), HostServiceError> { + if self.debug { + eprintln!("AGENTOS_WASM_THREAD_DEBUG start tid={tid} arg={start_arg}"); + } + let thread_id = u32::try_from(tid).map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_ID", + "WASI thread id does not fit the kernel signal-thread namespace", + ) + })?; + self.host + .submit( + crate::host::HostOperation::Signal(crate::host::SignalOperation::RegisterThread { + thread_id, + inherit_from: 0, + }), + std::mem::size_of::() * 2, + ) + .await?; + let result = self.run_registered_secondary(tid, start_arg).await; + let unregister = self + .host + .submit( + crate::host::HostOperation::Signal( + crate::host::SignalOperation::UnregisterThread { thread_id }, + ), + std::mem::size_of::(), + ) + .await; + match (result, unregister) { + (Err(error), Err(unregister)) => { + eprintln!( + "{}: pthread failed; signal-thread teardown also failed: {}", + error.code, unregister + ); + Err(error) + } + (Err(error), _) => Err(error), + (Ok(()), Err(error)) => Err(error), + (Ok(()), Ok(_)) => Ok(()), + } + } + + async fn run_registered_secondary( + self: &Arc, + tid: i32, + start_arg: i32, + ) -> Result<(), HostServiceError> { + let mut store = store::create_store( + Arc::clone(&self.engine), + &self.runtime, + self.host.clone(), + &self.request, + self.profile, + store::thread_cpu_time_ns(), + Arc::clone(&self.paused), + Arc::clone(&self.pause_notify), + true, + Some(Arc::clone(self)), + tid, + )?; + let mut linker = + linker::build_linker(self.engine.engine(), self.request.permission_tier, true) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASM_THREAD_LINKER: private linker diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_LINKER", + "failed to construct the threaded WebAssembly linker", + ) + })?; + linker + .define(&store, "env", "memory", self.memory.clone()) + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASM_THREAD_MEMORY_LINK: private linker diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_MEMORY_LINK", + "failed to link the threaded WebAssembly shared memory", + ) + })?; + let instance = linker + .instantiate_async(&mut store, &self.module) + .await + .map_err(|error| { + super::error::normalize("ERR_AGENTOS_WASM_THREAD_INSTANTIATE", &error, false) + })?; + linker::initialize_inherited_signal_mask(&mut store, &instance).await?; + let start = instance + .get_typed_func::<(i32, i32), ()>(&mut store, "wasi_thread_start") + .map_err(|error| { + eprintln!( + "ERR_AGENTOS_WASM_THREAD_ENTRYPOINT: private entrypoint diagnostic: {error:#}" + ); + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_ENTRYPOINT", + "threaded WebAssembly does not export a valid wasi_thread_start function", + ) + })?; + match start.call_async(&mut store, (tid, start_arg)).await { + Ok(()) => Ok(()), + Err(_) if store.data().exit_code.is_some() => Ok(()), + Err(error) => Err(super::error::normalize( + "ERR_AGENTOS_WASM_THREAD_TRAP", + &error, + store.data().canceled(), + )), + } + } + + fn finish_secondary(&self, result: Result<(), HostServiceError>) { + if self.debug { + eprintln!( + "AGENTOS_WASM_THREAD_DEBUG finish result={}", + if result.is_ok() { "ok" } else { "error" } + ); + } + let failure = result.err(); + let Ok(mut state) = self.state.lock() else { + eprintln!("ERR_AGENTOS_WASM_THREAD_GROUP_POISONED: pthread completion was lost"); + return; + }; + state.active = state.active.saturating_sub(1); + if let Some(error) = failure.as_ref() { + eprintln!( + "{}: pthread execution failed: {}", + error.code, error.message + ); + state.first_failure.get_or_insert_with(|| error.clone()); + self.failure_notify.notify_one(); + } + drop(state); + if let Some(error) = failure { + self.host.report_thread_group_failure(error); + } + } + + /// Resolve as soon as any secondary Store traps. The worker's main Store + /// races this against `_start`, so one bad pthread terminates the complete + /// process group instead of leaving another pthread parked indefinitely. + pub async fn wait_for_failure(&self) -> HostServiceError { + loop { + let notified = self.failure_notify.notified(); + match self.state.lock() { + Ok(state) => { + if let Some(error) = state.first_failure.as_ref() { + return error.clone(); + } + } + Err(_) => return group_poisoned(), + } + notified.await; + } + } + + /// Mark the group closed when the process main Store exits. Linux process + /// exit does not join detached pthreads: the enclosing worker process is + /// the teardown unit and its exit terminates every remaining native guest + /// thread. Finished JoinHandles are reaped here; unfinished handles are + /// deliberately detached immediately before the worker itself exits. + pub fn settle_main(&self) -> Result<(), HostServiceError> { + let mut state = self.state.lock().map_err(|_| group_poisoned())?; + state.shutting_down = true; + let handles = std::mem::take(&mut state.handles); + let failure = state.first_failure.take(); + drop(state); + for handle in handles { + if handle.is_finished() && handle.join().is_err() { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_PANIC", + "a threaded WebAssembly native worker panicked", + )); + } + } + failure.map_or(Ok(()), Err) + } +} + +fn group_poisoned() -> HostServiceError { + HostServiceError::new( + "ERR_AGENTOS_WASM_THREAD_GROUP_POISONED", + "threaded WebAssembly group state is poisoned", + ) +} diff --git a/crates/execution/src/wasm/wasmtime/worker.rs b/crates/execution/src/wasm/wasmtime/worker.rs new file mode 100644 index 0000000000..e024bcb0a3 --- /dev/null +++ b/crates/execution/src/wasm/wasmtime/worker.rs @@ -0,0 +1,1027 @@ +//! Killable subprocess boundary for explicitly threaded WebAssembly. +//! +//! The worker owns Wasmtime Engine/Store/Instance/native-thread state only. +//! Kernel state and every host capability remain in the parent sidecar. The +//! protocol is length-delimited, typed, bounded, and carries owned values; +//! guest memory is never shared with the parent across an async wait. + +use super::super::StartWasmExecutionRequest; +use super::lifecycle::Control; +use crate::backend::{HostCallReply, HostServiceError}; +use crate::host::{HostOperation, HostProcessContext, ProcessOperation, SignalOperation}; +use agentos_runtime::{RuntimeConfig, SidecarRuntime}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Stdio; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +pub const WORKER_MODE_ARGUMENT: &str = "--agentos-wasmtime-thread-worker"; +const MAX_STARTUP_HEADER_BYTES: usize = 1024 * 1024; +const DEFAULT_MAX_WORKER_FRAME_BYTES: usize = 32 * 1024 * 1024; +const MAX_WORKER_FRAME_BYTES: usize = 128 * 1024 * 1024; +const WORKER_FINISH_ACK_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug, Serialize, Deserialize)] +struct WorkerStartup { + request: StartWasmExecutionRequest, + process: HostProcessContext, + module_bytes: usize, + max_frame_bytes: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +enum WorkerCall { + Adapter { + method: String, + args: Vec, + raw: Vec, + }, + OpenExecutableImage { + descriptor: u32, + }, + ReadExecutableImage { + handle: u64, + offset: u64, + max_bytes: usize, + }, + CloseExecutableImage { + handle: u64, + }, + Signal(SignalOperation), +} + +#[derive(Debug, Serialize, Deserialize)] +struct RawArgument { + index: usize, + #[serde(with = "serde_bytes")] + bytes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +enum WorkerFrame { + Call { + id: u64, + call: WorkerCall, + }, + Stderr { + #[serde(with = "serde_bytes")] + bytes: Vec, + }, + Finished { + result: Result, + }, + GroupFailed { + error: HostServiceError, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(super) enum ParentFrame { + Reply { + id: u64, + result: Result, + }, + SignalWake, + FinishedAck, +} + +struct OutboundFrame { + frame: WorkerFrame, + flushed: Option>>, +} + +type PendingCall = tokio::sync::oneshot::Sender>; +type PendingCallMap = Mutex>; + +#[derive(Clone)] +pub(super) struct WorkerIpcClient { + process: HostProcessContext, + next_call_id: Arc, + sender: std::sync::mpsc::SyncSender, + pending: Arc, + signal_pending: Arc, + finish_ack: Arc>>>, + failed: Arc, +} + +impl std::fmt::Debug for WorkerIpcClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("WorkerIpcClient") + .field("process", &self.process) + .finish_non_exhaustive() + } +} + +impl WorkerIpcClient { + fn start( + process: HostProcessContext, + maximum_pending: usize, + max_frame_bytes: usize, + ) -> Result { + let maximum_pending = maximum_pending.max(1); + let (sender, receiver) = std::sync::mpsc::sync_channel::(maximum_pending); + let pending = Arc::new(Mutex::new(HashMap::new())); + let signal_pending = Arc::new(AtomicUsize::new(0)); + let finish_ack: Arc>>> = + Arc::new(Mutex::new(None)); + let failed = Arc::new(AtomicBool::new(false)); + + let writer_pending = Arc::clone(&pending); + let writer_failed = Arc::clone(&failed); + // AGENTOS_THREAD_SITE: threaded-wasmtime-ipc-writer + std::thread::Builder::new() + .name(String::from("agentos-wasmtime-worker-ipc-write")) + .spawn(move || { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + while let Ok(outbound) = receiver.recv() { + let result = + write_frame_blocking(&mut stdout, &outbound.frame, max_frame_bytes); + if let Some(flushed) = outbound.flushed { + if flushed.send(result.clone()).is_err() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_ACK: flush waiter was dropped" + ); + } + } + if let Err(error) = result { + writer_failed.store(true, Ordering::Release); + fail_pending(&writer_pending, error); + break; + } + } + }) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_THREAD", + format!("failed to start worker IPC writer: {error}"), + ) + })?; + + let reader_pending = Arc::clone(&pending); + let reader_signals = Arc::clone(&signal_pending); + let reader_finish_ack = Arc::clone(&finish_ack); + let reader_failed = Arc::clone(&failed); + // AGENTOS_THREAD_SITE: threaded-wasmtime-ipc-reader + std::thread::Builder::new() + .name(String::from("agentos-wasmtime-worker-ipc-read")) + .spawn(move || { + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + loop { + match read_frame_blocking::<_, ParentFrame>(&mut stdin, max_frame_bytes) { + Ok(ParentFrame::Reply { id, result }) => { + let waiter = match reader_pending.lock() { + Ok(mut pending) => pending.remove(&id), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_PENDING_POISONED: recovering reply state" + ); + poisoned.into_inner().remove(&id) + } + }; + if let Some(waiter) = waiter { + if waiter.send(result).is_err() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_REPLY_DROPPED: call {id} no longer has a waiter" + ); + } + } + } + Ok(ParentFrame::SignalWake) => { + if reader_signals + .fetch_update( + Ordering::AcqRel, + Ordering::Acquire, + |current| current.checked_add(1), + ) + .is_err() + { + let error = HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_SIGNAL_LIMIT", + "thread-worker signal wake counter overflowed", + ); + reader_failed.store(true, Ordering::Release); + fail_pending(&reader_pending, error); + break; + } + } + Ok(ParentFrame::FinishedAck) => { + let waiter = match reader_finish_ack.lock() { + Ok(mut waiter) => waiter.take(), + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_FINISH_POISONED: recovering finish acknowledgement state" + ); + poisoned.into_inner().take() + } + }; + if let Some(waiter) = waiter { + if waiter.send(()).is_err() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_FINISH_ACK_DROPPED: finish waiter was dropped" + ); + } + } + } + Err(error) => { + reader_failed.store(true, Ordering::Release); + fail_pending(&reader_pending, error); + match reader_finish_ack.lock() { + Ok(mut waiter) => { + waiter.take(); + } + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_FINISH_POISONED: recovering failed finish state" + ); + poisoned.into_inner().take(); + } + } + break; + } + } + } + }) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_THREAD", + format!("failed to start worker IPC reader: {error}"), + ) + })?; + + Ok(Self { + process, + next_call_id: Arc::new(AtomicU64::new(1)), + sender, + pending, + signal_pending, + finish_ack, + failed, + }) + } + + pub(super) fn process(&self) -> HostProcessContext { + self.process + } + + pub(super) fn signal_pending(&self) -> bool { + self.signal_pending.load(Ordering::Acquire) > 0 + } + + pub(super) async fn submit_adapter_call( + &self, + method: String, + args: Vec, + raw_bytes_args: HashMap>, + ) -> Result { + let raw = raw_bytes_args + .into_iter() + .map(|(index, bytes)| RawArgument { index, bytes }) + .collect(); + self.call(WorkerCall::Adapter { method, args, raw }).await + } + + pub(super) async fn submit( + &self, + operation: HostOperation, + ) -> Result { + let signal_delivery = matches!( + operation, + HostOperation::Signal( + SignalOperation::TakePublishedDelivery + | SignalOperation::TakePublishedDeliveryForThread { .. } + ) + ); + let call = match operation { + HostOperation::Process(ProcessOperation::OpenExecutableImage { + source: crate::host::ExecutableImageSource::Descriptor(descriptor), + resolution: None, + }) => WorkerCall::OpenExecutableImage { descriptor }, + HostOperation::Process(ProcessOperation::ReadExecutableImage { + handle, + offset, + max_bytes, + }) => WorkerCall::ReadExecutableImage { + handle, + offset, + max_bytes: max_bytes.get(), + }, + HostOperation::Process(ProcessOperation::CloseExecutableImage { handle }) => { + WorkerCall::CloseExecutableImage { handle } + } + HostOperation::Signal(operation) => WorkerCall::Signal(operation), + _ => { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_OPERATION", + "thread worker attempted an unsupported typed host operation", + )); + } + }; + let reply = self.call(call).await?; + if signal_delivery + && matches!(&reply, HostCallReply::Json(value) if !value.is_null()) + && self + .signal_pending + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { + Some(current.saturating_sub(1)) + }) + .is_err() + { + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_SIGNAL_STATE", + "thread-worker signal wake state could not be settled", + )); + } + Ok(reply) + } + + pub(super) async fn publish_stderr(&self, bytes: Vec) -> Result<(), HostServiceError> { + self.send(WorkerFrame::Stderr { bytes }, false) + } + + pub(super) fn report_group_failure( + &self, + error: HostServiceError, + ) -> Result<(), HostServiceError> { + self.send(WorkerFrame::GroupFailed { error }, false) + } + + fn call( + &self, + call: WorkerCall, + ) -> impl std::future::Future> { + let result = (|| { + if self.failed.load(Ordering::Acquire) { + return Err(worker_pipe_closed()); + } + let id = self + .next_call_id + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { + current.checked_add(1) + }) + .map_err(|_| { + HostServiceError::new( + "EOVERFLOW", + "thread-worker host-call identity space is exhausted", + ) + })?; + let (sender, receiver) = tokio::sync::oneshot::channel(); + self.pending + .lock() + .map_err(|_| worker_pipe_closed())? + .insert(id, sender); + if let Err(error) = self.send(WorkerFrame::Call { id, call }, false) { + match self.pending.lock() { + Ok(mut pending) => { + pending.remove(&id); + } + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_PENDING_POISONED: recovering failed call state" + ); + poisoned.into_inner().remove(&id); + } + } + return Err(error); + } + Ok((id, receiver)) + })(); + async move { + let (id, receiver) = result?; + receiver.await.map_err(|_| { + HostServiceError::new( + "EPIPE", + format!("thread-worker host reply {id} was dropped"), + ) + })? + } + } + + fn send(&self, frame: WorkerFrame, flush: bool) -> Result<(), HostServiceError> { + let (flushed, receiver) = if flush { + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + (Some(sender), Some(receiver)) + } else { + (None, None) + }; + self.sender + .try_send(OutboundFrame { frame, flushed }) + .map_err(|error| match error { + std::sync::mpsc::TrySendError::Full(_) => HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "thread-worker outbound IPC queue is full", + ), + std::sync::mpsc::TrySendError::Disconnected(_) => worker_pipe_closed(), + })?; + if let Some(receiver) = receiver { + receiver.recv().map_err(|_| worker_pipe_closed())??; + } + Ok(()) + } + + fn finish(&self, result: Result) -> Result<(), HostServiceError> { + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + self.finish_ack + .lock() + .map_err(|_| worker_pipe_closed())? + .replace(sender); + if let Err(error) = self.send(WorkerFrame::Finished { result }, true) { + match self.finish_ack.lock() { + Ok(mut waiter) => { + waiter.take(); + } + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_FINISH_POISONED: recovering failed completion state" + ); + poisoned.into_inner().take(); + } + } + return Err(error); + } + receiver + .recv_timeout(WORKER_FINISH_ACK_TIMEOUT) + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_FINISH_ACK", + format!("parent did not acknowledge worker completion: {error}"), + ) + }) + } +} + +fn fail_pending(pending: &PendingCallMap, error: HostServiceError) { + let mut pending = match pending.lock() { + Ok(pending) => pending, + Err(poisoned) => { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_PENDING_POISONED: recovering pending-call state during failure" + ); + poisoned.into_inner() + } + }; + let waiters = pending + .drain() + .map(|(_, waiter)| waiter) + .collect::>(); + drop(pending); + for waiter in waiters { + if waiter.send(Err(error.clone())).is_err() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_PENDING_DROPPED: pending call waiter was dropped" + ); + } + } +} + +fn worker_pipe_closed() -> HostServiceError { + HostServiceError::new( + "EPIPE", + "thread-worker host-operation IPC channel is closed", + ) +} + +pub fn run_worker_entry() -> Result<(), HostServiceError> { + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + let startup: WorkerStartup = read_frame_blocking(&mut stdin, MAX_STARTUP_HEADER_BYTES)?; + let maximum_module_bytes = startup + .request + .limits + .max_module_file_bytes + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(256 * 1024 * 1024); + if startup.module_bytes > maximum_module_bytes { + return Err(HostServiceError::limit( + "ERR_AGENTOS_WASMTIME_MODULE_FILE_LIMIT", + "limits.resources.maxWasmModuleFileBytes", + maximum_module_bytes as u64, + startup.module_bytes as u64, + )); + } + let mut module = vec![0; startup.module_bytes]; + stdin.read_exact(&mut module).map_err(worker_read_error)?; + drop(stdin); + + let client = WorkerIpcClient::start( + startup.process, + startup.request.limits.pending_event_count.unwrap_or(64), + startup.max_frame_bytes, + )?; + let runtime = SidecarRuntime::process(&RuntimeConfig::default()).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_RUNTIME", error.to_string()) + })?; + let context = runtime.context(); + let client_for_run = client.clone(); + let result = runtime.block_on(super::lifecycle::run_worker_loaded_module( + startup.request, + module, + context, + client_for_run, + )); + client.finish(result) +} + +pub(super) async fn run_worker_process( + module: Vec, + request: StartWasmExecutionRequest, + host: super::store::WasmtimeHostClient, + control: Arc, +) -> Result { + let executable = worker_executable()?; + let max_frame_bytes = request + .limits + .pending_event_bytes + .unwrap_or(DEFAULT_MAX_WORKER_FRAME_BYTES) + .saturating_add(super::store::max_host_reply_bytes(&request)?) + .clamp(DEFAULT_MAX_WORKER_FRAME_BYTES, MAX_WORKER_FRAME_BYTES); + let startup = WorkerStartup { + process: host.process(), + module_bytes: module.len(), + max_frame_bytes, + request: request.clone(), + }; + let mut child = tokio::process::Command::new(&executable) + .arg(WORKER_MODE_ARGUMENT) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .kill_on_drop(true) + .spawn() + .map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_SPAWN", + format!("failed to spawn {}: {error}", executable.display()), + ) + })?; + let pid = child.id().ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_SPAWN", + "thread worker did not expose a native process id", + ) + })?; + let Some(mut input) = child.stdin.take() else { + terminate_and_reap(&mut child, control.teardown_timeout).await?; + return Err(worker_pipe_closed()); + }; + let Some(mut output) = child.stdout.take() else { + terminate_and_reap(&mut child, control.teardown_timeout).await?; + return Err(worker_pipe_closed()); + }; + let startup_result = async { + write_frame_async(&mut input, &startup, MAX_STARTUP_HEADER_BYTES).await?; + input.write_all(&module).await.map_err(worker_write_error)?; + input.flush().await.map_err(worker_write_error) + } + .await; + if let Err(error) = startup_result { + terminate_and_reap(&mut child, control.teardown_timeout).await?; + return Err(error); + } + control.worker_pid.store(pid, Ordering::Release); + let (control_sender, mut control_receiver) = tokio::sync::mpsc::channel(16); + if let Err(error) = control.set_worker_input(control_sender) { + terminate_and_reap(&mut child, control.teardown_timeout).await?; + control.worker_pid.store(0, Ordering::Release); + return Err(error); + } + + let wall_clock_limit_ms = request.limits.wall_clock_limit_ms; + let wall_clock = async move { + if let Some(milliseconds) = wall_clock_limit_ms { + tokio::time::sleep(Duration::from_millis(milliseconds)).await; + } else { + std::future::pending::<()>().await; + } + }; + tokio::pin!(wall_clock); + let result = loop { + tokio::select! { + biased; + () = control.cancel_notify.notified() => { + break Err(HostServiceError::new( + "ECANCELED", + "threaded WebAssembly worker was canceled", + )); + } + () = &mut wall_clock => { + break Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WALL_CLOCK_LIMIT", + "threaded WebAssembly worker exceeded its wall-clock budget", + ).with_details(serde_json::json!({ + "limitName": "limits.resources.maxWasmWallClockTimeMs", + "limit": request.limits.wall_clock_limit_ms, + }))); + } + control_frame = control_receiver.recv() => { + if let Some(frame) = control_frame { + if let Err(error) = write_frame_async(&mut input, &frame, max_frame_bytes).await { + break Err(error); + } + } + } + frame = read_frame_async::<_, WorkerFrame>(&mut output, max_frame_bytes) => { + match frame { + Ok(WorkerFrame::Call { id, call }) => { + let reply = dispatch_worker_call(&host, call).await; + if let Err(error) = write_frame_async( + &mut input, + &ParentFrame::Reply { id, result: reply }, + max_frame_bytes, + ).await { + break Err(error); + } + } + Ok(WorkerFrame::Stderr { bytes }) => { + if let Err(error) = host.publish_stderr(bytes).await { + break Err(error); + } + } + Ok(WorkerFrame::Finished { result }) => { + if let Err(error) = write_frame_async( + &mut input, + &ParentFrame::FinishedAck, + max_frame_bytes, + ).await { + break Err(error); + } + break result; + } + Ok(WorkerFrame::GroupFailed { error }) => break Err(error), + Err(error) => break Err(error), + } + } + } + }; + control.clear_worker_input(); + let cleanup = reap_after_result(&mut child, control.teardown_timeout, result.is_err()).await; + control.worker_pid.store(0, Ordering::Release); + cleanup?; + result +} + +async fn reap_after_result( + child: &mut tokio::process::Child, + timeout: Duration, + force: bool, +) -> Result<(), HostServiceError> { + let status = match child.try_wait() { + Ok(status) => status, + Err(error) => { + let primary = worker_wait_error(error); + return match terminate_and_reap(child, timeout).await { + Ok(()) => Err(primary), + Err(cleanup) => Err(combined_cleanup_error(primary, cleanup)), + }; + } + }; + if status.is_some() { + return Ok(()); + } + if force { + if let Err(error) = child.start_kill() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_KILL: initial forced termination failed: {error}" + ); + terminate_and_reap(child, timeout).await?; + return Ok(()); + } + } + match tokio::time::timeout(timeout, child.wait()).await { + Ok(Ok(_)) => Ok(()), + Ok(Err(error)) => { + let primary = worker_wait_error(error); + match terminate_and_reap(child, timeout).await { + Ok(()) => Err(primary), + Err(cleanup) => Err(combined_cleanup_error(primary, cleanup)), + } + } + Err(_) => { + let primary = HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_REAP_TIMEOUT", + "threaded WebAssembly worker exceeded the cooperative reaping deadline", + ); + match terminate_and_reap(child, timeout).await { + Ok(()) => Err(primary), + Err(cleanup) => Err(combined_cleanup_error(primary, cleanup)), + } + } + } +} + +async fn terminate_and_reap( + child: &mut tokio::process::Child, + timeout: Duration, +) -> Result<(), HostServiceError> { + match child.try_wait() { + Ok(Some(_)) => return Ok(()), + Ok(None) => {} + Err(error) => eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_WAIT: pre-kill status check failed; forcing termination: {error}" + ), + } + if let Err(error) = child.start_kill() { + eprintln!( + "ERR_AGENTOS_WASMTIME_WORKER_KILL: forced termination request failed; waiting for reap: {error}" + ); + } + match tokio::time::timeout(timeout, child.wait()).await { + Ok(Ok(_)) => Ok(()), + Ok(Err(error)) => Err(worker_wait_error(error)), + Err(_) => Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_REAP_TIMEOUT", + "threaded WebAssembly worker could not be reaped after forced termination", + )), + } +} + +fn combined_cleanup_error( + primary: HostServiceError, + cleanup: HostServiceError, +) -> HostServiceError { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_CLEANUP", + format!( + "worker cleanup failed after {}: {}; cleanup failure {}: {}", + primary.code, primary.message, cleanup.code, cleanup.message + ), + ) +} + +async fn dispatch_worker_call( + host: &super::store::WasmtimeHostClient, + call: WorkerCall, +) -> Result { + match call { + WorkerCall::Adapter { method, args, raw } => { + host.submit_adapter_call( + method, + args, + raw.into_iter().map(|raw| (raw.index, raw.bytes)).collect(), + ) + .await + } + WorkerCall::OpenExecutableImage { descriptor } => { + host.submit( + HostOperation::Process(ProcessOperation::OpenExecutableImage { + source: crate::host::ExecutableImageSource::Descriptor(descriptor), + resolution: None, + }), + std::mem::size_of::(), + ) + .await + } + WorkerCall::ReadExecutableImage { + handle, + offset, + max_bytes, + } => { + let limit = crate::backend::PayloadLimit::new( + "limits.wasm.workerExecutableReadBytes", + max_bytes, + )?; + host.submit( + HostOperation::Process(ProcessOperation::ReadExecutableImage { + handle, + offset, + max_bytes: crate::host::BoundedUsize::try_new(max_bytes, &limit)?, + }), + std::mem::size_of::() * 2 + std::mem::size_of::(), + ) + .await + } + WorkerCall::CloseExecutableImage { handle } => { + host.submit( + HostOperation::Process(ProcessOperation::CloseExecutableImage { handle }), + std::mem::size_of::(), + ) + .await + } + WorkerCall::Signal(operation) => { + host.submit( + HostOperation::Signal(operation), + std::mem::size_of::(), + ) + .await + } + } +} + +fn worker_executable() -> Result { + if let Some(path) = std::env::var_os("AGENTOS_WASMTIME_WORKER_PATH") { + let path = PathBuf::from(path); + if path.is_file() { + return Ok(path); + } + return Err(HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_PATH", + "AGENTOS_WASMTIME_WORKER_PATH does not name a file", + )); + } + let current = std::env::current_exe().map_err(|error| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_PATH", + format!("cannot resolve current executable: {error}"), + ) + })?; + if current + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name == "agentos-native-sidecar") + { + return Ok(current); + } + let sibling = current + .parent() + .and_then(|directory| directory.parent()) + .map(|directory| directory.join("agentos-native-sidecar")); + sibling.filter(|path| path.is_file()).ok_or_else(|| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_PATH", + "cannot locate the agentos-native-sidecar worker executable", + ) + }) +} + +fn encode(value: &T, maximum: usize) -> Result, HostServiceError> { + let mut bytes = Vec::new(); + ciborium::into_writer(value, &mut bytes).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_IPC_ENCODE", error.to_string()) + })?; + if bytes.len() > maximum { + return Err(HostServiceError::limit( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "limits.wasm.workerIpcFrameBytes", + maximum as u64, + bytes.len() as u64, + )); + } + Ok(bytes) +} + +fn decode(bytes: &[u8]) -> Result { + ciborium::from_reader(bytes).map_err(|error| { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_IPC_DECODE", error.to_string()) + }) +} + +fn write_frame_blocking( + writer: &mut W, + value: &T, + maximum: usize, +) -> Result<(), HostServiceError> { + let bytes = encode(value, maximum)?; + let length = u32::try_from(bytes.len()).map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "IPC frame exceeds u32", + ) + })?; + writer + .write_all(&length.to_be_bytes()) + .map_err(worker_write_error)?; + writer.write_all(&bytes).map_err(worker_write_error)?; + writer.flush().map_err(worker_write_error) +} + +fn read_frame_blocking( + reader: &mut R, + maximum: usize, +) -> Result { + let mut length = [0; 4]; + reader.read_exact(&mut length).map_err(worker_read_error)?; + let length = u32::from_be_bytes(length) as usize; + if length > maximum { + return Err(HostServiceError::limit( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "limits.wasm.workerIpcFrameBytes", + maximum as u64, + length as u64, + )); + } + let mut bytes = vec![0; length]; + reader.read_exact(&mut bytes).map_err(worker_read_error)?; + decode(&bytes) +} + +async fn write_frame_async( + writer: &mut W, + value: &T, + maximum: usize, +) -> Result<(), HostServiceError> { + let bytes = encode(value, maximum)?; + let length = u32::try_from(bytes.len()).map_err(|_| { + HostServiceError::new( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "IPC frame exceeds u32", + ) + })?; + writer + .write_all(&length.to_be_bytes()) + .await + .map_err(worker_write_error)?; + writer.write_all(&bytes).await.map_err(worker_write_error)?; + writer.flush().await.map_err(worker_write_error) +} + +async fn read_frame_async( + reader: &mut R, + maximum: usize, +) -> Result { + let mut length = [0; 4]; + reader + .read_exact(&mut length) + .await + .map_err(worker_read_error)?; + let length = u32::from_be_bytes(length) as usize; + if length > maximum { + return Err(HostServiceError::limit( + "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT", + "limits.wasm.workerIpcFrameBytes", + maximum as u64, + length as u64, + )); + } + let mut bytes = vec![0; length]; + reader + .read_exact(&mut bytes) + .await + .map_err(worker_read_error)?; + decode(&bytes) +} + +fn worker_read_error(error: std::io::Error) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_IPC_READ", error.to_string()) +} + +fn worker_write_error(error: std::io::Error) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_IPC_WRITE", error.to_string()) +} + +fn worker_wait_error(error: std::io::Error) -> HostServiceError { + HostServiceError::new("ERR_AGENTOS_WASMTIME_WORKER_WAIT", error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn worker_ipc_rejects_oversized_declared_frames_before_payload_allocation() { + let mut bytes = Cursor::new(1024_u32.to_be_bytes().to_vec()); + let error = read_frame_blocking::<_, Vec>(&mut bytes, 16) + .expect_err("oversized frame header must fail closed"); + + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT"); + assert_eq!( + error + .details + .as_ref() + .and_then(|details| details.get("limitName")) + .and_then(serde_json::Value::as_str), + Some("limits.wasm.workerIpcFrameBytes") + ); + } + + #[test] + fn worker_ipc_rejects_malformed_cbor_with_a_stable_typed_error() { + let mut bytes = Vec::from(1_u32.to_be_bytes()); + bytes.push(0xff); + let error = read_frame_blocking::<_, Vec>(&mut Cursor::new(bytes), 16) + .expect_err("invalid CBOR must not enter worker state"); + + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_WORKER_IPC_DECODE"); + } + + #[test] + fn worker_ipc_rejects_truncated_payloads_with_a_stable_typed_error() { + let mut bytes = Vec::from(4_u32.to_be_bytes()); + bytes.extend_from_slice(&[0x80]); + let error = read_frame_blocking::<_, Vec>(&mut Cursor::new(bytes), 16) + .expect_err("truncated worker frame must fail closed"); + + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_WORKER_IPC_READ"); + } + + #[test] + fn worker_ipc_bounds_encoded_payloads_before_writing_a_header() { + let mut output = Vec::new(); + let error = write_frame_blocking(&mut output, &vec![0_u8; 32], 8) + .expect_err("oversized encoded frame must not be written"); + + assert_eq!(error.code, "ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT"); + assert!( + output.is_empty(), + "failed frame must not write a partial header" + ); + } +} diff --git a/crates/execution/tests/wasm.rs b/crates/execution/tests/wasm.rs index 96393a69ef..d3e021c56f 100644 --- a/crates/execution/tests/wasm.rs +++ b/crates/execution/tests/wasm.rs @@ -255,6 +255,7 @@ fn wasm_limits_from_env(env: &BTreeMap) -> WasmExecutionLimits { max_sync_rpc_response_line_bytes: None, pending_event_count: None, pending_event_bytes: None, + max_threads: None, } } @@ -3071,7 +3072,7 @@ fn wasm_configured_stack_byte_limit_fails_closed_for_v8() { // the binary's wall WORSE (33-92s vs this ~20s collapsed run) and is unsafe, so // the coverage stays collapsed here. #[test] -fn wasm_suite() { +fn v8_wasm_compatibility_runner_suite() { wasm_contexts_preserve_vm_and_module_configuration(); wasm_execution_stays_inside_v8_runtime_without_host_node_launches(); wasm_execution_runs_guest_module_through_v8(); diff --git a/crates/execution/tests/wasm_host_fs_errno_contract.rs b/crates/execution/tests/wasm_host_fs_errno_contract.rs index 9b1f6ad51e..f840ccc543 100644 --- a/crates/execution/tests/wasm_host_fs_errno_contract.rs +++ b/crates/execution/tests/wasm_host_fs_errno_contract.rs @@ -26,6 +26,25 @@ fn existing_path_errors_reach_libc_as_eexist() { ); } +#[test] +fn resource_limit_errors_reach_libc_as_enomem() { + let source = runner_source(); + let start = source + .find("function mapHostProcessError(") + .expect("host error mapper"); + let end = source[start..] + .find("\n}\n\nfunction seekGuestFileHandle") + .map(|offset| start + offset) + .expect("end of host error mapper"); + let error_map = &source[start..end]; + + assert!( + source.contains("const WASI_ERRNO_NOMEM = 48;") + && error_map.contains("case 'ENOMEM':\n return WASI_ERRNO_NOMEM;"), + "host ENOMEM must retain the preview1/WASI errno value instead of becoming EFAULT" + ); +} + #[test] fn path_open_preserves_directory_and_nofollow_before_kernel_mutation() { let source = runner_source(); diff --git a/crates/kernel/src/fd_table.rs b/crates/kernel/src/fd_table.rs index e8f67ea81e..e7f63a4866 100644 --- a/crates/kernel/src/fd_table.rs +++ b/crates/kernel/src/fd_table.rs @@ -419,10 +419,22 @@ pub struct FileDescription { backing: Mutex, lock_target: Option, cursor: AtomicU64, + directory_snapshot: Mutex>>, flags: AtomicU32, ref_count: AtomicUsize, } +/// One stable record in an open directory description's current getdents +/// stream. This is cursor state, not a second filesystem namespace: rewinding +/// to cookie zero replaces it from the authoritative VFS. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirectorySnapshotEntry { + pub name: String, + pub ino: u64, + pub is_directory: bool, + pub is_symbolic_link: bool, +} + impl FileDescription { pub fn new(id: u64, path: impl Into, flags: u32) -> Self { Self::with_ref_count_and_lock(id, path, flags, 1, None) @@ -453,6 +465,7 @@ impl FileDescription { backing: Mutex::new(FileBacking::Path(path.into())), lock_target, cursor: AtomicU64::new(0), + directory_snapshot: Mutex::new(None), flags: AtomicU32::new(flags), ref_count: AtomicUsize::new(ref_count), } @@ -758,6 +771,32 @@ impl FileDescription { self.cursor.store(cursor, Ordering::SeqCst); } + /// Replace the entries backing this open directory description's current + /// getdents stream. Linux directory offsets remain usable when entries + /// returned by an earlier page are removed, so a mutable vector index into + /// the live directory is not a valid cookie implementation. + pub fn reset_directory_snapshot(&self, entries: Vec) { + *lock_or_recover(&self.directory_snapshot) = Some(entries); + } + + /// Return the requested child-entry slice from the current directory + /// stream, together with the total number of snapshotted children. + pub fn directory_snapshot_page( + &self, + first_child: usize, + max_children: usize, + ) -> Option<(usize, Vec)> { + let snapshot = lock_or_recover(&self.directory_snapshot); + let entries = snapshot.as_ref()?; + let page = entries + .iter() + .skip(first_child) + .take(max_children) + .cloned() + .collect(); + Some((entries.len(), page)) + } + pub fn flags(&self) -> u32 { self.flags.load(Ordering::SeqCst) } diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 240196e3c5..d297a93484 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -9,13 +9,13 @@ use crate::dns::{ #[cfg(test)] use crate::fd_table::WASI_RIGHT_FD_FILESTAT_GET; use crate::fd_table::{ - AnonymousFile, AnonymousFileUsage, FdEntry, FdStat, FdTableError, FdTableManager, - FileDescription, FileLockManager, FileLockTarget, FlockOperation, ProcessFdTable, RecordLock, - RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, - FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SOCKET_DGRAM, - FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, F_SETFD, O_APPEND, O_CREAT, O_DIRECT, - O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, - WASI_PREOPEN_READ_RIGHTS_BASE, WASI_PREOPEN_READ_RIGHTS_INHERITING, + AnonymousFile, AnonymousFileUsage, DirectorySnapshotEntry, FdEntry, FdStat, FdTableError, + FdTableManager, FileDescription, FileLockManager, FileLockTarget, FlockOperation, + ProcessFdTable, RecordLock, RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, + FILETYPE_CHARACTER_DEVICE, FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, + FILETYPE_SOCKET_DGRAM, FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, F_SETFD, + O_APPEND, O_CREAT, O_DIRECT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_RDWR, + O_TRUNC, O_WRONLY, WASI_PREOPEN_READ_RIGHTS_BASE, WASI_PREOPEN_READ_RIGHTS_INHERITING, WASI_PREOPEN_READ_WRITE_RIGHTS_BASE, WASI_PREOPEN_READ_WRITE_RIGHTS_INHERITING, WASI_PREOPEN_WRITE_RIGHTS_BASE, WASI_PREOPEN_WRITE_RIGHTS_INHERITING, WASI_RIGHT_FD_ALLOCATE, WASI_RIGHT_FD_DATASYNC, WASI_RIGHT_FD_FILESTAT_SET_SIZE, WASI_RIGHT_FD_FILESTAT_SET_TIMES, @@ -114,6 +114,15 @@ pub struct RuntimeLaunchImage { pub mode: u32, } +/// Kernel-resolved executable image and Linux argv after following a bounded +/// shebang chain. Executors compile only `image`; process semantics remain in +/// the kernel so V8 and Wasmtime cannot disagree about interpreter handling. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedRuntimeLaunchImage { + pub image: RuntimeLaunchImage, + pub argv: Vec, +} + /// Bounded prefix used to classify a runtime image before the engine-specific /// full-image loader applies its own named size limit. #[derive(Debug, Clone, PartialEq, Eq)] @@ -183,7 +192,13 @@ impl fmt::Display for KernelError { impl Error for KernelError {} -fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult> { +#[derive(Debug, Clone, PartialEq, Eq)] +struct LinuxShebang { + interpreter: String, + optional_argument: Option, +} + +fn parse_linux_shebang(header: &[u8], path: &str) -> KernelResult> { if !header.starts_with(b"#!") { return Ok(None); } @@ -224,7 +239,32 @@ fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult KernelResult<()> { + Ok(self + .processes + .register_signal_thread(self.pid, thread_id, inherit_from)?) + } + + pub fn unregister_signal_thread(&self, thread_id: u32) -> KernelResult<()> { + Ok(self + .processes + .unregister_signal_thread(self.pid, thread_id)?) + } + + pub fn begin_signal_delivery_for_thread( + &self, + thread_id: u32, + ) -> KernelResult> { + Ok(self + .processes + .begin_signal_delivery_for_thread(self.pid, thread_id)?) + } + pub fn end_signal_delivery(&self, token: u64) -> KernelResult<()> { Ok(self.processes.end_signal_delivery(self.pid, token)?) } + pub fn end_signal_delivery_for_thread(&self, thread_id: u32, token: u64) -> KernelResult<()> { + Ok(self + .processes + .end_signal_delivery_for_thread(self.pid, thread_id, token)?) + } + pub fn sigprocmask(&self, how: SigmaskHow, set: SignalSet) -> KernelResult { Ok(self.processes.sigprocmask(self.pid, how, set)?) } + pub fn sigprocmask_for_thread( + &self, + thread_id: u32, + how: SigmaskHow, + set: SignalSet, + ) -> KernelResult { + Ok(self + .processes + .sigprocmask_for_thread(self.pid, thread_id, how, set)?) + } + pub fn sigpending(&self) -> KernelResult { Ok(self.processes.sigpending(self.pid)?) } @@ -606,10 +684,30 @@ impl KernelProcessHandle { Ok(self.processes.begin_temporary_signal_mask(self.pid, mask)?) } + pub fn begin_temporary_signal_mask_for_thread( + &self, + thread_id: u32, + mask: SignalSet, + ) -> KernelResult { + Ok(self + .processes + .begin_temporary_signal_mask_for_thread(self.pid, thread_id, mask)?) + } + pub fn end_temporary_signal_mask(&self, token: u64) -> KernelResult<()> { Ok(self.processes.end_temporary_signal_mask(self.pid, token)?) } + pub fn end_temporary_signal_mask_for_thread( + &self, + thread_id: u32, + token: u64, + ) -> KernelResult<()> { + Ok(self + .processes + .end_temporary_signal_mask_for_thread(self.pid, thread_id, token)?) + } + pub fn end_temporary_signal_mask_and_begin_signal_delivery( &self, token: u64, @@ -618,6 +716,18 @@ impl KernelProcessHandle { .processes .end_temporary_signal_mask_and_begin_signal_delivery(self.pid, token)?) } + + pub fn end_temporary_signal_mask_and_begin_signal_delivery_for_thread( + &self, + thread_id: u32, + token: u64, + ) -> KernelResult> { + Ok(self + .processes + .end_temporary_signal_mask_and_begin_signal_delivery_for_thread( + self.pid, thread_id, token, + )?) + } } #[derive(Debug, Clone)] @@ -3855,6 +3965,147 @@ impl KernelVm { }) } + /// Load the exact `fexecve(2)` source and resolve Linux shebang semantics + /// before handing an engine the final WebAssembly image. The descriptor + /// snapshot remains the source of truth for the first image; interpreters + /// are resolved live through the kernel VFS and registered command + /// projections. No executor-local filesystem mirror participates. + #[allow(clippy::too_many_arguments)] + pub fn load_resolved_process_runtime_image_from_fd( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + cwd: &str, + argv: &[String], + close_on_exec_fds: &[u32], + maximum_bytes: u64, + ) -> KernelResult { + let image = + self.load_process_runtime_image_from_fd(requester_driver, pid, fd, maximum_bytes)?; + self.resolve_process_runtime_image( + pid, + image, + format!("/proc/self/fd/{fd}"), + Some(fd), + cwd, + argv, + close_on_exec_fds, + maximum_bytes, + ) + } + + /// Resolve a pathname `execve(2)` image through the same kernel-owned + /// shebang path used by descriptor exec. `subject` remains the caller's + /// pathname spelling because Linux exposes it to the interpreter argv. + pub fn load_resolved_process_runtime_image( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + cwd: &str, + argv: &[String], + maximum_bytes: u64, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let image = self.load_process_runtime_exec_path(pid, path, cwd, maximum_bytes)?; + self.resolve_process_runtime_image( + pid, + image, + path.to_owned(), + None, + cwd, + argv, + &[], + maximum_bytes, + ) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_process_runtime_image( + &mut self, + pid: u32, + mut image: RuntimeLaunchImage, + mut subject: String, + descriptor_script_fd: Option, + cwd: &str, + argv: &[String], + close_on_exec_fds: &[u32], + maximum_bytes: u64, + ) -> KernelResult { + let mut resolved_argv = argv.to_vec(); + let mut interpreter_depth = 0; + let mut descriptor_script_fd = descriptor_script_fd; + + loop { + if image.bytes.starts_with(b"\0asm") { + return Ok(ResolvedRuntimeLaunchImage { + image, + argv: resolved_argv, + }); + } + let header = &image.bytes[..image.bytes.len().min(SHEBANG_LINE_MAX_BYTES)]; + let Some(shebang) = parse_linux_shebang(header, &subject)? else { + return Err(KernelError::new( + "ENOEXEC", + format!("exec format error: {subject}"), + )); + }; + if descriptor_script_fd.is_some_and(|fd| close_on_exec_fds.contains(&fd)) { + return Err(KernelError::new( + "ENOENT", + format!("{subject} will be closed before its interpreter opens it"), + )); + } + descriptor_script_fd = None; + if interpreter_depth >= MAX_EXEC_INTERPRETER_DEPTH { + return Err(KernelError::new( + "ELOOP", + format!("interpreter recursion for {subject} exceeds the Linux limit"), + )); + } + interpreter_depth += 1; + + let mut interpreter_argv = Vec::with_capacity(resolved_argv.len() + 2); + interpreter_argv.push(shebang.interpreter.clone()); + if let Some(argument) = shebang.optional_argument { + interpreter_argv.push(argument); + } + interpreter_argv.push(subject); + interpreter_argv.extend(resolved_argv.into_iter().skip(1)); + resolved_argv = interpreter_argv; + subject = shebang.interpreter.clone(); + + image = + self.load_process_runtime_exec_path(pid, &shebang.interpreter, cwd, maximum_bytes)?; + } + } + + fn load_process_runtime_exec_path( + &mut self, + pid: u32, + path: &str, + cwd: &str, + maximum_bytes: u64, + ) -> KernelResult { + let requested_path = if path.starts_with('/') { + normalize_path(path) + } else { + normalize_path(&format!("{cwd}/{path}")) + }; + let registered_command = self.resolve_registered_command_path(&requested_path); + let authorized_path = self + .resolve_executable_path(path, cwd, Some(pid))? + .ok_or_else(|| KernelError::command_not_found(path))?; + let runtime_path = if let Some(command) = registered_command { + self.resolve_registered_command_runtime_path(&command)? + } else { + authorized_path + }; + self.load_runtime_image_after_authorization(&runtime_path, maximum_bytes) + } + pub fn load_process_runtime_image_prefix( &mut self, requester_driver: &str, @@ -6378,6 +6629,26 @@ impl KernelVm { return Err(read_only_filesystem_error(path)); } + if let ProcNode::PidFdLink { + pid: target_pid, + fd, + } = &proc_node + { + if *target_pid == pid { + // `/proc/self/fd/N` is an open-file-description alias, not + // a pathname reopen. Following its display target would + // break Linux's unlinked-fd semantics (notably fexecve of + // scripts), because that target ends in ` (deleted)`. + return self.fd_open( + requester_driver, + pid, + &format!("/dev/fd/{fd}"), + flags, + mode, + ); + } + } + if matches!( proc_node, ProcNode::SelfLink { .. } @@ -6497,6 +6768,30 @@ impl KernelVm { self.assert_driver_owns(requester_driver, pid)?; let tier = self.processes.permission_tier(pid)?; + // Resolve the current process's proc-fd spelling to the existing + // `/dev/fd` capability path before pathname canonicalization. Both are + // Linux symlink aliases for the same open description, but following + // procfs's display target would fail for an unlinked file. + if parse_dev_fd_path(path)?.is_none() { + if let Some(ProcNode::PidFdLink { + pid: target_pid, + fd, + }) = self.resolve_proc_node(path, Some(pid))? + { + if target_pid == pid { + return self.fd_open_with_rights( + requester_driver, + pid, + parent_fd, + &format!("/dev/fd/{fd}"), + flags, + mode, + requested_rights, + ); + } + } + } + if requested_rights.is_some() && parent_fd.is_none() { return Err(KernelError::new( "EACCES", @@ -6598,8 +6893,18 @@ impl KernelVm { } } - let source_rights = if parse_dev_fd_path(path)?.is_some() { - let source_fd = parse_dev_fd_path(path)?.expect("checked above"); + let source_fd = if let Some(source_fd) = parse_dev_fd_path(path)? { + Some(source_fd) + } else { + match self.resolve_proc_node(path, Some(pid))? { + Some(ProcNode::PidFdLink { + pid: target_pid, + fd, + }) if target_pid == pid => Some(fd), + _ => None, + } + }; + let source_rights = if let Some(source_fd) = source_fd { let source = self.fd_stat(requester_driver, pid, source_fd)?; Some((source.rights, source.rights_inheriting)) } else { @@ -7864,12 +8169,14 @@ impl KernelVm { } let path = self.fd_path(requester_driver, pid, fd)?; let children = self.read_dir_with_types_for_process(requester_driver, pid, &path)?; - self.resources - .check_readdir_entries(children.len().saturating_add(2))?; // fd_readdir is the Linux-like descriptor traversal surface. Unlike // path-based Node readdir, it exposes the synthetic current/parent // entries and inode identities used by libc readdir/telldir/seekdir. + // The configured limit bounds real directory children; these two + // kernel-synthesized framing entries must not make an exactly-at-limit + // directory unreadable. `children` was already checked above, so this + // allocation remains bounded by maxReaddirEntries + 2. let current_stat = self.stat_internal(Some(pid), &path)?; let parent = parent_path(&path); let parent_stat = self.stat_internal(Some(pid), &parent)?; @@ -7899,6 +8206,93 @@ impl KernelVm { Ok(entries) } + /// Reads one descriptor-directory page without inode-statting entries that + /// cannot fit in the requested page. `cookie` and returned ordering use the + /// same logical stream as [`Self::fd_read_dir_with_types`], including `.` + /// and `..` at positions zero and one. A read beginning at cookie zero + /// snapshots the bounded child-name set on the shared open-file + /// description. Later pages use that snapshot so removing an earlier page + /// cannot shift live vector indices and skip undeleted entries. + pub fn fd_read_dir_page_with_types( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + cookie: usize, + max_entries: usize, + ) -> KernelResult> { + let stat = self.fd_stat(requester_driver, pid, fd)?; + if stat.filetype != FILETYPE_DIRECTORY { + return Err(KernelError::new( + "ENOTDIR", + format!("file descriptor {fd} is not a directory"), + )); + } + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_directory_stat().is_some() || max_entries == 0 { + return Ok(Vec::new()); + } + let path = self.fd_path(requester_driver, pid, fd)?; + if cookie == 0 || description.directory_snapshot_page(0, 0).is_none() { + let children = self.read_dir_with_types_for_process(requester_driver, pid, &path)?; + let mut snapshot = Vec::with_capacity(children.len()); + for child in children { + let child_path = join_child_path(&path, &child.name); + let child_stat = self.lstat_internal(Some(pid), &child_path)?; + snapshot.push(DirectorySnapshotEntry { + name: child.name, + ino: required_dirent_ino(&child_path, child_stat.ino)?, + is_directory: child.is_directory, + is_symbolic_link: child.is_symbolic_link, + }); + } + description.reset_directory_snapshot(snapshot); + } + let first_child = cookie.saturating_sub(2); + let requested_children = cookie + .saturating_add(max_entries) + .saturating_sub(cookie.max(2)); + let (child_count, child_names) = description + .directory_snapshot_page(first_child, requested_children) + .expect("directory snapshot was initialized above"); + let total_entries = child_count.saturating_add(2); + if cookie >= total_entries { + return Ok(Vec::new()); + } + + let mut entries = Vec::with_capacity(max_entries.min(total_entries - cookie)); + let page_end = cookie.saturating_add(max_entries).min(total_entries); + if cookie == 0 && page_end > 0 { + let current_stat = self.stat_internal(Some(pid), &path)?; + entries.push(ProcessFdDirEntry { + name: String::from("."), + ino: required_dirent_ino(&path, current_stat.ino)?, + is_directory: true, + is_symbolic_link: false, + }); + } + if cookie <= 1 && page_end > 1 { + let parent = parent_path(&path); + let parent_stat = self.stat_internal(Some(pid), &parent)?; + entries.push(ProcessFdDirEntry { + name: String::from(".."), + ino: required_dirent_ino(&parent, parent_stat.ino)?, + is_directory: true, + is_symbolic_link: false, + }); + } + + for child in child_names { + entries.push(ProcessFdDirEntry { + name: child.name, + ino: child.ino, + is_directory: child.is_directory, + is_symbolic_link: child.is_symbolic_link, + }); + } + Ok(entries) + } + pub fn fd_path(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { let description = self.description_for_fd(requester_driver, pid, fd)?; Ok(description.path()) @@ -9054,7 +9448,7 @@ impl KernelVm { return Ok(()); } - let Some(interpreter) = linux_shebang_interpreter(&header, &resolved)? else { + let Some(shebang) = parse_linux_shebang(&header, &resolved)? else { return Err(KernelError::new( "ENOEXEC", format!("exec format error: {resolved}"), @@ -9067,12 +9461,12 @@ impl KernelVm { )); } - self.validate_wasm_exec_image_inner(&interpreter, cwd, interpreter_depth + 1) + self.validate_wasm_exec_image_inner(&shebang.interpreter, cwd, interpreter_depth + 1) } fn resolve_registered_command_path(&self, path: &str) -> Option { let normalized = normalize_path(path); - for prefix in ["/bin/", "/usr/bin/", "/usr/local/bin/"] { + for prefix in ["/bin/", "/usr/bin/", "/usr/local/bin/", "/opt/agentos/bin/"] { let Some(name) = normalized.strip_prefix(prefix) else { continue; }; @@ -9093,6 +9487,44 @@ impl KernelVm { None } + fn resolve_registered_command_runtime_path(&mut self, command: &str) -> KernelResult { + let mut roots = match self.read_dir("/__secure_exec/commands") { + Ok(roots) => roots, + Err(error) if error.code() == "ENOENT" => Vec::new(), + Err(error) => return Err(error), + }; + roots.retain(|entry| { + !entry.is_empty() && entry.chars().all(|character| character.is_ascii_digit()) + }); + roots.sort(); + for root in roots { + let candidate = normalize_path(&format!("/__secure_exec/commands/{root}/{command}")); + if let Some(path) = self.resolve_live_runtime_file(&candidate)? { + return Ok(path); + } + } + + let projected = normalize_path(&format!("/opt/agentos/bin/{command}")); + if let Some(path) = self.resolve_live_runtime_file(&projected)? { + return Ok(path); + } + + Err(KernelError::new( + "ENOENT", + format!("registered command image is unavailable: {command}"), + )) + } + + fn resolve_live_runtime_file(&mut self, candidate: &str) -> KernelResult> { + let canonical = match self.raw_filesystem_mut().realpath(candidate) { + Ok(path) => normalize_path(&path), + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => return Ok(None), + Err(error) => return Err(error.into()), + }; + let stat = self.raw_filesystem_mut().lstat(&canonical)?; + Ok((!stat.is_directory && !stat.is_symbolic_link).then_some(canonical)) + } + fn parse_shebang_command(&mut self, path: &str) -> KernelResult> { let header = self.filesystem.pread(path, 0, SHEBANG_LINE_MAX_BYTES + 1)?; if !header.starts_with(b"#!") { @@ -12094,6 +12526,99 @@ mod tests { .expect("open description survives unlink"); assert_eq!(unlinked.bytes, image); assert!(unlinked.canonical_path.ends_with(" (deleted)")); + let alias = kernel + .fd_open("wasm", pid, &format!("/proc/self/fd/{fd}"), O_RDONLY, None) + .expect("proc fd alias duplicates an unlinked open description"); + assert_eq!( + kernel + .fd_pread("wasm", pid, alias, image.len(), 0) + .expect("read duplicated unlinked proc fd alias"), + image + ); + } + + #[test] + fn descriptor_runtime_image_resolves_shebang_in_kernel() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + kernel + .register_driver(CommandDriver::new("wasm", ["sh"])) + .expect("register shell projection"); + kernel + .mkdir("/__secure_exec/commands/0", true) + .expect("create command projection root"); + let shell_image = b"\0asm\x01\0\0\0".to_vec(); + kernel + .write_file("/__secure_exec/commands/0/sh", shell_image.clone()) + .expect("write projected shell image"); + kernel + .write_file_for_process( + "wasm", + pid, + "/script", + b"#!/bin/sh -e\necho ok\n".to_vec(), + Some(0o755), + ) + .expect("write executable script"); + let fd = kernel + .fd_open("wasm", pid, "/script", O_RDONLY, None) + .expect("open script"); + + let resolved = kernel + .load_resolved_process_runtime_image_from_fd( + "wasm", + pid, + fd, + "/", + &[String::from("script-name"), String::from("argument")], + &[], + 1024, + ) + .expect("resolve descriptor shebang"); + assert_eq!(resolved.image.bytes, shell_image); + assert_eq!( + resolved.argv, + vec![ + String::from("/bin/sh"), + String::from("-e"), + format!("/proc/self/fd/{fd}"), + String::from("argument"), + ] + ); + + let resolved_path = kernel + .load_resolved_process_runtime_image( + "wasm", + pid, + "/script", + "/", + &[String::from("script-name"), String::from("argument")], + 1024, + ) + .expect("resolve pathname shebang"); + assert_eq!(resolved_path.image.bytes, shell_image); + assert_eq!( + resolved_path.argv, + vec![ + String::from("/bin/sh"), + String::from("-e"), + String::from("/script"), + String::from("argument"), + ] + ); + + let error = kernel + .load_resolved_process_runtime_image_from_fd( + "wasm", + pid, + fd, + "/", + &[String::from("script-name")], + &[fd], + 1024, + ) + .expect_err("close-on-exec script descriptor must fail like Linux"); + assert_eq!(error.code(), "ENOENT"); } #[test] diff --git a/crates/kernel/src/process_table.rs b/crates/kernel/src/process_table.rs index f4d477a572..545910bcea 100644 --- a/crates/kernel/src/process_table.rs +++ b/crates/kernel/src/process_table.rs @@ -28,6 +28,8 @@ pub const SIGPIPE: i32 = 13; pub const SIGWINCH: i32 = 28; const MAX_SIGNAL: i32 = 64; const MAX_SIGNAL_HANDLER_DEPTH: usize = 64; +const MAX_SIGNAL_THREADS_PER_PROCESS: usize = 1024; +pub const MAIN_SIGNAL_THREAD_ID: u32 = 0; const SIGTTIN: i32 = 21; const SIGTTOU: i32 = 22; const SIGURG: i32 = 23; @@ -324,6 +326,8 @@ pub struct SignalDelivery { pub token: u64, pub signal: i32, pub action: SignalAction, + /// Kernel signal-thread record selected for this process-directed signal. + pub thread_id: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -538,17 +542,32 @@ struct ProcessRecord { entry: ProcessEntry, runtime_endpoint: Arc, pending_wait_events: VecDeque, - blocked_signals: SignalSet, pending_signals: SignalSet, signal_actions: [SignalAction; MAX_SIGNAL as usize], - signal_deliveries: Vec, - temporary_signal_masks: Vec, + signal_threads: BTreeMap, next_signal_delivery_token: u64, next_signal_mask_token: u64, resource_limits: ProcessResourceLimits, permission_tier: ProcessPermissionTier, } +#[derive(Debug, Clone)] +struct ProcessThreadSignalState { + blocked_signals: SignalSet, + signal_deliveries: Vec, + temporary_signal_masks: Vec, +} + +impl ProcessThreadSignalState { + fn new(blocked_signals: SignalSet) -> Self { + Self { + blocked_signals, + signal_deliveries: Vec::new(), + temporary_signal_masks: Vec::new(), + } + } +} + struct ScheduledSignalDelivery { pid: u32, signal: i32, @@ -742,11 +761,12 @@ impl ProcessTable { entry: entry.clone(), runtime_endpoint, pending_wait_events: VecDeque::new(), - blocked_signals: ctx.blocked_signals, pending_signals: ctx.pending_signals, signal_actions: inherited_signal_actions, - signal_deliveries: Vec::new(), - temporary_signal_masks: Vec::new(), + signal_threads: BTreeMap::from([( + MAIN_SIGNAL_THREAD_ID, + ProcessThreadSignalState::new(ctx.blocked_signals), + )]), next_signal_delivery_token: 1, next_signal_mask_token: 1, resource_limits: ctx.resource_limits, @@ -789,7 +809,11 @@ impl ProcessTable { umask: parent.entry.umask, fds: ProcessFileDescriptors::default(), identity: parent.entry.identity.clone(), - blocked_signals: parent.blocked_signals, + blocked_signals: parent + .signal_threads + .get(&MAIN_SIGNAL_THREAD_ID) + .map(|thread| thread.blocked_signals) + .unwrap_or_else(SignalSet::empty), pending_signals: SignalSet::empty(), resource_limits: parent.resource_limits.clone(), permission_tier: parent.permission_tier, @@ -896,8 +920,7 @@ impl ProcessTable { *action = SignalAction::DEFAULT; } } - record.signal_deliveries.clear(); - record.temporary_signal_masks.clear(); + reset_signal_threads_for_exec(record); self.inner.notify_waiters(); Ok(()) } @@ -1422,13 +1445,83 @@ impl ProcessTable { *action = SignalAction::DEFAULT; } } - record.signal_deliveries.clear(); - record.temporary_signal_masks.clear(); + reset_signal_threads_for_exec(record); Ok(()) } - /// Atomically installs the temporary process mask used by `ppoll`. - pub fn begin_temporary_signal_mask(&self, pid: u32, mut mask: SignalSet) -> ProcessResult { + pub fn register_signal_thread( + &self, + pid: u32, + thread_id: u32, + inherit_from: u32, + ) -> ProcessResult<()> { + let deliveries = { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + if record.signal_threads.contains_key(&thread_id) { + return Err(ProcessTableError::invalid_argument(format!( + "signal thread {thread_id} already exists for process {pid}" + ))); + } + if record.signal_threads.len() >= MAX_SIGNAL_THREADS_PER_PROCESS { + return Err(ProcessTableError { + code: "EAGAIN", + message: format!( + "process {pid} exceeded kernel.signal.maxThreadsPerProcess={MAX_SIGNAL_THREADS_PER_PROCESS}" + ), + }); + } + let inherited = record + .signal_threads + .get(&inherit_from) + .ok_or_else(|| { + ProcessTableError::invalid_argument(format!( + "signal thread {inherit_from} does not exist for process {pid}" + )) + })? + .blocked_signals; + record + .signal_threads + .insert(thread_id, ProcessThreadSignalState::new(inherited)); + collect_pending_signal_deliveries(record)? + }; + deliver_signals(&self.inner, deliveries); + Ok(()) + } + + pub fn unregister_signal_thread(&self, pid: u32, thread_id: u32) -> ProcessResult<()> { + if thread_id == MAIN_SIGNAL_THREAD_ID { + return Err(ProcessTableError::invalid_argument( + "the main process signal thread cannot be unregistered", + )); + } + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + record.signal_threads.remove(&thread_id).ok_or_else(|| { + ProcessTableError::invalid_argument(format!( + "signal thread {thread_id} does not exist for process {pid}" + )) + })?; + Ok(()) + } + + /// Atomically installs the temporary mask used by `ppoll` for one thread. + pub fn begin_temporary_signal_mask(&self, pid: u32, mask: SignalSet) -> ProcessResult { + self.begin_temporary_signal_mask_for_thread(pid, MAIN_SIGNAL_THREAD_ID, mask) + } + + pub fn begin_temporary_signal_mask_for_thread( + &self, + pid: u32, + thread_id: u32, + mut mask: SignalSet, + ) -> ProcessResult { mask.remove(SIGKILL)?; mask.remove(SIGSTOP)?; let (token, deliveries) = { @@ -1437,180 +1530,170 @@ impl ProcessTable { .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - if record.temporary_signal_masks.len() >= MAX_SIGNAL_HANDLER_DEPTH { - return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); - } let token = record.next_signal_mask_token; record.next_signal_mask_token = record.next_signal_mask_token.checked_add(1).unwrap_or(1); - record.temporary_signal_masks.push(TemporarySignalMask { + let thread = signal_thread_mut(record, pid, thread_id)?; + if thread.temporary_signal_masks.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } + thread.temporary_signal_masks.push(TemporarySignalMask { token, - previous_mask: record.blocked_signals, + previous_mask: thread.blocked_signals, }); - record.blocked_signals = mask; + thread.blocked_signals = mask; (token, collect_pending_signal_deliveries(record)?) }; deliver_signals(&self.inner, deliveries); Ok(token) } - /// Restores the previous mask for a `ppoll` scope and schedules any signal - /// that became deliverable as part of restoration. pub fn end_temporary_signal_mask(&self, pid: u32, token: u64) -> ProcessResult<()> { + self.end_temporary_signal_mask_for_thread(pid, MAIN_SIGNAL_THREAD_ID, token) + } + + pub fn end_temporary_signal_mask_for_thread( + &self, + pid: u32, + thread_id: u32, + token: u64, + ) -> ProcessResult<()> { let deliveries = { let mut state = self.inner.lock_state(); let record = state .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - let Some(scope) = record.temporary_signal_masks.last().copied() else { + let thread = signal_thread_mut(record, pid, thread_id)?; + let Some(scope) = thread.temporary_signal_masks.last().copied() else { return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); }; if scope.token != token { return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); } - record.temporary_signal_masks.pop(); - record.blocked_signals = scope.previous_mask; + thread.temporary_signal_masks.pop(); + thread.blocked_signals = scope.previous_mask; collect_pending_signal_deliveries(record)? }; deliver_signals(&self.inner, deliveries); Ok(()) } - /// Atomically selects one caught signal that became deliverable under a - /// `ppoll` mask, restores the caller's mask, and starts its handler using - /// that restored mask as the handler frame's previous state. - /// - /// Selection must happen before restoration or a signal unblocked only by - /// `ppoll` would disappear from the deliverable set. Handler setup must - /// happen after restoration so nested handlers and `sigprocmask` observe - /// the real caller mask, matching Linux's atomic ppoll return semantics. pub fn end_temporary_signal_mask_and_begin_signal_delivery( &self, pid: u32, token: u64, + ) -> ProcessResult> { + self.end_temporary_signal_mask_and_begin_signal_delivery_for_thread( + pid, + MAIN_SIGNAL_THREAD_ID, + token, + ) + } + + pub fn end_temporary_signal_mask_and_begin_signal_delivery_for_thread( + &self, + pid: u32, + thread_id: u32, + token: u64, ) -> ProcessResult> { let mut state = self.inner.lock_state(); let record = state .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - let Some(scope) = record.temporary_signal_masks.last().copied() else { - return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + let selected = { + let pending_signals = record.pending_signals; + let signal_actions = record.signal_actions; + let thread = signal_thread_mut(record, pid, thread_id)?; + let Some(scope) = thread.temporary_signal_masks.last().copied() else { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + }; + if scope.token != token { + return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); + } + if thread.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } + let selected = pending_signals + .difference(thread.blocked_signals) + .signals() + .into_iter() + .find(|signal| { + signal_actions[(*signal - 1) as usize].disposition == SignalDisposition::User + }); + thread.temporary_signal_masks.pop(); + thread.blocked_signals = scope.previous_mask; + selected }; - if scope.token != token { - return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); - } - if record.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { - return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); - } - - let selected = record - .pending_signals - .difference(record.blocked_signals) - .signals() - .into_iter() - .find(|signal| { - record.signal_actions[(*signal - 1) as usize].disposition == SignalDisposition::User - }); - record.temporary_signal_masks.pop(); - record.blocked_signals = scope.previous_mask; + selected + .map(|signal| claim_signal_for_thread(record, pid, thread_id, signal)) + .transpose() + } - let Some(signal) = selected else { + /// Claims one process-directed signal and deterministically selects the + /// lowest registered thread that does not block it. + pub fn begin_signal_delivery(&self, pid: u32) -> ProcessResult> { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + let Some((signal, thread_id)) = select_signal_target(record) else { return Ok(None); }; - record.pending_signals.remove(signal)?; - let action = record.signal_actions[(signal - 1) as usize]; - let previous_mask = record.blocked_signals; - record.blocked_signals = record.blocked_signals.union(action.mask); - if action.flags & SA_NODEFER == 0 { - record.blocked_signals.insert(signal)?; - } - record.blocked_signals.remove(SIGKILL)?; - record.blocked_signals.remove(SIGSTOP)?; - let delivery_token = record.next_signal_delivery_token; - record.next_signal_delivery_token = record - .next_signal_delivery_token - .checked_add(1) - .unwrap_or(1); - record.signal_deliveries.push(InProgressSignalDelivery { - token: delivery_token, - previous_mask, - }); - if action.flags & SA_RESETHAND != 0 { - record.signal_actions[(signal - 1) as usize] = SignalAction::DEFAULT; - } - Ok(Some(SignalDelivery { - token: delivery_token, - signal, - action, - })) - } - - /// Claims one caught, unblocked signal and applies its handler mask. - pub fn begin_signal_delivery(&self, pid: u32) -> ProcessResult> { + claim_signal_for_thread(record, pid, thread_id, signal).map(Some) + } + + pub fn begin_signal_delivery_for_thread( + &self, + pid: u32, + thread_id: u32, + ) -> ProcessResult> { let mut state = self.inner.lock_state(); let record = state .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - if record.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { - return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); - } - let Some(signal) = record + let thread = signal_thread(record, pid, thread_id)?; + let selected = record .pending_signals - .difference(record.blocked_signals) + .difference(thread.blocked_signals) .signals() .into_iter() .find(|signal| { record.signal_actions[(*signal - 1) as usize].disposition == SignalDisposition::User - }) - else { - return Ok(None); - }; - record.pending_signals.remove(signal)?; - let action = record.signal_actions[(signal - 1) as usize]; - let previous_mask = record.blocked_signals; - record.blocked_signals = record.blocked_signals.union(action.mask); - if action.flags & SA_NODEFER == 0 { - record.blocked_signals.insert(signal)?; - } - record.blocked_signals.remove(SIGKILL)?; - record.blocked_signals.remove(SIGSTOP)?; - let token = record.next_signal_delivery_token; - record.next_signal_delivery_token = record - .next_signal_delivery_token - .checked_add(1) - .unwrap_or(1); - record.signal_deliveries.push(InProgressSignalDelivery { - token, - previous_mask, - }); - if action.flags & SA_RESETHAND != 0 { - record.signal_actions[(signal - 1) as usize] = SignalAction::DEFAULT; - } - Ok(Some(SignalDelivery { - token, - signal, - action, - })) + }); + selected + .map(|signal| claim_signal_for_thread(record, pid, thread_id, signal)) + .transpose() } pub fn end_signal_delivery(&self, pid: u32, token: u64) -> ProcessResult<()> { + self.end_signal_delivery_for_thread(pid, MAIN_SIGNAL_THREAD_ID, token) + } + + pub fn end_signal_delivery_for_thread( + &self, + pid: u32, + thread_id: u32, + token: u64, + ) -> ProcessResult<()> { let deliveries = { let mut state = self.inner.lock_state(); let record = state .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - let Some(delivery) = record.signal_deliveries.last().copied() else { + let thread = signal_thread_mut(record, pid, thread_id)?; + let Some(delivery) = thread.signal_deliveries.last().copied() else { return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); }; if delivery.token != token { return Err(ProcessTableError::invalid_signal_delivery_token(pid, token)); } - record.signal_deliveries.pop(); - record.blocked_signals = delivery.previous_mask; + thread.signal_deliveries.pop(); + thread.blocked_signals = delivery.previous_mask; collect_pending_signal_deliveries(record)? }; deliver_signals(&self.inner, deliveries); @@ -1622,6 +1705,16 @@ impl ProcessTable { pid: u32, how: SigmaskHow, set: SignalSet, + ) -> ProcessResult { + self.sigprocmask_for_thread(pid, MAIN_SIGNAL_THREAD_ID, how, set) + } + + pub fn sigprocmask_for_thread( + &self, + pid: u32, + thread_id: u32, + how: SigmaskHow, + set: SignalSet, ) -> ProcessResult { let (previous, deliveries) = { let mut state = self.inner.lock_state(); @@ -1629,7 +1722,8 @@ impl ProcessTable { .entries .get_mut(&pid) .ok_or_else(|| ProcessTableError::no_such_process(pid))?; - let previous = record.blocked_signals; + let thread = signal_thread_mut(record, pid, thread_id)?; + let previous = thread.blocked_signals; let mut next = match how { SigmaskHow::Block => previous.union(set), SigmaskHow::Unblock => previous.difference(set), @@ -1637,12 +1731,10 @@ impl ProcessTable { }; next.remove(SIGKILL)?; next.remove(SIGSTOP)?; - record.blocked_signals = next; - + thread.blocked_signals = next; let deliveries = collect_pending_signal_deliveries(record)?; (previous, deliveries) }; - deliver_signals(&self.inner, deliveries); Ok(previous) } @@ -1983,6 +2075,100 @@ fn signal_can_be_blocked(signal: i32) -> bool { !matches!(signal, SIGKILL | SIGSTOP) } +fn signal_thread( + record: &ProcessRecord, + pid: u32, + thread_id: u32, +) -> ProcessResult<&ProcessThreadSignalState> { + record.signal_threads.get(&thread_id).ok_or_else(|| { + ProcessTableError::invalid_argument(format!( + "signal thread {thread_id} does not exist for process {pid}" + )) + }) +} + +fn signal_thread_mut( + record: &mut ProcessRecord, + pid: u32, + thread_id: u32, +) -> ProcessResult<&mut ProcessThreadSignalState> { + record.signal_threads.get_mut(&thread_id).ok_or_else(|| { + ProcessTableError::invalid_argument(format!( + "signal thread {thread_id} does not exist for process {pid}" + )) + }) +} + +fn select_signal_target(record: &ProcessRecord) -> Option<(i32, u32)> { + record + .pending_signals + .signals() + .into_iter() + .find_map(|signal| { + if record.signal_actions[(signal - 1) as usize].disposition != SignalDisposition::User { + return None; + } + record + .signal_threads + .iter() + .find(|(_, thread)| !thread.blocked_signals.contains(signal)) + .map(|(thread_id, _)| (signal, *thread_id)) + }) +} + +fn claim_signal_for_thread( + record: &mut ProcessRecord, + pid: u32, + thread_id: u32, + signal: i32, +) -> ProcessResult { + let action = record.signal_actions[(signal - 1) as usize]; + let token = record.next_signal_delivery_token; + record.next_signal_delivery_token = record + .next_signal_delivery_token + .checked_add(1) + .unwrap_or(1); + let thread = signal_thread(record, pid, thread_id)?; + if thread.signal_deliveries.len() >= MAX_SIGNAL_HANDLER_DEPTH { + return Err(ProcessTableError::signal_delivery_depth_exceeded(pid)); + } + record.pending_signals.remove(signal)?; + let thread = signal_thread_mut(record, pid, thread_id)?; + let previous_mask = thread.blocked_signals; + thread.blocked_signals = thread.blocked_signals.union(action.mask); + if action.flags & SA_NODEFER == 0 { + thread.blocked_signals.insert(signal)?; + } + thread.blocked_signals.remove(SIGKILL)?; + thread.blocked_signals.remove(SIGSTOP)?; + thread.signal_deliveries.push(InProgressSignalDelivery { + token, + previous_mask, + }); + if action.flags & SA_RESETHAND != 0 { + record.signal_actions[(signal - 1) as usize] = SignalAction::DEFAULT; + } + Ok(SignalDelivery { + token, + signal, + action, + thread_id, + }) +} + +fn reset_signal_threads_for_exec(record: &mut ProcessRecord) { + let blocked = record + .signal_threads + .get(&MAIN_SIGNAL_THREAD_ID) + .map(|thread| thread.blocked_signals) + .unwrap_or_else(SignalSet::empty); + record.signal_threads.clear(); + record.signal_threads.insert( + MAIN_SIGNAL_THREAD_ID, + ProcessThreadSignalState::new(blocked), + ); +} + fn queue_or_schedule_signal( record: &mut ProcessRecord, signal: i32, @@ -2006,7 +2192,12 @@ fn queue_or_schedule_signal( return scheduled_signal_delivery(record, signal, controls); } - if signal_can_be_blocked(signal) && record.blocked_signals.contains(signal) { + if signal_can_be_blocked(signal) + && record + .signal_threads + .values() + .all(|thread| thread.blocked_signals.contains(signal)) + { record.pending_signals.insert(signal)?; return scheduled_signal_delivery(record, signal, controls); } @@ -2090,8 +2281,18 @@ fn collect_pending_signal_deliveries( record: &mut ProcessRecord, ) -> ProcessResult> { let mut deliveries = Vec::new(); - let signals = record.pending_signals.difference(record.blocked_signals); - for signal in signals.signals() { + let signals = record + .pending_signals + .signals() + .into_iter() + .filter(|signal| { + record + .signal_threads + .values() + .any(|thread| !thread.blocked_signals.contains(*signal)) + }) + .collect::>(); + for signal in signals { record.pending_signals.remove(signal)?; if let Some(delivery) = queue_or_schedule_signal(record, signal)? { deliveries.push(delivery); @@ -2878,6 +3079,66 @@ mod tests { ); } + #[test] + fn process_signal_selects_an_unblocked_thread_and_keeps_masks_per_thread() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let endpoint = endpoint(); + table.register( + 10, + "test", + "pthread-signals", + Vec::new(), + context(0), + endpoint.clone(), + ); + table + .register_signal_thread(10, 1, MAIN_SIGNAL_THREAD_ID) + .expect("register pthread signal record"); + table + .signal_action( + 10, + SIGTERM, + Some(SignalAction { + disposition: SignalDisposition::User, + ..SignalAction::DEFAULT + }), + ) + .expect("install caught action"); + let mask = SignalSet::from_signal(SIGTERM).expect("mask"); + table + .sigprocmask_for_thread(10, MAIN_SIGNAL_THREAD_ID, SigmaskHow::Block, mask) + .expect("block signal only on main thread"); + table.kill(10, SIGTERM).expect("queue process signal"); + assert_eq!( + endpoint.take_controls(), + vec![ProcessControlRequest::Checkpoint] + ); + let delivery = table + .begin_signal_delivery(10) + .expect("select signal thread") + .expect("caught delivery"); + assert_eq!(delivery.thread_id, 1); + table + .end_signal_delivery_for_thread(10, 1, delivery.token) + .expect("settle on selected thread"); + let main_mask = table + .sigprocmask_for_thread( + 10, + MAIN_SIGNAL_THREAD_ID, + SigmaskHow::Block, + SignalSet::empty(), + ) + .expect("query main mask"); + let worker_mask = table + .sigprocmask_for_thread(10, 1, SigmaskHow::Block, SignalSet::empty()) + .expect("query worker mask"); + assert!(main_mask.contains(SIGTERM)); + assert!(!worker_mask.contains(SIGTERM)); + table + .unregister_signal_thread(10, 1) + .expect("remove pthread signal record"); + } + #[test] fn temporary_ppoll_mask_restores_atomically_and_releases_pending_signal() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); diff --git a/crates/kernel/src/socket_table.rs b/crates/kernel/src/socket_table.rs index 15ed121db3..bd0e9e66f6 100644 --- a/crates/kernel/src/socket_table.rs +++ b/crates/kernel/src/socket_table.rs @@ -2378,65 +2378,95 @@ impl SocketTable { } pub fn shutdown(&self, socket_id: SocketId, how: SocketShutdown) -> SocketResult { - let mut table = lock_or_recover(&self.inner.state); - let record = table - .sockets - .remove(&socket_id) - .ok_or_else(|| SocketTableError::not_found(socket_id))?; + let (record, readiness) = { + let mut table = lock_or_recover(&self.inner.state); + let record = table + .sockets + .remove(&socket_id) + .ok_or_else(|| SocketTableError::not_found(socket_id))?; - if record.state != SocketState::Connected { - table.sockets.insert(socket_id, record); - return Err(SocketTableError::not_connected(format!( - "socket {socket_id} is not connected" - ))); - } + if record.state != SocketState::Connected { + table.sockets.insert(socket_id, record); + return Err(SocketTableError::not_connected(format!( + "socket {socket_id} is not connected" + ))); + } - let Some(mut connection) = record.connection_state.clone() else { - table.sockets.insert(socket_id, record); - return Err(SocketTableError::not_connected(format!( - "socket {socket_id} is not connected" - ))); - }; + let Some(mut connection) = record.connection_state.clone() else { + table.sockets.insert(socket_id, record); + return Err(SocketTableError::not_connected(format!( + "socket {socket_id} is not connected" + ))); + }; - if matches!(how, SocketShutdown::Read | SocketShutdown::Both) { - connection.clear_recv(); - #[cfg(not(target_arch = "wasm32"))] - release_all_retained_bytes(&mut table, socket_id); - connection.read_shutdown = true; - } - if matches!(how, SocketShutdown::Write | SocketShutdown::Both) { - connection.write_shutdown = true; - if let Some(peer_socket_id) = connection.peer_socket_id { - if let Some(peer) = table.sockets.get_mut(&peer_socket_id) { - if let Some(peer_connection) = peer.connection_state.as_mut() { - peer_connection.peer_write_shutdown = true; + if matches!(how, SocketShutdown::Read | SocketShutdown::Both) { + connection.clear_recv(); + #[cfg(not(target_arch = "wasm32"))] + release_all_retained_bytes(&mut table, socket_id); + connection.read_shutdown = true; + } + let mut readiness = None; + if matches!(how, SocketShutdown::Write | SocketShutdown::Both) { + connection.write_shutdown = true; + if let Some(peer_socket_id) = connection.peer_socket_id { + if let Some(peer) = table.sockets.get_mut(&peer_socket_id) { + if let Some(peer_connection) = peer.connection_state.as_mut() { + let became_eof_ready = !peer_connection.peer_write_shutdown; + peer_connection.peer_write_shutdown = true; + readiness = became_eof_ready.then_some(SocketReadiness { + socket_id: peer_socket_id, + kind: SocketReadinessKind::Data, + }); + } } } } - } - let mut record = record; - record.connection_state = Some(connection); - let cloned = record.clone(); - table.sockets.insert(socket_id, record); - Ok(cloned) + let mut record = record; + record.connection_state = Some(connection); + let cloned = record.clone(); + table.sockets.insert(socket_id, record); + (cloned, readiness) + }; + self.emit_readiness(readiness); + Ok(record) } pub fn remove(&self, socket_id: SocketId) -> SocketResult { - let mut table = lock_or_recover(&self.inner.state); - remove_socket(&mut table, socket_id).ok_or_else(|| SocketTableError::not_found(socket_id)) + let (record, readiness) = { + let mut table = lock_or_recover(&self.inner.state); + let readiness = peer_eof_readiness(&table, socket_id); + let record = remove_socket(&mut table, socket_id) + .ok_or_else(|| SocketTableError::not_found(socket_id))?; + (record, readiness) + }; + self.emit_readiness(readiness); + Ok(record) } pub fn remove_all_for_pid(&self, owner_pid: u32) -> Vec { - let mut table = lock_or_recover(&self.inner.state); - let Some(socket_ids) = table.by_owner.remove(&owner_pid) else { - return Vec::new(); + let (records, readiness) = { + let mut table = lock_or_recover(&self.inner.state); + let Some(socket_ids) = table.by_owner.remove(&owner_pid) else { + return Vec::new(); + }; + let mut readiness = Vec::new(); + let records = socket_ids + .into_iter() + .filter_map(|socket_id| { + if let Some(event) = peer_eof_readiness(&table, socket_id) { + readiness.push(event); + } + remove_socket(&mut table, socket_id) + }) + .collect(); + readiness.retain(|event| table.sockets.contains_key(&event.socket_id)); + (records, readiness) }; - - socket_ids - .into_iter() - .filter_map(|socket_id| remove_socket(&mut table, socket_id)) - .collect() + for event in readiness { + self.emit_readiness(Some(event)); + } + records } pub fn snapshot(&self) -> SocketTableSnapshot { @@ -3000,6 +3030,24 @@ fn inet_datagram_bind_shares_port(requested: &SocketRecord, existing: &SocketRec || (requested.reuse_address() && existing.reuse_address()) } +fn peer_eof_readiness(table: &SocketTableState, socket_id: SocketId) -> Option { + let peer_socket_id = table + .sockets + .get(&socket_id)? + .connection_state + .as_ref()? + .peer_socket_id?; + let peer_connection = table + .sockets + .get(&peer_socket_id)? + .connection_state + .as_ref()?; + (!peer_connection.peer_write_shutdown).then_some(SocketReadiness { + socket_id: peer_socket_id, + kind: SocketReadinessKind::Data, + }) +} + fn remove_socket(table: &mut SocketTableState, socket_id: SocketId) -> Option { let record = table.sockets.remove(&socket_id)?; #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/kernel/tests/dns_resolution.rs b/crates/kernel/tests/dns_resolution.rs index e1fe3c3f58..2b6bdb40f6 100644 --- a/crates/kernel/tests/dns_resolution.rs +++ b/crates/kernel/tests/dns_resolution.rs @@ -12,7 +12,7 @@ use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::pin::Pin; use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll, Wake, Waker}; +use std::task::{Context, Poll, Waker}; #[derive(Debug, Clone)] struct MockDnsResolver { @@ -102,15 +102,8 @@ fn new_kernel(config: KernelVmConfig) -> KernelVm { KernelVm::new(MemoryFileSystem::new(), config) } -struct NoopWake; - -impl Wake for NoopWake { - fn wake(self: Arc) {} -} - fn poll_ready(future: F) -> F::Output { - let waker = Waker::from(Arc::new(NoopWake)); - let mut context = Context::from_waker(&waker); + let mut context = Context::from_waker(Waker::noop()); let mut future = std::pin::pin!(future); match future.as_mut().poll(&mut context) { Poll::Ready(output) => output, diff --git a/crates/kernel/tests/loopback_routing.rs b/crates/kernel/tests/loopback_routing.rs index f71238f54e..d771de1d9f 100644 --- a/crates/kernel/tests/loopback_routing.rs +++ b/crates/kernel/tests/loopback_routing.rs @@ -3,7 +3,8 @@ use agentos_kernel::kernel::{KernelProcessHandle, KernelVm, KernelVmConfig, Spaw use agentos_kernel::permissions::Permissions; use agentos_kernel::resource_accounting::ResourceLimits; use agentos_kernel::socket_table::{ - InetSocketAddress, SocketReadiness, SocketReadinessKind, SocketSpec, SocketState, + InetSocketAddress, SocketReadiness, SocketReadinessKind, SocketShutdown, SocketSpec, + SocketState, }; use agentos_kernel::vfs::MemoryFileSystem; use std::sync::{Arc, Mutex}; @@ -203,7 +204,7 @@ fn kernel_loopback_readiness_events_are_edge_triggered() { let accepted = kernel .socket_accept("shell", server.pid(), listener) .expect("accept first connection"); - let _second_accepted = kernel + let second_accepted = kernel .socket_accept("shell", server.pid(), listener) .expect("accept second connection"); assert!(take_readiness_events(&events).is_empty()); @@ -253,6 +254,56 @@ fn kernel_loopback_readiness_events_are_edge_triggered() { kind: SocketReadinessKind::Data, }] ); + assert_eq!( + kernel + .socket_read("shell", server.pid(), accepted, 8) + .expect("read final byte") + .expect("final payload"), + b"D" + ); + + kernel + .socket_shutdown("shell", client.pid(), client_socket, SocketShutdown::Write) + .expect("shutdown client write side"); + assert_eq!( + take_readiness_events(&events), + vec![SocketReadiness { + socket_id: accepted, + kind: SocketReadinessKind::Data, + }], + "write shutdown must wake the peer to observe EOF" + ); + assert_eq!( + kernel + .socket_read("shell", server.pid(), accepted, 8) + .expect("read shutdown EOF"), + None + ); + kernel + .socket_shutdown("shell", client.pid(), client_socket, SocketShutdown::Write) + .expect("repeat client write shutdown"); + assert!( + take_readiness_events(&events).is_empty(), + "repeated write shutdown must not emit a duplicate EOF edge" + ); + + kernel + .socket_close("shell", client.pid(), second_client_socket) + .expect("close second client"); + assert_eq!( + take_readiness_events(&events), + vec![SocketReadiness { + socket_id: second_accepted, + kind: SocketReadinessKind::Data, + }], + "close must wake the peer to observe EOF" + ); + assert_eq!( + kernel + .socket_read("shell", server.pid(), second_accepted, 8) + .expect("read close EOF"), + None + ); let udp_sender = kernel .socket_create("shell", client.pid(), SocketSpec::udp()) diff --git a/crates/kernel/tests/resource_accounting.rs b/crates/kernel/tests/resource_accounting.rs index 36c3fb7faa..fff2e53a74 100644 --- a/crates/kernel/tests/resource_accounting.rs +++ b/crates/kernel/tests/resource_accounting.rs @@ -1982,3 +1982,119 @@ fn resource_limits_reject_oversized_readdir_batches() { .to_string() .contains("limits.resources.maxReaddirEntries")); } + +#[test] +fn fd_readdir_does_not_charge_synthetic_dot_entries_to_the_child_limit() { + let mut config = KernelVmConfig::new("vm-fd-readdir-limit"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + max_readdir_entries: Some(2), + ..ResourceLimits::default() + }; + + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel.create_dir("/exact").expect("create exact directory"); + kernel + .write_file("/exact/a.txt", b"a".to_vec()) + .expect("write exact first entry"); + kernel + .write_file("/exact/b.txt", b"b".to_vec()) + .expect("write exact second entry"); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + let directory_fd = kernel + .fd_open("shell", process.pid(), "/exact", 0, None) + .expect("open exact directory"); + + let first_page = kernel + .fd_read_dir_page_with_types("shell", process.pid(), directory_fd, 0, 2) + .expect("exact-limit descriptor readdir must admit synthetic dot entries"); + assert_eq!(first_page.len(), 2); + assert_eq!(first_page[0].name, "."); + assert_eq!(first_page[1].name, ".."); + let second_page = kernel + .fd_read_dir_page_with_types("shell", process.pid(), directory_fd, 2, 2) + .expect("exact-limit descriptor readdir must return the child page"); + assert_eq!( + second_page + .iter() + .map(|entry| entry.name.as_str()) + .collect::>(), + vec!["a.txt", "b.txt"] + ); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap shell"); +} + +#[test] +fn fd_readdir_cookie_remains_stable_when_an_earlier_page_is_deleted() { + let mut config = KernelVmConfig::new("vm-fd-readdir-cookie"); + config.permissions = Permissions::allow_all(); + + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel.create_dir("/entries").expect("create directory"); + for name in ["a", "b", "c", "d"] { + kernel + .write_file(&format!("/entries/{name}"), name.as_bytes().to_vec()) + .expect("write directory child"); + } + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + let directory_fd = kernel + .fd_open("shell", process.pid(), "/entries", 0, None) + .expect("open directory"); + + let first_page = kernel + .fd_read_dir_page_with_types("shell", process.pid(), directory_fd, 0, 4) + .expect("read first page"); + assert_eq!( + first_page + .iter() + .map(|entry| entry.name.as_str()) + .collect::>(), + vec![".", "..", "a", "b"] + ); + kernel + .remove_file("/entries/a") + .expect("unlink first child"); + kernel + .remove_file("/entries/b") + .expect("unlink second child"); + + let second_page = kernel + .fd_read_dir_page_with_types("shell", process.pid(), directory_fd, 4, 4) + .expect("read stable second page"); + assert_eq!( + second_page + .iter() + .map(|entry| entry.name.as_str()) + .collect::>(), + vec!["c", "d"] + ); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap shell"); +} diff --git a/crates/native-sidecar-core/src/limits.rs b/crates/native-sidecar-core/src/limits.rs index a79f64722c..497f55ee0e 100644 --- a/crates/native-sidecar-core/src/limits.rs +++ b/crates/native-sidecar-core/src/limits.rs @@ -71,6 +71,8 @@ pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024; pub const DEFAULT_WASM_PREWARM_TIMEOUT_MS: u64 = 30_000; pub const DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB: u32 = 2048; pub const DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS: u32 = 30_000; +pub const DEFAULT_WASM_MAX_THREADS: usize = 16; +pub const DEFAULT_WASM_MAX_CONCURRENT_THREADS: usize = 64; pub const DEFAULT_PROCESS_PENDING_STDIN_BYTES: usize = 64 * 1024 * 1024; pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS: usize = 4096; pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES: usize = 1024 * 1024; @@ -353,6 +355,12 @@ pub struct WasmLimits { /// Optional deterministic instruction budget. The V8 compatibility backend /// rejects explicit values because V8 cannot meter deterministic fuel. pub deterministic_fuel: Option, + /// Maximum threads in one explicitly threaded standalone-WASM group, + /// including its initial thread. + pub max_threads: usize, + /// Maximum threads transactionally reserved by all concurrently running + /// threaded standalone-WASM groups in one VM. + pub max_concurrent_threads: usize, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -514,6 +522,8 @@ impl Default for WasmLimits { active_cpu_time_limit_ms: DEFAULT_WASM_ACTIVE_CPU_TIME_LIMIT_MS, wall_clock_limit_ms: None, deterministic_fuel: None, + max_threads: DEFAULT_WASM_MAX_THREADS, + max_concurrent_threads: DEFAULT_WASM_MAX_CONCURRENT_THREADS, } } } @@ -819,6 +829,16 @@ pub fn vm_limits_from_config( } limits.wasm.wall_clock_limit_ms = wasm.wall_clock_limit_ms; limits.wasm.deterministic_fuel = wasm.deterministic_fuel; + set_usize( + &mut limits.wasm.max_threads, + wasm.max_threads, + "limits.wasm.maxThreads", + )?; + set_usize( + &mut limits.wasm.max_concurrent_threads, + wasm.max_concurrent_threads, + "limits.wasm.maxConcurrentThreads", + )?; } if let Some(process) = config.process.as_ref() { set_usize( @@ -1739,6 +1759,21 @@ pub fn validate_vm_limits( "limits.wasm.max_module_file_bytes must be greater than zero".to_string(), )); } + if limits.wasm.max_threads == 0 { + return Err(SidecarCoreError::new( + "limits.wasm.max_threads must be greater than zero", + )); + } + if limits.wasm.max_concurrent_threads == 0 { + return Err(SidecarCoreError::new( + "limits.wasm.max_concurrent_threads must be greater than zero", + )); + } + if limits.wasm.max_threads > limits.wasm.max_concurrent_threads { + return Err(SidecarCoreError::new( + "limits.wasm.max_threads must not exceed limits.wasm.max_concurrent_threads", + )); + } if limits.js_runtime.v8_ipc_max_frame_bytes == 0 { return Err(SidecarCoreError::new( "limits.js_runtime.v8_ipc_max_frame_bytes must be greater than zero".to_string(), diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index 73b8a9b2e2..f7ec5343b7 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -442,6 +442,26 @@ mod descendant_rpc_route_tests { "descendant deferred completions must share root connect finalization" ); } + + #[test] + fn descendant_stream_routing_uses_execution_capability_not_wasm_engine() { + let source = include_str!("child_process.rs"); + let start = source + .rfind("fn route_child_process_bridge_event(") + .expect("descendant bridge event router"); + let end = source[start..] + .find("pub(super) async fn pump_detached_child_process_events(") + .map(|offset| start + offset) + .expect("descendant bridge event router end"); + let router = &source[start..end]; + + assert!(router.contains("descendant_output_ownership()")); + assert!(router.contains("DescendantOutputOwnership::GuestDescriptors")); + assert!( + !router.contains("standalone_wasm_backend"), + "POSIX stream ownership must not depend on the selected WASM engine" + ); + } } impl TransferredHostNetSocket { @@ -1754,6 +1774,74 @@ fn materialize_direct_runtime_stdio_mappings( Ok(()) } +/// Materialize every guest descriptor at its canonical kernel number before a +/// WASM image starts. File actions may allocate temporary kernel descriptors; +/// retaining those aliases both requires an executor-local translation table +/// and keeps pipe/socket descriptions alive after the guest closes its fd. +fn materialize_wasm_fd_mappings( + kernel: &mut SidecarKernel, + pid: u32, + applied: &mut AppliedPosixSpawnFileActions, +) -> Result<(), SidecarError> { + let snapshot = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)?; + let fd_flags = snapshot + .iter() + .map(|entry| (entry.fd, entry.fd_flags)) + .collect::>(); + let hidden_preopens = kernel + .wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)? + .into_iter() + .map(|entry| entry.fd) + .collect::>(); + let target_fds = applied + .fd_mappings + .iter() + .map(|mapping| mapping[0]) + .collect::>(); + let mut transfers = Vec::with_capacity(applied.fd_mappings.len()); + for [guest_fd, source_fd] in &applied.fd_mappings { + let flags = fd_flags.get(source_fd).copied().ok_or_else(|| { + SidecarError::host( + "EBADF", + format!("WASM guest fd {guest_fd} maps to closed kernel fd {source_fd}"), + ) + })?; + transfers.push(( + *guest_fd, + *source_fd, + flags, + kernel + .fd_transfer(EXECUTION_DRIVER_NAME, pid, *source_fd) + .map_err(kernel_error)?, + )); + } + for (guest_fd, source_fd, flags, transfer) in &transfers { + if guest_fd != source_fd { + kernel + .fd_install_spawn_transfer_at( + EXECUTION_DRIVER_NAME, + pid, + *guest_fd, + *flags, + transfer, + ) + .map_err(kernel_error)?; + } + } + for (_, source_fd, _, _) in &transfers { + if !target_fds.contains(source_fd) && !hidden_preopens.contains(source_fd) { + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, *source_fd) + .map_err(kernel_error)?; + } + } + applied.fd_mappings = target_fds.into_iter().map(|fd| [fd, fd]).collect(); + Ok(()) +} + #[cfg(test)] mod direct_runtime_stdio_mapping_tests { use super::*; @@ -4398,14 +4486,17 @@ where _ => None, } } else { - if parent.runtime == GuestRuntimeKind::WebAssembly - && parent.standalone_wasm_backend == ExecutionStandaloneWasmBackend::Wasmtime + if parent.execution.descendant_output_ownership() + == DescendantOutputOwnership::GuestDescriptors { - // Native Wasmtime children publish output through their - // inherited kernel descriptors and settle wait(2) through - // the guest-owned kernel process table. The proactive - // sidecar pump may still retire the child here, but there - // is no V8 stream session to notify. + // POSIX guests publish descendant output through inherited + // kernel descriptors and settle wait(2) through the + // guest-owned kernel process table. The proactive sidecar + // pump may still retire the child here, but forwarding the + // same event through an executor stream lane duplicates + // state. For V8-WASM those unconsumed duplicates also fill + // the bounded session command queue while the guest is in + // a synchronous host call. return Ok(true); } let payload = match event_type { @@ -5874,7 +5965,7 @@ where ) .map_err(kernel_error)?; let kernel_pid = kernel_handle.pid(); - let applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { + let mut applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { install_preapplied_posix_spawn_file_actions( &mut vm.kernel, &kernel_handle, @@ -5889,6 +5980,13 @@ where &prepared_host_net_fds.kernel_actions, )? }; + if resolved.adapter_policy.encodes_inherited_fd_bootstrap { + materialize_wasm_fd_mappings( + &mut vm.kernel, + kernel_pid, + &mut applied_spawn_actions, + )?; + } let posix_spawn_controls_stdin = !request.options.spawn_file_actions.is_empty() && (applied_spawn_actions .fd_mappings @@ -6086,7 +6184,8 @@ where Some(u64::from(parent_kernel_pid)), ); let module_path = match standalone_wasm_backend { - ExecutionStandaloneWasmBackend::Wasmtime => execution_env + ExecutionStandaloneWasmBackend::Wasmtime + | ExecutionStandaloneWasmBackend::WasmtimeThreads => execution_env .get("AGENTOS_GUEST_ENTRYPOINT") .cloned() .unwrap_or_else(|| resolved.entrypoint.clone()), @@ -6920,7 +7019,8 @@ where execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); execution_env.insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); let module_path = match standalone_wasm_backend { - ExecutionStandaloneWasmBackend::Wasmtime => execution_env + ExecutionStandaloneWasmBackend::Wasmtime + | ExecutionStandaloneWasmBackend::WasmtimeThreads => execution_env .get("AGENTOS_GUEST_ENTRYPOINT") .cloned() .unwrap_or_else(|| resolved.entrypoint.clone()), @@ -7626,7 +7726,7 @@ where ) .map_err(kernel_error)?; let kernel_pid = kernel_handle.pid(); - let applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { + let mut applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { install_preapplied_posix_spawn_file_actions( &mut vm.kernel, &kernel_handle, @@ -7641,6 +7741,13 @@ where &prepared_host_net_fds.kernel_actions, )? }; + if resolved.adapter_policy.encodes_inherited_fd_bootstrap { + materialize_wasm_fd_mappings( + &mut vm.kernel, + kernel_pid, + &mut applied_spawn_actions, + )?; + } let posix_spawn_controls_stdin = !request.options.spawn_file_actions.is_empty() && (applied_spawn_actions .fd_mappings @@ -7821,7 +7928,8 @@ where Some(u64::from(parent_kernel_pid)), ); let module_path = match standalone_wasm_backend { - ExecutionStandaloneWasmBackend::Wasmtime => execution_env + ExecutionStandaloneWasmBackend::Wasmtime + | ExecutionStandaloneWasmBackend::WasmtimeThreads => execution_env .get("AGENTOS_GUEST_ENTRYPOINT") .cloned() .unwrap_or_else(|| resolved.entrypoint.clone()), @@ -8655,6 +8763,60 @@ where .map_err(SidecarError::from) } + fn dispatch_descendant_context_fd_snapshot( + &self, + vm_id: &str, + root_process_id: &str, + caller_process_path: &[&str], + reply: DirectHostReplyHandle, + ) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get(vm_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call VM no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(root) = vm.active_processes.get(root_process_id) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call root process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let Some(caller) = Self::active_process_by_path(root, caller_process_path) else { + reply + .fail(HostServiceError::new( + "ESTALE", + "host call descendant process no longer exists", + )) + .map_err(SidecarError::from)?; + return Ok(()); + }; + let identity = reply.identity(); + if identity.generation != vm.generation || identity.pid != caller.kernel_pid { + reply + .fail(HostServiceError::new( + "ESTALE", + "fd snapshot identity does not match the descendant process", + )) + .map_err(SidecarError::from)?; + return Ok(()); + } + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + match fd_snapshot_with_managed_routes(vm, caller.kernel_pid) { + Ok(response) => reply.succeed(response), + Err(error) => reply.fail(host_service_error(&error)), + } + .map_err(SidecarError::from) + } + fn dispatch_descendant_context_dns_operation( &self, vm_id: &str, @@ -9803,6 +9965,7 @@ where agentos_execution::host::BoundedVec, Option, Option, + Option, DirectHostReplyHandle, ), ) -> Result<(), SidecarError> { @@ -10151,15 +10314,40 @@ where match event { ActiveExecutionEvent::Common(ExecutionEvent::HostCall { operation, reply }) => { drop(reservation); + if matches!( + operation, + HostOperation::Filesystem( + agentos_execution::host::FilesystemOperation::Snapshot + ) + ) { + self.dispatch_descendant_context_fd_snapshot( + vm_id, + process_id, + &child_path, + reply, + )?; + continue; + } + let default_blocking_read_ms = self + .vms + .get(vm_id) + .and_then(|vm| vm.limits.resources.max_blocking_read_ms) + .unwrap_or( + agentos_kernel::resource_accounting::DEFAULT_BLOCKING_READ_TIMEOUT_MS, + ); let deferred_kernel_read = match &operation { HostOperation::Filesystem( agentos_execution::host::FilesystemOperation::Read { fd, max_bytes, offset: None, - deadline_ms: Some(timeout_ms), + deadline_ms, }, - ) => Some((Some(*fd), *max_bytes, *timeout_ms)), + ) => Some(( + Some(*fd), + *max_bytes, + deadline_ms.unwrap_or(default_blocking_read_ms), + )), HostOperation::Filesystem( agentos_execution::host::FilesystemOperation::StdinRead { max_bytes, @@ -10222,11 +10410,19 @@ where interests, timeout_ms, signal_mask, + signal_thread_id, }, - ) => Some((interests.clone(), *timeout_ms, *signal_mask)), + ) => Some(( + interests.clone(), + *timeout_ms, + *signal_mask, + *signal_thread_id, + )), _ => None, }; - if let Some((interests, timeout_ms, signal_mask)) = deferred_posix_poll { + if let Some((interests, timeout_ms, signal_mask, signal_thread_id)) = + deferred_posix_poll + { let deadline = match timeout_ms .map(host_dispatch::checked_deferred_guest_wait_deadline) .transpose() @@ -10241,7 +10437,7 @@ where vm_id, process_id, &child_path, - (interests, deadline, signal_mask, reply), + (interests, deadline, signal_mask, signal_thread_id, reply), )?; continue; } diff --git a/crates/native-sidecar/src/execution/coordinator.rs b/crates/native-sidecar/src/execution/coordinator.rs index 6d69bb881d..4a40c0fb40 100644 --- a/crates/native-sidecar/src/execution/coordinator.rs +++ b/crates/native-sidecar/src/execution/coordinator.rs @@ -610,7 +610,27 @@ where ) .await? } else { - cancel_kernel_http_fetch_stream(&self.bridge, &vm_id, vm, stream_id).await? + let response_json = + cancel_kernel_http_fetch_stream(&self.bridge, &vm_id, vm, stream_id).await?; + // Cancellation closes the client socket immediately, but the + // server process retires its accepted peer only after the + // shared readiness/event pump delivers EOF. Give that + // sidecar-owned path a small fixed cleanup budget so a + // successful cancel does not return with a leaked socket. + let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id); + let process_event_notify = Arc::clone(&self.process_event_notify); + for _ in 0..32 { + let notified = process_event_notify.notified(); + if self.pump_process_events(&ownership).await? { + continue; + } + tokio::select! { + _ = notified => {} + _ = tokio::time::sleep(Duration::from_millis(1)) => {} + } + } + self.process_event_notify.notify_one(); + response_json }; let response = self.respond( request, @@ -634,9 +654,7 @@ where .vms .get_mut(&vm_id) .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?; - // HTTP origin-form has exactly one leading slash. Normalizing at the - // sidecar boundary keeps VM fetch behavior stable even when an - // upstream router hands us a network-path-style `//foo` URL. + // HTTP origin-form has exactly one leading slash. let target_path = format!("/{}", payload.path.trim_start_matches('/')); let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port)) .map_err(|error| { @@ -678,8 +696,8 @@ where let target_process_id = find_kernel_http_listener_process(vm, payload.port); if let Some(target_process_id) = target_process_id { let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes; - let fetch_result = if stream_operation.as_deref() == Some("start") { - start_kernel_http_fetch_stream( + if stream_operation.as_deref() == Some("start") { + let response_json = start_kernel_http_fetch_stream( &self.bridge, &vm_id, vm, @@ -691,32 +709,153 @@ where body_bytes.as_deref(), max_fetch_response_bytes, ) - .await + .await?; + let response = self.respond( + request, + ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), + ); + ensure_vm_fetch_response_frame_within_limit( + &response, + self.config.max_frame_bytes, + )?; + return Ok(DispatchResult { + response, + events: Vec::new(), + }); + } + let mut fetch = begin_kernel_http_fetch( + vm, + &target_process_id, + payload.port, + &target_path, + &options, + &headers, + body_bytes.as_deref(), + max_fetch_response_bytes, + )?; + let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id); + let process_event_notify = Arc::clone(&self.process_event_notify); + let _ = vm; + let mut target_exit_events = Vec::new(); + let mut target_exited = false; + let fetch_result: Result = async { + loop { + // Register before probing durable socket state and event + // queues so a racing completion cannot lose its wake. + let notified = process_event_notify.notified(); + let response = { + let vm = self.vms.get_mut(&vm_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} is no longer active during vm.fetch" + )) + })?; + poll_kernel_http_fetch(vm, &mut fetch)? + }; + if let Some(response) = response { + break Ok(response); + } + + if self.pump_process_events(&ownership).await? { + let queued_exit_code = self.pending_process_events.iter().find_map( + |envelope| { + if envelope.vm_id == vm_id + && envelope.process_id == target_process_id + { + match &envelope.event { + ActiveExecutionEvent::Exited(exit_code) => Some(*exit_code), + _ => None, + } + } else { + None + } + }, + ); + if let Some(exit_code) = queued_exit_code { + // Public process events normally finalize an exit when the caller + // polls them. vm.fetch is itself waiting on a socket owned by this + // target process, so deferring exit cleanup until a later poll would + // create a circular wait: the socket closes only after cleanup, while + // the request prevents the caller from polling. Drain this process's + // queued output and exit in order, retain the resulting public frames + // on the rejected response, and let exit finalization close every + // kernel socket and resource before returning. + while self + .vms + .get(&vm_id) + .is_some_and(|vm| { + vm.active_processes.contains_key(&target_process_id) + }) + { + let envelope = self + .take_matching_process_event_envelope( + &vm_id, + &target_process_id, + )? + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "vm.fetch lost the queued exit event for target process {target_process_id}" + )) + })?; + if let Some(frame) = + self.handle_process_event_envelope(envelope).await? + { + target_exit_events.push(frame); + } + } + let error = SidecarError::Execution(format!( + "vm.fetch target exited before responding (exit code {exit_code})" + )); + target_exited = true; + break Err(error); + } + continue; + } + + tokio::select! { + _ = notified => {} + _ = tokio::time::sleep(Duration::from_millis(5)) => {} + } + } + } + .await; + let close_result = if target_exited { + // Exit finalization already closed and reaped every socket owned by + // the target kernel process, including this fetch socket. + Ok(()) } else { - dispatch_kernel_http_fetch( - &self.bridge, - &vm_id, - vm, - &target_process_id, - payload.port, - &target_path, - &options, - &headers, - body_bytes.as_deref(), - max_fetch_response_bytes, - ) - .await + self.vms + .get_mut(&vm_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} disappeared while closing vm.fetch socket" + )) + }) + .and_then(|vm| close_kernel_http_fetch(vm, &fetch)) }; let response_json = match fetch_result { - Ok(response_json) => response_json, + Ok(response_json) => { + close_result?; + response_json + } Err(error) => { - if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) { - let _ = vm; - self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?; + if let Err(close_error) = close_result { + eprintln!( + "ERR_AGENTOS_HTTP_FETCH_CLEANUP: failed to close kernel socket after fetch error: {close_error}" + ); + } + if target_exited { + return Ok(DispatchResult { + response: self.reject_error(request, &error), + events: target_exit_events, + }); } return Err(error); } }; + // The inline pump may have moved public output/exit events into + // the durable queue while consuming the runtime wake that would + // normally prompt the stdio transport to drain it. + self.process_event_notify.notify_one(); let response = self.respond( request, ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), @@ -838,6 +977,7 @@ where request, ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), ); + self.process_event_notify.notify_one(); ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?; Ok(DispatchResult { diff --git a/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs index 7bd81268d2..3c6d201518 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/filesystem.rs @@ -24,16 +24,16 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } - let (fd, max_bytes, timeout_ms, response) = match operation { + let (fd, max_bytes, requested_timeout_ms, response) = match operation { FilesystemOperation::Read { fd, max_bytes, offset: None, - deadline_ms: Some(timeout_ms), + deadline_ms, } => ( Some(fd), max_bytes, - timeout_ms, + deadline_ms, DeferredKernelReadResponse::DescriptorBytes, ), FilesystemOperation::StdinRead { @@ -42,16 +42,24 @@ where } => ( None, max_bytes, - timeout_ms, + Some(timeout_ms), DeferredKernelReadResponse::KernelStdin, ), _ => { return Err(SidecarError::host( "EINVAL", - "deferred descriptor-read dispatcher received a non-timed read", + "deferred descriptor-read dispatcher received a non-read operation", )); } }; + let timeout_ms = requested_timeout_ms + .or_else(|| { + sidecar + .vms + .get(vm_id) + .and_then(|vm| vm.limits.resources.max_blocking_read_ms) + }) + .unwrap_or(agentos_kernel::resource_accounting::DEFAULT_BLOCKING_READ_TIMEOUT_MS); let deadline = match checked_deferred_guest_wait_deadline(timeout_ms) { Ok(deadline) => deadline, Err(error) => { @@ -268,6 +276,18 @@ fn service_deferred_kernel_read_with_response( .succeed(HostCallReply::Json(value)) .map_err(SidecarError::from); } + Err(error) + if matches!(error.code(), "EAGAIN" | "EWOULDBLOCK") + && kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, read.fd) + .map(|stat| stat.flags & agentos_kernel::fd_table::O_NONBLOCK != 0) + .map_err(kernel_error)? => + { + return read + .reply + .fail(kernel_host_error(error)) + .map_err(SidecarError::from); + } Err(error) if matches!(error.code(), "EAGAIN" | "EWOULDBLOCK") && now >= read.deadline => { return match read.response { DeferredKernelReadResponse::DescriptorBytes => read @@ -322,6 +342,14 @@ pub(super) fn decode( let path_limit = payload_limit("runtime.filesystem.maxPathBytes", MAX_PATH_BYTES)?; let name_limit = payload_limit("runtime.filesystem.maxXattrNameBytes", MAX_XATTR_NAME_BYTES)?; let response_limit = payload_limit("limits.reactor.maxBridgeResponseBytes", max_reply_bytes)?; + // This second instance only types the configured maximum forwarded to the + // kernel. Actual reply sizes are admitted through `response_limit` above. + let response_bound = agentos_execution::backend::PayloadLimit::with_warning_hook( + "limits.reactor.maxBridgeResponseBytes", + max_reply_bytes, + None, + ) + .map_err(SidecarError::Host)?; let request_limit = payload_limit("limits.reactor.maxBridgeRequestBytes", max_reply_bytes)?; let readdir_limit = payload_limit("runtime.filesystem.maxReaddirEntries", MAX_READDIR_ENTRIES)?; @@ -592,7 +620,7 @@ pub(super) fn decode( name: name_value, value, operation, - max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_bound) .map_err(SidecarError::Host)?, } } @@ -633,7 +661,7 @@ pub(super) fn decode( name: name_value, value, operation, - max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_bound) .map_err(SidecarError::Host)?, } } @@ -660,7 +688,7 @@ pub(super) fn decode( name: name_value, value, operation, - max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + max_result_bytes: BoundedUsize::try_new(max_reply_bytes, &response_bound) .map_err(SidecarError::Host)?, } } @@ -728,7 +756,7 @@ pub(super) fn decode( &readdir_limit, ) .map_err(SidecarError::Host)?, - max_bytes: BoundedUsize::try_new(max_reply_bytes, &response_limit) + max_bytes: BoundedUsize::try_new(max_reply_bytes, &response_bound) .map_err(SidecarError::Host)?, }, "process.fd_close" => FilesystemOperation::Close { @@ -953,6 +981,12 @@ fn decode_path_operation( request: &HostRpcRequest, path: &impl Fn(usize, &str) -> Result, ) -> Result { + let path_bound = agentos_execution::backend::PayloadLimit::with_warning_hook( + "runtime.filesystem.maxPathBytes", + MAX_PATH_BYTES, + None, + ) + .map_err(SidecarError::Host)?; Ok(match request.method.as_str() { "process.path_open_at" => FilesystemOperation::OpenAt { dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_open_at dir fd")?, @@ -1044,11 +1078,8 @@ fn decode_path_operation( "process.path_readlink_at" => FilesystemOperation::ReadLinkAt { dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_readlink_at dir fd")?, path: path(1, "path_readlink_at path")?, - max_bytes: BoundedUsize::try_new( - MAX_PATH_BYTES, - &payload_limit("runtime.filesystem.maxPathBytes", MAX_PATH_BYTES)?, - ) - .map_err(SidecarError::Host)?, + max_bytes: BoundedUsize::try_new(MAX_PATH_BYTES, &path_bound) + .map_err(SidecarError::Host)?, }, "process.path_remove_dir_at" => FilesystemOperation::UnlinkAt { dir_fd: javascript_sync_rpc_arg_u32(&request.args, 0, "path_remove_dir_at dir fd")?, @@ -1195,6 +1226,7 @@ impl SidecarHostCapability for FilesystemCapability { path, options, } => { + let dir_fd = super::canonical_path_dir_fd(dir_fd); let parent_fd = (dir_fd != NODE_CWD_FD).then_some(dir_fd); let requested_rights = match options.rights { GuestOpenRights::Explicit { base, inheriting } => Some((base, inheriting)), @@ -1269,7 +1301,7 @@ impl SidecarHostCapability for FilesystemCapability { .collect(), ), FilesystemOperation::Preopen { fd } => kernel - .wasi_preopen(EXECUTION_DRIVER_NAME, pid, fd) + .wasi_preopen(EXECUTION_DRIVER_NAME, pid, super::canonical_path_dir_fd(fd)) .map_err(kernel_host_error)? .map(preopen_value) .unwrap_or(Value::Null), @@ -1592,14 +1624,18 @@ impl SidecarHostCapability for FilesystemCapability { HostServiceError::new("EINVAL", "fd_readdir cookie exceeds usize") })?; let entries = kernel - .fd_read_dir_with_types(EXECUTION_DRIVER_NAME, pid, fd) + .fd_read_dir_page_with_types( + EXECUTION_DRIVER_NAME, + pid, + fd, + cookie, + max_entries.get(), + ) .map_err(kernel_host_error)?; Value::Array( entries .into_iter() .enumerate() - .skip(cookie) - .take(max_entries.get()) .map(|(index, entry)| { json!({ "name": entry.name, @@ -1611,7 +1647,7 @@ impl SidecarHostCapability for FilesystemCapability { } else { agentos_kernel::fd_table::FILETYPE_REGULAR_FILE }, - "next": index.saturating_add(1).to_string(), + "next": cookie.saturating_add(index).saturating_add(1).to_string(), }) }) .collect(), @@ -2065,6 +2101,9 @@ fn resolve_path( dir_fd: u32, path: &str, ) -> Result { + // AgentOS extension imports use NODE_CWD_FD for an ordinary POSIX path. + // Patched libc instead tags its preserved private WASI preopen; strip only + // that tag and retain the preopen's capability-root semantics. if dir_fd == NODE_CWD_FD { let path = if path.starts_with('/') { normalize_path(path) @@ -2086,6 +2125,7 @@ fn resolve_path( } return Ok(path); } + let dir_fd = super::canonical_path_dir_fd(dir_fd); if path.starts_with('/') { return Err(HostServiceError::new( "EACCES", @@ -2688,20 +2728,18 @@ mod tests { let (mut kernel, handle) = kernel_process_at_tier(ProcessPermissionTier::Full); let pid = handle.pid(); kernel - .mkdir("/cap", true) - .expect("create capability directory"); + .mkdir("/workspace", true) + .expect("create workspace capability directory"); kernel .mkdir("/outside", true) .expect("create outside directory"); let cap_fd = kernel - .fd_open( - EXECUTION_DRIVER_NAME, - pid, - "/cap", - agentos_kernel::fd_table::O_DIRECTORY, - None, - ) - .expect("open directory capability"); + .initialize_canonical_wasi_preopens(EXECUTION_DRIVER_NAME, pid) + .expect("initialize canonical preopens") + .into_iter() + .find(|entry| entry.guest_path == "/workspace") + .expect("workspace preopen") + .fd; let process = ActiveProcess::new( pid, handle, @@ -2710,7 +2748,8 @@ mod tests { agentos_runtime::DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS, GuestRuntimeKind::WebAssembly, ActiveExecution::Binding(BindingExecution::default()), - ); + ) + .with_guest_cwd(String::from("/workspace")); let operation = HostOperation::Filesystem(FilesystemOperation::OpenAt { dir_fd: cap_fd, path: bounded_string("/outside"), @@ -2734,8 +2773,44 @@ mod tests { assert_eq!( resolve_path(&mut kernel, &process, cap_fd, "child") .expect("relative path remains confined"), - "/cap/child" + "/workspace/child" + ); + assert_eq!( + resolve_path( + &mut kernel, + &process, + agentos_kernel::fd_table::WASI_HIDDEN_PREOPEN_FD_TAG | cap_fd, + "/outside" + ) + .expect_err("private libc preopen retains capability confinement") + .code, + "EACCES" + ); + assert_eq!( + resolve_path( + &mut kernel, + &process, + agentos_kernel::fd_table::WASI_HIDDEN_PREOPEN_FD_TAG | cap_fd, + "child", + ) + .expect("private libc preopen resolves from its capability root"), + "/workspace/child" + ); + assert_eq!( + resolve_path(&mut kernel, &process, NODE_CWD_FD, "child") + .expect("extension cwd sentinel resolves from process cwd"), + "/workspace/child" ); + let cwd_metadata = HostOperation::Filesystem(FilesystemOperation::SetMode { + target: MetadataTarget::Path { + dir_fd: NODE_CWD_FD, + follow_symlinks: true, + }, + path: Some(bounded_string("child")), + mode: 0o600, + }); + super::authorize_host_operation(&kernel, pid, &cwd_metadata) + .expect("cwd sentinel bypasses descriptor-right lookup"); } fn request(method: &str) -> HostRpcRequest { @@ -3314,6 +3389,63 @@ mod tests { .fd_close(EXECUTION_DRIVER_NAME, parent_pid, timeout_write_fd) .expect("close timeout writer"); + let (nonblocking_read_fd, nonblocking_write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent_pid) + .expect("open nonblocking pipe"); + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + parent_pid, + nonblocking_read_fd, + agentos_kernel::fd_table::F_SETFL, + agentos_kernel::fd_table::O_NONBLOCK, + ) + .expect("mark pipe reader nonblocking"); + let nonblocking_target = Arc::new(RecordingTarget::default()); + service_deferred_kernel_read( + identity.generation, + &runtime, + kernel.poll_wait_handle(), + Arc::new(tokio::sync::Notify::new()), + &mut kernel, + &mut parent, + Some(( + nonblocking_read_fd, + BoundedUsize::try_new(64, &read_limit).expect("bounded nonblocking read"), + Instant::now() + Duration::from_secs(1), + direct_reply( + Arc::clone(&nonblocking_target), + identity.generation, + parent_pid, + 4, + ), + )), + ) + .expect("settle nonblocking not-ready read"); + assert!( + parent.deferred_kernel_read.is_none(), + "O_NONBLOCK must never park a descriptor read" + ); + let replies = nonblocking_target + .replies + .lock() + .expect("nonblocking reply lock"); + assert_eq!(replies.len(), 1); + assert_eq!( + replies[0] + .as_ref() + .expect_err("nonblocking not-ready read must fail") + .code, + "EAGAIN" + ); + drop(replies); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, nonblocking_read_fd) + .expect("close nonblocking reader"); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent_pid, nonblocking_write_fd) + .expect("close nonblocking writer"); + child.finish(0); parent.kernel_handle.finish(0); } diff --git a/crates/native-sidecar/src/execution/host_dispatch/mod.rs b/crates/native-sidecar/src/execution/host_dispatch/mod.rs index c7628fa0b6..ce9eff0f73 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/mod.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/mod.rs @@ -25,9 +25,10 @@ pub(in crate::execution) use network_compat::{ close_with_managed_retirement, closefrom_with_managed_retirement, dispatch_claimed_context_stream_read, dispatch_claimed_context_udp_poll, dispatch_descendant_context_stream_read, dispatch_descendant_context_udp_poll, - prune_managed_process_routes_without_aliases, replace_descriptor_with_managed_retirement, - retire_managed_process_routes, retire_orphaned_managed_descriptions, - service_deferred_posix_poll, service_descendant_managed_fd_network_operation, + fd_snapshot_with_managed_routes, prune_managed_process_routes_without_aliases, + replace_descriptor_with_managed_retirement, retire_managed_process_routes, + retire_orphaned_managed_descriptions, service_deferred_posix_poll, + service_descendant_managed_fd_network_operation, }; mod process; mod signal; @@ -36,9 +37,10 @@ mod terminal; use super::*; use agentos_execution::backend::{ExecutionEvent, HostCallReply, PayloadLimit}; use agentos_execution::host::{ - BoundedBytes, BoundedProcessLaunchRequest, BoundedString, BoundedUsize, BoundedVec, - ClockOperation, CommittedProcessImage, DescriptorSyncKind, DescriptorWhence, DnsAddressFamily, - EntropyOperation, ExecutableImageSource, FileRangeOperation, FileTimeUpdate, + BoundedBytes, BoundedExecutableImageResolutionRequest, BoundedProcessLaunchRequest, + BoundedString, BoundedUsize, BoundedVec, ClockOperation, CommittedProcessImage, + DescriptorSyncKind, DescriptorWhence, DnsAddressFamily, EntropyOperation, + ExecutableImageResolutionRequest, ExecutableImageSource, FileRangeOperation, FileTimeUpdate, FilesystemOperation, FilesystemRecordLockKind, GuestClockId, GuestOpenRights, GuestOpenSpec, HostOperation, IdentityIdKind, IdentityOperation, KernelPollInterest, ManagedTcpEndpoint, ManagedUdpFamily, ManagedUnixAddress, MetadataTarget, NetworkOperation, PollInterest, @@ -69,6 +71,13 @@ const MAX_SIGNAL_SET_ENTRIES: usize = 64; const MAX_SIGNAL_STATE_MASK_JSON_BYTES: usize = 4 * 1024; const MAX_ENTROPY_CHUNK_BYTES: usize = 64 * 1024; const MAX_DEFERRED_GUEST_WAIT_MS: u64 = u32::MAX as u64; +fn canonical_path_dir_fd(fd: u32) -> u32 { + // Hidden preopen aliases are real kernel-owned descriptor identities. Keep + // the tag so ordinary pathname resolution continues to use the capability + // root even after the guest closes or replaces the same-numbered visible + // preopen descriptor. + fd +} pub(crate) fn checked_deferred_guest_wait_deadline( delay_ms: u64, @@ -292,6 +301,7 @@ fn require_path_right( required: u64, operation: &FilesystemOperation, ) -> Result<(), HostServiceError> { + let dir_fd = canonical_path_dir_fd(dir_fd); if dir_fd == u32::MAX { return Ok(()); } @@ -681,13 +691,23 @@ pub(super) fn authorize_host_operation( )), HostOperation::Terminal(_) => Ok(()), HostOperation::Signal( - SignalOperation::UpdateMask { + SignalOperation::RegisterThread { .. } + | SignalOperation::UnregisterThread { .. } + | SignalOperation::UpdateMask { + how: SignalMaskHow::Block, + set: SignalSetValue(0), + } + | SignalOperation::UpdateMaskForThread { how: SignalMaskHow::Block, set: SignalSetValue(0), + .. } | SignalOperation::BeginDelivery + | SignalOperation::BeginDeliveryForThread { .. } | SignalOperation::TakePublishedDelivery - | SignalOperation::EndDelivery { .. }, + | SignalOperation::TakePublishedDeliveryForThread { .. } + | SignalOperation::EndDelivery { .. } + | SignalOperation::EndDeliveryForThread { .. }, ) => Ok(()), HostOperation::Signal(operation) => Err(tier_denied("EACCES", tier, "signal", operation)), HostOperation::Identity(_) | HostOperation::Entropy(_) => Ok(()), @@ -1042,6 +1062,7 @@ fn decode_host_operation( 3, "TLS connect deadline", )?, + reject_unauthorized: request.args.get(4).and_then(Value::as_bool).unwrap_or(true), }) } "process.fd_socket_shutdown" => { @@ -1187,6 +1208,22 @@ fn decode_host_operation( .filter(|value| !value.is_null()) .map(|value| decode_signal_set(Some(value))) .transpose()?, + signal_thread_id: request + .args + .get(3) + .filter(|value| !value.is_null()) + .map(|value| { + value + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| { + SidecarError::host( + "EINVAL", + "POSIX poll signal thread id is invalid", + ) + }) + }) + .transpose()?, }) } "dns.lookup" => { @@ -1315,6 +1352,7 @@ fn decode_host_operation( ) .map_err(SidecarError::from)?, ), + resolution: decode_executable_image_resolution(&request.args, 1, max_reply_bytes)?, }) } "process.exec_image_open_fd" => { @@ -1324,6 +1362,7 @@ fn decode_host_operation( 0, "process.exec_image_open_fd fd", )?), + resolution: decode_executable_image_resolution(&request.args, 1, max_reply_bytes)?, }) } "process.exec_image_read" => { @@ -1421,7 +1460,11 @@ fn decode_host_operation( 1, "waitpid options", )?)?, - deadline_ms: None, + deadline_ms: javascript_sync_rpc_arg_u64_optional( + &request.args, + 2, + "waitpid deadline ms", + )?, temporary_mask: None, }), "process.waitpid_transition" => HostOperation::Process(ProcessOperation::WaitTransition { @@ -1440,8 +1483,12 @@ fn decode_host_operation( "process.image" => HostOperation::Process(ProcessOperation::GetImage { max_reply_bytes: BoundedUsize::try_new( max_reply_bytes, - &PayloadLimit::new("limits.reactor.maxBridgeResponseBytes", max_reply_bytes) - .map_err(SidecarError::from)?, + &PayloadLimit::with_warning_hook( + "limits.reactor.maxBridgeResponseBytes", + max_reply_bytes, + None, + ) + .map_err(SidecarError::from)?, ) .map_err(SidecarError::from)?, }), @@ -1837,7 +1884,10 @@ fn optional_identity_id( fn account_record_limit(max_reply_bytes: usize) -> Result { let maximum = max_reply_bytes.min(MAX_ACCOUNT_RECORD_BYTES); - let limit = PayloadLimit::new("maxAccountRecordBytes", maximum).map_err(SidecarError::from)?; + // `maximum` is carried to the kernel as a bound. It is not an observed + // account-record size, so registering it must not emit a near-limit warning. + let limit = PayloadLimit::with_warning_hook("maxAccountRecordBytes", maximum, None) + .map_err(SidecarError::from)?; BoundedUsize::try_new(maximum, &limit).map_err(SidecarError::from) } @@ -1927,6 +1977,44 @@ fn decode_process_exec_request( Ok(request) } +fn decode_executable_image_resolution( + args: &[Value], + argv_index: usize, + max_request_bytes: usize, +) -> Result, SidecarError> { + let Some(argv_value) = args.get(argv_index) else { + return Ok(None); + }; + let argv = serde_json::from_value::>(argv_value.clone()).map_err(|error| { + SidecarError::host( + "EINVAL", + format!("invalid exec image resolution argv: {error}"), + ) + })?; + let close_on_exec_fds = serde_json::from_value::>( + args.get(argv_index + 1) + .cloned() + .unwrap_or_else(|| json!([])), + ) + .map_err(|error| { + SidecarError::host( + "EINVAL", + format!("invalid exec image close-on-exec descriptors: {error}"), + ) + })?; + let limit = PayloadLimit::new("limits.reactor.maxBridgeRequestBytes", max_request_bytes) + .map_err(SidecarError::from)?; + BoundedExecutableImageResolutionRequest::try_new( + ExecutableImageResolutionRequest { + argv, + close_on_exec_fds, + }, + &limit, + ) + .map(Some) + .map_err(SidecarError::from) +} + fn bounded_dns_value( value: &str, limit_name: &'static str, @@ -2271,6 +2359,18 @@ pub(super) fn dispatch_host_operation( } other => Err(unsupported("host", other)), }; + // Host operations such as kill(self), SIGPIPE-producing writes, and timer + // probes can queue kernel runtime controls. Publish those controls before + // releasing the direct waiter so every executor observes the checkpoint at + // the safe point immediately following this import. + let result = result.and_then(|response| { + if response.is_some() { + process + .apply_runtime_controls() + .map_err(|error| host_service_error(&error))?; + } + Ok(response) + }); match result { Ok(Some(response)) => { reply.succeed(response).map_err(SidecarError::from)?; @@ -2291,16 +2391,13 @@ pub(super) fn requires_context_host_dispatch(operation: &HostOperation) -> bool matches!( operation, HostOperation::Filesystem( - FilesystemOperation::Close { .. } + FilesystemOperation::Snapshot + | FilesystemOperation::Close { .. } | FilesystemOperation::CloseFrom { .. } | FilesystemOperation::Renumber { .. } | FilesystemOperation::DuplicateTo { .. } | FilesystemOperation::Move { .. } - | FilesystemOperation::Read { - offset: None, - deadline_ms: Some(_), - .. - } + | FilesystemOperation::Read { offset: None, .. } | FilesystemOperation::StdinRead { .. }, ) | HostOperation::Network( NetworkOperation::HttpRequest { .. } @@ -2385,6 +2482,10 @@ where dispatch_context_http_operation(sidecar, vm_id, process_id, operation, reply)?; Ok(None) } + HostOperation::Filesystem(FilesystemOperation::Snapshot) => { + network_compat::dispatch_context_fd_snapshot(sidecar, vm_id, process_id, reply)?; + Ok(None) + } HostOperation::Filesystem(FilesystemOperation::Close { fd }) => { network_compat::dispatch_context_close_with_managed_retirement( sidecar, vm_id, process_id, fd, reply, @@ -2408,11 +2509,7 @@ where Ok(None) } HostOperation::Filesystem( - operation @ (FilesystemOperation::Read { - offset: None, - deadline_ms: Some(_), - .. - } + operation @ (FilesystemOperation::Read { offset: None, .. } | FilesystemOperation::StdinRead { .. }), ) => { filesystem::dispatch_context_deferred_kernel_read( @@ -2637,6 +2734,15 @@ pub(super) fn service_deferred_guest_wait( }); } + // ITIMER_REAL state remains sidecar-owned. A deferred syscall must wake at + // the timer deadline as well as its own deadline so the kernel can publish + // SIGALRM promptly without an executor-local timer or polling loop. + signal::materialize_real_timer_signal(process); + process.apply_runtime_controls()?; + if process.deferred_guest_wait.is_none() { + return Ok(()); + } + let now = Instant::now(); let should_probe = process.deferred_guest_wait.as_ref().is_some_and(|wait| { newly_admitted @@ -2701,14 +2807,18 @@ pub(super) fn service_deferred_guest_wait( }; } - let deadline = wait.deadline; + let wake_deadline = match (wait.deadline, process.real_interval_timer.next_deadline()) { + (Some(wait), Some(timer)) => Some(wait.min(timer)), + (wait @ Some(_), None) => wait, + (None, timer) => timer, + }; let task_class = if wait.kind == DeferredGuestWaitKind::Sleep { agentos_runtime::TaskClass::Timer } else { agentos_runtime::TaskClass::Vm }; let wake_task = runtime.spawn(task_class, async move { - match deadline { + match wake_deadline { Some(deadline) => { let delay = deadline.saturating_duration_since(Instant::now()); tokio::select! { @@ -3353,6 +3463,27 @@ mod tests { assert_eq!(details["observed"], MAX_DEFERRED_GUEST_WAIT_MS + 1); } + #[test] + fn typed_waitpid_decodes_an_optional_bounded_probe_deadline() { + let operation = decode_host_operation( + &request("process.waitpid", vec![json!(-1), json!(0), json!(10_000)]), + true, + 1024, + ) + .expect("decode waitpid") + .expect("typed waitpid operation"); + + assert!(matches!( + operation, + HostOperation::Process(ProcessOperation::Wait { + target: WaitTarget::Any, + options: 0, + deadline_ms: Some(10_000), + temporary_mask: None, + }) + )); + } + #[test] fn typed_identity_route_accepts_explicit_unchanged_ids() { let operation = decode_host_operation( @@ -3660,7 +3791,7 @@ mod tests { ), ("process.system_identity", vec![]), ("process.umask", vec![Value::Null]), - ("process.waitpid", vec![json!(-1), json!(1)]), + ("process.waitpid", vec![json!(-1), json!(1), json!(10_000)]), ("process.waitpid_transition", vec![json!(-1), json!(1)]), ]; let mut covered = BTreeSet::new(); @@ -3976,6 +4107,7 @@ mod tests { fn every_vm_scoped_host_family_is_classified_before_kernel_only_dispatch() { let cases = [ request("__kernel_stdin_read", vec![json!(4096), json!(0)]), + request("process.fd_read", vec![json!(3), json!(4096), Value::Null]), request( "dns.lookup", vec![json!({"hostname":"localhost","family":4})], diff --git a/crates/native-sidecar/src/execution/host_dispatch/network.rs b/crates/native-sidecar/src/execution/host_dispatch/network.rs index af75b02e54..44330144c6 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/network.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/network.rs @@ -193,6 +193,7 @@ pub(in crate::execution) fn service_deferred_kernel_poll( deadline, wake_task: None, temporary_signal_mask_token: None, + temporary_signal_thread_id: None, combined: false, }); } diff --git a/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs index 570125208e..d8b74877a2 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/network_compat.rs @@ -223,6 +223,7 @@ where interests, timeout_ms, signal_mask, + signal_thread_id, } = operation else { return Err(SidecarError::host( @@ -257,10 +258,14 @@ where let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let capabilities = vm.capabilities.clone(); let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated POSIX-poll process remains registered"); + let kernel_pid = reply.identity().pid; + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; service_deferred_posix_poll( generation, &runtime, @@ -272,7 +277,7 @@ where managed_descriptions, &mut vm.kernel, process, - Some((interests, deadline, signal_mask, reply)), + Some((interests, deadline, signal_mask, signal_thread_id, reply)), ) } @@ -283,10 +288,14 @@ fn restore_deferred_posix_mask( let Some(token) = poll.temporary_signal_mask_token.take() else { return Ok(()); }; - process - .kernel_handle - .end_temporary_signal_mask(token) - .map_err(|error| HostServiceError::new(error.code(), error.to_string())) + let result = if let Some(thread_id) = poll.temporary_signal_thread_id.take() { + process + .kernel_handle + .end_temporary_signal_mask_for_thread(thread_id, token) + } else { + process.kernel_handle.end_temporary_signal_mask(token) + }; + result.map_err(|error| HostServiceError::new(error.code(), error.to_string())) } fn fail_deferred_posix_poll( @@ -324,11 +333,12 @@ pub(in crate::execution) fn service_deferred_posix_poll( BoundedVec, Option, Option, + Option, DirectHostReplyHandle, )>, ) -> Result<(), SidecarError> { let newly_admitted = incoming.is_some(); - if let Some((interests, deadline, signal_mask, reply)) = incoming { + if let Some((interests, deadline, signal_mask, signal_thread_id, reply)) = incoming { if process.deferred_kernel_poll.is_some() { reply .fail(HostServiceError::new( @@ -373,7 +383,14 @@ pub(in crate::execution) fn service_deferred_posix_poll( return Ok(()); } }; - match process.kernel_handle.begin_temporary_signal_mask(mask) { + let result = if let Some(thread_id) = signal_thread_id { + process + .kernel_handle + .begin_temporary_signal_mask_for_thread(thread_id, mask) + } else { + process.kernel_handle.begin_temporary_signal_mask(mask) + }; + match result { Ok(token) => Some(token), Err(error) => { reply @@ -391,6 +408,7 @@ pub(in crate::execution) fn service_deferred_posix_poll( deadline, wake_task: None, temporary_signal_mask_token, + temporary_signal_thread_id: temporary_signal_mask_token.and(signal_thread_id), combined: true, }); // Installing a ppoll mask can make a previously blocked signal @@ -552,6 +570,7 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } + let kernel_pid = reply.identity().pid; let socket_paths = build_socket_path_context( sidecar .vms @@ -566,11 +585,6 @@ where .vms .get_mut(vm_id) .expect("validated close VM remains registered"); - let process = vm - .active_processes - .get(process_id) - .expect("validated close process remains registered"); - let kernel_pid = process.kernel_pid; let result = close_with_managed_retirement(&bridge, vm_id, &socket_paths, vm, kernel_pid, fd); match result { Ok(response) => reply.succeed(response).map_err(SidecarError::from), @@ -580,6 +594,84 @@ where } } +pub(super) fn dispatch_context_fd_snapshot( + sidecar: &mut NativeSidecar, + vm_id: &str, + process_id: &str, + reply: DirectHostReplyHandle, +) -> Result<(), SidecarError> +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { + return Ok(()); + } + let pid = reply.identity().pid; + if !reply.claim().map_err(SidecarError::from)? { + return Ok(()); + } + let vm = sidecar + .vms + .get(vm_id) + .expect("validated fd-snapshot VM remains registered"); + let result = fd_snapshot_with_managed_routes(vm, pid); + match result { + Ok(response) => reply.succeed(response).map_err(SidecarError::from), + Err(error) => reply + .fail(host_service_error(&error)) + .map_err(SidecarError::from), + } +} + +pub(in crate::execution) fn fd_snapshot_with_managed_routes( + vm: &VmState, + pid: u32, +) -> Result { + let entries = vm + .kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)?; + let managed_ids = vm + .managed_host_net_descriptions + .lock() + .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? + .iter() + .filter_map(|(description_id, description)| { + description + .route_for(pid) + .is_some() + .then_some(*description_id) + }) + .collect::>(); + Ok(HostCallReply::Json(Value::Array( + entries + .into_iter() + .map(|entry| { + json!({ + "fd": entry.fd, + "descriptionId": entry.description_id.to_string(), + "managedHostNet": managed_ids.contains(&entry.description_id), + "fdFlags": entry.fd_flags, + "statusFlags": entry.status_flags, + "filetype": entry.filetype, + "rightsBase": entry.rights_base, + "rightsInheriting": entry.rights_inheriting, + "kind": if entry.is_socket { + "socket" + } else if entry.is_pipe { + "pipe" + } else if entry.is_pty { + "pty" + } else { + "file" + }, + }) + }) + .collect(), + ))) +} + pub(in crate::execution) fn close_with_managed_retirement( bridge: &SharedBridge, vm_id: &str, @@ -606,7 +698,7 @@ where socket_paths, vm, kernel_pid, - description_id.into_iter(), + description_id, )?; Ok(HostCallReply::Json(Value::Null)) } @@ -626,6 +718,7 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } + let kernel_pid = reply.identity().pid; let socket_paths = build_socket_path_context( sidecar .vms @@ -640,11 +733,6 @@ where .vms .get_mut(vm_id) .expect("validated closefrom VM remains registered"); - let process = vm - .active_processes - .get(process_id) - .expect("validated closefrom process remains registered"); - let kernel_pid = process.kernel_pid; let response = closefrom_with_managed_retirement( &bridge, vm_id, @@ -690,22 +778,18 @@ where let exact_set = exact_fds .as_ref() .map(|fds| fds.as_slice().iter().copied().collect::>()); - let candidate_descriptions = match vm + let candidate_descriptions = vm .kernel .fd_snapshot(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error) - { - Ok(snapshot) => snapshot - .into_iter() - .filter_map(|entry| { - exact_set - .as_ref() - .map_or(entry.fd >= min_fd, |fds| fds.contains(&entry.fd)) - .then_some(entry.description_id) - }) - .collect::>(), - Err(error) => return Err(error), - }; + .map_err(kernel_error)? + .into_iter() + .filter_map(|entry| { + exact_set + .as_ref() + .map_or(entry.fd >= min_fd, |fds| fds.contains(&entry.fd)) + .then_some(entry.description_id) + }) + .collect::>(); // `fd_close_from` removes all matching table entries before it performs // fallible resource cleanup. Always reconcile managed routes from the @@ -748,6 +832,7 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } + let pid = reply.identity().pid; if !reply.claim().map_err(SidecarError::from)? { return Ok(()); } @@ -762,11 +847,6 @@ where .vms .get_mut(vm_id) .expect("validated descriptor-mutation VM remains registered"); - let process = vm - .active_processes - .get(process_id) - .expect("validated descriptor-mutation process remains registered"); - let pid = process.kernel_pid; let result = replace_descriptor_with_managed_retirement( &bridge, vm_id, @@ -844,7 +924,7 @@ where socket_paths, vm, pid, - replaced_description_id.into_iter(), + replaced_description_id, )?; Ok(HostCallReply::Json(value)) } @@ -1521,6 +1601,18 @@ where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { + let kernel_pid = reply.identity().pid; + let process_path = sidecar + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|root| NativeSidecar::::active_process_path_by_kernel_pid(root, kernel_pid)) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; if !reply.claim().map_err(SidecarError::from)? { return Ok(()); } @@ -1537,7 +1629,7 @@ where process_id, ManagedStreamReadRecheck { root_process_id: process_id.to_owned(), - process_path: Vec::new(), + process_path, socket_id, max_bytes, peek, @@ -1744,6 +1836,7 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } + let kernel_pid = reply.identity().pid; if let NetworkOperation::ManagedRead { socket_id, max_bytes, @@ -1774,13 +1867,9 @@ where .vms .get(vm_id) .expect("validated fd-network VM remains registered"); - let process = vm - .active_processes - .get(process_id) - .expect("validated fd-network process remains registered"); let description_id = vm .kernel - .fd_description_identity(EXECUTION_DRIVER_NAME, process.kernel_pid, *fd) + .fd_description_identity(EXECUTION_DRIVER_NAME, kernel_pid, *fd) .map_err(kernel_error)? .0; let route = vm @@ -1788,7 +1877,7 @@ where .lock() .map_err(|_| SidecarError::host("EIO", "managed description registry lock poisoned"))? .get(&description_id) - .and_then(|description| description.route_for(process.kernel_pid).cloned()); + .and_then(|description| description.route_for(kernel_pid).cloned()); if let Some(ManagedHostNetRoute::UdpSocket(socket_id)) = route { return dispatch_context_udp_poll( sidecar, @@ -1839,10 +1928,13 @@ where let managed_descriptions = Arc::clone(&vm.managed_host_net_descriptions); let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated fd-network process remains registered"); + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; let response = service_managed_fd_network_operation( ManagedFdNetworkServiceContext { bridge: &bridge, @@ -1890,10 +1982,13 @@ where let capabilities = vm.capabilities.clone(); let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated managed UDP process remains registered"); + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; let response = service_managed_udp_operation( ManagedUdpServiceRequest { bridge: &bridge, @@ -1941,10 +2036,13 @@ where let runtime = vm.runtime_context.clone(); let capabilities = vm.capabilities.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated managed-network process remains registered"); + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; let response = service_managed_network_operation( ManagedNetworkServiceContext { vm_id, @@ -1982,10 +2080,13 @@ where let capabilities = vm.capabilities.clone(); let dns = vm.dns.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated managed-endpoint process remains registered"); + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; let response = service_managed_endpoint_operation( ManagedEndpointServiceContext { bridge: &bridge, @@ -2031,10 +2132,13 @@ where let capabilities = vm.capabilities.clone(); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let dns = vm.dns.clone(); - let process = vm - .active_processes - .get_mut(process_id) - .expect("validated managed-network process remains registered"); + let process = active_process_by_kernel_pid_mut(&mut vm.active_processes, kernel_pid) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; let response = service_descriptor_rights_compat_operation( &bridge, vm_id, @@ -3228,6 +3332,7 @@ where fd, server_name, alpn, + reject_unauthorized, .. } => { let description_id = managed_fd_description_id(&context, fd)?; @@ -3251,6 +3356,7 @@ where json!({ "servername": server_name.as_str(), "ALPNProtocols": protocols, + "rejectUnauthorized": reject_unauthorized, }) .to_string(), )?, @@ -3445,6 +3551,7 @@ where if !validate_context_host_call(sidecar, vm_id, process_id, &reply)? { return Ok(()); } + let kernel_pid = reply.identity().pid; let NetworkOperation::ManagedUdpPoll { socket_id, wait_ms, @@ -3460,10 +3567,23 @@ where if !reply.claim().map_err(SidecarError::from)? { return Ok(()); } + let process_path = sidecar + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|root| NativeSidecar::::active_process_path_by_kernel_pid(root, kernel_pid)) + .ok_or_else(|| { + SidecarError::host( + "ESTALE", + format!("active process for kernel pid {kernel_pid} disappeared"), + ) + })?; + let path = process_path.iter().map(String::as_str).collect::>(); let socket = sidecar .vms .get(vm_id) .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|root| NativeSidecar::::active_process_by_path(root, &path)) .and_then(|process| process.udp_sockets.get(socket_id.as_str())) .ok_or_else(|| SidecarError::host("EBADF", "unknown UDP socket")); let socket = match socket { @@ -3484,7 +3604,7 @@ where process_id, ManagedUdpPollRecheck { root_process_id: process_id.to_owned(), - process_path: Vec::new(), + process_path, socket_id: socket_id.into_string(), peek, max_bytes, diff --git a/crates/native-sidecar/src/execution/host_dispatch/process.rs b/crates/native-sidecar/src/execution/host_dispatch/process.rs index 68ae876ae9..6909fde41f 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/process.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/process.rs @@ -35,39 +35,76 @@ impl SidecarHostCapability for ProcessCapability { operation: ProcessOperation, ) -> Result { let value = match operation { - ProcessOperation::OpenExecutableImage { source } => { + ProcessOperation::OpenExecutableImage { source, resolution } => { if process.executable_image.is_some() { return Err(HostServiceError::new( "EBUSY", "this process already owns an executable-image snapshot", )); } - let image = match source { - ExecutableImageSource::TrustedInitialPath(path) => kernel + let (image, resolved_argv) = match (source, resolution) { + (ExecutableImageSource::TrustedInitialPath(path), None) => kernel .load_trusted_initial_runtime_image( path.as_str(), process.limits.wasm.max_module_file_bytes, - ), - ExecutableImageSource::Path(path) => { + ) + .map(|image| (image, None)), + (ExecutableImageSource::Path(path), None) => { let path = if path.as_str().starts_with('/') { normalize_path(path.as_str()) } else { normalize_path(&format!("{}/{}", process.guest_cwd, path.as_str())) }; - kernel.load_process_runtime_image( - EXECUTION_DRIVER_NAME, - process.kernel_pid, - &path, - process.limits.wasm.max_module_file_bytes, - ) + kernel + .load_process_runtime_image( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + process.limits.wasm.max_module_file_bytes, + ) + .map(|image| (image, None)) } - ExecutableImageSource::Descriptor(fd) => kernel + (ExecutableImageSource::Descriptor(fd), None) => kernel .load_process_runtime_image_from_fd( EXECUTION_DRIVER_NAME, process.kernel_pid, fd, process.limits.wasm.max_module_file_bytes, - ), + ) + .map(|image| (image, None)), + (ExecutableImageSource::Descriptor(fd), Some(resolution)) => { + let request = resolution.as_request(); + kernel + .load_resolved_process_runtime_image_from_fd( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + &process.guest_cwd, + &request.argv, + &request.close_on_exec_fds, + process.limits.wasm.max_module_file_bytes, + ) + .map(|resolved| (resolved.image, Some(resolved.argv))) + } + (ExecutableImageSource::Path(path), Some(resolution)) => { + let request = resolution.as_request(); + kernel + .load_resolved_process_runtime_image( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + path.as_str(), + &process.guest_cwd, + &request.argv, + process.limits.wasm.max_module_file_bytes, + ) + .map(|resolved| (resolved.image, Some(resolved.argv))) + } + (ExecutableImageSource::TrustedInitialPath(_), Some(_)) => { + return Err(HostServiceError::new( + "EINVAL", + "trusted initial images cannot request guest exec resolution", + )); + } } .map_err(kernel_host_error)?; let size = image.bytes.len(); @@ -84,6 +121,7 @@ impl SidecarHostCapability for ProcessCapability { "canonicalPath": canonical_path, "size": size, "mode": mode, + "argv": resolved_argv, }) } ProcessOperation::ReadExecutableImage { diff --git a/crates/native-sidecar/src/execution/host_dispatch/signal.rs b/crates/native-sidecar/src/execution/host_dispatch/signal.rs index 5f8e33a51f..4ad6f54a4e 100644 --- a/crates/native-sidecar/src/execution/host_dispatch/signal.rs +++ b/crates/native-sidecar/src/execution/host_dispatch/signal.rs @@ -7,13 +7,21 @@ impl SidecarHostCapability for SignalCapability { fn requires_claim(operation: &SignalOperation) -> bool { matches!( operation, - SignalOperation::SetAction { .. } + SignalOperation::RegisterThread { .. } + | SignalOperation::UnregisterThread { .. } + | SignalOperation::SetAction { .. } | SignalOperation::UpdateMask { .. } + | SignalOperation::UpdateMaskForThread { .. } | SignalOperation::BeginDelivery + | SignalOperation::BeginDeliveryForThread { .. } | SignalOperation::TakePublishedDelivery + | SignalOperation::TakePublishedDeliveryForThread { .. } | SignalOperation::EndDelivery { .. } + | SignalOperation::EndDeliveryForThread { .. } | SignalOperation::BeginTemporaryMask { .. } | SignalOperation::EndTemporaryMask { .. } + | SignalOperation::BeginTemporaryMaskForThread { .. } + | SignalOperation::EndTemporaryMaskForThread { .. } ) } @@ -23,6 +31,23 @@ impl SidecarHostCapability for SignalCapability { operation: SignalOperation, ) -> Result { let value = match operation { + SignalOperation::RegisterThread { + thread_id, + inherit_from, + } => { + process + .kernel_handle + .register_signal_thread(thread_id, inherit_from) + .map_err(kernel_host_error)?; + Value::Null + } + SignalOperation::UnregisterThread { thread_id } => { + process + .kernel_handle + .unregister_signal_thread(thread_id) + .map_err(kernel_host_error)?; + Value::Null + } SignalOperation::GetAction { signal } => { let action = process .kernel_handle @@ -52,8 +77,20 @@ impl SidecarHostCapability for SignalCapability { }) .unwrap_or(Value::Null) } + SignalOperation::BeginDeliveryForThread { thread_id } => { + materialize_real_timer_signal(process); + process + .kernel_handle + .begin_signal_delivery_for_thread(thread_id) + .map_err(kernel_host_error)? + .map(signal_delivery_value) + .unwrap_or(Value::Null) + } SignalOperation::TakePublishedDelivery => { materialize_real_timer_signal(process); + process + .apply_runtime_controls() + .map_err(|error| host_service_error(&error))?; let identity = process.kernel_handle.runtime_identity(); let delivery = ExecutionBackend::take_signal_checkpoint( &process.execution, @@ -62,9 +99,11 @@ impl SidecarHostCapability for SignalCapability { pid: identity.pid, }, )?; - if delivery.is_none() { - process.guest_signal_checkpoint_pending = false; - } + // apply_runtime_controls publishes at most one strict-LIFO + // delivery at a time. Once the executor takes it, the sidecar + // admission guard must no longer treat that checkpoint as + // queued; otherwise every later ppoll spuriously returns EINTR. + process.guest_signal_checkpoint_pending = false; delivery .map(|delivery| { json!({ @@ -75,6 +114,31 @@ impl SidecarHostCapability for SignalCapability { }) .unwrap_or(Value::Null) } + SignalOperation::TakePublishedDeliveryForThread { thread_id } => { + materialize_real_timer_signal(process); + process + .apply_runtime_controls() + .map_err(|error| host_service_error(&error))?; + let identity = process.kernel_handle.runtime_identity(); + let delivery = process.execution.take_signal_checkpoint_for_thread( + ExecutionWakeIdentity { + generation: identity.generation, + pid: identity.pid, + }, + thread_id, + )?; + process.guest_signal_checkpoint_pending = false; + delivery + .map(|delivery| { + json!({ + "signal": delivery.signal, + "token": delivery.delivery_token, + "flags": delivery.flags, + "threadId": delivery.thread_id, + }) + }) + .unwrap_or(Value::Null) + } SignalOperation::EndDelivery { token } => { process .kernel_handle @@ -82,6 +146,13 @@ impl SidecarHostCapability for SignalCapability { .map_err(kernel_host_error)?; Value::Null } + SignalOperation::EndDeliveryForThread { thread_id, token } => { + process + .kernel_handle + .end_signal_delivery_for_thread(thread_id, token) + .map_err(kernel_host_error)?; + Value::Null + } SignalOperation::UpdateMask { how, set } => { let previous = process .kernel_handle @@ -89,6 +160,21 @@ impl SidecarHostCapability for SignalCapability { .map_err(kernel_host_error)?; json!({ "signals": previous.signals() }) } + SignalOperation::UpdateMaskForThread { + thread_id, + how, + set, + } => { + let previous = process + .kernel_handle + .sigprocmask_for_thread( + thread_id, + kernel_mask_how(how), + kernel_signal_set(set)?, + ) + .map_err(kernel_host_error)?; + json!({ "signals": previous.signals() }) + } SignalOperation::Pending => json!({ "signals": process .kernel_handle @@ -110,13 +196,36 @@ impl SidecarHostCapability for SignalCapability { .map_err(kernel_host_error)?; Value::Null } + SignalOperation::BeginTemporaryMaskForThread { thread_id, mask } => { + let token = process + .kernel_handle + .begin_temporary_signal_mask_for_thread(thread_id, kernel_signal_set(mask)?) + .map_err(kernel_host_error)?; + json!(token) + } + SignalOperation::EndTemporaryMaskForThread { thread_id, token } => { + process + .kernel_handle + .end_temporary_signal_mask_for_thread(thread_id, token) + .map_err(kernel_host_error)?; + Value::Null + } other => return Err(unsupported("signal", other)), }; Ok(HostCallReply::Json(value)) } } -fn materialize_real_timer_signal(process: &ActiveProcess) { +fn signal_delivery_value(delivery: agentos_kernel::process_table::SignalDelivery) -> Value { + json!({ + "signal": delivery.signal, + "token": delivery.token, + "flags": delivery.action.flags, + "threadId": delivery.thread_id, + }) +} + +pub(super) fn materialize_real_timer_signal(process: &ActiveProcess) { if process.real_interval_timer.take_expiry() { process.kernel_handle.kill(libc::SIGALRM); } diff --git a/crates/native-sidecar/src/execution/javascript/http.rs b/crates/native-sidecar/src/execution/javascript/http.rs index 5b346852da..68a10728a1 100644 --- a/crates/native-sidecar/src/execution/javascript/http.rs +++ b/crates/native-sidecar/src/execution/javascript/http.rs @@ -232,19 +232,6 @@ fn serialize_kernel_http_fetch_request( request } -pub(in crate::execution) fn kernel_http_fetch_target_exit_code( - error: &SidecarError, -) -> Option { - let SidecarError::Execution(message) = error else { - return None; - }; - message - .strip_prefix("vm.fetch target exited before responding (exit code ")? - .strip_suffix(')')? - .parse() - .ok() -} - fn find_http_header_end(buffer: &[u8]) -> Option { buffer.windows(4).position(|window| window == b"\r\n\r\n") } @@ -482,16 +469,10 @@ where { let identity = process.kernel_handle.runtime_identity(); let max_reply_bytes = process.limits.reactor.max_bridge_response_bytes; - let event = if wait.is_zero() { - process - .execution - .try_poll_event(identity, max_reply_bytes)? - } else { - process - .execution - .poll_event(identity, max_reply_bytes, wait) - .await? - }; + let event = process + .execution + .poll_event(identity, max_reply_bytes, wait) + .await?; let Some(event) = event else { return Ok(false) }; match event { @@ -570,10 +551,19 @@ where Ok(()) } +pub(in crate::execution) struct KernelHttpFetch { + kernel_pid: u32, + socket_id: SocketId, + response_buffer: Vec, + peer_closed: bool, + url: String, + deadline: Instant, + max_fetch_response_bytes: usize, + _capability: agentos_runtime::capability::CapabilityLease, +} + #[allow(clippy::too_many_arguments)] -pub(in crate::execution) async fn dispatch_kernel_http_fetch( - bridge: &SharedBridge, - vm_id: &str, +pub(in crate::execution) fn begin_kernel_http_fetch( vm: &mut VmState, target_process_id: &str, port: u16, @@ -582,12 +572,7 @@ pub(in crate::execution) async fn dispatch_kernel_http_fetch( headers: &HttpHeaderCollection, body_bytes: Option<&[u8]>, max_fetch_response_bytes: usize, -) -> Result -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - let socket_paths = build_socket_path_context(vm)?; +) -> Result { // Client source ports belong to the kernel socket table. The listen-port // allocator does not reserve active client sockets and can hand the same // source port to concurrent requests. @@ -607,56 +592,129 @@ where .kernel .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp()) .map_err(kernel_error)?; - let _fetch_capability = pending_capability + let capability = pending_capability .commit(CapabilityBackend::Kernel { socket_id }) .map_err(|error| SidecarError::Execution(error.to_string()))?; + vm.kernel + .socket_bind_inet( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new("127.0.0.1", local_port), + ) + .map_err(kernel_error)?; + vm.kernel + .socket_connect_inet_loopback( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new("127.0.0.1", port), + ) + .map_err(kernel_error)?; + let request_bytes = + serialize_kernel_http_fetch_request(port, path, options, headers, body_bytes); + vm.kernel + .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes) + .map_err(kernel_error)?; - let result = dispatch_kernel_http_fetch_with_socket( - bridge, - vm_id, - vm, - target_process_id, + Ok(KernelHttpFetch { kernel_pid, socket_id, - local_port, - port, - path, - options, - headers, - body_bytes, - &socket_paths, + response_buffer: Vec::new(), + peer_closed: false, + url: format!("http://127.0.0.1:{port}{path}"), + deadline: Instant::now() + http_loopback_request_timeout(), max_fetch_response_bytes, - ) - .await; - let close_result = vm + _capability: capability, + }) +} + +pub(in crate::execution) fn poll_kernel_http_fetch( + vm: &mut VmState, + fetch: &mut KernelHttpFetch, +) -> Result, SidecarError> { + if let Some(response) = + parse_kernel_http_fetch_response(&fetch.response_buffer, fetch.peer_closed, &fetch.url) + .map_err(sidecar_core_execution_error)? + { + ensure_vm_fetch_response_within_limit( + &response, + "vm.fetch", + fetch.max_fetch_response_bytes, + ) + .map_err(sidecar_core_execution_error)?; + return Ok(Some(response)); + } + if Instant::now() >= fetch.deadline { + let preview = String::from_utf8_lossy(&fetch.response_buffer); + return Err(SidecarError::Execution(format!( + "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})", + fetch.response_buffer.len(), + preview.chars().take(200).collect::() + ))); + } + + let poll = vm .kernel - .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) - .map_err(kernel_error); - let cleanup_result = if result.is_err() { - drain_host_fetch_target_events(bridge, vm_id, vm, target_process_id, &socket_paths).await - } else { - Ok(()) - }; - match result { - Ok(response) => { - close_result?; - cleanup_result?; - Ok(response) - } - Err(error) => { - if let Err(close_error) = close_result { - eprintln!( - "ERR_AGENTOS_HTTP_FETCH_CLEANUP: failed to close kernel socket {socket_id} after fetch error: {close_error}" - ); - } - if let Err(cleanup_error) = cleanup_result { - eprintln!( - "ERR_AGENTOS_HTTP_FETCH_CLEANUP: failed to drain target events after fetch error: {cleanup_error}" - ); + .poll_targets( + EXECUTION_DRIVER_NAME, + fetch.kernel_pid, + vec![PollTargetEntry::socket( + fetch.socket_id, + POLLIN | POLLHUP | POLLERR, + )], + 0, + ) + .map_err(kernel_error)?; + let revents = poll + .targets + .first() + .map(|entry| entry.revents) + .unwrap_or_else(PollEvents::empty); + if revents.intersects(POLLERR) { + return Err(SidecarError::Execution(String::from( + "vm.fetch kernel TCP socket reported POLLERR", + ))); + } + if revents.intersects(POLLIN) { + loop { + match vm.kernel.socket_read( + EXECUTION_DRIVER_NAME, + fetch.kernel_pid, + fetch.socket_id, + 64 * 1024, + ) { + Ok(Some(bytes)) if !bytes.is_empty() => { + fetch.response_buffer.extend(bytes); + ensure_vm_fetch_raw_response_buffer_within_limit( + fetch.response_buffer.len(), + "vm.fetch", + ) + .map_err(sidecar_core_execution_error)?; + } + Ok(Some(_)) => break, + Ok(None) => { + fetch.peer_closed = true; + break; + } + Err(error) if error.code() == "EAGAIN" => break, + Err(error) => return Err(kernel_error(error)), } - Err(error) } } + if revents.intersects(POLLHUP) { + fetch.peer_closed = true; + } + Ok(None) +} + +pub(in crate::execution) fn close_kernel_http_fetch( + vm: &mut VmState, + fetch: &KernelHttpFetch, +) -> Result<(), SidecarError> { + vm.kernel + .socket_close(EXECUTION_DRIVER_NAME, fetch.kernel_pid, fetch.socket_id) + .map_err(kernel_error) } #[allow(clippy::too_many_arguments)] @@ -1073,150 +1131,6 @@ where Ok(String::from("{\"cancelled\":true}")) } -#[allow(clippy::too_many_arguments)] -async fn dispatch_kernel_http_fetch_with_socket( - bridge: &SharedBridge, - vm_id: &str, - vm: &mut VmState, - target_process_id: &str, - kernel_pid: u32, - socket_id: SocketId, - local_port: u16, - port: u16, - path: &str, - options: &JavascriptHttpRequestOptions, - headers: &HttpHeaderCollection, - body_bytes: Option<&[u8]>, - socket_paths: &SocketPathContext, - max_fetch_response_bytes: usize, -) -> Result -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - vm.kernel - .socket_bind_inet( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new("127.0.0.1", local_port), - ) - .map_err(kernel_error)?; - vm.kernel - .socket_connect_inet_loopback( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new("127.0.0.1", port), - ) - .map_err(kernel_error)?; - - let request_bytes = - serialize_kernel_http_fetch_request(port, path, options, headers, body_bytes); - vm.kernel - .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes) - .map_err(kernel_error)?; - - let mut response_buffer = Vec::new(); - let mut peer_closed = false; - let url = format!("http://127.0.0.1:{port}{path}"); - let deadline = Instant::now() + http_loopback_request_timeout(); - loop { - if let Some(response) = - parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url) - .map_err(sidecar_core_execution_error)? - { - ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes) - .map_err(sidecar_core_execution_error)?; - return Ok(response); - } - if Instant::now() >= deadline { - let preview = String::from_utf8_lossy(&response_buffer); - return Err(SidecarError::Execution(format!( - "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})", - response_buffer.len(), - preview.chars().take(200).collect::() - ))); - } - - { - let dns = vm.dns.clone(); - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let capabilities = vm.capabilities.clone(); - let process = vm - .active_processes - .get_mut(target_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "vm.fetch target process disappeared: {target_process_id}" - )) - })?; - service_host_fetch_target_event( - bridge, - vm_id, - &dns, - socket_paths, - &mut vm.kernel, - &kernel_readiness, - process, - Duration::from_millis(5), - &capabilities, - ) - .await?; - } - - let poll = vm - .kernel - .poll_targets( - EXECUTION_DRIVER_NAME, - kernel_pid, - vec![PollTargetEntry::socket( - socket_id, - POLLIN | POLLHUP | POLLERR, - )], - 5, - ) - .map_err(kernel_error)?; - let revents = poll - .targets - .first() - .map(|entry| entry.revents) - .unwrap_or_else(PollEvents::empty); - if revents.intersects(POLLERR) { - return Err(SidecarError::Execution(String::from( - "vm.fetch kernel TCP socket reported POLLERR", - ))); - } - if revents.intersects(POLLIN) { - loop { - match vm - .kernel - .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024) - { - Ok(Some(bytes)) if !bytes.is_empty() => { - response_buffer.extend(bytes); - ensure_vm_fetch_raw_response_buffer_within_limit( - response_buffer.len(), - "vm.fetch", - ) - .map_err(sidecar_core_execution_error)?; - } - Ok(Some(_)) => break, - Ok(None) => { - peer_closed = true; - break; - } - Err(error) if error.code() == "EAGAIN" => break, - Err(error) => return Err(kernel_error(error)), - } - } - } - if revents.intersects(POLLHUP) { - peer_closed = true; - } - } -} - pub(in crate::execution) fn begin_loopback_http_request( process: &mut ActiveProcess, server_id: u64, diff --git a/crates/native-sidecar/src/execution/javascript/rpc.rs b/crates/native-sidecar/src/execution/javascript/rpc.rs index be20148ba0..405fba7d4b 100644 --- a/crates/native-sidecar/src/execution/javascript/rpc.rs +++ b/crates/native-sidecar/src/execution/javascript/rpc.rs @@ -1878,36 +1878,54 @@ where }) .map_err(kernel_error) } - "process.fd_snapshot" => kernel - .fd_snapshot(EXECUTION_DRIVER_NAME, process.kernel_pid) - .map(|entries| { - Value::Array( - entries - .into_iter() - .map(|entry| { - json!({ - "fd": entry.fd, - "descriptionId": entry.description_id.to_string(), - "fdFlags": entry.fd_flags, - "statusFlags": entry.status_flags, - "filetype": entry.filetype, - "rightsBase": entry.rights_base, - "rightsInheriting": entry.rights_inheriting, - "kind": if entry.is_socket { - "socket" - } else if entry.is_pipe { - "pipe" - } else if entry.is_pty { - "pty" - } else { - "file" - }, - }) + "process.fd_snapshot" => { + let entries = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map_err(kernel_error)?; + let managed_ids = if let Some(registry) = managed_descriptions.as_ref() { + registry + .lock() + .map_err(|_| { + SidecarError::host("EIO", "managed description registry lock poisoned") + })? + .iter() + .filter_map(|(description_id, description)| { + description + .route_for(process.kernel_pid) + .is_some() + .then_some(*description_id) + }) + .collect::>() + } else { + BTreeSet::new() + }; + Ok(Value::Array( + entries + .into_iter() + .map(|entry| { + json!({ + "fd": entry.fd, + "descriptionId": entry.description_id.to_string(), + "managedHostNet": managed_ids.contains(&entry.description_id), + "fdFlags": entry.fd_flags, + "statusFlags": entry.status_flags, + "filetype": entry.filetype, + "rightsBase": entry.rights_base, + "rightsInheriting": entry.rights_inheriting, + "kind": if entry.is_socket { + "socket" + } else if entry.is_pipe { + "pipe" + } else if entry.is_pty { + "pty" + } else { + "file" + }, }) - .collect(), - ) - }) - .map_err(kernel_error), + }) + .collect(), + )) + } "process.hostnet_fd_open" => { let datagram = javascript_sync_rpc_arg_bool( &request.args, diff --git a/crates/native-sidecar/src/execution/launch.rs b/crates/native-sidecar/src/execution/launch.rs index 48c45c2e7e..7207858e72 100644 --- a/crates/native-sidecar/src/execution/launch.rs +++ b/crates/native-sidecar/src/execution/launch.rs @@ -2406,6 +2406,7 @@ pub(super) fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits { max_sync_rpc_response_line_bytes: Some(vm.limits.reactor.max_bridge_response_bytes as u64), pending_event_count: Some(vm.limits.process.pending_event_count), pending_event_bytes: Some(vm.limits.process.pending_event_bytes), + max_threads: Some(vm.limits.wasm.max_threads), } } @@ -4545,6 +4546,14 @@ where } let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); + let standalone_wasm_backend = match payload.wasm_backend { + Some(StandaloneWasmBackend::V8) => ExecutionStandaloneWasmBackend::V8, + Some(StandaloneWasmBackend::Wasmtime) => ExecutionStandaloneWasmBackend::Wasmtime, + Some(StandaloneWasmBackend::WasmtimeThreads) => { + ExecutionStandaloneWasmBackend::WasmtimeThreads + } + None => vm.standalone_wasm_backend, + }; if let Some(command) = payload.command.as_deref() { if let Some(binding_resolution) = @@ -4616,6 +4625,7 @@ where Arc::clone(&self.process_event_notify), ) .with_adapter_policy(ExecutionAdapterPolicy::BINDING) + .with_standalone_wasm_backend(standalone_wasm_backend) .with_vm_pending_byte_budgets( Arc::clone(&vm_pending_stdin_bytes_budget), Arc::clone(&vm_pending_event_bytes_budget), @@ -4801,6 +4811,14 @@ where ), "top-level runtime-control attachment" ); + if resolved.runtime == GuestRuntimeKind::WebAssembly { + top_level_start_step!( + vm.kernel + .initialize_canonical_wasi_preopens(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error), + "top-level WASI capability-root initialization" + ); + } let tty_master_fd = if requested_tty { let (master_fd, slave_fd, _) = top_level_start_step!( vm.kernel @@ -5048,11 +5066,17 @@ where "top-level compatibility-WASM permission lookup" ); let module_path = match payload.wasm_backend { - Some(StandaloneWasmBackend::Wasmtime) => env - .get("AGENTOS_GUEST_ENTRYPOINT") - .map(|path| format!("{TRUSTED_INITIAL_MODULE_PREFIX}{path}")) - .unwrap_or_else(|| resolved.entrypoint.clone()), - Some(StandaloneWasmBackend::V8) | None => resolved.entrypoint.clone(), + _ if matches!( + standalone_wasm_backend, + ExecutionStandaloneWasmBackend::Wasmtime + | ExecutionStandaloneWasmBackend::WasmtimeThreads + ) => + { + env.get("AGENTOS_GUEST_ENTRYPOINT") + .map(|path| format!("{TRUSTED_INITIAL_MODULE_PREFIX}{path}")) + .unwrap_or_else(|| resolved.entrypoint.clone()) + } + _ => resolved.entrypoint.clone(), }; let context = self.wasm_engine.create_context(CreateWasmContextRequest { vm_id: vm_id.clone(), @@ -5074,14 +5098,7 @@ where guest_runtime: wasm_guest_runtime, }, vm.runtime_context.clone(), - match payload.wasm_backend { - Some(StandaloneWasmBackend::Wasmtime) => { - ExecutionStandaloneWasmBackend::Wasmtime - } - Some(StandaloneWasmBackend::V8) | None => { - ExecutionStandaloneWasmBackend::V8 - } - }, + standalone_wasm_backend, ) .await .map_err(wasm_error) @@ -5119,6 +5136,7 @@ where Arc::clone(&self.process_event_notify), ) .with_adapter_policy(resolved.adapter_policy) + .with_standalone_wasm_backend(standalone_wasm_backend) .with_vm_pending_byte_budgets(vm_pending_stdin_bytes_budget, vm_pending_event_bytes_budget) .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd) .with_tty_master_fd(tty_master_fd) diff --git a/crates/native-sidecar/src/execution/mod.rs b/crates/native-sidecar/src/execution/mod.rs index 21181ee229..773dfc0ba0 100644 --- a/crates/native-sidecar/src/execution/mod.rs +++ b/crates/native-sidecar/src/execution/mod.rs @@ -431,7 +431,18 @@ pub(crate) async fn operation_deadline_timeout( where F: Future, { - let mut deadline = OperationDeadlineTracker::new(limit); + operation_deadline_timeout_with_tracker(operation, OperationDeadlineTracker::new(limit), future) + .await +} + +async fn operation_deadline_timeout_with_tracker( + operation: &str, + mut deadline: OperationDeadlineTracker, + future: F, +) -> Result +where + F: Future, +{ tokio::pin!(future); match tokio::time::timeout_at(deadline.next_edge().into(), &mut future).await { Ok(output) => Ok(output), @@ -465,15 +476,17 @@ const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str = "AGENTOS_TEST_HTTP_LOOPBACK_R #[cfg(test)] mod configured_socket_capacity_tests { use super::{ - listener_accept_capacity, operation_deadline_timeout, reactor_io_limits, + listener_accept_capacity, operation_deadline_timeout, + operation_deadline_timeout_with_tracker, reactor_io_limits, set_deadline_limit_warning_handler, socket_completion_capacity, write_all_nonblocking, + OperationDeadlineTracker, }; use crate::limits::VmLimits; use std::io::{self, Write}; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::sync::{Arc, Mutex}; - use std::time::Duration; + use std::time::{Duration, Instant}; struct AlwaysWouldBlock { fd: UnixStream, @@ -510,6 +523,9 @@ mod configured_socket_capacity_tests { async fn operation_deadline_warns_at_eighty_percent_before_success_or_typed_expiry() { let captured = Arc::new(Mutex::new(Vec::new())); let sink = Arc::clone(&captured); + let (completion_sender, completion_receiver) = tokio::sync::oneshot::channel(); + let completion_sender = Arc::new(Mutex::new(Some(completion_sender))); + let warning_completion_sender = Arc::clone(&completion_sender); set_deadline_limit_warning_handler(move |warning| { if warning.operation.starts_with("deadline-test-") || warning.operation == "synchronous socket write" @@ -518,13 +534,32 @@ mod configured_socket_capacity_tests { .expect("deadline warning sink") .push(warning.clone()); } + if warning.operation == "deadline-test-completes-near-limit" { + if let Some(sender) = warning_completion_sender + .lock() + .expect("deadline completion sender") + .take() + { + sender.send(()).expect("release post-warning completion"); + } + } }); - let completed = operation_deadline_timeout( + // Construct the same 80%-warning state with the warning edge already + // reached and one full second remaining. A 5 ms real-time window made + // this regression test fail under concurrent linker load even though + // the production state machine behaved correctly. + let completed = operation_deadline_timeout_with_tracker( "deadline-test-completes-near-limit", - Duration::from_millis(50), - async { - tokio::time::sleep(Duration::from_millis(45)).await; + OperationDeadlineTracker::from_deadline( + Instant::now() + Duration::from_secs(1), + Duration::from_secs(5), + false, + ), + async move { + completion_receiver + .await + .expect("warning releases completion"); 7 }, ) @@ -542,14 +577,20 @@ mod configured_socket_capacity_tests { let warnings = captured.lock().expect("deadline warnings").clone(); assert_eq!(warnings.len(), 2); - for warning in warnings.iter() { - assert_eq!(warning.limit_name, "limits.reactor.operationDeadlineMs"); - assert_eq!(warning.limit_ms, 50); - assert!( - warning.observed_ms >= 40 && warning.observed_ms < 50, - "warning must precede the hard deadline: {warning:?}" - ); - } + assert_eq!(warnings[0].limit_name, "limits.reactor.operationDeadlineMs"); + assert_eq!(warnings[0].limit_ms, 5_000); + assert!( + warnings[0].observed_ms >= 4_000 && warnings[0].observed_ms < 5_000, + "warning must precede the hard deadline: {:?}", + warnings[0] + ); + assert_eq!(warnings[1].limit_name, "limits.reactor.operationDeadlineMs"); + assert_eq!(warnings[1].limit_ms, 50); + assert!( + warnings[1].observed_ms >= 40 && warnings[1].observed_ms <= 50, + "warning must reach the hard deadline: {:?}", + warnings[1] + ); // The synchronous TCP/Unix write path uses the same warning state // machine even though its readiness wait is `poll(2)`, not a Future. diff --git a/crates/native-sidecar/src/execution/process.rs b/crates/native-sidecar/src/execution/process.rs index f0e015deaf..0affa52bc8 100644 --- a/crates/native-sidecar/src/execution/process.rs +++ b/crates/native-sidecar/src/execution/process.rs @@ -449,15 +449,22 @@ impl ActiveProcess { // caller's mask before constructing the handler frame. This is // what lets a signal unblocked only by ppoll interrupt the // wait while nested handlers still observe the real mask. - let first_delivery = if let Some(token) = self - .deferred_kernel_poll - .as_mut() - .and_then(|poll| poll.temporary_signal_mask_token.take()) - { - match self - .kernel_handle - .end_temporary_signal_mask_and_begin_signal_delivery(token) - { + let temporary_mask = self.deferred_kernel_poll.as_mut().and_then(|poll| { + poll.temporary_signal_mask_token + .take() + .map(|token| (token, poll.temporary_signal_thread_id.take())) + }); + let first_delivery = if let Some((token, thread_id)) = temporary_mask { + let result = if let Some(thread_id) = thread_id { + self.kernel_handle + .end_temporary_signal_mask_and_begin_signal_delivery_for_thread( + thread_id, token, + ) + } else { + self.kernel_handle + .end_temporary_signal_mask_and_begin_signal_delivery(token) + }; + match result { Ok(delivery) => delivery, Err(error) => { let failure = HostServiceError::new(error.code(), error.to_string()); @@ -494,6 +501,7 @@ impl ActiveProcess { delivery.signal, delivery.token, delivery.action.flags, + delivery.thread_id, ) { Ok(SignalCheckpointOutcome::Published) => { // Every caught signal must release a parked guest syscall so @@ -2747,6 +2755,7 @@ impl ExecutionBackend for BindingExecution { _signal: i32, _delivery_token: u64, _flags: u32, + _thread_id: u32, ) -> Result { Ok(SignalCheckpointOutcome::Unsupported) } @@ -2896,9 +2905,17 @@ impl ActiveExecution { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result { - ExecutionBackend::deliver_signal_checkpoint(self, identity, signal, delivery_token, flags) - .map_err(SidecarError::from) + ExecutionBackend::deliver_signal_checkpoint( + self, + identity, + signal, + delivery_token, + flags, + thread_id, + ) + .map_err(SidecarError::from) } // Source-included integration tests exercise the adapter without a host @@ -2989,14 +3006,6 @@ impl ActiveExecution { /// Probe the runtime event queue once without parking the sidecar thread or /// registering a waker outside the coalesced process-event broker. - pub(crate) fn try_poll_event( - &mut self, - identity: ProcessRuntimeIdentity, - max_reply_bytes: usize, - ) -> Result, SidecarError> { - self.try_poll_event_inner(identity, max_reply_bytes, None) - } - pub(crate) fn try_poll_event_with_host( &mut self, identity: ProcessRuntimeIdentity, @@ -3124,9 +3133,10 @@ impl ExecutionBackend for ActiveExecution { signal: i32, delivery_token: u64, flags: u32, + thread_id: u32, ) -> Result { self.backend() - .deliver_signal_checkpoint(identity, signal, delivery_token, flags) + .deliver_signal_checkpoint(identity, signal, delivery_token, flags, thread_id) } fn take_signal_checkpoint( @@ -3136,6 +3146,15 @@ impl ExecutionBackend for ActiveExecution { self.backend().take_signal_checkpoint(identity) } + fn take_signal_checkpoint_for_thread( + &self, + identity: ExecutionWakeIdentity, + thread_id: u32, + ) -> Result, HostServiceError> { + self.backend() + .take_signal_checkpoint_for_thread(identity, thread_id) + } + fn discard_signal_checkpoints( &self, identity: ExecutionWakeIdentity, @@ -4260,7 +4279,14 @@ pub(super) fn flush_parked_kernel_wait_rpc(process: &mut ActiveProcess) { } if let Some(poll) = process.clear_deferred_kernel_poll() { if let Some(token) = poll.temporary_signal_mask_token { - if let Err(error) = process.kernel_handle.end_temporary_signal_mask(token) { + let result = if let Some(thread_id) = poll.temporary_signal_thread_id { + process + .kernel_handle + .end_temporary_signal_mask_for_thread(thread_id, token) + } else { + process.kernel_handle.end_temporary_signal_mask(token) + }; + if let Err(error) = result { eprintln!("ERR_AGENTOS_PPOLL_MASK_RESTORE_TEARDOWN: {error}"); } } diff --git a/crates/native-sidecar/src/execution/process_events.rs b/crates/native-sidecar/src/execution/process_events.rs index f1b0d332bd..db5aac83b8 100644 --- a/crates/native-sidecar/src/execution/process_events.rs +++ b/crates/native-sidecar/src/execution/process_events.rs @@ -845,7 +845,7 @@ where let Some(event) = event else { continue; }; - if matches!(event.event(), ActiveExecutionEvent::Exited(_)) { + if Self::terminal_execution_event(event.event()) { record_execute_response_to_exit_milestone( "execute_response_to_exit_event_polled", &vm_id, @@ -1088,13 +1088,22 @@ where event, ActiveExecutionEvent::Common(ExecutionEvent::HostCall { .. }) | ActiveExecutionEvent::Common(ExecutionEvent::Warning(_)) - | ActiveExecutionEvent::Common(ExecutionEvent::RuntimeFault(_)) | ActiveExecutionEvent::HostRpcRequest(_) | ActiveExecutionEvent::ManagedUdpPollRecheck(_) | ActiveExecutionEvent::SignalState { .. } ) } + pub(crate) fn terminal_execution_event(event: &ActiveExecutionEvent) -> bool { + matches!( + event, + ActiveExecutionEvent::Exited(_) + | ActiveExecutionEvent::Common( + ExecutionEvent::Exited(_) | ExecutionEvent::RuntimeFault(_) + ) + ) + } + pub(super) fn recover_closed_root_runtime_process_event( &mut self, vm_id: &str, diff --git a/crates/native-sidecar/src/main.rs b/crates/native-sidecar/src/main.rs index 019ac89ada..04faad9db6 100644 --- a/crates/native-sidecar/src/main.rs +++ b/crates/native-sidecar/src/main.rs @@ -17,6 +17,15 @@ fn main() { .with_writer(std::io::stderr) .with_max_level(level) .init(); + if std::env::args().nth(1).as_deref() + == Some(agentos_execution::WASMTIME_THREAD_WORKER_ARGUMENT) + { + if let Err(error) = agentos_execution::run_wasmtime_thread_worker() { + tracing::error!(code = %error.code, message = %error.message, "Wasmtime thread worker failed"); + std::process::exit(1); + } + return; + } if let Err(error) = fcntl(CONTROL_FD, FcntlArg::F_GETFD) { tracing::error!( ?error, diff --git a/crates/native-sidecar/src/service.rs b/crates/native-sidecar/src/service.rs index 6c8d1c777e..fa4f03d788 100644 --- a/crates/native-sidecar/src/service.rs +++ b/crates/native-sidecar/src/service.rs @@ -1807,7 +1807,7 @@ where event, } = envelope; - let is_exit_event = matches!(event, ActiveExecutionEvent::Exited(_)); + let is_exit_event = Self::terminal_execution_event(&event); if is_exit_event { record_execute_exit_event_queue_wait( @@ -1821,7 +1821,7 @@ where while let Some(pending) = self.pending_process_events.pop_front() { if pending.vm_id == vm_id && pending.process_id == process_id - && !matches!(pending.event, ActiveExecutionEvent::Exited(_)) + && !Self::terminal_execution_event(&pending.event) { trailing.push(pending.event); } else { @@ -3045,7 +3045,7 @@ where format!("conn-{}", self.next_connection_id) } - fn take_matching_process_event_envelope( + pub(crate) fn take_matching_process_event_envelope( &mut self, vm_id: &str, process_id: &str, @@ -3148,7 +3148,11 @@ where shared_reject(request, code, message) } - fn reject_error(&self, request: &RequestFrame, error: &SidecarError) -> ResponseFrame { + pub(crate) fn reject_error( + &self, + request: &RequestFrame, + error: &SidecarError, + ) -> ResponseFrame { if let SidecarError::Host(host_error) = error { if host_error.code == "ERR_AGENTOS_RESOURCE_LIMIT" { let details = host_error.details.as_ref(); @@ -3237,6 +3241,7 @@ where | ResourceClass::Http2DataBytes | ResourceClass::Http2CommandBytes | ResourceClass::Http2EventBytes => "bytes", + ResourceClass::WasmThreads => "threads", ResourceClass::Tasks => "tasks", ResourceClass::Timers => "timers", ResourceClass::Connections | ResourceClass::Http2Connections => "connections", diff --git a/crates/native-sidecar/src/state.rs b/crates/native-sidecar/src/state.rs index e390c8f004..3cc7364706 100644 --- a/crates/native-sidecar/src/state.rs +++ b/crates/native-sidecar/src/state.rs @@ -95,6 +95,7 @@ pub(crate) struct DeferredKernelPoll { /// Kernel-owned temporary mask scope for a combined ppoll. The sidecar /// restores this before publishing a caught signal or settling the reply. pub(crate) temporary_signal_mask_token: Option, + pub(crate) temporary_signal_thread_id: Option, /// Distinguishes the combined kernel/managed-fd path from the legacy /// kernel-only compatibility operation. pub(crate) combined: bool, @@ -423,6 +424,12 @@ impl ActiveRealIntervalTimer { refresh_real_interval_timer(&mut timer, Instant::now()); std::mem::take(&mut timer.pending_expiry) } + + pub(crate) fn next_deadline(&self) -> Option { + let mut timer = self.state.lock().unwrap_or_else(|error| error.into_inner()); + refresh_real_interval_timer(&mut timer, Instant::now()); + timer.deadline + } } fn refresh_real_interval_timer(timer: &mut RealIntervalTimerState, now: Instant) { @@ -1190,6 +1197,9 @@ pub(crate) struct VmState { pub(crate) listen_policy: VmListenPolicy, pub(crate) create_loopback_exempt_ports: BTreeSet, pub(crate) guest_env: BTreeMap, + /// VM-wide standalone-WASM engine policy. JavaScript itself remains on V8; + /// this affinity is copied to every top-level process for spawn/exec. + pub(crate) standalone_wasm_backend: StandaloneWasmBackend, pub(crate) requested_runtime: GuestRuntimeKind, pub(crate) root_filesystem_mode: RootFilesystemMode, pub(crate) guest_cwd: String, diff --git a/crates/native-sidecar/src/stdio.rs b/crates/native-sidecar/src/stdio.rs index 9207ace07a..17aa474c08 100644 --- a/crates/native-sidecar/src/stdio.rs +++ b/crates/native-sidecar/src/stdio.rs @@ -1194,17 +1194,20 @@ async fn run_async( } _ = process_event_notify.notified() => { for session in active_sessions.iter().cloned().collect::>() { - if sidecar.pump_process_events(&session.compat_ownership_scope()).await? { - match event_ready_tx.try_send(()) { - Ok(()) - | Err(tokio::sync::mpsc::error::TrySendError::Full(())) => {} - Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => { - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "event-ready wake receiver closed", - ) - .into()); - } + sidecar.pump_process_events(&session.compat_ownership_scope()).await?; + // A request-scoped inline pump can already have moved + // public events into the durable queue before issuing + // this wake, so probe that queue even when this pump turn + // finds no new executor event. + match event_ready_tx.try_send(()) { + Ok(()) + | Err(tokio::sync::mpsc::error::TrySendError::Full(())) => {} + Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "event-ready wake receiver closed", + ) + .into()); } } } diff --git a/crates/native-sidecar/src/vm.rs b/crates/native-sidecar/src/vm.rs index 03e6b162e6..772daa24fc 100644 --- a/crates/native-sidecar/src/vm.rs +++ b/crates/native-sidecar/src/vm.rs @@ -548,6 +548,17 @@ where listen_policy, create_loopback_exempt_ports, guest_env, + standalone_wasm_backend: match create_config.wasm_backend.unwrap_or_default() { + vm_config::StandaloneWasmBackend::V8 => { + agentos_execution::StandaloneWasmBackend::V8 + } + vm_config::StandaloneWasmBackend::Wasmtime => { + agentos_execution::StandaloneWasmBackend::Wasmtime + } + vm_config::StandaloneWasmBackend::WasmtimeThreads => { + agentos_execution::StandaloneWasmBackend::WasmtimeThreads + } + }, requested_runtime: payload.runtime, root_filesystem_mode: protocol_root_filesystem_mode(root_filesystem.mode), guest_cwd, @@ -1606,6 +1617,32 @@ fn vm_resource_ledger( "limits.resources.maxSocketDatagramQueueLen must be bounded for sidecar VMs", )) })?; + let wasm_linear_memory_limit = limits.resources.max_wasm_memory_bytes.ok_or_else(|| { + SidecarError::InvalidState(String::from( + "limits.resources.maxWasmMemoryBytes must be bounded for sidecar VMs", + )) + })?; + let _wasm_linear_memory_limit = usize::try_from(wasm_linear_memory_limit).map_err(|_| { + SidecarError::InvalidState(String::from( + "limits.resources.maxWasmMemoryBytes exceeds the host address space", + )) + })?; + // `limits.resources.maxWasmMemoryBytes` is the accessible linear-memory + // cap for each guest memory. Wasmtime's admission reservation also includes + // bounded table and async-stack envelopes, so reusing the linear cap as the + // child ledger's aggregate maximum makes one otherwise-valid Store + // impossible to admit. Keep aggregate Store admission under the distinct, + // process-wide runtime envelope; the child ledger still gives each VM a + // bounded scope while the Store limiter independently enforces the exact + // per-memory linear cap. + let wasm_aggregate_memory_limit = process + .usage(ResourceClass::WasmMemoryBytes) + .limit + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "runtime.resources.maxWasmMemoryBytes must be bounded for sidecar VMs", + )) + })?; let child_limits = [ ( ResourceClass::Capabilities, @@ -1726,6 +1763,20 @@ fn vm_resource_ledger( "limits.reactor.maxBlockingBytes", ), ), + ( + ResourceClass::WasmMemoryBytes, + ResourceLimit::new( + wasm_aggregate_memory_limit, + "runtime.resources.maxWasmMemoryBytes", + ), + ), + ( + ResourceClass::WasmThreads, + ResourceLimit::new( + limits.wasm.max_concurrent_threads, + "limits.wasm.maxConcurrentThreads", + ), + ), ( ResourceClass::Http2Connections, ResourceLimit::new(limits.http2.max_connections, "limits.http2.maxConnections"), @@ -1823,6 +1874,7 @@ fn vm_resource_ledger( ResourceClass::WasmMemoryBytes => { "runtime.resources.maxWasmMemoryBytes" } + ResourceClass::WasmThreads => "runtime.resources.maxWasmThreads", ResourceClass::Http2Connections => "limits.http2.maxConnections", ResourceClass::Http2Streams => "limits.http2.maxStreams", ResourceClass::Http2BufferedBytes => "limits.http2.maxBufferedBytes", @@ -2976,6 +3028,22 @@ mod tests { resource.name() ); } + assert_eq!( + ledger.usage(ResourceClass::WasmMemoryBytes).limit, + process + .resources() + .usage(ResourceClass::WasmMemoryBytes) + .limit, + "the VM aggregate Store envelope must inherit the bounded process ceiling" + ); + assert_ne!( + ledger.usage(ResourceClass::WasmMemoryBytes).limit, + crate::limits::VmLimits::default() + .resources + .max_wasm_memory_bytes + .and_then(|value| usize::try_from(value).ok()), + "the per-memory linear cap must not be reused as Store-overhead admission" + ); } fn block_on(future: F) -> F::Output { @@ -3087,7 +3155,16 @@ mod tests { ] { let process = Arc::new(ResourceLedger::root( format!("executor-ceiling-test-{resource:?}"), - [(resource, ResourceLimit::new(maximum, process_path))], + [ + (resource, ResourceLimit::new(maximum, process_path)), + ( + ResourceClass::WasmMemoryBytes, + ResourceLimit::new( + 1024 * 1024 * 1024, + "runtime.resources.maxWasmMemoryBytes", + ), + ), + ], )); let error = vm_resource_ledger("vm-test", 70_005, &limits, process) .expect_err("VM executor limit must not exceed its process ceiling"); diff --git a/crates/native-sidecar/tests/architecture_guards.rs b/crates/native-sidecar/tests/architecture_guards.rs index d9083b8f7e..43a518acc5 100644 --- a/crates/native-sidecar/tests/architecture_guards.rs +++ b/crates/native-sidecar/tests/architecture_guards.rs @@ -341,8 +341,9 @@ fn phase_two_keeps_wasmtime_scoped_to_the_standalone_wasm_adapter() { .matches("validate_module_profile(&resolved_module)?;") .count() >= 2 - && wasmtime_module.contains("profile::validate_locked_profile"), - "both V8-WASM start paths and Wasmtime compilation must use the shared wasmparser profile" + && wasmtime_module.contains("validate_locked_profile(bytes)?;") + && wasmtime_module.contains("validate_locked_threaded_profile(bytes)?;"), + "both V8-WASM start paths and both Wasmtime profiles must use the shared wasmparser validators" ); let publish = std::fs::read_to_string(root.join(".github/workflows/publish.yaml")) @@ -366,8 +367,10 @@ fn phase_two_keeps_wasmtime_scoped_to_the_standalone_wasm_adapter() { && publish.contains("runner: macos-15-intel") && publish.contains("runner: macos-15") && publish.contains("cargo test -p agentos-execution wasmtime --lib") - && publish.contains("needs.wasmtime-darwin-smoke.result == 'success'"), - "all four release platforms must compile and natively smoke the reviewed Wasmtime embedding" + && publish.contains("smoke-sidecar-artifacts:") + && publish.contains("scripts/ci/smoke-packed-wasm-backends.mjs") + && publish.contains("needs.smoke-sidecar-artifacts.result == 'success'"), + "all four release platforms must compile Wasmtime and natively smoke the actual packaged artifact" ); let mut manifests = Vec::new(); @@ -412,6 +415,126 @@ fn phase_two_keeps_wasmtime_scoped_to_the_standalone_wasm_adapter() { } } +#[test] +fn owned_toolchain_pins_binaryen_for_finalized_wasm_exceptions() { + let root = repo_root(); + let toolchain = + std::fs::read_to_string(root.join("toolchain/Makefile")).expect("read toolchain Makefile"); + let c_toolchain = std::fs::read_to_string(root.join("toolchain/c/Makefile")) + .expect("read C toolchain Makefile"); + let installer = std::fs::read_to_string(root.join("toolchain/scripts/ensure-wasm-opt.sh")) + .expect("read pinned Binaryen installer"); + let duckdb = std::fs::read_to_string(root.join("toolchain/c/scripts/build-duckdb.sh")) + .expect("read DuckDB build script"); + + assert!( + toolchain.contains("BINARYEN_VERSION := 128") + && toolchain.contains("./scripts/ensure-wasm-opt.sh \"$(WASM_OPT)\"") + && !toolchain.contains("\tcargo install wasm-opt") + && c_toolchain.contains("BINARYEN_VERSION := 128") + && c_toolchain.contains("../scripts/ensure-wasm-opt.sh \"$(WASM_OPT)\"") + && c_toolchain.contains("include/sys/ioctl.h wasm-opt-check"), + "all canonical command builds must use the pinned Binaryen tool" + ); + for required in [ + "BINARYEN_VERSION=128", + "--translate-to-exnref", + "binaryen-version_${BINARYEN_VERSION}-${PLATFORM}.tar.gz", + "4ce79586d1c4762502eebe9a1db071fa5e446ef8897f2f766eb1cce5ec6dee9e", + "bafe0468976d923f09052f8ec6a6a0a9d942ee7f02ac113c85a80afea7ba3679", + "0b4bbd58c46b73a3de1fd485579a56cd413dd395414306d9f33df407fde58b9b", + "0ef730ecedf2dac894812185fc78f5940ab980cdde79427e49fa87331d24422f", + ] { + assert!( + installer.contains(required), + "pinned Binaryen installer omitted {required}" + ); + } + assert!( + duckdb.contains("Binaryen 128 is required") && duckdb.contains("--translate-to-exnref"), + "DuckDB must reject a toolchain that cannot finalize exception references" + ); +} + +#[test] +fn maintained_wasm_surfaces_are_mechanically_gated_on_both_backends() { + let root = repo_root(); + let ci = + std::fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read CI workflow"); + for required in [ + "backend: [v8, wasmtime]", + "AGENTOS_TEST_WASM_BACKEND: ${{ matrix.backend }}", + "cargo test --release -p agentos-native-sidecar --tests -- --test-threads=1", + "toolchain/conformance/c-parity.test.ts", + "wasm-c-parity-fixtures", + "packages/runtime-core exec vitest run", + "packages/core exec vitest run", + "cargo test -p agentos-client -- --test-threads=1", + "turbo test --concurrency=1 --filter='@agentos-software/*'", + "@rivet-dev/agentos test:e2e:run", + "pthread-conformance-wasm pthread-benchmark-wasm", + "owned_pthread_libc_mutex_cond_tls_join_detach_and_cancel_conform", + "test:wasm-mixed-smoke", + "AGENT_OS_CLIENT_ALLOW_E2E_SKIPS: '0'", + ] { + assert!( + ci.contains(required), + "CI must mechanically require the dual-backend gate: {required}" + ); + } + + let turbo = std::fs::read_to_string(root.join("turbo.json")).expect("read Turbo configuration"); + assert!( + turbo.contains("\"AGENTOS_TEST_WASM_BACKEND\""), + "Turbo must pass and hash the backend selector for registry software tests" + ); + + let publish = std::fs::read_to_string(root.join(".github/workflows/publish.yaml")) + .expect("read publish workflow"); + let justfile = std::fs::read_to_string(root.join("justfile")).expect("read justfile"); + assert!( + publish.contains("make -C toolchain cmd/duckdb cmd/vim") + && publish.contains("smoke-packed-wasm-backends.mjs") + && justfile.contains("make -C toolchain cmd/duckdb cmd/vim"), + "publish must build the complete parity-tested command corpus before packaged backend smokes" + ); + + let xfstests = std::fs::read_to_string(root.join("tests/xfstests/Makefile")) + .expect("read xfstests Makefile"); + assert!( + xfstests.contains("XFSTESTS_WASM_BACKENDS ?= v8 wasmtime") + && xfstests.contains("AGENTOS_TEST_WASM_BACKEND=\"$$wasm_backend\""), + "xfstests must execute its WASM helper corpus through V8 and Wasmtime" + ); + + let nightly = std::fs::read_to_string(root.join(".github/workflows/ci-nightly.yml")) + .expect("read nightly CI workflow"); + for required in [ + "xfstests_wasi_dirstress_process_matrix", + "if: matrix.storage_backend == 'chunked_local'", + "AGENTOS_TEST_WASM_BACKEND: ${{ matrix.wasm_backend }}", + ] { + assert!( + nightly.contains(required), + "nightly CI must retain the dual-engine directory stress gate: {required}" + ); + } + + let packaged = std::fs::read_to_string(root.join("scripts/ci/smoke-packed-wasm-backends.mjs")) + .expect("read packaged WASM backend smoke"); + for required in [ + "[\"v8\", \"wasmtime\", \"wasmtime-threads\"]", + "pthread-conformance.wasm", + "pack(runtimeSidecarDir)", + "--platform-package", + ] { + assert!( + packaged.contains(required), + "packaged artifacts must exercise the public backend surface: {required}" + ); + } +} + #[test] fn kernel_process_table_is_the_only_durable_signal_state_owner() { let root = repo_root(); @@ -419,17 +542,26 @@ fn kernel_process_table_is_the_only_durable_signal_state_owner() { .expect("read kernel process table"); let record = rust_braced_item(&process_table, "struct ProcessRecord {"); for field in [ - "blocked_signals: SignalSet", "pending_signals: SignalSet", "signal_actions: [SignalAction", - "signal_deliveries: Vec", - "temporary_signal_masks: Vec", + "signal_threads: BTreeMap", ] { assert!( record.contains(field), "kernel ProcessRecord is missing {field}" ); } + let thread_record = rust_braced_item(&process_table, "struct ProcessThreadSignalState {"); + for field in [ + "blocked_signals: SignalSet", + "signal_deliveries: Vec", + "temporary_signal_masks: Vec", + ] { + assert!( + thread_record.contains(field), + "kernel thread signal record is missing {field}" + ); + } let schedule = rust_braced_item(&process_table, "fn queue_or_schedule_signal("); assert!( schedule.contains("record.pending_signals.insert(signal)?") @@ -1347,6 +1479,10 @@ const PROCESS_ALLOW: &[&str] = &[ // (SNAPSHOT_HELPER_ENV) so snapshot creation runs in a clean process. // Host-side bootstrap only; no guest-controlled input picks the program. "crates/v8-runtime/src/snapshot.rs", + // Explicitly threaded WASM is isolated in a re-exec of the reviewed + // native-sidecar binary. The parent keeps every kernel capability and the + // child receives only bounded typed host-operation IPC. + "crates/execution/src/wasm/wasmtime/worker.rs", ]; /// env: process-environment reads. @@ -1368,6 +1504,9 @@ const ENV_ALLOW: &[&str] = &[ // Host-side perf phase diagnostics toggles, read from operator env and not // guest-reachable. "crates/execution/src/javascript.rs", + // Operator/test override for the reviewed native-sidecar worker binary; + // guest input cannot select or mutate this path. + "crates/execution/src/wasm/wasmtime/worker.rs", "crates/native-sidecar/src/filesystem.rs", "crates/v8-runtime/src/bridge.rs", "crates/native-sidecar/src/execution/", @@ -1545,7 +1684,7 @@ fn production_execution_lifecycle_is_runtime_neutral_and_delegated() { && compact.contains("self.backend_mut().write_stdin(bytes)") && compact.contains("self.backend_mut().close_stdin()") && compact.contains( - "self.backend().deliver_signal_checkpoint(identity,signal,delivery_token,flags)" + "self.backend().deliver_signal_checkpoint(identity,signal,delivery_token,flags,thread_id)" ), "ActiveExecution must delegate every common lifecycle method through ExecutionBackend" ); @@ -1616,7 +1755,9 @@ fn production_execution_lifecycle_is_runtime_neutral_and_delegated() { .collect(); assert!( wasm_backend - .matches("execution.deliver_signal_checkpoint(identity,signal,delivery_token,flags)") + .matches( + "execution.deliver_signal_checkpoint(identity,signal,delivery_token,flags,thread_id,)" + ) .count() >= 2 && wasm_backend.contains("WasmExecutionBackend::Wasmtime(execution)"), @@ -3098,6 +3239,31 @@ fn v8_platform_worker_pool_has_a_reviewed_fixed_bound() { ); } +#[test] +fn canonical_wasm_exceptions_use_one_finalized_artifact_on_both_engines() { + let root = repo_root(); + let v8 = std::fs::read_to_string(root.join("crates/v8-runtime/src/isolate.rs")) + .expect("read V8 platform source"); + assert!( + v8.contains("v8::V8::set_flags_from_string(\"--experimental-wasm-exnref\")"), + "V8 130 must enable the finalized exception encoding used by canonical C++ commands" + ); + + let profile = std::fs::read_to_string(root.join("crates/execution/src/wasm/profile.rs")) + .expect("read shared WASM profile"); + assert!(profile.contains("features.set(WasmFeatures::EXCEPTIONS, true)")); + assert!(profile.contains("features.set(WasmFeatures::LEGACY_EXCEPTIONS, false)")); + + let duckdb = std::fs::read_to_string(root.join("toolchain/c/scripts/build-duckdb.sh")) + .expect("read DuckDB toolchain script"); + assert!( + duckdb.contains("--translate-to-exnref") + && duckdb + .contains("Binaryen 128 is required to finalize DuckDB exception instructions"), + "the owned toolchain must translate LLVM 19's legacy exceptions before staging DuckDB" + ); +} + #[test] fn production_threads_match_the_reviewed_topology_manifest() { const MANIFEST: &[(&str, &str)] = &[ @@ -3131,6 +3297,18 @@ fn production_threads_match_the_reviewed_topology_manifest() { "admitted-wasmtime-guest-executor", "crates/execution/src/wasm/wasmtime/lifecycle.rs", ), + ( + "admitted-threaded-wasmtime-guest", + "crates/execution/src/wasm/wasmtime/threads.rs", + ), + ( + "threaded-wasmtime-ipc-writer", + "crates/execution/src/wasm/wasmtime/worker.rs", + ), + ( + "threaded-wasmtime-ipc-reader", + "crates/execution/src/wasm/wasmtime/worker.rs", + ), ( "constant-stdio-writer", "crates/native-sidecar/src/stdio.rs", diff --git a/crates/native-sidecar/tests/fixtures/limits-inventory.json b/crates/native-sidecar/tests/fixtures/limits-inventory.json index 71b479d8f9..6d51428680 100644 --- a/crates/native-sidecar/tests/fixtures/limits-inventory.json +++ b/crates/native-sidecar/tests/fixtures/limits-inventory.json @@ -2617,5 +2617,56 @@ "class": "policy", "rationale": "Default aggregate linear-memory envelope for active standalone-WASM Stores.", "wired": "RuntimeConfig.resources.max_wasm_memory_bytes" + }, + { + "name": "DEFAULT_MAX_WORKER_FRAME_BYTES", + "path": "crates/execution/src/wasm/wasmtime/worker.rs", + "class": "policy-deferred", + "rationale": "Bounded default for the private threaded-WASM worker transport; expose through process runtime configuration if larger configured host replies require it." + }, + { + "name": "MAX_STARTUP_HEADER_BYTES", + "path": "crates/execution/src/wasm/wasmtime/worker.rs", + "class": "invariant", + "rationale": "Private worker startup protocol ceiling above the separately bounded argv, environment, and typed request fields." + }, + { + "name": "MAX_WORKER_FRAME_BYTES", + "path": "crates/execution/src/wasm/wasmtime/worker.rs", + "class": "policy-deferred", + "rationale": "Hard safety ceiling for one private threaded-WASM worker frame; expose through process runtime configuration if host-operation budgets grow beyond it." + }, + { + "name": "MAX_WASI_THREAD_ID", + "path": "crates/execution/src/wasm/wasmtime/threads.rs", + "class": "invariant", + "rationale": "Positive signed thread-id range reserved by the owned WASI-threads ABI." + }, + { + "name": "MAX_SIGNAL_THREADS_PER_PROCESS", + "path": "crates/kernel/src/process_table.rs", + "class": "policy-deferred", + "rationale": "Defense-in-depth bound on per-process kernel signal-thread records above current VM thread admission; expose through kernel/process limits if supported thread counts grow." + }, + { + "name": "DEFAULT_WASM_MAX_CONCURRENT_THREADS", + "path": "crates/native-sidecar-core/src/limits.rs", + "class": "policy", + "rationale": "Default aggregate per-VM threaded-WASM admission bound.", + "wired": "VmLimits.wasm.max_concurrent_threads" + }, + { + "name": "DEFAULT_WASM_MAX_THREADS", + "path": "crates/native-sidecar-core/src/limits.rs", + "class": "policy", + "rationale": "Default maximum thread count for one explicitly threaded standalone-WASM group.", + "wired": "VmLimits.wasm.max_threads" + }, + { + "name": "DEFAULT_MAX_PROCESS_WASM_THREADS", + "path": "crates/runtime/src/lib.rs", + "class": "policy", + "rationale": "Default aggregate sidecar-process threaded-WASM admission bound.", + "wired": "RuntimeConfig.resources.max_wasm_threads" } ] diff --git a/crates/native-sidecar/tests/guest_identity.rs b/crates/native-sidecar/tests/guest_identity.rs index db7d7feb7a..149ce42874 100644 --- a/crates/native-sidecar/tests/guest_identity.rs +++ b/crates/native-sidecar/tests/guest_identity.rs @@ -1,9 +1,8 @@ mod support; use agentos_native_sidecar::wire::{ - CreateVmRequest, GuestRuntimeKind, RequestId, RequestPayload, ResponsePayload, - RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryEncoding, - RootFilesystemEntryKind, RootFilesystemMode, + GuestRuntimeKind, RequestId, RequestPayload, ResponsePayload, RootFilesystemDescriptor, + RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemEntryKind, RootFilesystemMode, }; use base64::Engine as _; use serde_json::Value; @@ -13,8 +12,9 @@ use std::process::Command; use std::time::Duration; use support::{ assert_node_available, authenticate_wire, collect_process_output_wire_with_timeout, - create_vm_wire, dispose_vm_and_close_session, execute_wire, new_sidecar, open_session_wire, - temp_dir, wire_permissions_allow_all, wire_request, wire_session, + create_vm_request_with_selected_wasm_backend, create_vm_wire, dispose_vm_and_close_session, + execute_wire, new_sidecar, open_session_wire, temp_dir, wire_permissions_allow_all, + wire_request, wire_session, }; const DEFAULT_GUEST_PATH_ENV: &str = @@ -67,7 +67,7 @@ fn create_vm_with_root_filesystem_and_metadata( .dispatch_wire_blocking(wire_request( request_id, wire_session(connection_id, session_id), - RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config( + RequestPayload::CreateVmRequest(create_vm_request_with_selected_wasm_backend( runtime, metadata, root_filesystem, diff --git a/crates/native-sidecar/tests/limits.rs b/crates/native-sidecar/tests/limits.rs index 5a591e7f6a..72a415f92f 100644 --- a/crates/native-sidecar/tests/limits.rs +++ b/crates/native-sidecar/tests/limits.rs @@ -35,6 +35,8 @@ fn defaults_match_struct_default() { "WASM wall-clock cutoff must remain opt-in" ); assert_eq!(parsed.wasm.deterministic_fuel, None); + assert_eq!(parsed.wasm.max_threads, 16); + assert_eq!(parsed.wasm.max_concurrent_threads, 64); } #[test] @@ -49,6 +51,8 @@ fn overrides_only_present_keys() { active_cpu_time_limit_ms: Some(90_000), wall_clock_limit_ms: Some(120_000), deterministic_fuel: Some(1_000_000), + max_threads: Some(8), + max_concurrent_threads: Some(24), ..Default::default() }), js_runtime: Some(JsRuntimeLimitsConfig { @@ -71,6 +75,8 @@ fn overrides_only_present_keys() { assert_eq!(parsed.wasm.active_cpu_time_limit_ms, 90_000); assert_eq!(parsed.wasm.wall_clock_limit_ms, Some(120_000)); assert_eq!(parsed.wasm.deterministic_fuel, Some(1_000_000)); + assert_eq!(parsed.wasm.max_threads, 8); + assert_eq!(parsed.wasm.max_concurrent_threads, 24); assert_eq!(parsed.js_runtime.v8_heap_limit_mb, Some(256)); assert_eq!(parsed.python.execution_timeout_ms, 1000); assert_eq!(parsed.http.max_fetch_response_bytes, 65536); @@ -151,3 +157,19 @@ fn rejects_zero_buffer_cap() { vm_limits_from_config(Some(&config), SIDECAR_FRAME_CAP).expect_err("zero buffer cap"); assert!(error.to_string().contains("captured_output_limit_bytes")); } + +#[test] +fn rejects_per_group_threads_above_vm_concurrent_threads() { + let config = VmLimitsConfig { + wasm: Some(WasmLimitsConfig { + max_threads: Some(5), + max_concurrent_threads: Some(4), + ..Default::default() + }), + ..Default::default() + }; + let error = vm_limits_from_config(Some(&config), SIDECAR_FRAME_CAP) + .expect_err("per-group thread maximum above VM aggregate rejected"); + assert!(error.to_string().contains("max_threads")); + assert!(error.to_string().contains("max_concurrent_threads")); +} diff --git a/crates/native-sidecar/tests/limits_audit.rs b/crates/native-sidecar/tests/limits_audit.rs index 7e3fa9735f..576ec561f0 100644 --- a/crates/native-sidecar/tests/limits_audit.rs +++ b/crates/native-sidecar/tests/limits_audit.rs @@ -4,7 +4,7 @@ //! with instructions, so operator-tunable bounds cannot silently accumulate as hardcoded values. //! //! This is a pure filesystem test: no VM, no V8, no new dependencies (`serde_json` is already a -//! sidecar dependency). The match rule is hand-rolled string checks, asserted by its own unit +//! native-sidecar dependency). The match rule is hand-rolled string checks, asserted by its own unit //! cases below. use std::collections::BTreeSet; @@ -19,13 +19,13 @@ struct ScannedConst { path: String, } -/// Resolve the workspace root from `CARGO_MANIFEST_DIR` (which points at `crates/sidecar`). +/// Resolve the workspace root from `CARGO_MANIFEST_DIR` (which points at `crates/native-sidecar`). fn workspace_root() -> PathBuf { let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); manifest .parent() .and_then(Path::parent) - .expect("crates/sidecar has a workspace root two levels up") + .expect("crates/native-sidecar has a workspace root two levels up") .to_path_buf() } @@ -289,7 +289,7 @@ fn limit_constants_are_classified() { failures.push(format!( "unclassified limit constant {} in {}: wire it through a typed configuration field and mark it \ \"policy\", or add an \"invariant\"/\"policy-deferred\" entry to \ - crates/sidecar/tests/fixtures/limits-inventory.json with a one-line rationale", + crates/native-sidecar/tests/fixtures/limits-inventory.json with a one-line rationale", c.name, c.path )); } diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index abfadea74e..ad8aa795db 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -108,7 +108,7 @@ mod service { RequestFrame, RequestPayload, ResponsePayload, RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemEntryKind, SessionOpenedResponse, SidecarPlacement, SidecarPlacementShared, SidecarRequestFrame, SidecarRequestPayload, - SidecarResponsePayload, WriteStdinRequest, + SidecarResponsePayload, StandaloneWasmBackend, WriteStdinRequest, }; use crate::state::VmDnsConfig; use crate::state::{ @@ -385,7 +385,7 @@ mod service { ServerConnection, SignatureScheme, }; use serde_json::{json, Value}; - use std::collections::BTreeMap; + use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io::{BufReader, Read, Write}; use std::net::{SocketAddr, TcpListener, UdpSocket}; @@ -1646,6 +1646,66 @@ ykAheWCsAteSEWVc0w==\n\ )); } + fn runtime_fault_pump_publishes_exit_and_cleans_up_process() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm_with_metadata( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + BTreeMap::new(), + ) + .expect("create vm"); + let process_id = String::from("proc-runtime-fault"); + let process = spawn_vm_wasm_binding_process(&mut sidecar, &vm_id); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .insert(process_id.clone(), process); + + let fault = agentos_execution::backend::ExecutionEvent::runtime_fault( + HostServiceError::new("ERR_AGENTOS_TEST_RUNTIME_FAULT", "test runtime fault"), + &agentos_execution::backend::PayloadLimit::new("test.maxRuntimeFaultBytes", 4096) + .expect("runtime fault limit"), + ) + .expect("bounded runtime fault"); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .get_mut(&process_id) + .expect("test process") + .queue_pending_execution_event(ActiveExecutionEvent::Common(fault)) + .expect("queue runtime fault"); + + let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id); + let frame = block_on_sidecar!( + sidecar, + sidecar.poll_event(&ownership, Duration::from_secs(1)) + ) + .expect("poll runtime fault exit") + .expect("runtime fault must publish an exit frame"); + let EventPayload::ProcessExited(exit) = frame.payload else { + panic!("runtime fault must publish ProcessExited"); + }; + assert_eq!(exit.process_id, process_id); + assert_eq!(exit.exit_code, 1); + assert!( + !sidecar + .vms + .get(&vm_id) + .expect("test vm") + .active_processes + .contains_key(&process_id), + "runtime fault cleanup must remove the active process" + ); + } + fn assert_handle_limit_error(error: SidecarError) { assert!( error.to_string().contains("handle limit exceeded"), @@ -2403,15 +2463,27 @@ ykAheWCsAteSEWVc0w==\n\ permissions: PermissionsPolicy, metadata: BTreeMap, ) -> Result { + let legacy_request = CreateVmRequest::legacy_test_config( + GuestRuntimeKind::JavaScript, + metadata.into_iter().collect(), + Default::default(), + Some(permissions), + ); + let mut config: agentos_vm_config::CreateVmConfig = + serde_json::from_str(&legacy_request.config).expect("decode test VM config"); + config.wasm_backend = match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { + Ok("v8") => Some(agentos_vm_config::StandaloneWasmBackend::V8), + Ok("wasmtime") => Some(agentos_vm_config::StandaloneWasmBackend::Wasmtime), + Ok(value) => panic!("unknown AGENTOS_TEST_WASM_BACKEND value {value:?}"), + Err(_) => None, + }; let response = sidecar .dispatch_blocking(request( 3, OwnershipScope::session(connection_id, session_id), - RequestPayload::CreateVm(CreateVmRequest::legacy_test_config( + RequestPayload::CreateVm(CreateVmRequest::json_config( GuestRuntimeKind::JavaScript, - metadata.into_iter().collect(), - Default::default(), - Some(permissions), + config, )), )) .expect("create vm"); @@ -2974,7 +3046,7 @@ console.log(JSON.stringify({ status: "ok", summary })); outputs } - fn ledger_usage_snapshot(resources: &ResourceLedger) -> [usize; 30] { + fn ledger_usage_snapshot(resources: &ResourceLedger) -> [usize; ResourceClass::ALL.len()] { ResourceClass::ALL.map(|class| resources.usage(class).used) } @@ -3776,9 +3848,26 @@ console.log(JSON.stringify({ status: "ok", summary })); let mut stderr = Vec::new(); let mut exit_code = None; let deadline = Instant::now() + Duration::from_secs(30); - let mut events_drained = 0; - while events_drained < 10_000 && Instant::now() < deadline { - events_drained += 1; + let mut events_drained: usize = 0; + let mut matched_envelopes = 0; + let mut empty_polls = 0; + let mut common_events = 0; + let mut common_host_calls = 0; + let mut common_outputs = 0; + let mut common_warnings = 0; + let mut common_faults = 0; + let mut common_exits = 0; + let mut rpc_events = 0; + let mut completion_events = 0; + let mut read_rechecks = 0; + let mut udp_rechecks = 0; + let mut signal_events = 0; + // Wasmtime exposes each owned ABI call as an individual common + // event, so a syscall-heavy command can legitimately exceed the + // old 10,000-iteration V8-oriented guard. The wall-clock deadline + // is the authoritative bound for this synchronous test helper. + while Instant::now() < deadline { + events_drained = events_drained.saturating_add(1); pump_sibling_internal_process_events(sidecar, vm_id, process_id); block_on_sidecar!(sidecar, sidecar.pump_child_process_events(vm_id)) .expect("pump attached child process events"); @@ -3786,6 +3875,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .take_matching_process_event_envelope(vm_id, process_id) .expect("drain queued sidecar process event") { + matched_envelopes += 1; block_on_sidecar!(sidecar, sidecar.handle_process_event_envelope(envelope)) .expect("handle queued sidecar process event"); continue; @@ -3805,6 +3895,7 @@ console.log(JSON.stringify({ status: "ok", summary })); if exit_code.is_some() { break; } + empty_polls += 1; continue; }; @@ -3818,12 +3909,32 @@ console.log(JSON.stringify({ status: "ok", summary })); ActiveExecutionEvent::Exited(code) => { exit_code = Some(*code); } - ActiveExecutionEvent::Common(_) - | ActiveExecutionEvent::HostRpcRequest(_) - | ActiveExecutionEvent::HostCallCompletion(_) - | ActiveExecutionEvent::ManagedStreamReadRecheck(_) - | ActiveExecutionEvent::ManagedUdpPollRecheck(_) - | ActiveExecutionEvent::SignalState { .. } => {} + ActiveExecutionEvent::Common(event) => { + common_events += 1; + match event { + agentos_execution::backend::ExecutionEvent::HostCall { .. } => { + common_host_calls += 1; + } + agentos_execution::backend::ExecutionEvent::Output { .. } => { + common_outputs += 1; + } + agentos_execution::backend::ExecutionEvent::Warning(_) => { + common_warnings += 1; + } + agentos_execution::backend::ExecutionEvent::RuntimeFault(_) => { + common_faults += 1; + } + agentos_execution::backend::ExecutionEvent::Exited(_) => { + common_exits += 1; + } + _ => {} + } + } + ActiveExecutionEvent::HostRpcRequest(_) => rpc_events += 1, + ActiveExecutionEvent::HostCallCompletion(_) => completion_events += 1, + ActiveExecutionEvent::ManagedStreamReadRecheck(_) => read_rechecks += 1, + ActiveExecutionEvent::ManagedUdpPollRecheck(_) => udp_rechecks += 1, + ActiveExecutionEvent::SignalState { .. } => signal_events += 1, } block_on_sidecar!( @@ -3836,6 +3947,17 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("pump attached child process events"); } + if exit_code.is_none() { + let active_processes = sidecar + .vms + .get(vm_id) + .map(|vm| vm.active_processes.keys().cloned().collect::>()) + .unwrap_or_default(); + eprintln!( + "TEST_PROCESS_DRAIN_TIMEOUT: process={process_id} loops={events_drained} matched_envelopes={matched_envelopes} empty_polls={empty_polls} common_events={common_events} common_host_calls={common_host_calls} common_outputs={common_outputs} common_warnings={common_warnings} common_faults={common_faults} common_exits={common_exits} rpc_events={rpc_events} completion_events={completion_events} read_rechecks={read_rechecks} udp_rechecks={udp_rechecks} signal_events={signal_events} active_processes={active_processes:?}" + ); + } + ( process_stream_to_string(&stdout), process_stream_to_string(&stderr), @@ -10483,6 +10605,118 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect_err("read should be denied"); assert_eq!(read_error.code(), "EACCES"); } + fn create_vm_stores_standalone_wasm_backend_policy() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let response = sidecar + .dispatch_blocking(request( + 3, + OwnershipScope::session(&connection_id, &session_id), + RequestPayload::CreateVm(CreateVmRequest::json_config( + GuestRuntimeKind::JavaScript, + agentos_vm_config::CreateVmConfig { + wasm_backend: Some( + agentos_vm_config::StandaloneWasmBackend::WasmtimeThreads, + ), + ..Default::default() + }, + )), + )) + .expect("create VM with standalone-WASM policy"); + let vm_id = created_vm_id(response).expect("VM created"); + assert_eq!( + sidecar + .vms + .get(&vm_id) + .expect("created VM") + .standalone_wasm_backend, + agentos_execution::StandaloneWasmBackend::WasmtimeThreads + ); + } + fn vm_default_and_process_override_select_standalone_wasm_backend() { + let cwd = temp_dir("agentos-native-sidecar-vm-wasm-backend-policy"); + let module_path = cwd.join("guest.wasm"); + write_fixture( + &module_path, + wat::parse_str("(module (func (export \"_start\")))") + .expect("compile backend policy fixture"), + ); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let legacy = CreateVmRequest::legacy_test_config( + GuestRuntimeKind::WebAssembly, + HashMap::from([(String::from("cwd"), cwd.to_string_lossy().into_owned())]), + Default::default(), + Some(PermissionsPolicy::allow_all()), + ); + let mut config: agentos_vm_config::CreateVmConfig = + serde_json::from_str(&legacy.config).expect("decode VM config"); + config.wasm_backend = Some(agentos_vm_config::StandaloneWasmBackend::Wasmtime); + let created = sidecar + .dispatch_blocking(request( + 3, + OwnershipScope::session(&connection_id, &session_id), + RequestPayload::CreateVm(CreateVmRequest::json_config( + GuestRuntimeKind::WebAssembly, + config, + )), + )) + .expect("create Wasmtime-default VM"); + let vm_id = created_vm_id(created).expect("VM created"); + + for (request_id, process_id, override_backend, expected_backend) in [ + ( + 4, + "vm-default", + None, + agentos_execution::StandaloneWasmBackend::Wasmtime, + ), + ( + 5, + "process-override", + Some(StandaloneWasmBackend::V8), + agentos_execution::StandaloneWasmBackend::V8, + ), + ] { + let started = sidecar + .dispatch_blocking(request( + request_id, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::Execute(crate::protocol::ExecuteRequest { + process_id: process_id.to_owned(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(module_path.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: None, + wasm_backend: override_backend, + }), + )) + .expect("start backend policy fixture"); + assert!(matches!( + started.response.payload, + ResponsePayload::ProcessStarted(_) + )); + assert_eq!( + sidecar + .vms + .get(&vm_id) + .expect("VM") + .active_processes + .get(process_id) + .expect("active process") + .standalone_wasm_backend, + expected_backend + ); + let (_, stderr, exit_code) = drain_process_output(&mut sidecar, &vm_id, process_id); + assert_eq!(exit_code, Some(0), "stderr: {stderr}"); + } + } fn create_vm_without_permissions_defaults_to_static_deny_all() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = @@ -25722,6 +25956,7 @@ try { service_case!(bridge_permissions_map_symlink_operations_to_symlink_access); service_case!(vm_limits_config_reads_filesystem_limits); service_case!(create_vm_applies_filesystem_permission_descriptors_to_kernel_access); + service_case!(create_vm_stores_standalone_wasm_backend_policy); service_case!(create_vm_without_permissions_defaults_to_static_deny_all); service_case!(configure_vm_rollback_restore_failure_falls_back_to_static_deny_all); service_case!(binding_registration_rollback_restore_failure_keeps_registry_consistent); @@ -25852,6 +26087,7 @@ try { descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes ); service_case!(exit_trailing_requeue_preserves_exit_when_queue_is_full); + service_case!(runtime_fault_pump_publishes_exit_and_cleans_up_process); service_case!( javascript_child_process_poll_reports_echild_when_child_disappears_after_drain ); @@ -25896,6 +26132,7 @@ try { descendant_transfer_overflow_preserves_global_queue(); descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes(); exit_trailing_requeue_preserves_exit_when_queue_is_full(); + runtime_fault_pump_publishes_exit_and_cleans_up_process(); } #[test] @@ -25961,6 +26198,16 @@ try { javascript_sqlite_builtin_round_trips_through_sidecar_sync_rpc(); } + #[test] + fn create_vm_standalone_wasm_backend_policy_regression() { + create_vm_stores_standalone_wasm_backend_policy(); + } + + #[test] + fn vm_default_and_process_override_wasm_backend_regression() { + vm_default_and_process_override_select_standalone_wasm_backend(); + } + #[test] fn aad_javascript_network_dns_javascript_net_poll_suite() { run_service_suite(); diff --git a/crates/native-sidecar/tests/support/mod.rs b/crates/native-sidecar/tests/support/mod.rs index d7071f422b..81416096ab 100644 --- a/crates/native-sidecar/tests/support/mod.rs +++ b/crates/native-sidecar/tests/support/mod.rs @@ -305,23 +305,23 @@ pub fn create_vm_wire_with_metadata( .entry(String::from("cwd")) .or_insert_with(|| cwd.to_string_lossy().into_owned()); + let request = create_vm_request_with_selected_wasm_backend( + runtime.clone(), + metadata, + agentos_native_sidecar::wire::RootFilesystemDescriptor { + mode: agentos_native_sidecar::wire::RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: Vec::new(), + }, + Some(wire_permissions_allow_all()), + ); + let result = sidecar .dispatch_wire_blocking(wire_request( request_id, wire_session(connection_id, session_id), - agentos_native_sidecar::wire::RequestPayload::CreateVmRequest( - agentos_native_sidecar::wire::CreateVmRequest::legacy_test_config( - runtime, - metadata, - agentos_native_sidecar::wire::RootFilesystemDescriptor { - mode: agentos_native_sidecar::wire::RootFilesystemMode::Ephemeral, - disable_default_base_layer: false, - lowers: Vec::new(), - bootstrap_entries: Vec::new(), - }, - Some(wire_permissions_allow_all()), - ), - ), + agentos_native_sidecar::wire::RequestPayload::CreateVmRequest(request), )) .expect("create sidecar VM through wire"); @@ -334,6 +334,29 @@ pub fn create_vm_wire_with_metadata( (vm_id, result) } +pub fn create_vm_request_with_selected_wasm_backend( + runtime: agentos_native_sidecar::wire::GuestRuntimeKind, + metadata: HashMap, + root_filesystem: agentos_native_sidecar::wire::RootFilesystemDescriptor, + permissions: Option, +) -> agentos_native_sidecar::wire::CreateVmRequest { + let legacy_request = agentos_native_sidecar::wire::CreateVmRequest::legacy_test_config( + runtime.clone(), + metadata, + root_filesystem, + permissions, + ); + let mut config: agentos_vm_config::CreateVmConfig = + serde_json::from_str(&legacy_request.config).expect("decode test VM config"); + config.wasm_backend = match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { + Ok("v8") => Some(agentos_vm_config::StandaloneWasmBackend::V8), + Ok("wasmtime") => Some(agentos_vm_config::StandaloneWasmBackend::Wasmtime), + Ok(value) => panic!("unknown AGENTOS_TEST_WASM_BACKEND value {value:?}"), + Err(_) => None, + }; + agentos_native_sidecar::wire::CreateVmRequest::json_config(runtime, config) +} + #[allow(clippy::too_many_arguments)] pub fn execute_wire( sidecar: &mut NativeSidecar, @@ -346,15 +369,6 @@ pub fn execute_wire( entrypoint: &Path, args: Vec, ) { - let wasm_backend = if runtime == agentos_native_sidecar::wire::GuestRuntimeKind::WebAssembly { - match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { - Ok("wasmtime") => Some(agentos_native_sidecar::wire::StandaloneWasmBackend::Wasmtime), - Ok("v8") | Err(_) => Some(agentos_native_sidecar::wire::StandaloneWasmBackend::V8), - Ok(value) => panic!("unknown AGENTOS_TEST_WASM_BACKEND value {value:?}"), - } - } else { - None - }; let result = sidecar .dispatch_wire_blocking(wire_request( request_id, @@ -369,7 +383,7 @@ pub fn execute_wire( env: HashMap::new(), cwd: None, wasm_permission_tier: None, - wasm_backend, + wasm_backend: None, }, ), )) diff --git a/crates/native-sidecar/tests/wasm_software_parity.rs b/crates/native-sidecar/tests/wasm_software_parity.rs index c6b6b83f8e..ea24e2667c 100644 --- a/crates/native-sidecar/tests/wasm_software_parity.rs +++ b/crates/native-sidecar/tests/wasm_software_parity.rs @@ -24,8 +24,13 @@ fn command_artifact(name: &str) -> PathBuf { if staged.is_file() { return staged; } - root.join("toolchain/target/wasm32-wasip1/release/commands") - .join(name) + let toolchain = root + .join("toolchain/target/wasm32-wasip1/release/commands") + .join(name); + if toolchain.is_file() { + return toolchain; + } + root.join("toolchain/c/build").join(name) } fn run_command( @@ -105,13 +110,18 @@ fn run_command_with_files_and_metadata( "fixture\n", ); for (index, extra) in extra_commands.iter().enumerate() { + let guest_path = if *extra == "sh" { + String::from("/bin/sh") + } else { + format!("/workspace/{extra}") + }; write_guest_binary( &mut sidecar, 10 + index as i64, &connection_id, &session_id, &vm_id, - &format!("/workspace/{extra}"), + &guest_path, &std::fs::read(command_artifact(extra)).expect("read generated child command"), ); } @@ -388,6 +398,36 @@ fn shell_pipeline_and_child_backend_affinity_match_v8_wasm() { assert_eq!(stdout, "beta\n"); } +#[test] +#[ignore = "requires the generated toolchain/c/build/exec_variants fixture"] +fn exec_variants_match_linux_behavior_on_both_wasm_backends() { + for (mode, marker) in [ + ("execle", "execle: ok"), + ("execvpe", "execvpe: ok"), + ("execve-shebang", "execve_shebang: ok"), + ("shell-fallback", "shell_fallback: ok"), + ("fexecve", "fexecve_unlinked_cloexec: ok"), + ("fexecve-script", "fexecve_script_unlinked: ok"), + ( + "fexecve-script-cloexec", + "fexecve_script_cloexec_enoent: ok", + ), + ] { + // Launch the fixture through the projected guest path. The trusted + // initial entrypoint is a host-only image source and intentionally is + // not mirrored into the kernel VFS, whereas a process that self-execs + // must name the live kernel-owned executable. + let script = format!("/workspace/exec_variants {mode}"); + let (stdout, stderr, exit_code) = + assert_command_parity_with_files("sh", &["-c", &script], &["exec_variants", "sh"]); + assert_eq!(exit_code, 0, "{mode} stderr: {stderr}"); + assert!( + stdout.contains(marker), + "{mode} output omitted {marker:?}: {stdout:?}" + ); + } +} + #[test] #[ignore = "requires the focused generated vim command artifact"] fn vim_batch_edit_matches_v8_wasm() { diff --git a/crates/native-sidecar/tests/wasmtime_safety.rs b/crates/native-sidecar/tests/wasmtime_safety.rs index 80b1a16105..73b4ba7270 100644 --- a/crates/native-sidecar/tests/wasmtime_safety.rs +++ b/crates/native-sidecar/tests/wasmtime_safety.rs @@ -4,12 +4,12 @@ use std::collections::HashMap; use std::time::Duration; use agentos_native_sidecar::wire::{ - ExecuteRequest, GuestRuntimeKind, KillProcessRequest, RequestPayload, ResponsePayload, - StandaloneWasmBackend, WasmPermissionTier, + DisposeReason, DisposeVmRequest, EventPayload, ExecuteRequest, GuestRuntimeKind, + KillProcessRequest, RequestPayload, ResponsePayload, StandaloneWasmBackend, WasmPermissionTier, }; use support::{ authenticate_wire, collect_process_output_wire_with_timeout, new_sidecar, open_session_wire, - temp_dir, wire_request, wire_vm, write_fixture, + temp_dir, wire_request, wire_session, wire_vm, write_fixture, }; fn run_wasmtime( @@ -26,6 +26,28 @@ fn run_wasmtime_with_tier( metadata: HashMap, permission_tier: WasmPermissionTier, ) -> (String, String, i32) { + run_wasm_backend_with_tier( + name, + module, + metadata, + permission_tier, + StandaloneWasmBackend::Wasmtime, + ) +} + +fn run_wasm_backend_with_tier( + name: &str, + module: &[u8], + metadata: HashMap, + permission_tier: WasmPermissionTier, + backend: StandaloneWasmBackend, +) -> (String, String, i32) { + if backend == StandaloneWasmBackend::WasmtimeThreads { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + } let mut sidecar = new_sidecar(name); let cwd = temp_dir(&format!("{name}-cwd")); let entrypoint = cwd.join("fixture.wasm"); @@ -55,7 +77,7 @@ fn run_wasmtime_with_tier( env: HashMap::new(), cwd: None, wasm_permission_tier: Some(permission_tier), - wasm_backend: Some(StandaloneWasmBackend::Wasmtime), + wasm_backend: Some(backend), }), )) .expect("start Wasmtime safety fixture"); @@ -73,6 +95,837 @@ fn run_wasmtime_with_tier( ) } +fn start_threaded_process( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + request_id: i64, + connection_id: &str, + session_id: &str, + vm_id: &str, + process_id: &str, + entrypoint: &std::path::Path, +) { + let started = sidecar + .dispatch_wire_blocking(wire_request( + request_id, + wire_vm(connection_id, session_id, vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.to_owned(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: Some(WasmPermissionTier::Full), + wasm_backend: Some(StandaloneWasmBackend::WasmtimeThreads), + }), + )) + .expect("start threaded Wasmtime process"); + assert!(matches!( + started.response.payload, + ResponsePayload::ProcessStartedResponse(_) + )); +} + +fn threaded_wait_forever_module() -> Vec { + wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))))"#, + ) + .expect("threaded wait-forever fixture") +} + +fn threaded_noop_module() -> Vec { + wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32)) + (func (export "_start")))"#, + ) + .expect("threaded no-op fixture") +} + +#[test] +fn threaded_backend_accepts_ordinary_single_thread_wasm_commands() { + let module = wat::parse_str( + r#"(module + (memory (export "memory") 1 2) + (func (export "_start")))"#, + ) + .expect("ordinary single-thread fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-ordinary-command", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn explicit_threaded_backend_shares_atomic_memory_across_stores() { + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (i32.atomic.rmw.add (i32.const 0) (i32.const 1))) + (drop (memory.atomic.notify (i32.const 4) (i32.const 1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 99)) (i32.const 1)) + (then unreachable)) + (loop $wait + (if (i32.lt_u (i32.atomic.load (i32.const 0)) (i32.const 1)) + (then + (drop (memory.atomic.wait32 + (i32.const 4) (i32.const 0) (i64.const -1))) + (br $wait))))))"#, + ) + .expect("threaded sidecar fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-atomic", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn process_signal_selects_the_unblocked_pthread_and_settles_its_handler() { + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (import "host_process" "proc_sigaction" + (func $sigaction (param i32 i32 i32 i32 i32) (result i32))) + (import "host_process" "proc_signal_mask_v2" + (func $sigmask (param i32 i32 i32 i32 i32) (result i32))) + (import "host_process" "proc_getpid" + (func $getpid (param i32) (result i32))) + (import "host_process" "proc_kill" + (func $kill (param i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "sched_yield" + (func $yield (result i32))) + (export "memory" (memory 0)) + (func (export "__wasi_signal_trampoline") (param i32) + (i32.atomic.store (i32.const 4) (i32.const 1)) + (drop (memory.atomic.notify (i32.const 4) (i32.const 1)))) + (func (export "wasi_thread_start") (param i32 i32) + (i32.atomic.store (i32.const 0) (i32.const 1)) + (drop (memory.atomic.notify (i32.const 0) (i32.const 1))) + (loop $dispatch + (drop (call $yield)) + (br_if $dispatch + (i32.eqz (i32.atomic.load (i32.const 4)))))) + (func (export "_start") + (local $pid i32) + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (loop $started + (if (i32.eqz (i32.atomic.load (i32.const 0))) + (then + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const 100000000))) + (br $started)))) + (if (call $sigaction + (i32.const 10) (i32.const 2) (i32.const 0) + (i32.const 0) (i32.const 0)) + (then unreachable)) + (if (call $sigmask + (i32.const 0) (i32.const 512) (i32.const 0) + (i32.const 104) (i32.const 108)) + (then unreachable)) + (if (call $getpid (i32.const 100)) (then unreachable)) + (local.set $pid (i32.load (i32.const 100))) + (if (call $kill (local.get $pid) (i32.const 10)) + (then unreachable)) + (loop $handled + (if (i32.eqz (i32.atomic.load (i32.const 4))) + (then + (drop (memory.atomic.wait32 + (i32.const 4) (i32.const 0) (i64.const 100000000))) + (br $handled))))))"#, + ) + .expect("pthread signal-selection fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-signal-selection", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn shared_memory_growth_and_cross_store_visibility_are_group_owned() { + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (if (i32.ne (memory.grow (i32.const 1)) (i32.const 1)) + (then unreachable)) + (i32.atomic.store (i32.const 4) (i32.const 1)) + (drop (memory.atomic.notify (i32.const 4) (i32.const 1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (loop $wait + (if (i32.eqz (i32.atomic.load (i32.const 4))) + (then + (drop (memory.atomic.wait32 + (i32.const 4) (i32.const 0) (i64.const 100000000))) + (br $wait)))) + (if (i32.ne (memory.size) (i32.const 2)) (then unreachable))))"#, + ) + .expect("shared-memory growth fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-memory-growth", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn thread_admission_fails_transactionally_at_the_configured_group_limit() { + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (loop $wait + (if (i32.eqz (i32.atomic.load (i32.const 0))) + (then + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const 100000000))) + (br $wait))))) + (func (export "_start") + (local $second i32) + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (local.set $second (call $spawn (i32.const 2))) + (i32.atomic.store (i32.const 0) (i32.const 1)) + (drop (memory.atomic.notify (i32.const 0) (i32.const 8))) + (if (i32.ge_s (local.get $second) (i32.const 0)) + (then unreachable))))"#, + ) + .expect("thread admission fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-admission", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn multiple_thread_groups_run_concurrently_inside_one_vm() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + let name = "wasmtime-threaded-multiple-groups-one-vm"; + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let parked_entrypoint = cwd.join("parked.wasm"); + let noop_entrypoint = cwd.join("noop.wasm"); + write_fixture(&parked_entrypoint, threaded_wait_forever_module()); + write_fixture(&noop_entrypoint, threaded_noop_module()); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-multi-group"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([ + (String::from("limits.wasm.max_threads"), String::from("2")), + ( + String::from("limits.wasm.max_concurrent_threads"), + String::from("4"), + ), + ]), + ); + let process_ids = ["process-thread-group-a", "process-thread-group-b"]; + for (index, process_id) in process_ids.iter().enumerate() { + start_threaded_process( + &mut sidecar, + 4 + index as i64, + &connection_id, + &session_id, + &vm_id, + process_id, + &parked_entrypoint, + ); + } + + // Both groups remain live only if their complete two-thread reservations + // fit concurrently in the distinct four-thread VM aggregate. + std::thread::sleep(Duration::from_millis(250)); + for (index, process_id) in process_ids.iter().enumerate() { + sidecar + .dispatch_wire_blocking(wire_request( + 10 + index as i64, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::KillProcessRequest(KillProcessRequest { + process_id: (*process_id).to_owned(), + signal: String::from("SIGKILL"), + }), + )) + .expect("kill concurrently admitted threaded group"); + let (_, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + process_id, + Duration::from_secs(5), + ); + assert_eq!(exit_code, 137, "{process_id}: stderr={stderr}"); + } + + start_threaded_process( + &mut sidecar, + 20, + &connection_id, + &session_id, + &vm_id, + "process-thread-group-after-release", + &noop_entrypoint, + ); + let (stdout, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-thread-group-after-release", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[cfg(unix)] +#[test] +fn malformed_and_crashed_thread_workers_fail_closed_and_release_the_vm() { + use std::os::unix::fs::PermissionsExt; + + let name = "wasmtime-threaded-worker-fault-isolation"; + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("noop.wasm"); + let fake_worker = cwd.join("fake-worker.sh"); + write_fixture(&entrypoint, threaded_noop_module()); + write_fixture( + &fake_worker, + b"#!/bin/sh\nprintf '\\377\\377\\377\\177'\nsleep 10\n", + ); + let mut permissions = std::fs::metadata(&fake_worker) + .expect("fake worker metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&fake_worker, permissions).expect("make fake worker executable"); + std::env::set_var("AGENTOS_WASMTIME_WORKER_PATH", &fake_worker); + + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-worker-fault"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([ + (String::from("limits.wasm.max_threads"), String::from("2")), + ( + String::from("limits.wasm.max_concurrent_threads"), + String::from("2"), + ), + ]), + ); + + start_threaded_process( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "process-malformed-worker", + &entrypoint, + ); + let (_, malformed_stderr, malformed_exit) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-malformed-worker", + Duration::from_secs(5), + ); + assert_ne!(malformed_exit, 0, "malformed worker must fail closed"); + assert!( + malformed_stderr.contains("ERR_AGENTOS_WASMTIME_WORKER_IPC_LIMIT"), + "missing typed malformed-frame error: {malformed_stderr}" + ); + + write_fixture(&fake_worker, b"#!/bin/sh\nexit 86\n"); + start_threaded_process( + &mut sidecar, + 5, + &connection_id, + &session_id, + &vm_id, + "process-crashed-worker", + &entrypoint, + ); + let (_, crashed_stderr, crashed_exit) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-crashed-worker", + Duration::from_secs(5), + ); + assert_ne!(crashed_exit, 0, "crashed worker must fail closed"); + assert!( + crashed_stderr.contains("ERR_AGENTOS_WASMTIME_WORKER_IPC_") + || crashed_stderr.contains("ERR_AGENTOS_WASMTIME_WORKER_WAIT"), + "missing typed crashed-worker error: {crashed_stderr}" + ); + + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + start_threaded_process( + &mut sidecar, + 6, + &connection_id, + &session_id, + &vm_id, + "process-after-worker-faults", + &entrypoint, + ); + let (stdout, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-after-worker-faults", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn atomic_race_stress_preserves_cross_thread_updates() { + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (local $remaining i32) + (local.set $remaining (i32.const 1000)) + (loop $add + (drop (i32.atomic.rmw.add (i32.const 0) (i32.const 1))) + (local.set $remaining + (i32.sub (local.get $remaining) (i32.const 1))) + (br_if $add (local.get $remaining))) + (drop (i32.atomic.rmw.add (i32.const 4) (i32.const 1))) + (drop (memory.atomic.notify (i32.const 4) (i32.const 1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) (then unreachable)) + (if (i32.lt_s (call $spawn (i32.const 2)) (i32.const 1)) (then unreachable)) + (if (i32.lt_s (call $spawn (i32.const 3)) (i32.const 1)) (then unreachable)) + (if (i32.lt_s (call $spawn (i32.const 4)) (i32.const 1)) (then unreachable)) + (loop $wait + (if (i32.lt_u (i32.atomic.load (i32.const 4)) (i32.const 4)) + (then + (drop (memory.atomic.wait32 + (i32.const 4) + (i32.atomic.load (i32.const 4)) + (i64.const 100000000))) + (br $wait)))) + (if (i32.ne (i32.atomic.load (i32.const 0)) (i32.const 4000)) + (then unreachable))))"#, + ) + .expect("atomic race fixture"); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-threaded-atomic-race", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("5"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +#[ignore = "requires `make -C toolchain/c pthread-conformance-wasm` generated artifact"] +fn owned_pthread_libc_mutex_cond_tls_join_detach_and_cancel_conform() { + let artifact = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../toolchain/c/build/pthread_conformance.wasm"); + let module = std::fs::read(&artifact) + .unwrap_or_else(|error| panic!("generated fixture {}: {error}", artifact.display())); + let (stdout, stderr, exit_code) = run_wasm_backend_with_tier( + "wasmtime-pthread-libc", + &module, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("8"))]), + WasmPermissionTier::Full, + StandaloneWasmBackend::WasmtimeThreads, + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); + assert!( + stdout.contains("pthread-ok"), + "stdout={stdout} stderr={stderr}" + ); +} + +#[test] +fn secondary_thread_trap_reaps_group_releases_admission_and_preserves_sidecar() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + let name = "wasmtime-threaded-trap-isolation"; + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) unreachable) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))))"#, + ) + .expect("secondary trap fixture"); + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let trap_entrypoint = cwd.join("trap.wasm"); + let noop_entrypoint = cwd.join("noop.wasm"); + write_fixture(&trap_entrypoint, module); + write_fixture(&noop_entrypoint, threaded_noop_module()); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-trap"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + ); + + let started = std::time::Instant::now(); + start_threaded_process( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "process-thread-trap", + &trap_entrypoint, + ); + let (_, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-thread-trap", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 1, "stderr={stderr}"); + assert!(stderr.contains("ERR_AGENTOS_WASM_TRAP"), "stderr={stderr}"); + assert!( + started.elapsed() < Duration::from_secs(2), + "secondary trap did not reap the complete worker group" + ); + + start_threaded_process( + &mut sidecar, + 5, + &connection_id, + &session_id, + &vm_id, + "process-after-thread-trap", + &noop_entrypoint, + ); + let (stdout, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-after-thread-trap", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); +} + +#[test] +fn process_exit_and_wall_timeout_reap_threads_parked_in_atomic_wait() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + let name = "wasmtime-threaded-exit-timeout"; + let exit_module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (call $exit (i32.const 23))))"#, + ) + .expect("threaded process-exit fixture"); + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let exit_entrypoint = cwd.join("exit.wasm"); + let timeout_entrypoint = cwd.join("timeout.wasm"); + write_fixture(&exit_entrypoint, exit_module); + write_fixture(&timeout_entrypoint, threaded_wait_forever_module()); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-exit"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([ + (String::from("limits.wasm.max_threads"), String::from("2")), + ( + String::from("limits.wasm.wall_clock_limit_ms"), + String::from("100"), + ), + ]), + ); + + let started = std::time::Instant::now(); + start_threaded_process( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "process-thread-exit", + &exit_entrypoint, + ); + let (_, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-thread-exit", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 23, "stderr={stderr}"); + assert!(started.elapsed() < Duration::from_secs(2)); + + let started = std::time::Instant::now(); + start_threaded_process( + &mut sidecar, + 5, + &connection_id, + &session_id, + &vm_id, + "process-thread-timeout", + &timeout_entrypoint, + ); + let (_, stderr, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "process-thread-timeout", + Duration::from_secs(5), + ); + assert_eq!(exit_code, 1, "stderr={stderr}"); + assert!( + stderr.contains("ERR_AGENTOS_WASMTIME_WALL_CLOCK_LIMIT"), + "stderr={stderr}" + ); + assert!(started.elapsed() < Duration::from_secs(2)); +} + +#[test] +fn vm_teardown_reaps_a_complete_atomic_wait_thread_group() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + let name = "wasmtime-threaded-vm-teardown"; + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("wait.wasm"); + write_fixture(&entrypoint, threaded_wait_forever_module()); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-dispose"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + ); + start_threaded_process( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "process-thread-dispose", + &entrypoint, + ); + std::thread::sleep(Duration::from_millis(200)); + let started = std::time::Instant::now(); + let disposed = sidecar + .dispatch_wire_blocking(wire_request( + 5, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::DisposeVmRequest(DisposeVmRequest { + reason: DisposeReason::Requested, + }), + )) + .expect("dispose VM with parked pthread group"); + assert!(matches!( + disposed.response.payload, + ResponsePayload::VmDisposedResponse(_) + )); + assert!(disposed.events.iter().any(|event| { + matches!( + &event.payload, + EventPayload::ProcessExitedEvent(exited) + if exited.process_id == "process-thread-dispose" + ) + })); + assert!( + started.elapsed() < Duration::from_secs(2), + "VM teardown exceeded the fixed threaded-worker reaping deadline" + ); +} + +#[test] +fn concurrent_thread_groups_preserve_memory_isolation_and_process_admission() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + const GROUPS: usize = 8; + let name = "wasmtime-threaded-high-concurrency"; + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (i32.atomic.rmw.add (i32.const 0) (i32.const 1))) + (drop (memory.atomic.notify (i32.const 0) (i32.const 2)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) (then unreachable)) + (if (i32.lt_s (call $spawn (i32.const 2)) (i32.const 1)) (then unreachable)) + (loop $wait + (if (i32.lt_u (i32.atomic.load (i32.const 0)) (i32.const 2)) + (then + (drop (memory.atomic.wait32 + (i32.const 0) + (i32.atomic.load (i32.const 0)) + (i64.const 100000000))) + (br $wait)))) + (if (i32.ne (i32.atomic.load (i32.const 0)) (i32.const 2)) + (then unreachable))))"#, + ) + .expect("high-concurrency pthread fixture"); + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("concurrent.wasm"); + write_fixture(&entrypoint, module); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-concurrency"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let mut processes = HashMap::new(); + for index in 0..GROUPS { + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 10 + index as i64, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("3"))]), + ); + let process_id = format!("process-thread-concurrent-{index}"); + start_threaded_process( + &mut sidecar, + 100 + index as i64, + &connection_id, + &session_id, + &vm_id, + &process_id, + &entrypoint, + ); + processes.insert(process_id, vm_id); + } + + let deadline = std::time::Instant::now() + Duration::from_secs(10); + let mut exits = HashMap::new(); + while exits.len() < GROUPS && std::time::Instant::now() < deadline { + let event = sidecar + .poll_event_wire_blocking( + &wire_session(&connection_id, &session_id), + Duration::from_millis(100), + ) + .expect("poll concurrent pthread events"); + if let Some(event) = event { + if let EventPayload::ProcessExitedEvent(exited) = event.payload { + if processes.contains_key(&exited.process_id) { + exits.insert(exited.process_id, exited.exit_code); + } + } + } + } + assert_eq!(exits.len(), GROUPS, "not every threaded group completed"); + assert!( + exits.values().all(|exit_code| *exit_code == 0), + "threaded group failures: {exits:?}" + ); +} + #[test] fn permission_filter_denies_ungranted_host_families_without_ambient_fallback() { let denied = wat::parse_str( @@ -303,3 +1156,85 @@ fn terminal_signal_interrupts_and_reaps_pure_guest_compute() { ); assert_eq!(exit_code, 137); } + +#[test] +fn threaded_atomic_wait_is_killed_and_reaped_before_the_fixed_deadline() { + std::env::set_var( + "AGENTOS_WASMTIME_WORKER_PATH", + env!("CARGO_BIN_EXE_agentos-native-sidecar"), + ); + let name = "wasmtime-threaded-atomic-wait-kill"; + let module = wat::parse_str( + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (import "wasi" "thread-spawn" (func $spawn (param i32) (result i32))) + (export "memory" (memory 0)) + (func (export "wasi_thread_start") (param i32 i32) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))) + (func (export "_start") + (if (i32.lt_s (call $spawn (i32.const 1)) (i32.const 1)) + (then unreachable)) + (drop (memory.atomic.wait32 + (i32.const 0) (i32.const 0) (i64.const -1)))))"#, + ) + .expect("atomic-wait fixture"); + let mut sidecar = new_sidecar(name); + let cwd = temp_dir(&format!("{name}-cwd")); + let entrypoint = cwd.join("atomic-wait.wasm"); + write_fixture(&entrypoint, &module); + let connection_id = authenticate_wire(&mut sidecar, "conn-wasmtime-thread-kill"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let (vm_id, _) = support::create_vm_wire_with_metadata( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::WebAssembly, + &cwd, + HashMap::from([(String::from("limits.wasm.max_threads"), String::from("2"))]), + ); + let process_id = String::from("process-wasmtime-atomic-wait"); + sidecar + .dispatch_wire_blocking(wire_request( + 4, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::ExecuteRequest(ExecuteRequest { + process_id: process_id.clone(), + command: None, + runtime: Some(GuestRuntimeKind::WebAssembly), + entrypoint: Some(entrypoint.to_string_lossy().into_owned()), + args: Vec::new(), + env: HashMap::new(), + cwd: None, + wasm_permission_tier: Some(WasmPermissionTier::Full), + wasm_backend: Some(StandaloneWasmBackend::WasmtimeThreads), + }), + )) + .expect("start atomic-wait worker"); + std::thread::sleep(Duration::from_millis(200)); + let started = std::time::Instant::now(); + sidecar + .dispatch_wire_blocking(wire_request( + 5, + wire_vm(&connection_id, &session_id, &vm_id), + RequestPayload::KillProcessRequest(KillProcessRequest { + process_id: process_id.clone(), + signal: String::from("SIGKILL"), + }), + )) + .expect("kill atomic-wait worker"); + let (_, _, exit_code) = collect_process_output_wire_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &process_id, + Duration::from_secs(5), + ); + assert_eq!(exit_code, 137); + assert!( + started.elapsed() < Duration::from_secs(2), + "atomic-wait worker exceeded the fixed reaping deadline" + ); +} diff --git a/crates/native-sidecar/tests/xfstests_correctness.rs b/crates/native-sidecar/tests/xfstests_correctness.rs index d17fa080f3..e72269a4bc 100644 --- a/crates/native-sidecar/tests/xfstests_correctness.rs +++ b/crates/native-sidecar/tests/xfstests_correctness.rs @@ -217,6 +217,14 @@ fn create_xfstests_vm_wire( ); let mut config: agentos_vm_config::CreateVmConfig = serde_json::from_str(&payload.config).expect("decode xfstests VM config"); + config.wasm_backend = match std::env::var("AGENTOS_TEST_WASM_BACKEND").as_deref() { + Ok("v8") => Some(agentos_vm_config::StandaloneWasmBackend::V8), + Ok("wasmtime") => Some(agentos_vm_config::StandaloneWasmBackend::Wasmtime), + Ok(value) => panic!( + "AGENTOS_TEST_WASM_BACKEND must be \"v8\" or \"wasmtime\" for xfstests, got {value:?}" + ), + Err(_) => None, + }; config.limits = Some(agentos_vm_config::VmLimitsConfig { resources: Some(agentos_vm_config::ResourceLimitsConfig { max_open_fds: Some(4096), @@ -236,7 +244,7 @@ fn create_xfstests_vm_wire( }), ..agentos_vm_config::VmLimitsConfig::default() }); - config.user = Some(agentos_vm_config::VmUserConfig { + let user = agentos_vm_config::VmUserConfig { uid: Some(0), gid: Some(0), username: Some(String::from("root")), @@ -318,7 +326,9 @@ fn create_xfstests_vm_wire( }, ]), ..agentos_vm_config::VmUserConfig::default() - }); + }; + config.root_filesystem.bootstrap_entries = xfstests_account_database_entries(&user); + config.user = Some(user); payload.config = serde_json::to_string(&config).expect("encode xfstests VM config"); let result = sidecar @@ -334,6 +344,56 @@ fn create_xfstests_vm_wire( } } +fn xfstests_account_database_entries( + user: &agentos_vm_config::VmUserConfig, +) -> Vec { + let username = user.username.as_deref().expect("xfstests primary username"); + let uid = user.uid.expect("xfstests primary uid"); + let gid = user.gid.expect("xfstests primary gid"); + let homedir = user.homedir.as_deref().expect("xfstests primary homedir"); + let shell = user.shell.as_deref().expect("xfstests primary shell"); + let gecos = user.gecos.as_deref().unwrap_or_default(); + + let mut passwd = format!("{username}:x:{uid}:{gid}:{gecos}:{homedir}:{shell}\n"); + for account in user.accounts.as_deref().unwrap_or_default() { + passwd.push_str(&format!( + "{}:x:{}:{}:{}:{}:{}\n", + account.username, + account.uid, + account.gid, + account.gecos.as_deref().unwrap_or_default(), + account.homedir, + account.shell + )); + } + + let group_name = user.group_name.as_deref().unwrap_or(username); + let mut group = format!("{group_name}:x:{gid}:{username}\n"); + for configured_group in user.groups.as_deref().unwrap_or_default() { + group.push_str(&format!( + "{}:x:{}:{}\n", + configured_group.name, + configured_group.gid, + configured_group.members.join(",") + )); + } + + [("/etc/passwd", passwd), ("/etc/group", group)] + .into_iter() + .map(|(path, content)| agentos_vm_config::RootFilesystemEntry { + path: path.to_owned(), + kind: agentos_vm_config::RootFilesystemEntryKind::File, + mode: Some(0o644), + uid: Some(0), + gid: Some(0), + content: Some(content), + encoding: Some(agentos_vm_config::RootFilesystemEntryEncoding::Utf8), + target: None, + executable: false, + }) + .collect() +} + fn command_root(package: &str) -> PathBuf { PathBuf::from(env::var("XFSTESTS_ROOT").expect("XFSTESTS_ROOT from Makefile")) .join("agentos-command-packages") @@ -1088,6 +1148,19 @@ fn verify_first(backend: &str, include_fd_lifecycle_probe: bool) { filesystem_request(GuestFilesystemOperation::ReadFile, "/mnt/test/root-only"), ); assert_eq!(root_only.content.as_deref(), Some("secret")); + let root_only_stat = guest_filesystem_call( + &mut sidecar, + 166, + &connection_id, + &session_id, + &vm_id, + filesystem_request(GuestFilesystemOperation::Stat, "/mnt/test/root-only"), + ) + .stat + .expect("root-only file stat"); + assert_eq!(root_only_stat.uid, 0, "root-only file owner"); + assert_eq!(root_only_stat.gid, 0, "root-only file group"); + assert_eq!(root_only_stat.mode & 0o777, 0o600, "root-only file mode"); let (stdout, stderr, exit_code) = execute_command( &mut sidecar, @@ -1097,10 +1170,13 @@ fn verify_first(backend: &str, include_fd_lifecycle_probe: bool) { &vm_id, "xfstests-runas-dac-denied", "sh", - &["-c", "runas -u 1000 -g 1000 -- id -u; if runas -u 1000 -g 1000 -- cat /mnt/test/root-only; then exit 9; else echo denied; fi"], + &["-c", "runas -u 1000 -g 1000 -- id -u; runas -u 1000 -g 1000 -- sh -c 'id -u; stat -c \"%u:%g %a\" /mnt/test/root-only; if cat /mnt/test/root-only; then exit 9; else echo denied; fi'"], ); assert_eq!(exit_code, 0, "stdout: {stdout}\nstderr: {stderr}"); - assert_eq!(stdout, "1000\ndenied\n", "su/DAC stderr: {stderr}"); + assert_eq!( + stdout, "1000\n1000\n0:0 600\ndenied\n", + "su/DAC stderr: {stderr}" + ); let root_only = guest_filesystem_call( &mut sidecar, @@ -3879,7 +3955,7 @@ fn xfstests_mknod_creates_a_working_null_device() { "sh", &[ "-c", - "printf 'No such attribute\\nOperation not permitted\\n' | sed -e 's:\\(No such attribute\\|Operation not permitted\\):normalized:'; printf '# file: b\\nx=2\\n\\n# file: a\\nx=1\\n\\n' | awk '{a[FNR]=$0}END{n = asort(a); for(i=1; i <= n; i++) print a[i]\"\\n\"}' RS=; touch /mnt/test/syscalltest && setfattr -n user.xfstests -v attr /mnt/test/syscalltest > /mnt/test/syscalltest.out 2>&1 && test ! -s /mnt/test/syscalltest.out && mknod /mnt/test/null c 1 3 && mknod -m 640 /mnt/test/block b 8 1 && mkfifo -m 600 /mnt/test/fifo && mkdir -p /mnt/test/link-target/child && ln -s link-target /mnt/test/link && test \"$(find /mnt/test | grep -c '/link/child')\" = 0 && setfattr -h -n trusted.link -v symlink /mnt/test/link && test \"$(getfattr -h --only-values -n trusted.link /mnt/test/link)\" = symlink && setfattr -h -n trusted.binary -v 0xbabe /mnt/test/link && getfattr --absolute-names -dh -m trusted.binary /mnt/test/link > /mnt/results/xattr.backup && setfattr -h -x trusted.binary /mnt/test/link && setfattr -h --restore=/mnt/results/xattr.backup && getfattr -h -e hex -n trusted.binary /mnt/test/link | grep -q 'trusted.binary=0xbabe' && setfattr -n trusted.walk -v child /mnt/test/link-target/child && getfattr -L -R -m trusted.walk /mnt/test/link | grep -q '# file: mnt/test/link/child' && if setfattr -h -n user.denied -v no /mnt/test/link 2>/dev/null; then exit 31; fi && if setfattr -n user.denied -v no /mnt/test/block 2>/dev/null; then exit 32; fi && if setfattr -n user.denied -v no /mnt/test/fifo 2>/dev/null; then exit 33; fi && stat -c '%F %t:%T' /mnt/test/null /mnt/test/block /mnt/test/fifo && printf x | sh -c 'test \"$(stat -c %F /dev/fd/0)\" = fifo; cat >/dev/null' && echo fred > /mnt/test/null && fifo_test /mnt/test/fifo && setfattr -n trusted.probe -v fifo /mnt/test/fifo && test \"$(getfattr --only-values -n trusted.probe /mnt/test/fifo)\" = fifo", + "printf 'No such attribute\\nOperation not permitted\\n' | sed -e 's:\\(No such attribute\\|Operation not permitted\\):normalized:'; printf '# file: b\\nx=2\\n\\n# file: a\\nx=1\\n\\n' | awk '{a[FNR]=$0}END{n = asort(a); for(i=1; i <= n; i++) print a[i]\"\\n\"}' RS=; touch /mnt/test/syscalltest && setfattr -n user.xfstests -v attr /mnt/test/syscalltest > /mnt/test/syscalltest.out 2>&1 && test ! -s /mnt/test/syscalltest.out && mknod /mnt/test/null c 1 3 && mknod /mnt/test/zerozero c 0 0 && mknod -m 640 /mnt/test/block b 8 1 && mkfifo -m 600 /mnt/test/fifo && mkdir -p /mnt/test/link-target/child && ln -s link-target /mnt/test/link && test \"$(find /mnt/test | grep -c '/link/child')\" = 0 && setfattr -h -n trusted.link -v symlink /mnt/test/link && test \"$(getfattr -h --only-values -n trusted.link /mnt/test/link)\" = symlink && setfattr -h -n trusted.binary -v 0xbabe /mnt/test/link && getfattr --absolute-names -dh -m trusted.binary /mnt/test/link > /mnt/results/xattr.backup && setfattr -h -x trusted.binary /mnt/test/link && setfattr -h --restore=/mnt/results/xattr.backup && getfattr -h -e hex -n trusted.binary /mnt/test/link | grep -q 'trusted.binary=0xbabe' && setfattr -n trusted.walk -v child /mnt/test/link-target/child && getfattr -L -R -m trusted.walk /mnt/test/link | grep -q '# file: mnt/test/link/child' && if setfattr -h -n user.denied -v no /mnt/test/link 2>/dev/null; then exit 31; fi && if setfattr -n user.denied -v no /mnt/test/block 2>/dev/null; then exit 32; fi && if setfattr -n user.denied -v no /mnt/test/fifo 2>/dev/null; then exit 33; fi && stat -c '%F %t:%T' /mnt/test/null /mnt/test/zerozero /mnt/test/block /mnt/test/fifo && printf x | sh -c 'test \"$(stat -c %F /dev/fd/0)\" = fifo; cat >/dev/null' && echo fred > /mnt/test/null && fifo_test /mnt/test/fifo && setfattr -n trusted.probe -v fifo /mnt/test/fifo && test \"$(getfattr --only-values -n trusted.probe /mnt/test/fifo)\" = fifo", ], command_env, Duration::from_secs(60), @@ -3893,6 +3969,10 @@ fn xfstests_mknod_creates_a_working_null_device() { stdout.contains("character special file 1:3"), "stdout: {stdout}\nstderr: {stderr}" ); + assert!( + stdout.contains("character special file 0:0"), + "stdout: {stdout}\nstderr: {stderr}" + ); assert!( stdout.contains("block special file 8:1"), "stdout: {stdout}\nstderr: {stderr}" @@ -4937,12 +5017,12 @@ fn xfstests_wasi_dirstress_process_matrix() { support::assert_node_available(); let source = PathBuf::from(env::var("XFSTESTS_ROOT").expect("XFSTESTS_ROOT from Makefile")); let file_count = env::var("XFSTESTS_DIRSTRESS_FILES") - .unwrap_or_else(|_| String::from("8")) + .unwrap_or_else(|_| String::from("1000")) .parse::() .expect("XFSTESTS_DIRSTRESS_FILES must be a positive integer"); assert!(file_count > 0, "XFSTESTS_DIRSTRESS_FILES must be positive"); let timeout = env::var("XFSTESTS_DIRSTRESS_TIMEOUT_SECONDS") - .unwrap_or_else(|_| String::from("120")) + .unwrap_or_else(|_| String::from("3600")) .parse::() .expect("XFSTESTS_DIRSTRESS_TIMEOUT_SECONDS must be a positive integer"); assert!( @@ -4996,6 +5076,13 @@ run_case five-by-five 5 5 printf 'dirstress-ok\n' "# ); + let mut command_env = HashMap::new(); + if env::var_os("XFSTESTS_TRACE_HOST_PROCESS").is_some() { + command_env.insert( + String::from("AGENTOS_TRACE_HOST_PROCESS"), + String::from("1"), + ); + } let output = try_execute_command_with_env( &mut sidecar, 4, @@ -5005,7 +5092,7 @@ printf 'dirstress-ok\n' "xfstests-dirstress-process-matrix", "sh", &["-c", &script], - HashMap::new(), + command_env, Duration::from_secs(timeout), ); let (stdout, stderr, exit_code) = output.unwrap_or_else(|output| { diff --git a/crates/resource/src/lib.rs b/crates/resource/src/lib.rs index 9c6f4b1bb6..c00f1e60fe 100644 --- a/crates/resource/src/lib.rs +++ b/crates/resource/src/lib.rs @@ -36,6 +36,7 @@ pub enum ResourceClass { ExecutorSlots, ExecutorBytes, WasmMemoryBytes, + WasmThreads, Http2Connections, Http2Streams, Http2BufferedBytes, @@ -48,7 +49,7 @@ pub enum ResourceClass { } impl ResourceClass { - pub const ALL: [Self; 30] = [ + pub const ALL: [Self; 31] = [ Self::Capabilities, Self::ReadyHandles, Self::Sockets, @@ -70,6 +71,7 @@ impl ResourceClass { Self::ExecutorSlots, Self::ExecutorBytes, Self::WasmMemoryBytes, + Self::WasmThreads, Self::Http2Connections, Self::Http2Streams, Self::Http2BufferedBytes, @@ -104,6 +106,7 @@ impl ResourceClass { Self::ExecutorSlots => "executorSlots", Self::ExecutorBytes => "executorBytes", Self::WasmMemoryBytes => "wasmMemoryBytes", + Self::WasmThreads => "wasmThreads", Self::Http2Connections => "http2Connections", Self::Http2Streams => "http2Streams", Self::Http2BufferedBytes => "http2BufferedBytes", diff --git a/crates/runtime/src/accounting.rs b/crates/runtime/src/accounting.rs index de3446dc85..07608b76db 100644 --- a/crates/runtime/src/accounting.rs +++ b/crates/runtime/src/accounting.rs @@ -50,6 +50,7 @@ impl ResourceUsageObserver for RuntimeMetrics { ResourceClass::WasmMemoryBytes => { self.observe_buffer(BufferMetricClass::Executor, used) } + ResourceClass::WasmThreads => {} ResourceClass::Http2BufferedBytes => { self.observe_buffer(BufferMetricClass::Http2, used) } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 8b850e72da..84aaeff8c0 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -63,6 +63,7 @@ const DEFAULT_MAX_PROCESS_UDP_DATAGRAMS: usize = 65_536; const DEFAULT_MAX_PROCESS_UDP_BYTES: usize = 256 * 1024 * 1024; const DEFAULT_MAX_PROCESS_TLS_BYTES: usize = 256 * 1024 * 1024; const DEFAULT_MAX_PROCESS_WASM_MEMORY_BYTES: usize = 8 * 1024 * 1024 * 1024; +const DEFAULT_MAX_PROCESS_WASM_THREADS: usize = 256; const DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS: usize = 4_096; const DEFAULT_MAX_PROCESS_HTTP2_STREAMS: usize = 65_536; const DEFAULT_MAX_PROCESS_HTTP2_BYTES: usize = 512 * 1024 * 1024; @@ -221,6 +222,8 @@ pub struct RuntimeResourceConfig { /// Aggregate admitted linear-memory envelopes for active standalone WASM /// Stores. This must not share the blocking-work byte ledger. pub max_wasm_memory_bytes: usize, + /// Aggregate admitted native threads across explicitly threaded WASM VMs. + pub max_wasm_threads: usize, pub max_http2_connections: usize, pub max_http2_streams: usize, pub max_http2_buffered_bytes: usize, @@ -254,6 +257,7 @@ impl Default for RuntimeResourceConfig { max_udp_bytes: DEFAULT_MAX_PROCESS_UDP_BYTES, max_tls_bytes: DEFAULT_MAX_PROCESS_TLS_BYTES, max_wasm_memory_bytes: DEFAULT_MAX_PROCESS_WASM_MEMORY_BYTES, + max_wasm_threads: DEFAULT_MAX_PROCESS_WASM_THREADS, max_http2_connections: DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS, max_http2_streams: DEFAULT_MAX_PROCESS_HTTP2_STREAMS, max_http2_buffered_bytes: DEFAULT_MAX_PROCESS_HTTP2_BYTES, @@ -370,6 +374,10 @@ impl RuntimeResourceConfig { "runtime.resources.maxWasmMemoryBytes", ), ), + ( + ResourceClass::WasmThreads, + ResourceLimit::new(self.max_wasm_threads, "runtime.resources.maxWasmThreads"), + ), ( ResourceClass::Http2Connections, ResourceLimit::new( diff --git a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare index 81306ecad2..040802fcc7 100644 --- a/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare +++ b/crates/sidecar-protocol/protocol/agentos_sidecar_v1.bare @@ -219,6 +219,7 @@ type WasmPermissionTier enum { type StandaloneWasmBackend enum { V8 WASMTIME + WASMTIME_THREADS } # agentOS package descriptor. `path` is the trusted host path of the package: diff --git a/crates/sidecar-protocol/src/wire.rs b/crates/sidecar-protocol/src/wire.rs index 08805011bd..cf9bad8dee 100644 --- a/crates/sidecar-protocol/src/wire.rs +++ b/crates/sidecar-protocol/src/wire.rs @@ -565,6 +565,8 @@ fn legacy_limits_config( active_cpu_time_limit_ms: legacy_u64(metadata, "limits.wasm.active_cpu_time_limit_ms"), wall_clock_limit_ms: legacy_u64(metadata, "limits.wasm.wall_clock_limit_ms"), deterministic_fuel: legacy_u64(metadata, "limits.wasm.deterministic_fuel"), + max_threads: legacy_u64(metadata, "limits.wasm.max_threads"), + max_concurrent_threads: legacy_u64(metadata, "limits.wasm.max_concurrent_threads"), }; let process = agentos_vm_config::ProcessLimitsConfig { max_spawn_file_actions: legacy_u64(metadata, "limits.process.max_spawn_file_actions") @@ -717,6 +719,8 @@ fn legacy_has_wasm_limits(config: &agentos_vm_config::WasmLimitsConfig) -> bool || config.active_cpu_time_limit_ms.is_some() || config.wall_clock_limit_ms.is_some() || config.deterministic_fuel.is_some() + || config.max_threads.is_some() + || config.max_concurrent_threads.is_some() } fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> bool { diff --git a/crates/v8-runtime/src/host_call.rs b/crates/v8-runtime/src/host_call.rs index c18d6801e9..e36b539306 100644 --- a/crates/v8-runtime/src/host_call.rs +++ b/crates/v8-runtime/src/host_call.rs @@ -446,6 +446,21 @@ struct RetiredBridgeCalls { impl RetiredBridgeCalls { fn insert(&mut self, call_id: u64, target: &BridgeCallTarget, limit: usize) { + self.insert_identity( + call_id, + &target.session_id, + target.session_generation, + limit, + ); + } + + fn insert_identity( + &mut self, + call_id: u64, + session_id: &str, + session_generation: Option, + limit: usize, + ) { while self.order.len() >= limit { let Some((oldest_call_id, oldest_epoch)) = self.order.pop_front() else { break; @@ -464,8 +479,8 @@ impl RetiredBridgeCalls { self.by_call_id.insert( call_id, RetiredBridgeCall { - session_id: target.session_id.clone(), - session_generation: target.session_generation, + session_id: session_id.to_owned(), + session_generation, retirement_epoch, }, ); @@ -1012,13 +1027,33 @@ impl BridgeCallRegistry { response.payload.len(), )); - target.sender.try_send(response).map_err(|error| { - format!( + match target.sender.try_send(response) { + Ok(()) => Ok(()), + Err(crossbeam_channel::TrySendError::Disconnected(_)) if target.host_visible => { + self.retired + .lock() + .map_err(|_| { + String::from( + "ERR_AGENTOS_BRIDGE_REGISTRY_POISONED: retired bridge registry lock poisoned", + ) + })? + .insert_identity( + call_id, + &target.session_id, + target.session_generation, + self.max_pending, + ); + Err(BridgeSettlementError::stale_completion(format!( + "ERR_AGENTOS_BRIDGE_STALE_COMPLETION: published {:?} response target disconnected before settlement for call_id {} in session {} generation {:?}", + target.kind, call_id, target.session_id, target.session_generation + ))) + } + Err(error) => Err(format!( "ERR_AGENTOS_BRIDGE_RESPONSE_DELIVERY: {:?} response target for session {} generation {:?} rejected settlement: {error}", target.kind, target.session_id, target.session_generation ) - })?; - Ok(()) + .into()), + } } fn cancel_with_visibility(&self, call_id: u64, force_unpublished: bool) { @@ -2417,6 +2452,58 @@ mod tests { assert_eq!(registry.retired_len(), 1); } + #[test] + fn bridge_registry_classifies_only_published_disconnected_waiters_as_stale() { + let registry = BridgeCallRegistry::new(2); + let runtime = test_runtime_context(); + + let visible_waiter = registry + .register_sync(&runtime, 0, 1, 23, "session-a", Some(4)) + .expect("register host-visible route"); + registry + .mark_host_visible(23) + .expect("mark route host-visible"); + drop(visible_waiter); + + let stale = registry + .settle( + "session-a", + Some(4), + BridgeResponse { + call_id: 23, + status: 0, + payload: Vec::new(), + reservation: None, + }, + ) + .expect_err("a published route may disconnect during guest teardown"); + assert_eq!(stale.kind(), BridgeSettlementErrorKind::StaleCompletion); + assert!(stale.contains("ERR_AGENTOS_BRIDGE_STALE_COMPLETION")); + assert_eq!(registry.pending_len(), 0); + assert_eq!(registry.retired_len(), 1); + + let unpublished_waiter = registry + .register_sync(&runtime, 0, 1, 24, "session-a", Some(4)) + .expect("register unpublished route"); + drop(unpublished_waiter); + + let delivery_error = registry + .settle( + "session-a", + Some(4), + BridgeResponse { + call_id: 24, + status: 0, + payload: Vec::new(), + reservation: None, + }, + ) + .expect_err("an unpublished disconnected route remains a hard error"); + assert_eq!(delivery_error.kind(), BridgeSettlementErrorKind::Other); + assert!(delivery_error.contains("ERR_AGENTOS_BRIDGE_RESPONSE_DELIVERY")); + assert_eq!(registry.retired_len(), 1); + } + #[test] fn bridge_registry_does_not_hide_duplicate_settlement_as_teardown() { let registry = BridgeCallRegistry::new(1); diff --git a/crates/v8-runtime/src/isolate.rs b/crates/v8-runtime/src/isolate.rs index 11acea69d7..8dedf14491 100644 --- a/crates/v8-runtime/src/isolate.rs +++ b/crates/v8-runtime/src/isolate.rs @@ -133,6 +133,12 @@ pub fn init_v8_platform() { .spawn(move || { v8::icu::set_common_data_74(&ICU_COMMON_DATA.0) .expect("failed to initialize V8 ICU common data"); + // LLVM 19 emits the legacy Phase-3 WebAssembly exception + // encoding and our toolchain translates it to the finalized + // exnref form required by Wasmtime. V8 130 keeps that finalized + // form behind this process-global flag. The shared AgentOS + // validator still controls which guest proposals are accepted. + v8::V8::set_flags_from_string("--experimental-wasm-exnref"); let platform = v8::new_default_platform(V8_PLATFORM_WORKER_THREADS, false).make_shared(); v8::V8::initialize_platform(platform); diff --git a/crates/vfs-store/src/local/sqlite_metadata_store.rs b/crates/vfs-store/src/local/sqlite_metadata_store.rs index 89990d4b5e..01112eeb4e 100644 --- a/crates/vfs-store/src/local/sqlite_metadata_store.rs +++ b/crates/vfs-store/src/local/sqlite_metadata_store.rs @@ -132,6 +132,26 @@ const LOCAL_FS_MIGRATIONS: &[LocalFsMigration] = &[ DROP TABLE agentos_fs_inodes_v1; "#, }, + LocalFsMigration { + version: 3, + statements: r#" + DROP INDEX agentos_fs_dentries_parent; + ALTER TABLE agentos_fs_dentries RENAME TO agentos_fs_dentries_v2; + CREATE TABLE agentos_fs_dentries ( + parent_ino INTEGER NOT NULL CHECK (parent_ino > 0), + name TEXT NOT NULL CHECK (length(name) > 0), + child_ino INTEGER NOT NULL CHECK (child_ino > 0), + kind INTEGER NOT NULL CHECK (kind IN (0, 1, 2, 3, 4, 5)), + PRIMARY KEY (parent_ino, name) + ) STRICT; + INSERT INTO agentos_fs_dentries (parent_ino, name, child_ino, kind) + SELECT parent_ino, name, child_ino, kind + FROM agentos_fs_dentries_v2; + DROP TABLE agentos_fs_dentries_v2; + CREATE INDEX agentos_fs_dentries_parent + ON agentos_fs_dentries(parent_ino); + "#, + }, ]; pub struct SqliteMetadataStore { diff --git a/crates/vfs-store/tests/local.rs b/crates/vfs-store/tests/local.rs index 15bb9f9858..22791eacbb 100644 --- a/crates/vfs-store/tests/local.rs +++ b/crates/vfs-store/tests/local.rs @@ -4,7 +4,7 @@ use rusqlite::Connection; use vfs::adapter::MountedEngineFileSystem; use vfs::engine::engines::{ChunkedFs, ChunkedFsOptions}; use vfs::engine::mem::MemoryBlockStore; -use vfs::engine::{BlockKey, BlockStore, VirtualFileSystem}; +use vfs::engine::{BlockKey, BlockStore, VirtualFileSystem, S_IFBLK, S_IFCHR, S_IFIFO}; use vfs::posix::MountedFileSystem; use vfs::posix::{MemoryFileSystem, MountOptions, MountTable}; @@ -42,7 +42,7 @@ async fn sqlite_store_installs_canonical_schema() { |row| row.get(0), ) .unwrap(); - assert_eq!(version, 2); + assert_eq!(version, 3); let mut statement = connection .prepare( @@ -140,7 +140,7 @@ fn sqlite_store_rejects_future_schema_versions() { singleton INTEGER PRIMARY KEY CHECK (singleton = 1), schema_version INTEGER NOT NULL CHECK (schema_version >= 0) ) STRICT; - INSERT INTO agentos_fs_schema_version (singleton, schema_version) VALUES (1, 3);", + INSERT INTO agentos_fs_schema_version (singleton, schema_version) VALUES (1, 4);", ) .unwrap(); drop(connection); @@ -150,7 +150,54 @@ fn sqlite_store_rejects_future_schema_versions() { .expect("future schema must be rejected"); assert!(error .message() - .contains("version 3; latest supported version is 2")); + .contains("version 4; latest supported version is 3")); +} + +#[test] +fn sqlite_store_migrates_v2_dentries_to_all_supported_inode_kinds() { + let temp = tempfile::tempdir().unwrap(); + let db = temp.path().join("v2.sqlite"); + drop(SqliteMetadataStore::open(&db).unwrap()); + + let connection = Connection::open(&db).unwrap(); + connection + .execute_batch( + "DROP INDEX agentos_fs_dentries_parent; + ALTER TABLE agentos_fs_dentries RENAME TO agentos_fs_dentries_v3; + CREATE TABLE agentos_fs_dentries ( + parent_ino INTEGER NOT NULL CHECK (parent_ino > 0), + name TEXT NOT NULL CHECK (length(name) > 0), + child_ino INTEGER NOT NULL CHECK (child_ino > 0), + kind INTEGER NOT NULL CHECK (kind IN (0, 1, 2)), + PRIMARY KEY (parent_ino, name) + ) STRICT; + INSERT INTO agentos_fs_dentries (parent_ino, name, child_ino, kind) + SELECT parent_ino, name, child_ino, kind FROM agentos_fs_dentries_v3; + DROP TABLE agentos_fs_dentries_v3; + CREATE INDEX agentos_fs_dentries_parent ON agentos_fs_dentries(parent_ino); + UPDATE agentos_fs_schema_version SET schema_version = 2 WHERE singleton = 1;", + ) + .unwrap(); + drop(connection); + + drop(SqliteMetadataStore::open(&db).unwrap()); + let connection = Connection::open(&db).unwrap(); + let version: i64 = connection + .query_row( + "SELECT schema_version FROM agentos_fs_schema_version WHERE singleton = 1", + [], + |row| row.get(0), + ) + .unwrap(); + let dentry_sql: String = connection + .query_row( + "SELECT sql FROM sqlite_schema WHERE name = 'agentos_fs_dentries'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(version, 3); + assert!(dentry_sql.contains("kind IN (0, 1, 2, 3, 4, 5)")); } #[tokio::test] @@ -197,6 +244,44 @@ async fn sqlite_store_reopens_persisted_metadata() { ); } +#[tokio::test] +async fn sqlite_store_reopens_all_special_inode_kinds() { + let temp = tempfile::tempdir().unwrap(); + let db = temp.path().join("special-inodes.sqlite"); + + { + let fs = ChunkedFs::new( + SqliteMetadataStore::open(&db).unwrap(), + MemoryBlockStore::new(), + ); + fs.mkdir("/devices", false).await.unwrap(); + fs.mknod("/devices/char", S_IFCHR | 0o600, (1 << 8) | 3) + .await + .unwrap(); + fs.mknod("/devices/block", S_IFBLK | 0o640, (8 << 8) | 1) + .await + .unwrap(); + fs.mknod("/devices/fifo", S_IFIFO | 0o600, 0).await.unwrap(); + } + + let fs = ChunkedFs::new( + SqliteMetadataStore::open(&db).unwrap(), + MemoryBlockStore::new(), + ); + assert_eq!( + fs.stat("/devices/char").await.unwrap().mode & 0o170000, + S_IFCHR + ); + assert_eq!( + fs.stat("/devices/block").await.unwrap().mode & 0o170000, + S_IFBLK + ); + assert_eq!( + fs.stat("/devices/fifo").await.unwrap().mode & 0o170000, + S_IFIFO + ); +} + #[tokio::test] async fn sqlite_store_reopens_incremental_pwrite_overwrite_and_truncate() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/vm-config/src/lib.rs b/crates/vm-config/src/lib.rs index 4d17820061..7b4770dfe2 100644 --- a/crates/vm-config/src/lib.rs +++ b/crates/vm-config/src/lib.rs @@ -3,6 +3,20 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use ts_rs::TS; +/// VM-wide default engine for standalone WebAssembly process images. +/// +/// This does not affect JavaScript's `WebAssembly.*` APIs, which always run in +/// the owning V8 isolate. Individual process launches may override this value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)] +#[serde(rename_all = "kebab-case")] +#[ts(export, export_to = "../../../packages/runtime-core/src/generated/")] +pub enum StandaloneWasmBackend { + #[default] + V8, + Wasmtime, + WasmtimeThreads, +} + /// Canonical Rust-side VM config. Unknown fields must stay rejected here and in /// the TS preflight schema at /// `packages/core/src/node-runtime-options-schema.ts`; update both when a @@ -18,6 +32,13 @@ pub struct CreateVmConfig { #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[ts(type = "Record")] pub env: BTreeMap, + #[serde( + default, + rename = "wasmBackend", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional)] + pub wasm_backend: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub database: Option, @@ -1415,6 +1436,8 @@ limits_struct!(WasmLimitsConfig { active_cpu_time_limit_ms, wall_clock_limit_ms, deterministic_fuel, + max_threads, + max_concurrent_threads, }); limits_struct!(ProcessLimitsConfig { @@ -1562,6 +1585,31 @@ mod tests { assert_eq!(decoded, config); } + #[test] + fn standalone_wasm_backend_round_trips_and_defaults_to_v8() { + let omitted: CreateVmConfig = + serde_json::from_str(r#"{"env":{},"rootFilesystem":{},"loopbackExemptPorts":[]}"#) + .expect("decode omitted backend"); + assert_eq!( + omitted.wasm_backend.unwrap_or_default(), + StandaloneWasmBackend::V8 + ); + + for backend in [ + StandaloneWasmBackend::V8, + StandaloneWasmBackend::Wasmtime, + StandaloneWasmBackend::WasmtimeThreads, + ] { + let config = CreateVmConfig { + wasm_backend: Some(backend), + ..CreateVmConfig::default() + }; + let json = serde_json::to_string(&config).expect("serialize backend"); + let decoded: CreateVmConfig = serde_json::from_str(&json).expect("decode backend"); + assert_eq!(decoded.wasm_backend, Some(backend)); + } + } + #[test] fn unknown_fields_are_rejected() { let error = diff --git a/docs/design/wasmtime-executor.md b/docs/design/wasmtime-executor.md index 96f1ef25c8..ae32a900db 100644 --- a/docs/design/wasmtime-executor.md +++ b/docs/design/wasmtime-executor.md @@ -664,7 +664,7 @@ revision has been sealed: - [x] Phase 2: production Wasmtime executor at current V8-WASM parity. - [x] Phase 3: performance decision, preferred-backend rollout, and initial project completion. -- [ ] Phase 4: separately gated threaded-WASM roadmap completion. +- [x] Phase 4: separately gated threaded-WASM roadmap completion. ### Phase 0 revision: Specification, inventory, and baseline @@ -820,8 +820,12 @@ revision has been sealed: - [x] Keep V8 permanently for JavaScript and keep V8-WASM as an independent, maintained compatibility backend; add no V8-to-Wasmtime bridge. - [x] Keep shared memory, threads, memory64, multi-memory, relaxed SIMD, tail - calls, GC/function references, exceptions, components, custom page sizes, + calls, GC/function references, components, custom page sizes, AOT, pooling, Wizer, and live snapshots disabled for initial parity. + Enable finalized core exception tags/instructions and translate LLVM + 19's legacy DuckDB encoding with checksum-verified Binaryen 128 during + the owned toolchain build; Wasmtime's compiler intentionally does not + accept the legacy encoding. - [x] Pass the complete differential ABI and working-software corpus—including `ls`, `vim`, `grep`, `curl`, shell pipelines, sqlite, git, tar/gzip, and metadata tools—against both standalone-WASM backends. @@ -938,41 +942,90 @@ workspace, 2026-07-20 Pacific): ### Phase 4 revision: Threading as a separate later project -- [ ] Confirm Phase 3 is complete before enabling shared memory or threads. -- [ ] If threading cannot remain reviewable as one revision, approve a +- [x] Confirm Phase 3 is complete before enabling shared memory or threads. +- [x] If threading cannot remain reviewable as one revision, approve a replacement multi-revision threading specification before implementation; do not silently fragment this phase. -- [ ] Rebuild the owned sysroot and libc for real pthread semantics instead of +- [x] Rebuild the owned sysroot and libc for real pthread semantics instead of the current emulated single-thread implementation. -- [ ] Add an explicit AgentOS thread-spawn ABI and enable the exact shared-memory +- [x] Add an explicit AgentOS thread-spawn ABI and enable the exact shared-memory and atomic WASM feature profile only for configured threaded executions. -- [ ] Implement bounded per-VM and process-wide thread admission, one accounted +- [x] Implement bounded per-VM and process-wide thread admission, one accounted Store/instance/native stack per admitted guest thread where required, and transactional failure when capacity is unavailable. -- [ ] Isolate each threaded WASM thread group in a killable worker process, +- [x] Isolate each threaded WASM thread group in a killable worker process, keep AgentOS kernel state in the parent, use bounded typed host-operation IPC, and prove fixed-deadline termination/reaping for a guest parked in `memory.atomic.wait`. -- [ ] Implement pthread mutex, condition variable, TLS, join/detach, exit, +- [x] Implement pthread mutex, condition variable, TLS, join/detach, exit, cancellation, robust teardown, and required libc behavior. -- [ ] Move masks and in-progress signal delivery to per-thread kernel records +- [x] Move masks and in-progress signal delivery to per-thread kernel records while retaining process-wide dispositions and correct process/thread signal selection. -- [ ] Define shared-memory ownership, growth, atomic wait/notify, limits, +- [x] Define shared-memory ownership, growth, atomic wait/notify, limits, retained-memory accounting, and cross-thread guest-memory mutation rules. -- [ ] Make trap, exit, cancellation, timeout, and VM teardown terminate and reap +- [x] Make trap, exit, cancellation, timeout, and VM teardown terminate and reap the complete thread group without terminating or corrupting the sidecar. -- [ ] Pass pthread/libc, signal, shared-memory, race, resource-exhaustion, +- [x] Pass pthread/libc, signal, shared-memory, race, resource-exhaustion, teardown, isolation, and high-concurrency memory tests for hostile VMs. -- [ ] Re-run the full single-thread parity and performance gates to prove the +- [x] Re-run the full single-thread parity and performance gates to prove the threaded profile does not regress ordinary V8-WASM or Wasmtime execution. -- [ ] Keep browser support, AOT artifacts, Wizer, components, pooling, and live +- [x] Keep browser support, AOT artifacts, Wizer, components, pooling, and live process snapshot/fork outside the threading milestone unless separately specified and approved. -- [ ] Seal the approved threading implementation and conformance evidence in +- [x] Seal the approved threading implementation and conformance evidence in its own JJ revision or approved replacement revision stack. Completion of this checkbox means the full roadmap in this document is complete. +Phase 4 evidence (Rust 1.94.0, release performance profile, Linux x86-64 +canonical workspace, 2026-07-21 Pacific): + +- The explicit `wasmtime-threads` selector uses the sealed + `AgentOsOwnedWasiV1Threads` profile. Plain `wasmtime` and V8-WASM remain + single-threaded and continue rejecting shared memory, atomics, and + `wasi.thread-spawn`. JavaScript remains on V8 and there is no V8/Wasmtime + memory bridge. +- Each configured threaded group starts in its own native-sidecar worker + process. The child owns only Wasmtime Engine/Store/Instance/shared-memory and + guest-thread state; the parent retains the kernel, VFS, descriptors, sockets, + permissions, processes, and signal dispositions. Bounded CBOR frames carry + typed owned host operations, results, signals, stderr, group failures, and a + final completion acknowledgement. The child has two fixed IPC support + threads and one process-level Tokio runtime rather than per-operation threads + or per-thread/subsystem runtimes. +- Admission reserves the complete configured group before guest entry: + per-VM and process-wide thread capacity, one Store/Instance/native stack per + guest thread, table space, and the maximum shared-memory envelope. The + group-owned `SharedMemory` supplies growth, atomic wait/notify, and + cross-Store mutation; every reservation is released on all teardown paths. +- The owned pthread sysroot/libc passes the generated mutex, condition + variable, TLS, join, detach, exit, and cooperative-cancellation conformance + program. Kernel signal records now keep masks, temporary `ppoll` masks, and + in-progress handler state per thread while retaining process-wide + dispositions and deterministic process-directed thread selection. +- The serial threaded safety suite passed 20 default tests plus its generated + pthread/libc test. It covers shared-memory growth/visibility, a four-thread + atomic race, transactional resource exhaustion, per-thread signals, eight + concurrent isolated groups, secondary-thread traps, process exit, timeout, + `SIGKILL`, VM disposal, and threads parked indefinitely in + `memory.atomic.wait`; fixed-deadline group reaping leaves the sidecar usable. +- The ordinary single-thread raw-ABI suite passed 9/9 and the real-software + parity suite passed 6/6 (`ls`, loopback `curl`, the direct corpus, + shell/children, Vim, and the release command set). Wasmtime units and + architecture guards passed. Strict all-target native Clippy, Rust formatting, the native + workspace check, fixed-version/package/protocol inventory checks, all 54 + JavaScript build tasks, and all 146 type-check tasks passed. +- The post-threading canonical single-thread performance matrix completed with + zero correctness failures. Geometric-mean p50 (`0.2741` Wasmtime/V8) and + throughput passed; individual cold p95, retained RSS (`161,894,400` V8 versus + `256,995,328` Wasmtime), and retained PSS (`162,531,328` versus `257,593,344`) + failed. V8 therefore remains the omission/default and rollback backend. Raw + evidence is committed in + `packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json`. +- Browser entrypoints remain dormant and excluded. AOT/serialized artifacts, + Wizer, components, pooling, and live process snapshots/fork remain disabled. + The repository-wide package-layout and fixed-version checks pass. + ## 17. Principal risks | Risk | Severity | Required mitigation | diff --git a/docs/design/wasmtime-phase-0.md b/docs/design/wasmtime-phase-0.md index 25107f7d3a..5aaa7662bf 100644 --- a/docs/design/wasmtime-phase-0.md +++ b/docs/design/wasmtime-phase-0.md @@ -429,8 +429,11 @@ by `just tools-rebuild` before a release-quality capture. 1. **Feature profile.** Both standalone backends prevalidate with one `wasmparser` profile. Enable MVP, mutable globals, sign extension, nontrapping float-to-int, bulk memory, reference types, multivalue, and - SIMD128. Disable threads/shared memory, memory64, multi-memory, relaxed SIMD, - tail calls, function references/GC, exceptions, components, custom page + SIMD128 and finalized exnref exception instructions. The owned toolchain + translates the legacy encoding emitted by LLVM 19 for DuckDB with pinned, + checksum-verified Binaryen 128. + Disable threads/shared memory, memory64, multi-memory, relaxed SIMD, tail + calls, function references, components, custom page sizes, and other proposals until an explicit profile revision. Engine defaults never silently expand the accepted language. 2. **Code placement.** Kernel semantic APIs stay in `agentos-kernel`. Shared diff --git a/docs/wasmvm/executors.md b/docs/wasmvm/executors.md index da3dc41727..e150f5fee1 100644 --- a/docs/wasmvm/executors.md +++ b/docs/wasmvm/executors.md @@ -1,15 +1,16 @@ # Standalone WebAssembly executors -AgentOS has two maintained native standalone-WebAssembly executors: V8-WASM -and Wasmtime. JavaScript always remains in V8; the selector described here -only chooses the engine for a standalone WASM command. The executors do not +AgentOS has two maintained native standalone-WebAssembly engines: V8-WASM and +Wasmtime. Wasmtime has separate single-threaded and threaded execution +profiles. JavaScript always remains in V8; the selector described here only +chooses the engine/profile for a standalone WASM command. The engines do not share guest memory and there is no V8-to-Wasmtime bridge. ## Production decision Omitting the selector currently chooses **V8**. Wasmtime is production-ready -and explicitly selectable, but the July 2026 canonical comparison did not pass -the cold-start p95, low-concurrency throughput, or retained RSS/PSS gates. +and explicitly selectable, but the July 2026 post-threading canonical +comparison did not pass the cold-start p95 or retained RSS/PSS gates. Warm Wasmtime module-cache hits were generally much faster than V8-WASM, so an explicit Wasmtime selection is appropriate for a process expected to reuse a small module set. It is not yet the safe fleet-wide default for cold or @@ -19,13 +20,19 @@ Select a backend per execution: ```ts await vm.execCommand("ls", ["-la"], { wasmBackend: "wasmtime" }); +await vm.execCommand("threaded-tool", [], { + wasmBackend: "wasmtime-threads", +}); await vm.execCommand("ls", ["-la"], { wasmBackend: "v8" }); ``` -The selector is sealed to `"wasmtime" | "v8"`. Omission is the sidecar-owned -default; clients must not invent another default. The immediate rollback for a -Wasmtime workload is an explicit `wasmBackend: "v8"`. A fleet rollback is to -omit Wasmtime overrides; no code, cache, or data migration is required. +The selector is sealed to `"wasmtime" | "wasmtime-threads" | "v8"`. +`"wasmtime"` remains the exact single-threaded profile and rejects shared +memory, atomics, and the AgentOS thread-spawn import. Omission is the +sidecar-owned default; clients must not invent another default. The immediate +rollback for a Wasmtime workload is an explicit `wasmBackend: "v8"`. A fleet +rollback is to omit Wasmtime overrides; no code, cache, or data migration is +required. ## Shared host behavior and safety @@ -41,6 +48,14 @@ across the wait, then reacquires and revalidates every output range before commit. Guest execution runs on the bounded non-Tokio VM executor; host I/O continues on the process's one Tokio runtime. +The threaded profile puts each guest thread group in a dedicated killable +worker process. Wasmtime Engines, Stores, Instances, shared linear memory, and +native guest threads live in that child; the kernel, VFS, descriptors, sockets, +permissions, process table, and signal dispositions remain authoritative in +the parent. A bounded typed protocol carries owned host-operation values over +stdio. The child receives no ambient host capability, and guest memory is +never mapped into the parent as an IPC shortcut. + V8-WASM remains on the same parity and safety suite. AgentOS never shadow-runs a side-effecting command through both engines. @@ -67,6 +82,8 @@ bounded Engine-profile and module-cache limits and typed errors at the limit: - `WARN_AGENTOS_WASMTIME_ENGINE_PROFILES_NEAR_LIMIT` / `ERR_AGENTOS_WASMTIME_ENGINE_PROFILE_LIMIT` - `WARN_AGENTOS_WASMTIME_MODULE_CACHE_NEAR_LIMIT` / `ERR_AGENTOS_WASMTIME_MODULE_CACHE_LIMIT` - `WARN_AGENTOS_WASMTIME_LIMIT_WARNING` for aggregate Store reservations +- `WARN_AGENTOS_RESOURCE_NEAR_LIMIT` / `ERR_AGENTOS_WASM_THREAD_LIMIT` for + per-VM and process-wide threaded-WASM admission Limit errors include `limitName`, `limit`, and `observed` details where those values apply. Guest traps use `ERR_AGENTOS_WASM_TRAP` plus a stable `trapKind`; @@ -99,11 +116,15 @@ Memory figures must be kept separate: recorded per Wasmtime execution in opt-in phase diagnostics. - compiled-module charge and kernel buffered bytes are reported separately; neither is guest linear memory. +- an active `wasmtime-threads` group also has a dedicated child-process image, + two fixed IPC support threads, and one Store/Instance/native stack for each + admitted guest thread. This overhead is intentionally not conflated with the + ordinary single-thread Wasmtime RSS numbers below. ## Canonical benchmark and rollback criteria -The raw release result is -[`packages/runtime-benchmarks/results/wasm-backend-comparison.json`](../../packages/runtime-benchmarks/results/wasm-backend-comparison.json). +The latest raw release result is +[`packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json`](../../packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json). It uses identical hashed source modules and host-service paths on one machine, five independent sidecar processes per engine, five samples per workload, and warm cache hits. V8 adds its existing two-byte memory-maximum rewrite before @@ -120,22 +141,22 @@ The canonical result completed with this decision table: | Gate | Result | Evidence | | --- | --- | --- | | Correctness and safety | Pass | Zero V8 or Wasmtime workload validation failures; denial, cancellation, and CPU-limit paths passed. | -| Geometric-mean p50 | Pass | Wasmtime/V8 ratio `0.2972` (about 70% lower latency across the mixed sample set). | +| Geometric-mean p50 | Pass | Wasmtime/V8 ratio `0.2741` (about 73% lower latency across the mixed sample set). | | Individual p95 | **Fail** | Cold Wasmtime compilation dominates substantive-module p95; several workload ratios exceed the `1.20` ceiling. | -| Throughput | Pass | Wasmtime exceeded V8 on every comparable 1/10/50/100/200 repeated/diverse row; overload produced typed executor/protocol admission errors. | -| Retained RSS | **Fail** | V8 `127,930,368` bytes; Wasmtime `264,069,120` bytes. | -| Retained PSS | **Fail** | V8 `129,094,656` bytes; Wasmtime `264,836,096` bytes. | +| Throughput | Pass | Wasmtime exceeded V8 on every comparable repeated/diverse row through concurrency 100; at 200, Wasmtime completed admitted work while V8 produced its typed admission outcome. | +| Retained RSS | **Fail** | V8 `161,894,400` bytes; Wasmtime `256,995,328` bytes. | +| Retained PSS | **Fail** | V8 `162,531,328` bytes; Wasmtime `257,593,344` bytes. | -Across workload medians, Wasmtime cold p50 ranged from approximately equal to -V8 for the trivial module to about 17× slower for Vim; warm p50 was about -2.5–4.6× faster. These results support explicit warm-cache use, but the failed +Across workload medians, Wasmtime cold p50 ranged from slightly faster than V8 +for the trivial module to about 13× slower for Vim; warm p50 was about +2.8–5.2× faster. These results support explicit warm-cache use, but the failed p95 and retained-memory gates require the omission default to remain V8. Run it from the repository root with a release sidecar and rebuilt canonical commands: ```bash -AGENTOS_SIDECAR_BIN=/absolute/path/to/release/agentos-sidecar \ +AGENTOS_SIDECAR_BIN=/absolute/path/to/release/agentos-native-sidecar \ AGENTOS_WASM_COMMANDS_DIR=/absolute/path/to/packages/runtime-core/commands \ pnpm --dir packages/runtime-benchmarks bench:wasm-backends ``` @@ -156,9 +177,31 @@ memory evidence supports that choice. ## Threads -Shared WebAssembly memory and pthreads are not enabled by the initial -Wasmtime executor. AgentOS currently does not rely on shared memory between V8 -isolates, and no memory is shared between V8 and Wasmtime. Threading requires -the separately specified worker-process isolation, bounded thread admission, -threaded sysroot/libc, per-thread signal state, atomic wait/notify, teardown, -and hostile-VM conformance milestone before the feature profile can be enabled. +Shared WebAssembly memory and pthreads are enabled only by the explicit +`"wasmtime-threads"` profile. AgentOS does not rely on shared memory between V8 +isolates, and no memory is shared between V8 and Wasmtime. Threaded programs use +the owned AgentOS sysroot/libc and import `wasi.thread-spawn`; each pthread gets +its own Wasmtime Store and Instance over one group-owned `SharedMemory`. + +Before any guest code runs, admission transactionally reserves the configured +`limits.wasm.maxThreads` (including the initial thread), maximum shared-memory +envelope, table capacity, async stacks, and native thread capacity. The +per-group default is 16 threads, `limits.wasm.maxConcurrentThreads` bounds the +aggregate reservations of concurrent groups in one VM at 64 by default, and +the process-wide default is 256. Capacity failure is typed and starts no +partial group. Shared-memory growth stays within the pre-reserved maximum, and +atomics, wait/notify, and cross-Store mutation operate on the group-owned +memory. + +Signal dispositions and process-pending signals remain process-wide in the +kernel. Masks, temporary `ppoll` masks, and in-progress handler state are +per-thread; process-directed delivery deterministically selects an unblocked +thread. Trap, process exit, cancellation, timeout, `SIGKILL`, and VM disposal +terminate and reap the entire child process, including threads indefinitely +parked in `memory.atomic.wait`, without terminating the sidecar. + +The threaded libc covers mutexes, condition variables, TLS, join/detach, +thread exit, and cooperative cancellation. Live Store/Instance snapshots, +cross-engine memory, AOT artifacts, Wizer, pooling, components, and browser +execution remain unsupported. Main-thread exit tears down the group; detached +guest threads do not outlive the command. diff --git a/justfile b/justfile index 1f61be42a5..13c1bf4eb9 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,7 @@ release-preview REF: # --- @agentos-software/* software packages (independent, PER-PACKAGE versions) --- toolchain-build: make -C toolchain commands + make -C toolchain cmd/duckdb cmd/vim toolchain-cmd name: make -C toolchain cmd/{{ name }} diff --git a/packages/agentos/src/actor.ts b/packages/agentos/src/actor.ts index b0af9c59d7..a9e5545f20 100644 --- a/packages/agentos/src/actor.ts +++ b/packages/agentos/src/actor.ts @@ -1258,6 +1258,7 @@ const agentOsOptionKeys = [ "loopbackExemptPorts", "allowedNodeBuiltins", "highResolutionTime", + "wasmBackend", "database", "rootFilesystem", "mounts", diff --git a/packages/agentos/tests/fixtures/actor-runtime-server.mjs b/packages/agentos/tests/fixtures/actor-runtime-server.mjs index c77d5ee27c..8d5a05e7b1 100644 --- a/packages/agentos/tests/fixtures/actor-runtime-server.mjs +++ b/packages/agentos/tests/fixtures/actor-runtime-server.mjs @@ -12,6 +12,10 @@ const conformanceAgent = createProjectedAgentPackage({ name: CONFORMANCE_AGENT_NAME, adapterScript: CONFORMANCE_ACP_ADAPTER, }); +const wasmBackend = process.env.AGENTOS_TEST_WASM_BACKEND; +if (wasmBackend !== undefined && wasmBackend !== "v8" && wasmBackend !== "wasmtime") { + throw new Error(`invalid AGENTOS_TEST_WASM_BACKEND: ${wasmBackend}`); +} for (const signal of ["SIGINT", "SIGTERM"]) { process.once(signal, () => { conformanceAgent.cleanup(); @@ -20,6 +24,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) { } const vm = agentOS({ + wasmBackend, defaultSoftware: false, software: [coreutils, conformanceAgent.software], mounts: [ diff --git a/packages/build-tools/bridge-src/builtins/http.ts b/packages/build-tools/bridge-src/builtins/http.ts index f338244653..bb7eb89553 100644 --- a/packages/build-tools/bridge-src/builtins/http.ts +++ b/packages/build-tools/bridge-src/builtins/http.ts @@ -4129,7 +4129,12 @@ async function dispatchLoopbackServerRequest(serverOrId, requestInput) { } } -async function dispatchSocketBackedServerRequest(server, requestInput, streamSocket) { +async function dispatchSocketBackedServerRequest( + server, + requestInput, + streamSocket, + connectionClosed, +) { const request = typeof requestInput === "string" ? JSON.parse(requestInput) : requestInput; const incoming = new ServerIncomingMessage(request); const outgoing = new ServerResponseBridge(); @@ -4163,7 +4168,14 @@ async function dispatchSocketBackedServerRequest(server, requestInput, streamSoc // Ending here as soon as the listener returns races that continuation and // produces a synthetic empty 200 response. Leave the request open until // ServerResponse.end()/destroy() closes it, matching native Node. - await outgoing.waitForClose(); + const connectionClosedFirst = await Promise.race([ + outgoing.waitForClose().then(() => false), + connectionClosed.then(() => true), + ]); + if (connectionClosedFirst && !outgoing.writableFinished) { + incoming._abort(); + outgoing.destroy(); + } let aborted = false; const abortRequest = () => { if (aborted) { @@ -4195,6 +4207,14 @@ function attachHttpServerSocket(server, socket) { let dispatchPending = false; let ended = false; let detached = false; + let resolveConnectionClosed; + const connectionClosed = new Promise((resolve) => { + resolveConnectionClosed = resolve; + }); + const markConnectionClosed = () => { + resolveConnectionClosed?.(); + resolveConnectionClosed = null; + }; const cleanup = () => { if (detached) { return; @@ -4238,16 +4258,19 @@ function attachHttpServerSocket(server, socket) { }; const onEnd = () => { ended = true; + markConnectionClosed(); if (buffer.length === 0) { - cleanup(); + finishSocket(); return; } scheduleDispatch(); }; const onClose = () => { + markConnectionClosed(); cleanup(); }; const onError = () => { + markConnectionClosed(); cleanup(); }; async function processRequests() { @@ -4289,6 +4312,7 @@ function attachHttpServerSocket(server, socket) { server, parsed.request, socket, + connectionClosed, ); if (detached || socket.destroyed) { return; diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index cf0a50d886..5adff5843b 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -17,8 +17,11 @@ import type { VmUserConfig, } from "@rivet-dev/agentos-runtime-core/vm-config"; import type { + AcpSessionEvent, CancelPromptResult, + PromptResult as DurablePromptResult, DurableSessionEventEntry, + SessionInfo as DurableSessionInfo, EphemeralSessionEventEntry, HistoryPage, JsonValue, @@ -28,13 +31,10 @@ import type { PermissionResponseResult, PermissionTerminalReason, PromptInput, - PromptResult as DurablePromptResult, ReadHistoryInput, - AcpSessionEvent, - SessionCapabilities, SessionAgentInfo, + SessionCapabilities, SessionConfig, - SessionInfo as DurableSessionInfo, SessionPage, SessionStreamEntry, SessionTarget, @@ -692,6 +692,10 @@ export interface AgentOsLimits { activeCpuTimeLimitMs?: number; wallClockLimitMs?: number; deterministicFuel?: number; + /** Maximum threads, including the initial thread, for the explicit Wasmtime threaded backend. */ + maxThreads?: number; + /** Maximum threads reserved by all concurrent threaded WASM processes in this VM. */ + maxConcurrentThreads?: number; }; /** Process spawn, I/O, and lifecycle-event backlog limits. */ process?: { @@ -805,6 +809,8 @@ export interface AgentOsOptions { * Defaults to the hardened builtin set used by the native sidecar bridge. */ allowedNodeBuiltins?: string[]; + /** VM-wide default for standalone WASM commands. JavaScript remains on V8. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; /** * Opt in to a high-resolution monotonic guest clock (microsecond class) * for guest Node processes. Default `false` keeps the security-oriented @@ -1443,9 +1449,9 @@ async function bootstrapLiveBootstrapDirectories( config: RootFilesystemConfig | undefined, ): Promise { const existingPaths = new Set( - (await client.snapshotRootFilesystem(session, vm, Number.MAX_SAFE_INTEGER)).map( - (entry) => entry.path, - ), + ( + await client.snapshotRootFilesystem(session, vm, Number.MAX_SAFE_INTEGER) + ).map((entry) => entry.path), ); const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config); if (entries.length === 0) { @@ -2807,6 +2813,7 @@ export class AgentOs { serializePermissionsForSidecar(hostPermissions); const createVmConfig: CreateVmConfig = { env, + wasmBackend: options?.wasmBackend, database: options?.database, ...(options?.user ? { user: options.user } : {}), rootFilesystem: serializeRootFilesystemForSidecar( @@ -3121,7 +3128,13 @@ export class AgentOs { }, }); - return this._trackProcess(proc, command, args, outputHandlers, exitHandlers); + return this._trackProcess( + proc, + command, + args, + outputHandlers, + exitHandlers, + ); } /** Write data to a process's stdin. */ @@ -3139,7 +3152,10 @@ export class AgentOs { } /** Subscribe to stdout and stderr from a process. */ - onProcessOutput(pid: number, handler: (event: ProcessOutput) => void): () => void { + onProcessOutput( + pid: number, + handler: (event: ProcessOutput) => void, + ): () => void { const entry = this._processes.get(pid); if (!entry) throw new Error(`Process not found: ${pid}`); entry.outputHandlers.add(handler); @@ -3149,7 +3165,10 @@ export class AgentOs { } /** Subscribe to process exit. Returns an unsubscribe function. */ - onProcessExit(pid: number, handler: (event: ProcessExit) => void): () => void { + onProcessExit( + pid: number, + handler: (event: ProcessExit) => void, + ): () => void { const entry = this._processes.get(pid); if (!entry) throw new Error(`Process not found: ${pid}`); // If already exited, call immediately. @@ -3629,7 +3648,10 @@ export class AgentOs { } /** Subscribe to shell exit. */ - onShellExit(shellId: string, handler: (event: ShellExit) => void): () => void { + onShellExit( + shellId: string, + handler: (event: ShellExit) => void, + ): () => void { const entry = this._shells.get(shellId); if (!entry) { const exitCode = this._closedShellIds.get(shellId); @@ -3910,7 +3932,9 @@ export class AgentOs { } if (response.val.status === "accepted") { if (response.val.reason !== null) { - throw new Error("accepted permission response must not include a reason"); + throw new Error( + "accepted permission response must not include a reason", + ); } return { status: "accepted" }; } @@ -4047,9 +4071,7 @@ export class AgentOs { // openSession/listAgents from it). Nothing to record client-side. } - async listSoftware(): Promise< - { packageName: string; commands: string[] }[] - > { + async listSoftware(): Promise<{ packageName: string; commands: string[] }[]> { return this._sidecarClient.providedCommands( this._sidecarSession, this._sidecarVm, @@ -4192,9 +4214,7 @@ export class AgentOs { const event = decodeAcpEvent(envelope.payload); switch (event.tag) { case "AcpDurableSessionEvent": { - this._emitDurableSessionEvent( - decodeDurableSessionEvent(event.val), - ); + this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val)); return; } case "AcpEphemeralSessionUpdateEvent": { @@ -4684,12 +4704,14 @@ export class AgentOs { return { terminalId }; } - private _handleAcpWriteTerminal(params: Record): null { + private async _handleAcpWriteTerminal( + params: Record, + ): Promise { const method = "terminal/write"; const terminal = this._requireAcpTerminal(params, method); const data = this._requireAcpStringParam(params, "data", method); const encoding = this._optionalAcpStringParam(params, "encoding", method); - terminal.handle.write( + await terminal.handle.write( encoding === "base64" ? Buffer.from(data, "base64") : data, ); return null; diff --git a/packages/core/src/generated/WasmLimitsConfig.ts b/packages/core/src/generated/WasmLimitsConfig.ts index 7743ce23bd..216bc8234a 100644 --- a/packages/core/src/generated/WasmLimitsConfig.ts +++ b/packages/core/src/generated/WasmLimitsConfig.ts @@ -1,3 +1,13 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, activeCpuTimeLimitMs?: number, wallClockLimitMs?: number, deterministicFuel?: number, }; +export type WasmLimitsConfig = { + maxModuleFileBytes?: number; + capturedOutputLimitBytes?: number; + syncReadLimitBytes?: number; + prewarmTimeoutMs?: number; + runnerHeapLimitMb?: number; + activeCpuTimeLimitMs?: number; + wallClockLimitMs?: number; + deterministicFuel?: number; + maxThreads?: number; +}; diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts index 9992feabbc..cbd6d1edaf 100644 --- a/packages/core/src/options-schema.ts +++ b/packages/core/src/options-schema.ts @@ -376,6 +376,8 @@ export const agentOsLimitsSchema = z activeCpuTimeLimitMs: nonNegativeInteger.optional(), wallClockLimitMs: nonNegativeInteger.optional(), deterministicFuel: nonNegativeInteger.optional(), + maxThreads: positiveInteger.optional(), + maxConcurrentThreads: positiveInteger.optional(), }) .strict() .optional(), @@ -533,6 +535,7 @@ export const agentOsOptionFieldSchemas = { defaultSoftware: z.boolean().optional(), loopbackExemptPorts: z.array(z.number().int().min(0).max(65535)).optional(), allowedNodeBuiltins: stringArray.optional(), + wasmBackend: z.enum(["v8", "wasmtime", "wasmtime-threads"]).optional(), highResolutionTime: z.boolean().optional(), database: z .discriminatedUnion("type", [ diff --git a/packages/core/src/runtime-compat.ts b/packages/core/src/runtime-compat.ts index ad8cd34b6a..acf30fe38d 100644 --- a/packages/core/src/runtime-compat.ts +++ b/packages/core/src/runtime-compat.ts @@ -248,6 +248,8 @@ export interface OpenShellOptions { cwd?: string; cols?: number; rows?: number; + /** Engine affinity inherited by standalone WASM commands launched by the shell. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; /** Optional stderr-only diagnostic tap; do not render it alongside `onData`. */ onStderr?: (data: Uint8Array) => void; } @@ -268,7 +270,7 @@ export interface ExecOptions { cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; /** Selects standalone WASM commands only; JavaScript remains on V8. */ - wasmBackend?: "v8" | "wasmtime"; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; } export interface ExecResult { @@ -2104,6 +2106,7 @@ class NativeKernel implements Kernel { permissions?: Permissions; env?: Record; cwd?: string; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; mounts?: Array<{ @@ -2514,6 +2517,7 @@ class NativeKernel implements Kernel { const session = await client.authenticateAndOpenSession(); const createVmConfig: CreateVmConfig = { env: createVmEnv, + wasmBackend: this.options.wasmBackend, rootFilesystem, permissions: bootstrapPermissions ? serializePermissionsForSidecar(bootstrapPermissions) @@ -2605,6 +2609,7 @@ export function createKernel(options: { permissions?: Permissions; env?: Record; cwd?: string; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; maxProcesses?: number; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 06600c474c..b251c30841 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -143,6 +143,8 @@ export interface OpenShellOptions { cwd?: string; cols?: number; rows?: number; + /** Engine affinity inherited by standalone WASM commands launched by the shell. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; /** Optional stderr-only diagnostic tap; do not render it alongside `onData`. */ onStderr?: (data: Uint8Array) => void; } @@ -163,7 +165,7 @@ export interface ExecOptions { cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; /** Selects standalone WASM commands only; JavaScript remains on V8. */ - wasmBackend?: "v8" | "wasmtime"; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; } export interface ExecResult { diff --git a/packages/core/src/sidecar/rpc-client.ts b/packages/core/src/sidecar/rpc-client.ts index 4ad819b138..11d9ff799d 100644 --- a/packages/core/src/sidecar/rpc-client.ts +++ b/packages/core/src/sidecar/rpc-client.ts @@ -307,7 +307,7 @@ interface TrackedProcessEntry { driver: string; cwd: string; env: Record; - wasmBackend: "v8" | "wasmtime" | undefined; + wasmBackend: "v8" | "wasmtime" | "wasmtime-threads" | undefined; startTime: number; exitTime: number | null; hostPid: number | null; @@ -582,15 +582,15 @@ export class NativeSidecarKernelProxy { readExitCode?: () => Promise, ): Promise => { if (stdinOverride !== undefined) { - proc.writeStdin(stdinOverride); + await proc.writeStdin(stdinOverride); } else if (options?.stdin !== undefined) { - proc.writeStdin(options.stdin); + await proc.writeStdin(options.stdin); } // `kernel.exec()` is a non-interactive run-to-completion API: when the // caller does not opt into a streaming stdin handle, the guest process // should observe EOF after any provided input so commands like // `node -e ...` do not linger behind an inherited open stdin pipe. - proc.closeStdin(); + await proc.closeStdin(); const waitPromise = proc.wait(); const shellExitCode = @@ -679,9 +679,9 @@ export class NativeSidecarKernelProxy { proc: ManagedProcess, ): Promise => { if (options?.stdin !== undefined) { - proc.writeStdin(options.stdin); + await proc.writeStdin(options.stdin); } - proc.closeStdin(); + await proc.closeStdin(); const waitPromise = proc.wait(); const exitCode = @@ -1153,6 +1153,7 @@ export class NativeSidecarKernelProxy { { env: shellEnv, cwd: shellCwd, + wasmBackend: options?.wasmBackend, streamStdin: true, onStdout: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)), @@ -1174,6 +1175,7 @@ export class NativeSidecarKernelProxy { const result = await execCommand(nextCommand, { env: shellEnv, cwd: shellCwd, + wasmBackend: options?.wasmBackend, }); const sanitizedStdout = sanitizeSyntheticShellText( result.stdout, @@ -1226,6 +1228,7 @@ export class NativeSidecarKernelProxy { AGENTOS_EXEC_TTY: "1", }, cwd: options?.cwd, + wasmBackend: options?.wasmBackend, streamStdin: true, onStdout: (chunk) => { const sanitized = sanitizeNativeShellOutput(chunk); @@ -1323,7 +1326,12 @@ export class NativeSidecarKernelProxy { const restoreRawMode = stdin.isTTY && typeof stdin.setRawMode === "function"; const onStdinData = (data: Uint8Array | string) => { - shell.write(data); + void shell.write(data).catch((error) => { + console.error( + "ERR_AGENTOS_TERMINAL_STDIN: failed to forward terminal input", + error, + ); + }); }; const onResize = () => { shell.resize(stdout.columns, stdout.rows); @@ -1661,7 +1669,7 @@ export class NativeSidecarKernelProxy { } private async waitForMountReconfigure(): Promise { - if (this.mountReconfigurePromise) { + if (this.mountReconfigurePromise !== null) { await this.mountReconfigurePromise; } } @@ -1759,7 +1767,7 @@ export class NativeSidecarKernelProxy { } private async refreshProcessSnapshot(): Promise { - if (this.processSnapshotRefresh) { + if (this.processSnapshotRefresh !== null) { await this.processSnapshotRefresh; return; } diff --git a/packages/core/tests/helpers/default-vm-permissions.ts b/packages/core/tests/helpers/default-vm-permissions.ts index f18315c48e..ed4389c9fc 100644 --- a/packages/core/tests/helpers/default-vm-permissions.ts +++ b/packages/core/tests/helpers/default-vm-permissions.ts @@ -3,8 +3,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll } from "vitest"; import { - AgentOs, __disposeAllSharedSidecarsForTesting, + AgentOs, } from "../../src/agent-os.js"; import { ALLOW_ALL_VM_PERMISSIONS } from "./permissions.js"; @@ -14,6 +14,15 @@ const globalState = globalThis as typeof globalThis & { }; const databaseDirectories: string[] = []; +function configuredTestWasmBackend(): "v8" | "wasmtime" | undefined { + const backend = process.env.AGENTOS_TEST_WASM_BACKEND; + if (backend === undefined || backend === "") return undefined; + if (backend === "v8" || backend === "wasmtime") return backend; + throw new Error( + `AGENTOS_TEST_WASM_BACKEND must be "v8" or "wasmtime", got ${JSON.stringify(backend)}`, + ); +} + function testDatabase() { const directory = mkdtempSync(join(tmpdir(), "agentos-test-sqlite-")); databaseDirectories.push(directory); @@ -34,6 +43,7 @@ if (!globalState.__agentOsDefaultPermissionsPatched) { ...(options ?? {}), database: options?.database ?? testDatabase(), permissions: options?.permissions ?? ALLOW_ALL_VM_PERMISSIONS, + wasmBackend: options?.wasmBackend ?? configuredTestWasmBackend(), }); }) as typeof AgentOs.create; } diff --git a/packages/core/tests/options-schema.test.ts b/packages/core/tests/options-schema.test.ts index 591399d5b7..73b1482821 100644 --- a/packages/core/tests/options-schema.test.ts +++ b/packages/core/tests/options-schema.test.ts @@ -73,6 +73,32 @@ describe("AgentOsOptions validation", () => { ).toThrow(/runnerCpuTimeLimitMs/); }); + test("accepts distinct per-process and per-VM WASM thread limits", () => { + expect( + agentOsOptionsSchema.safeParse({ + limits: { + wasm: { maxThreads: 8, maxConcurrentThreads: 32 }, + }, + }).success, + ).toBe(true); + expect( + agentOsOptionsSchema.safeParse({ + limits: { wasm: { maxConcurrentThreads: 0 } }, + }).success, + ).toBe(false); + }); + + test("accepts only supported VM-wide standalone WASM backends", () => { + for (const wasmBackend of ["v8", "wasmtime", "wasmtime-threads"] as const) { + expect(agentOsOptionsSchema.safeParse({ wasmBackend }).success).toBe( + true, + ); + } + expect( + agentOsOptionsSchema.safeParse({ wasmBackend: "automatic" }).success, + ).toBe(false); + }); + test("bounds and materializes Linux account records", () => { const exactPasswdRecord = { uid: 0, diff --git a/packages/core/tests/wasm-backend-selectors.e2e.test.ts b/packages/core/tests/wasm-backend-selectors.e2e.test.ts new file mode 100644 index 0000000000..4c99a5b73e --- /dev/null +++ b/packages/core/tests/wasm-backend-selectors.e2e.test.ts @@ -0,0 +1,37 @@ +import { coreutils } from "@agentos-software/common"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { AgentOs } from "../src/index.js"; + +vi.setConfig({ testTimeout: 120_000 }); + +const backends = ["v8", "wasmtime", "wasmtime-threads"] as const; +const liveVms: AgentOs[] = []; + +afterEach(async () => { + await Promise.all(liveVms.splice(0).map((vm) => vm.dispose())); +}); + +describe("public standalone WASM backend selectors", () => { + test.each(backends)("executes shell children through %s", async (backend) => { + const vm = await AgentOs.create({ + wasmBackend: backend, + defaultSoftware: false, + software: [coreutils], + permissions: { + fs: "allow", + network: "allow", + childProcess: "allow", + process: "allow", + env: "allow", + binding: "allow", + }, + }); + liveVms.push(vm); + + const result = await vm.exec( + `printf 'selector-${backend}\\n' | tr '[:lower:]' '[:upper:]'`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe(`SELECTOR-${backend.toUpperCase()}\n`); + }); +}); diff --git a/packages/runtime-benchmarks/package.json b/packages/runtime-benchmarks/package.json index 1bdc9d3936..430b06d601 100644 --- a/packages/runtime-benchmarks/package.json +++ b/packages/runtime-benchmarks/package.json @@ -11,6 +11,9 @@ "bench:memory": "node --expose-gc --import tsx/esm memory.bench.ts", "bench:matrix": "tsx src/run-all.ts", "bench:wasm-backends": "tsx src/focused/wasm-backend-comparison.bench.ts", + "bench:wasm-threads": "tsx src/focused/wasmtime-threads.bench.ts", + "test:wasm-mixed-smoke": "AGENTOS_WASM_MIXED_SOAK_WARMUP=2 AGENTOS_WASM_MIXED_SOAK_CYCLES=6 tsx src/focused/wasm-mixed-soak.ts", + "test:wasm-mixed-soak": "AGENTOS_WASM_MIXED_SOAK_WARMUP=10 AGENTOS_WASM_MIXED_SOAK_CYCLES=200 tsx src/focused/wasm-mixed-soak.ts", "check-types": "pnpm --dir ../runtime-core build && tsc --noEmit" }, "dependencies": { diff --git a/packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json b/packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json new file mode 100644 index 0000000000..3108f78ff3 --- /dev/null +++ b/packages/runtime-benchmarks/results/wasm-backend-comparison-phase4.json @@ -0,0 +1,61444 @@ +{ + "metadata": { + "startedAt": "2026-07-21T09:07:02.379Z", + "hostname": "nathan-dev", + "platform": "linux", + "arch": "x64", + "cpuModel": "12th Gen Intel(R) Core(TM) i7-12700KF", + "logicalCpus": 20, + "totalMemoryBytes": 67170398208, + "kernel": "Linux 6.1.0-41-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.158-1 (2025-11-09) x86_64", + "node": "v24.17.0", + "sidecar": { + "path": "/home/nathan/.cache/agentos-wasmtime-rust194-phase4/release/agentos-native-sidecar", + "profile": "release", + "mtimeMs": 1784624816343.0898, + "mtimeIso": "2026-07-21T02:06:56.343-07:00", + "sizeBytes": 143480280 + }, + "commandsDir": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands", + "freshProcesses": 5, + "samplesPerProcess": 5, + "concurrencyLevels": [ + 1, + 10, + 50, + 100, + 200 + ], + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "pooling": false, + "aot": false, + "wizer": false, + "liveSnapshots": false + }, + "modules": [ + { + "command": "basename", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/basename", + "bytes": 517345, + "sha256": "4925e6fd365a649a8ae2defb56efb793b37046ad9e4df093f74a3f189cfc428e" + }, + { + "command": "curl", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/curl", + "bytes": 1561500, + "sha256": "5d44e7e68808294b7a155a6ae3c030b02adbf8969657b0ac41802d43b2ea6444" + }, + { + "command": "date", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/date", + "bytes": 2618604, + "sha256": "f49cbb0b6b9aedd645900c41b121485582d72944291a90db385618cbd9f27f76" + }, + { + "command": "dirname", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/dirname", + "bytes": 498912, + "sha256": "773449f7d0723bac7355b6d4ff4149707b877b98f2fdfab9fc6af160ff20a701" + }, + { + "command": "find", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/find", + "bytes": 1509578, + "sha256": "79174d7fa9bbf2a72747a71cc3b1e113a0a1768e95d456c7813e39a7559a1e25" + }, + { + "command": "git", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/git", + "bytes": 3397393, + "sha256": "0491fbbbfcf192e28872c1fe37819861fd54c4f8cb18f28e5ddf8a5297081d2e" + }, + { + "command": "id", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/id", + "bytes": 57250, + "sha256": "1d56d6c9b893f89919b17acc491579ee5f216e3b706ed009308eac594b80ac26" + }, + { + "command": "ls", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/ls", + "bytes": 1208565, + "sha256": "86da1fbf155760c33aa2519e058b6ea95266c9239def7f72faa136b1dcbc9b4f" + }, + { + "command": "printf", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/printf", + "bytes": 652796, + "sha256": "e8a170ba2b94f8f906c2a69b355d13a45f3ef9006143ed63f6994b6f22a8c01a" + }, + { + "command": "pwd", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/pwd", + "bytes": 501598, + "sha256": "40bf2b78bd7c081b6fbafb26c6641404f6062f8fd065e85dcf217b98e269ee86" + }, + { + "command": "sh", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sh", + "bytes": 3082692, + "sha256": "25042baa43977d9b31e302d6a008e172d722f8d9df83b6cab9d84db71618713a" + }, + { + "command": "sha256sum", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sha256sum", + "bytes": 1349812, + "sha256": "7e9fca50d94a69d0854f3092c33565ff0f82af80320f732889623b091e4a73fd" + }, + { + "command": "sqlite3", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/sqlite3", + "bytes": 878882, + "sha256": "3f87de5e79cf6cc55eac3e3eeb643f2522b838c1829d7644ab13afd7d917d881" + }, + { + "command": "true", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/true", + "bytes": 28203, + "sha256": "d8cee1b90b65bd7571197ac6fde57f46d538bea6c1b09a1ac404b5368631b2bc" + }, + { + "command": "uname", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/uname", + "bytes": 502129, + "sha256": "70bf67158f10102c5af11b7e6cf7ba5d937b0a9a49ddad4e05bf3fc80726606e" + }, + { + "command": "vim", + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/packages/runtime-core/commands/vim", + "bytes": 2854951, + "sha256": "d1db095826460b79b4970bebe4d3bb1d4b754ba82a93a36a207f37e4dc053121" + } + ], + "fresh": [ + { + "backend": "v8", + "processIndex": 0, + "vmSetupMs": 503.91885599999995, + "fixtureSetupMs": 443.4107650000001, + "baseline": { + "rssBytes": 238604288, + "peakRssBytes": 244019200, + "pssBytes": 239974400, + "virtualBytes": 3885953024, + "minorFaults": 68477, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353337344, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 369057792, + "peakRssBytes": 486854656, + "pssBytes": 370748416, + "virtualBytes": 4119547904, + "minorFaults": 1678362, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 130453504, + "peakRssBytes": 242835456, + "pssBytes": 130774016, + "virtualBytes": 233594880, + "minorFaults": 1609885, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 84.92550200000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 3.090088 + }, + { + "name": "WebAssembly.Module", + "ms": 0.267919 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.097296 + }, + { + "name": "wasi.start", + "ms": 0.115672 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 238604288, + "peakRssBytes": 244019200, + "pssBytes": 239982592, + "virtualBytes": 3888066560, + "minorFaults": 68479, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 265924608, + "peakRssBytes": 266506240, + "pssBytes": 255642624, + "virtualBytes": 4641595392, + "minorFaults": 78563, + "majorFaults": 0 + }, + "end": { + "rssBytes": 254435328, + "peakRssBytes": 266506240, + "pssBytes": 255642624, + "virtualBytes": 3957276672, + "minorFaults": 78563, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 238604288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 254435328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 79.34596999999985, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.099161 + }, + { + "name": "WebAssembly.Module", + "ms": 0.166594 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.072092 + }, + { + "name": "wasi.start", + "ms": 0.085215 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 254435328, + "peakRssBytes": 266506240, + "pssBytes": 255642624, + "virtualBytes": 3957276672, + "minorFaults": 78563, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 271781888, + "peakRssBytes": 271966208, + "pssBytes": 273177600, + "virtualBytes": 4642381824, + "minorFaults": 85999, + "majorFaults": 0 + }, + "end": { + "rssBytes": 259084288, + "peakRssBytes": 271966208, + "pssBytes": 260447232, + "virtualBytes": 3957276672, + "minorFaults": 85999, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 254435328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 259084288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 75.36980100000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.585979 + }, + { + "name": "WebAssembly.Module", + "ms": 1.249938 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.066103 + }, + { + "name": "wasi.start", + "ms": 0.088594 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 259084288, + "peakRssBytes": 271966208, + "pssBytes": 260447232, + "virtualBytes": 3957276672, + "minorFaults": 85999, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 272490496, + "peakRssBytes": 272748544, + "pssBytes": 273980416, + "virtualBytes": 4642525184, + "minorFaults": 92465, + "majorFaults": 0 + }, + "end": { + "rssBytes": 259866624, + "peakRssBytes": 272748544, + "pssBytes": 261286912, + "virtualBytes": 3957276672, + "minorFaults": 92465, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 259084288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 259866624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 81.99911700000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.068608 + }, + { + "name": "WebAssembly.Module", + "ms": 0.147424 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.075093 + }, + { + "name": "wasi.start", + "ms": 0.096592 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 259866624, + "peakRssBytes": 272748544, + "pssBytes": 261286912, + "virtualBytes": 3957276672, + "minorFaults": 92465, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 279138304, + "peakRssBytes": 279343104, + "pssBytes": 273861632, + "virtualBytes": 4708179968, + "minorFaults": 100834, + "majorFaults": 0 + }, + "end": { + "rssBytes": 268353536, + "peakRssBytes": 279343104, + "pssBytes": 269921280, + "virtualBytes": 4024385536, + "minorFaults": 100834, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 259866624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 268353536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 98.18688300000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.142246 + }, + { + "name": "WebAssembly.Module", + "ms": 0.169458 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.067422 + }, + { + "name": "wasi.start", + "ms": 0.135561 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 268353536, + "peakRssBytes": 279343104, + "pssBytes": 269921280, + "virtualBytes": 4024385536, + "minorFaults": 100834, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 283959296, + "peakRssBytes": 284459008, + "pssBytes": 273311744, + "virtualBytes": 4708442112, + "minorFaults": 107926, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271695872, + "peakRssBytes": 284459008, + "pssBytes": 273311744, + "virtualBytes": 4024385536, + "minorFaults": 107926, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 268353536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271695872, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 330.21409300000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 40.333092 + }, + { + "name": "WebAssembly.Module", + "ms": 2.929449 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.515134 + }, + { + "name": "wasi.start", + "ms": 136.01695 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271695872, + "peakRssBytes": 284459008, + "pssBytes": 273311744, + "virtualBytes": 4024385536, + "minorFaults": 107926, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 340250624, + "peakRssBytes": 341479424, + "pssBytes": 337021952, + "virtualBytes": 4763549696, + "minorFaults": 136891, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290131968, + "peakRssBytes": 341479424, + "pssBytes": 241154048, + "virtualBytes": 4565725184, + "minorFaults": 136891, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271695872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290594816, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 382.93910600000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 59.088239 + }, + { + "name": "WebAssembly.Module", + "ms": 5.849621 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.172427 + }, + { + "name": "wasi.start", + "ms": 144.190103 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 291135488, + "peakRssBytes": 341479424, + "pssBytes": 110179328, + "virtualBytes": 4024385536, + "minorFaults": 137131, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 348798976, + "peakRssBytes": 348798976, + "pssBytes": 346958848, + "virtualBytes": 4763693056, + "minorFaults": 163654, + "majorFaults": 0 + }, + "end": { + "rssBytes": 299458560, + "peakRssBytes": 348798976, + "pssBytes": 113828864, + "virtualBytes": 4024385536, + "minorFaults": 163654, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290865152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 299728896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 246.49008200000026, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.155793 + }, + { + "name": "WebAssembly.Module", + "ms": 1.449001 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.30862 + }, + { + "name": "wasi.start", + "ms": 94.998766 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 299999232, + "peakRssBytes": 348798976, + "pssBytes": 202830848, + "virtualBytes": 4024385536, + "minorFaults": 163779, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 357355520, + "peakRssBytes": 357355520, + "pssBytes": 358521856, + "virtualBytes": 4762763264, + "minorFaults": 189602, + "majorFaults": 0 + }, + "end": { + "rssBytes": 304795648, + "peakRssBytes": 357355520, + "pssBytes": 306190336, + "virtualBytes": 4024385536, + "minorFaults": 189602, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 299728896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304795648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 284.395974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 40.551205 + }, + { + "name": "WebAssembly.Module", + "ms": 1.711905 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.369715 + }, + { + "name": "wasi.start", + "ms": 104.89788 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 304795648, + "peakRssBytes": 357355520, + "pssBytes": 306190336, + "virtualBytes": 4024385536, + "minorFaults": 189602, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 362160128, + "peakRssBytes": 362700800, + "pssBytes": 358484992, + "virtualBytes": 4762763264, + "minorFaults": 216045, + "majorFaults": 0 + }, + "end": { + "rssBytes": 312479744, + "peakRssBytes": 362700800, + "pssBytes": 152206336, + "virtualBytes": 4024385536, + "minorFaults": 216045, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304795648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 312750080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 280.0534469999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.345138 + }, + { + "name": "WebAssembly.Module", + "ms": 3.29068 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.653996 + }, + { + "name": "wasi.start", + "ms": 100.123256 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 313020416, + "peakRssBytes": 362700800, + "pssBytes": 58444800, + "virtualBytes": 4024385536, + "minorFaults": 216215, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 372756480, + "peakRssBytes": 372756480, + "pssBytes": 368988160, + "virtualBytes": 4762906624, + "minorFaults": 242709, + "majorFaults": 0 + }, + "end": { + "rssBytes": 345194496, + "peakRssBytes": 372756480, + "pssBytes": 104585216, + "virtualBytes": 4597673984, + "minorFaults": 242709, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 313020416, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 322445312, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 410.996345, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.259642 + }, + { + "name": "WebAssembly.Module", + "ms": 6.736529 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.788872 + }, + { + "name": "wasi.start", + "ms": 137.018745 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 321867776, + "peakRssBytes": 372756480, + "pssBytes": 56103936, + "virtualBytes": 4024385536, + "minorFaults": 242904, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429051904, + "peakRssBytes": 429051904, + "pssBytes": 430320640, + "virtualBytes": 5470597120, + "minorFaults": 303451, + "majorFaults": 0 + }, + "end": { + "rssBytes": 335351808, + "peakRssBytes": 429051904, + "pssBytes": 337996800, + "virtualBytes": 4028755968, + "minorFaults": 303451, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 321875968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335351808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 437.6196759999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 97.195868 + }, + { + "name": "WebAssembly.Module", + "ms": 5.257225 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.144088 + }, + { + "name": "wasi.start", + "ms": 125.124419 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 335351808, + "peakRssBytes": 429051904, + "pssBytes": 337996800, + "virtualBytes": 4028755968, + "minorFaults": 303451, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431607808, + "peakRssBytes": 432062464, + "pssBytes": 433544192, + "virtualBytes": 5539807232, + "minorFaults": 363948, + "majorFaults": 0 + }, + "end": { + "rssBytes": 349818880, + "peakRssBytes": 432062464, + "pssBytes": 352455680, + "virtualBytes": 4097966080, + "minorFaults": 363948, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 335351808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349818880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 485.10953599999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 76.921387 + }, + { + "name": "WebAssembly.Module", + "ms": 5.816064 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.816921 + }, + { + "name": "wasi.start", + "ms": 156.872263 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349818880, + "peakRssBytes": 432062464, + "pssBytes": 352455680, + "virtualBytes": 4097966080, + "minorFaults": 363948, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455610368, + "peakRssBytes": 455749632, + "pssBytes": 458000384, + "virtualBytes": 5540474880, + "minorFaults": 424109, + "majorFaults": 0 + }, + "end": { + "rssBytes": 363003904, + "peakRssBytes": 455749632, + "pssBytes": 365550592, + "virtualBytes": 4097966080, + "minorFaults": 424109, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349818880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363003904, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 486.5630520000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 123.167068 + }, + { + "name": "WebAssembly.Module", + "ms": 5.910622 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.926657 + }, + { + "name": "wasi.start", + "ms": 153.41648 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 363003904, + "peakRssBytes": 455749632, + "pssBytes": 365550592, + "virtualBytes": 4097966080, + "minorFaults": 424109, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 453365760, + "peakRssBytes": 455749632, + "pssBytes": 455178240, + "virtualBytes": 5540212736, + "minorFaults": 481152, + "majorFaults": 0 + }, + "end": { + "rssBytes": 362971136, + "peakRssBytes": 455749632, + "pssBytes": 365636608, + "virtualBytes": 4097966080, + "minorFaults": 481152, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363003904, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362971136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 450.86133900000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 92.385408 + }, + { + "name": "WebAssembly.Module", + "ms": 3.683395 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.470888 + }, + { + "name": "wasi.start", + "ms": 133.951679 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 362971136, + "peakRssBytes": 455749632, + "pssBytes": 365636608, + "virtualBytes": 4097966080, + "minorFaults": 481152, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 455569408, + "peakRssBytes": 455786496, + "pssBytes": 456472576, + "virtualBytes": 5540069376, + "minorFaults": 537607, + "majorFaults": 0 + }, + "end": { + "rssBytes": 363225088, + "peakRssBytes": 455786496, + "pssBytes": 365763584, + "virtualBytes": 4097966080, + "minorFaults": 537607, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 362971136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363225088, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 280.14638899999954, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 70.922011 + }, + { + "name": "WebAssembly.Module", + "ms": 3.893918 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.372497 + }, + { + "name": "wasi.start", + "ms": 36.63264 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 363225088, + "peakRssBytes": 455786496, + "pssBytes": 365763584, + "virtualBytes": 4097966080, + "minorFaults": 537607, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431058944, + "peakRssBytes": 455786496, + "pssBytes": 433137664, + "virtualBytes": 4856045568, + "minorFaults": 569900, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364343296, + "peakRssBytes": 455786496, + "pssBytes": 366434304, + "virtualBytes": 4097966080, + "minorFaults": 569900, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 363225088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364343296, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 235.65659600000072, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.655193 + }, + { + "name": "WebAssembly.Module", + "ms": 2.706842 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.417895 + }, + { + "name": "wasi.start", + "ms": 30.791619 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364343296, + "peakRssBytes": 455786496, + "pssBytes": 366434304, + "virtualBytes": 4097966080, + "minorFaults": 569900, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430813184, + "peakRssBytes": 455786496, + "pssBytes": 433138688, + "virtualBytes": 4855783424, + "minorFaults": 602178, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364085248, + "peakRssBytes": 455786496, + "pssBytes": 366435328, + "virtualBytes": 4097966080, + "minorFaults": 602178, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364343296, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364085248, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 279.01530399999956, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 78.899695 + }, + { + "name": "WebAssembly.Module", + "ms": 2.332815 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.654022 + }, + { + "name": "wasi.start", + "ms": 39.293707 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364085248, + "peakRssBytes": 455786496, + "pssBytes": 366435328, + "virtualBytes": 4097966080, + "minorFaults": 602178, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431108096, + "peakRssBytes": 455786496, + "pssBytes": 433142784, + "virtualBytes": 4856045568, + "minorFaults": 634460, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364355584, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 634460, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364085248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364355584, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 224.4800150000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 50.795752 + }, + { + "name": "WebAssembly.Module", + "ms": 4.095763 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.261786 + }, + { + "name": "wasi.start", + "ms": 29.139245 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364355584, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 634460, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431079424, + "peakRssBytes": 455786496, + "pssBytes": 432991232, + "virtualBytes": 4855783424, + "minorFaults": 666738, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364371968, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 666738, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364355584, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364371968, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 270.1559960000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 64.692484 + }, + { + "name": "WebAssembly.Module", + "ms": 3.819485 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.893746 + }, + { + "name": "wasi.start", + "ms": 36.217642 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364371968, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 666738, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430800896, + "peakRssBytes": 455786496, + "pssBytes": 432970752, + "virtualBytes": 4855783424, + "minorFaults": 699017, + "majorFaults": 0 + }, + "end": { + "rssBytes": 364048384, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 699017, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364371968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364048384, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 159.63220499999989, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.345122 + }, + { + "name": "WebAssembly.Module", + "ms": 1.483795 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.961518 + }, + { + "name": "wasi.start", + "ms": 17.259656 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364048384, + "peakRssBytes": 455786496, + "pssBytes": 366443520, + "virtualBytes": 4097966080, + "minorFaults": 699017, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 401272832, + "peakRssBytes": 455786496, + "pssBytes": 403581952, + "virtualBytes": 4825337856, + "minorFaults": 719330, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374042624, + "peakRssBytes": 455786496, + "pssBytes": 320484352, + "virtualBytes": 4097966080, + "minorFaults": 719330, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364048384, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374583296, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 224.10263599999962, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 50.173548 + }, + { + "name": "WebAssembly.Module", + "ms": 5.236612 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.998689 + }, + { + "name": "wasi.start", + "ms": 19.445086 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375664640, + "peakRssBytes": 455786496, + "pssBytes": 320440320, + "virtualBytes": 4097966080, + "minorFaults": 719757, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425328640, + "peakRssBytes": 455786496, + "pssBytes": 412862464, + "virtualBytes": 4824813568, + "minorFaults": 746230, + "majorFaults": 0 + }, + "end": { + "rssBytes": 381353984, + "peakRssBytes": 455786496, + "pssBytes": 327773184, + "virtualBytes": 4097966080, + "minorFaults": 746230, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375394304, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 382164992, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 224.1402509999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.266395 + }, + { + "name": "WebAssembly.Module", + "ms": 2.721631 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.00339 + }, + { + "name": "wasi.start", + "ms": 18.99239 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 382435328, + "peakRssBytes": 455786496, + "pssBytes": 119638016, + "virtualBytes": 4097966080, + "minorFaults": 746423, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 437792768, + "peakRssBytes": 455786496, + "pssBytes": 439225344, + "virtualBytes": 4825481216, + "minorFaults": 770544, + "majorFaults": 0 + }, + "end": { + "rssBytes": 379006976, + "peakRssBytes": 455786496, + "pssBytes": 214697984, + "virtualBytes": 4097966080, + "minorFaults": 770544, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 382435328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 379547648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 240.27712499999961, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.452827 + }, + { + "name": "WebAssembly.Module", + "ms": 3.312906 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.7722 + }, + { + "name": "wasi.start", + "ms": 20.928466 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 379817984, + "peakRssBytes": 455786496, + "pssBytes": 329700352, + "virtualBytes": 4097966080, + "minorFaults": 770786, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423239680, + "peakRssBytes": 455786496, + "pssBytes": 413341696, + "virtualBytes": 4825219072, + "minorFaults": 797238, + "majorFaults": 0 + }, + "end": { + "rssBytes": 385777664, + "peakRssBytes": 455786496, + "pssBytes": 389118976, + "virtualBytes": 4097966080, + "minorFaults": 797238, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 379817984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 386859008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 249.06565800000044, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 34.350088 + }, + { + "name": "WebAssembly.Module", + "ms": 1.494649 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.321886 + }, + { + "name": "wasi.start", + "ms": 25.318172 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 386859008, + "peakRssBytes": 455786496, + "pssBytes": 389118976, + "virtualBytes": 4097966080, + "minorFaults": 797507, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 430579712, + "peakRssBytes": 455786496, + "pssBytes": 426267648, + "virtualBytes": 4825481216, + "minorFaults": 817701, + "majorFaults": 0 + }, + "end": { + "rssBytes": 367427584, + "peakRssBytes": 455786496, + "pssBytes": 314324992, + "virtualBytes": 4638781440, + "minorFaults": 817701, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 386859008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367357952, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 458.1513100000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 146.497464 + }, + { + "name": "WebAssembly.Module", + "ms": 5.95466 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.414266 + }, + { + "name": "wasi.start", + "ms": 24.517465 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 367357952, + "peakRssBytes": 455786496, + "pssBytes": 369425408, + "virtualBytes": 4097966080, + "minorFaults": 817702, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 437420032, + "peakRssBytes": 455786496, + "pssBytes": 439404544, + "virtualBytes": 4836765696, + "minorFaults": 865377, + "majorFaults": 0 + }, + "end": { + "rssBytes": 349102080, + "peakRssBytes": 455786496, + "pssBytes": 351144960, + "virtualBytes": 4097966080, + "minorFaults": 865377, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367357952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349102080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 367.9585110000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 90.251208 + }, + { + "name": "WebAssembly.Module", + "ms": 6.433116 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.713393 + }, + { + "name": "wasi.start", + "ms": 70.919074 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349102080, + "peakRssBytes": 455786496, + "pssBytes": 351144960, + "virtualBytes": 4097966080, + "minorFaults": 865377, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 440938496, + "peakRssBytes": 455786496, + "pssBytes": 442160128, + "virtualBytes": 4889448448, + "minorFaults": 910543, + "majorFaults": 0 + }, + "end": { + "rssBytes": 349306880, + "peakRssBytes": 455786496, + "pssBytes": 351374336, + "virtualBytes": 4097966080, + "minorFaults": 910543, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349102080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349306880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 341.8429560000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.014622 + }, + { + "name": "WebAssembly.Module", + "ms": 3.834693 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.429821 + }, + { + "name": "wasi.start", + "ms": 64.404355 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 349306880, + "peakRssBytes": 455786496, + "pssBytes": 351374336, + "virtualBytes": 4097966080, + "minorFaults": 910543, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 440885248, + "peakRssBytes": 455786496, + "pssBytes": 443340800, + "virtualBytes": 4888924160, + "minorFaults": 955689, + "majorFaults": 0 + }, + "end": { + "rssBytes": 348889088, + "peakRssBytes": 455786496, + "pssBytes": 351393792, + "virtualBytes": 4097966080, + "minorFaults": 955689, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 349306880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 348889088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 328.4801700000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.817389 + }, + { + "name": "WebAssembly.Module", + "ms": 4.858605 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.928842 + }, + { + "name": "wasi.start", + "ms": 56.132484 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 348889088, + "peakRssBytes": 455786496, + "pssBytes": 351393792, + "virtualBytes": 4097966080, + "minorFaults": 955689, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 441319424, + "peakRssBytes": 455786496, + "pssBytes": 442211328, + "virtualBytes": 4888662016, + "minorFaults": 1001401, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351498240, + "peakRssBytes": 455786496, + "pssBytes": 353717248, + "virtualBytes": 4097966080, + "minorFaults": 1001401, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 348889088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351498240, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 372.48869600000035, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 79.633223 + }, + { + "name": "WebAssembly.Module", + "ms": 5.078883 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.430157 + }, + { + "name": "wasi.start", + "ms": 47.553888 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351498240, + "peakRssBytes": 455786496, + "pssBytes": 353717248, + "virtualBytes": 4097966080, + "minorFaults": 1001401, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 442302464, + "peakRssBytes": 455786496, + "pssBytes": 444351488, + "virtualBytes": 4836622336, + "minorFaults": 1046579, + "majorFaults": 0 + }, + "end": { + "rssBytes": 351694848, + "peakRssBytes": 455786496, + "pssBytes": 353717248, + "virtualBytes": 4097966080, + "minorFaults": 1046579, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351498240, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351694848, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 403.6235549999983, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 129.365919 + }, + { + "name": "WebAssembly.Module", + "ms": 6.1033 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.957588 + }, + { + "name": "wasi.start", + "ms": 5.240922 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351694848, + "peakRssBytes": 455786496, + "pssBytes": 353717248, + "virtualBytes": 4097966080, + "minorFaults": 1046579, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483672064, + "peakRssBytes": 483692544, + "pssBytes": 485641216, + "virtualBytes": 4879130624, + "minorFaults": 1111182, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353820672, + "peakRssBytes": 483692544, + "pssBytes": 355884032, + "virtualBytes": 4098949120, + "minorFaults": 1111182, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351694848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353820672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 355.5160329999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 120.162314 + }, + { + "name": "WebAssembly.Module", + "ms": 7.120345 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.128602 + }, + { + "name": "wasi.start", + "ms": 7.271341 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353820672, + "peakRssBytes": 483692544, + "pssBytes": 355884032, + "virtualBytes": 4098949120, + "minorFaults": 1111182, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 446296064, + "peakRssBytes": 483692544, + "pssBytes": 448391168, + "virtualBytes": 4880142336, + "minorFaults": 1175703, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355762176, + "peakRssBytes": 483692544, + "pssBytes": 357754880, + "virtualBytes": 4099960832, + "minorFaults": 1175703, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353820672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355762176, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 377.3414270000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 111.92525 + }, + { + "name": "WebAssembly.Module", + "ms": 8.091943 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.352034 + }, + { + "name": "wasi.start", + "ms": 5.417088 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355762176, + "peakRssBytes": 483692544, + "pssBytes": 357754880, + "virtualBytes": 4099960832, + "minorFaults": 1175703, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 486825984, + "peakRssBytes": 486854656, + "pssBytes": 488668160, + "virtualBytes": 4879998976, + "minorFaults": 1240035, + "majorFaults": 0 + }, + "end": { + "rssBytes": 357662720, + "peakRssBytes": 486854656, + "pssBytes": 62002176, + "virtualBytes": 4653096960, + "minorFaults": 1240035, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355762176, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357027840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 382.8811540000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 119.387985 + }, + { + "name": "WebAssembly.Module", + "ms": 4.360659 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.648561 + }, + { + "name": "wasi.start", + "ms": 6.609396 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356999168, + "peakRssBytes": 486854656, + "pssBytes": 358868992, + "virtualBytes": 4099960832, + "minorFaults": 1240035, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 486756352, + "peakRssBytes": 486854656, + "pssBytes": 488868864, + "virtualBytes": 4879998976, + "minorFaults": 1304132, + "majorFaults": 0 + }, + "end": { + "rssBytes": 357015552, + "peakRssBytes": 486854656, + "pssBytes": 358964224, + "virtualBytes": 4099960832, + "minorFaults": 1304132, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356999168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357015552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 344.80199699999866, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 102.139081 + }, + { + "name": "WebAssembly.Module", + "ms": 6.648171 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.924837 + }, + { + "name": "wasi.start", + "ms": 8.978097 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 357015552, + "peakRssBytes": 486854656, + "pssBytes": 358964224, + "virtualBytes": 4099960832, + "minorFaults": 1304132, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 447991808, + "peakRssBytes": 486854656, + "pssBytes": 449595392, + "virtualBytes": 4880142336, + "minorFaults": 1366659, + "majorFaults": 0 + }, + "end": { + "rssBytes": 357236736, + "peakRssBytes": 486854656, + "pssBytes": 358964224, + "virtualBytes": 4099960832, + "minorFaults": 1366659, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357015552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357236736, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 364.35553699999946, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.685137 + }, + { + "name": "WebAssembly.Module", + "ms": 4.196346 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.606078 + }, + { + "name": "wasi.start", + "ms": 151.304457 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 357236736, + "peakRssBytes": 486854656, + "pssBytes": 358964224, + "virtualBytes": 4099960832, + "minorFaults": 1366659, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413949952, + "peakRssBytes": 486854656, + "pssBytes": 409802752, + "virtualBytes": 4837257216, + "minorFaults": 1391795, + "majorFaults": 0 + }, + "end": { + "rssBytes": 380715008, + "peakRssBytes": 486854656, + "pssBytes": 327501824, + "virtualBytes": 4692398080, + "minorFaults": 1391795, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357236736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358973440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 355.79749600000105, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.843521 + }, + { + "name": "WebAssembly.Module", + "ms": 4.970359 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.447882 + }, + { + "name": "wasi.start", + "ms": 90.512903 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351137792, + "peakRssBytes": 486854656, + "pssBytes": 296412160, + "virtualBytes": 4662558720, + "minorFaults": 1391795, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414187520, + "peakRssBytes": 486854656, + "pssBytes": 416037888, + "virtualBytes": 4837781504, + "minorFaults": 1421641, + "majorFaults": 0 + }, + "end": { + "rssBytes": 340094976, + "peakRssBytes": 486854656, + "pssBytes": 341875712, + "virtualBytes": 4100194304, + "minorFaults": 1421641, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351137792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 340094976, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 377.56164700000045, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 53.650466 + }, + { + "name": "WebAssembly.Module", + "ms": 2.176215 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.0513 + }, + { + "name": "wasi.start", + "ms": 84.586194 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 340094976, + "peakRssBytes": 486854656, + "pssBytes": 341875712, + "virtualBytes": 4100194304, + "minorFaults": 1421641, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414060544, + "peakRssBytes": 486854656, + "pssBytes": 414759936, + "virtualBytes": 4837662720, + "minorFaults": 1451479, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339955712, + "peakRssBytes": 486854656, + "pssBytes": 341875712, + "virtualBytes": 4100194304, + "minorFaults": 1451479, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 340094976, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339955712, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 295.81161299999985, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.065531 + }, + { + "name": "WebAssembly.Module", + "ms": 4.519325 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.953724 + }, + { + "name": "wasi.start", + "ms": 86.573616 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339955712, + "peakRssBytes": 486854656, + "pssBytes": 341875712, + "virtualBytes": 4100194304, + "minorFaults": 1451479, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413949952, + "peakRssBytes": 486854656, + "pssBytes": 415125504, + "virtualBytes": 4837662720, + "minorFaults": 1481317, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339873792, + "peakRssBytes": 486854656, + "pssBytes": 341874688, + "virtualBytes": 4100194304, + "minorFaults": 1481317, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339955712, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339873792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 320.79443999999967, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.088147 + }, + { + "name": "WebAssembly.Module", + "ms": 8.260452 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.436942 + }, + { + "name": "wasi.start", + "ms": 92.310239 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339873792, + "peakRssBytes": 486854656, + "pssBytes": 341874688, + "virtualBytes": 4100194304, + "minorFaults": 1481317, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 414224384, + "peakRssBytes": 486854656, + "pssBytes": 414927872, + "virtualBytes": 4837400576, + "minorFaults": 1511154, + "majorFaults": 0 + }, + "end": { + "rssBytes": 340127744, + "peakRssBytes": 486854656, + "pssBytes": 341896192, + "virtualBytes": 4100194304, + "minorFaults": 1511154, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339873792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 340127744, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 249.80005299999902, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.023672 + }, + { + "name": "WebAssembly.Module", + "ms": 4.241428 + }, + { + "name": "WebAssembly.Instance", + "ms": 5.743819 + }, + { + "name": "wasi.start", + "ms": 25.944144 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 340127744, + "peakRssBytes": 486854656, + "pssBytes": 341896192, + "virtualBytes": 4100194304, + "minorFaults": 1511154, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419127296, + "peakRssBytes": 486854656, + "pssBytes": 421018624, + "virtualBytes": 4858494976, + "minorFaults": 1545976, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353038336, + "peakRssBytes": 486854656, + "pssBytes": 355023872, + "virtualBytes": 4101701632, + "minorFaults": 1545976, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 340127744, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353038336, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 209.97542499999872, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.010274 + }, + { + "name": "WebAssembly.Module", + "ms": 1.806547 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.280143 + }, + { + "name": "wasi.start", + "ms": 14.237094 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353038336, + "peakRssBytes": 486854656, + "pssBytes": 355023872, + "virtualBytes": 4101701632, + "minorFaults": 1545976, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419287040, + "peakRssBytes": 486854656, + "pssBytes": 421379072, + "virtualBytes": 4858241024, + "minorFaults": 1578173, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353300480, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1578173, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353038336, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353300480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 205.3991449999994, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 45.436979 + }, + { + "name": "WebAssembly.Module", + "ms": 3.662069 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.952681 + }, + { + "name": "wasi.start", + "ms": 16.902779 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353300480, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1578173, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419192832, + "peakRssBytes": 486854656, + "pssBytes": 421239808, + "virtualBytes": 4857835520, + "minorFaults": 1610304, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353189888, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1610304, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353300480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353189888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 240.03207800000018, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.759255 + }, + { + "name": "WebAssembly.Module", + "ms": 5.088882 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.891692 + }, + { + "name": "wasi.start", + "ms": 25.031383 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 353189888, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1610304, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 418934784, + "peakRssBytes": 486854656, + "pssBytes": 421379072, + "virtualBytes": 4858359808, + "minorFaults": 1642436, + "majorFaults": 0 + }, + "end": { + "rssBytes": 352976896, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1642436, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353189888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352976896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 337.2799219999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624822457415921/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 95.952037 + }, + { + "name": "WebAssembly.Module", + "ms": 2.173371 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.457718 + }, + { + "name": "wasi.start", + "ms": 24.634651 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352976896, + "peakRssBytes": 486854656, + "pssBytes": 355294208, + "virtualBytes": 4101967872, + "minorFaults": 1642436, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419328000, + "peakRssBytes": 486854656, + "pssBytes": 421386240, + "virtualBytes": 4858765312, + "minorFaults": 1674575, + "majorFaults": 0 + }, + "end": { + "rssBytes": 353337344, + "peakRssBytes": 486854656, + "pssBytes": 355293184, + "virtualBytes": 4101967872, + "minorFaults": 1674575, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352976896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 353337344, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 0, + "vmSetupMs": 500.1549990000003, + "fixtureSetupMs": 438.9555970000001, + "baseline": { + "rssBytes": 238358528, + "peakRssBytes": 243752960, + "pssBytes": 239708160, + "virtualBytes": 3885953024, + "minorFaults": 68455, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 495353856, + "peakRssBytes": 516235264, + "pssBytes": 497301504, + "virtualBytes": 4202614784, + "minorFaults": 157549, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 256995328, + "peakRssBytes": 272482304, + "pssBytes": 257593344, + "virtualBytes": 316661760, + "minorFaults": 89094, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 81.32793099999981, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.127043, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.114987, + "name": "Engine" + }, + { + "ms": 0.20643899999999998, + "name": "canonicalPreopens" + }, + { + "ms": 2.966456, + "name": "moduleRead" + }, + { + "ms": 0.21342, + "name": "profileValidation" + }, + { + "ms": 55.603871999999996, + "name": "moduleCompile" + }, + { + "ms": 0.005603, + "name": "importValidation" + }, + { + "ms": 0.214906, + "name": "Linker" + }, + { + "ms": 0.027317, + "name": "Store" + }, + { + "ms": 0.054918, + "name": "Instance" + }, + { + "ms": 0.072982, + "name": "signalMaskInit" + }, + { + "ms": 0.0043159999999999995, + "name": "entrypointLookup" + }, + { + "ms": 0.034582999999999996, + "name": "wasi.start" + }, + { + "ms": 0.023904, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 59.62536 + }, + "memory": { + "start": { + "rssBytes": 238358528, + "peakRssBytes": 243752960, + "pssBytes": 239716352, + "virtualBytes": 3888066560, + "minorFaults": 68457, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69596, + "majorFaults": 0 + }, + "end": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69596, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 238358528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 26.349991999999475, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.016402, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.0026089999999999998, + "name": "Engine" + }, + { + "ms": 0.219469, + "name": "canonicalPreopens" + }, + { + "ms": 2.401579, + "name": "moduleRead" + }, + { + "ms": 0.357854, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010023, + "name": "importValidation" + }, + { + "ms": 0.299541, + "name": "Linker" + }, + { + "ms": 0.031888, + "name": "Store" + }, + { + "ms": 0.079731, + "name": "Instance" + }, + { + "ms": 0.31741400000000003, + "name": "signalMaskInit" + }, + { + "ms": 0.00696, + "name": "entrypointLookup" + }, + { + "ms": 1.360969, + "name": "wasi.start" + }, + { + "ms": 0.069065, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.265591 + }, + "memory": { + "start": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69596, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 8319553536, + "minorFaults": 69618, + "majorFaults": 0 + }, + "end": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69618, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 29.911330999999336, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.019476, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001629, + "name": "Engine" + }, + { + "ms": 0.127107, + "name": "canonicalPreopens" + }, + { + "ms": 3.430124, + "name": "moduleRead" + }, + { + "ms": 0.32097099999999995, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.005422, + "name": "importValidation" + }, + { + "ms": 0.285449, + "name": "Linker" + }, + { + "ms": 0.024763, + "name": "Store" + }, + { + "ms": 0.07031, + "name": "Instance" + }, + { + "ms": 0.385714, + "name": "signalMaskInit" + }, + { + "ms": 0.008601, + "name": "entrypointLookup" + }, + { + "ms": 1.889169, + "name": "wasi.start" + }, + { + "ms": 0.047389, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 6.744252 + }, + "memory": { + "start": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69618, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 8319553536, + "minorFaults": 69640, + "majorFaults": 0 + }, + "end": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69640, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 31.287318000000596, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.013845999999999999, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001554, + "name": "Engine" + }, + { + "ms": 0.145797, + "name": "canonicalPreopens" + }, + { + "ms": 3.457056, + "name": "moduleRead" + }, + { + "ms": 0.318419, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0055639999999999995, + "name": "importValidation" + }, + { + "ms": 0.284702, + "name": "Linker" + }, + { + "ms": 0.022698, + "name": "Store" + }, + { + "ms": 3.013309, + "name": "Instance" + }, + { + "ms": 0.075435, + "name": "signalMaskInit" + }, + { + "ms": 0.011337, + "name": "entrypointLookup" + }, + { + "ms": 0.044396, + "name": "wasi.start" + }, + { + "ms": 0.037203, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 7.552734999999999 + }, + "memory": { + "start": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250125312, + "virtualBytes": 3957465088, + "minorFaults": 69640, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 3957477376, + "minorFaults": 69662, + "majorFaults": 0 + }, + "end": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 3957465088, + "minorFaults": 69662, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 25.964702999999645, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.014017, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.0017239999999999998, + "name": "Engine" + }, + { + "ms": 0.160267, + "name": "canonicalPreopens" + }, + { + "ms": 3.415853, + "name": "moduleRead" + }, + { + "ms": 0.24026599999999998, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00398, + "name": "importValidation" + }, + { + "ms": 0.19702399999999998, + "name": "Linker" + }, + { + "ms": 0.01603, + "name": "Store" + }, + { + "ms": 0.057378, + "name": "Instance" + }, + { + "ms": 0.581591, + "name": "signalMaskInit" + }, + { + "ms": 0.00359, + "name": "entrypointLookup" + }, + { + "ms": 1.825474, + "name": "wasi.start" + }, + { + "ms": 0.046779, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 6.683195 + }, + "memory": { + "start": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 3957465088, + "minorFaults": 69662, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 8319553536, + "minorFaults": 69684, + "majorFaults": 0 + }, + "end": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 3957465088, + "minorFaults": 69684, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1301.9156930000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1179.0727969999998, + "firstHostCallMs": 0.00776, + "firstOutputMs": 1262.353022, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.000931, + "name": "Engine" + }, + { + "ms": 0.177092, + "name": "canonicalPreopens" + }, + { + "ms": 6.433083, + "name": "moduleRead" + }, + { + "ms": 3.862655, + "name": "profileValidation" + }, + { + "ms": 1167.1555620000001, + "name": "moduleCompile" + }, + { + "ms": 0.009661, + "name": "importValidation" + }, + { + "ms": 0.204884, + "name": "Linker" + }, + { + "ms": 0.022320999999999997, + "name": "Store" + }, + { + "ms": 0.27911199999999997, + "name": "Instance" + }, + { + "ms": 0.114141, + "name": "signalMaskInit" + }, + { + "ms": 0.005655, + "name": "entrypointLookup" + }, + { + "ms": 83.634264, + "name": "wasi.start" + }, + { + "ms": 3.962821, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 1266.628039 + }, + "memory": { + "start": { + "rssBytes": 248770560, + "peakRssBytes": 248975360, + "pssBytes": 250126336, + "virtualBytes": 3957465088, + "minorFaults": 69684, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286814208, + "peakRssBytes": 286838784, + "pssBytes": 288272384, + "virtualBytes": 8327368704, + "minorFaults": 82037, + "majorFaults": 0 + }, + "end": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82037, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 55603, + "wasmtimeProcessRetainedRssBytes": 248770560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 151.87632799999847, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 21.98078, + "firstHostCallMs": 0.017625000000000002, + "firstOutputMs": 110.684512, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0023420000000000003, + "name": "Engine" + }, + { + "ms": 0.22344, + "name": "canonicalPreopens" + }, + { + "ms": 10.611066000000001, + "name": "moduleRead" + }, + { + "ms": 5.870318, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011987999999999999, + "name": "importValidation" + }, + { + "ms": 0.340573, + "name": "Linker" + }, + { + "ms": 0.03793, + "name": "Store" + }, + { + "ms": 3.322609, + "name": "Instance" + }, + { + "ms": 0.109793, + "name": "signalMaskInit" + }, + { + "ms": 0.013255, + "name": "entrypointLookup" + }, + { + "ms": 89.66167399999999, + "name": "wasi.start" + }, + { + "ms": 2.716323, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 113.65275 + }, + "memory": { + "start": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287513600, + "virtualBytes": 3962912768, + "minorFaults": 82037, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286691328, + "peakRssBytes": 286838784, + "pssBytes": 288148480, + "virtualBytes": 8327368704, + "minorFaults": 82112, + "majorFaults": 0 + }, + "end": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82112, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 103.92956599999889, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.16093, + "firstHostCallMs": 0.011339, + "firstOutputMs": 67.887811, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0016950000000000001, + "name": "Engine" + }, + { + "ms": 0.194482, + "name": "canonicalPreopens" + }, + { + "ms": 6.85845, + "name": "moduleRead" + }, + { + "ms": 3.9381470000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.01073, + "name": "importValidation" + }, + { + "ms": 0.223692, + "name": "Linker" + }, + { + "ms": 0.022925, + "name": "Store" + }, + { + "ms": 0.060282999999999996, + "name": "Instance" + }, + { + "ms": 0.042871, + "name": "signalMaskInit" + }, + { + "ms": 0.004587, + "name": "entrypointLookup" + }, + { + "ms": 56.040008, + "name": "wasi.start" + }, + { + "ms": 1.343539, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 69.452349 + }, + "memory": { + "start": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82112, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286691328, + "peakRssBytes": 286838784, + "pssBytes": 288149504, + "virtualBytes": 8327368704, + "minorFaults": 82186, + "majorFaults": 0 + }, + "end": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82186, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 103.74533499999961, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.237670999999999, + "firstHostCallMs": 0.017634, + "firstOutputMs": 79.45683000000001, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001306, + "name": "Engine" + }, + { + "ms": 0.191884, + "name": "canonicalPreopens" + }, + { + "ms": 6.612954, + "name": "moduleRead" + }, + { + "ms": 4.048311, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011899000000000002, + "name": "importValidation" + }, + { + "ms": 0.232957, + "name": "Linker" + }, + { + "ms": 0.026525, + "name": "Store" + }, + { + "ms": 0.064768, + "name": "Instance" + }, + { + "ms": 0.06572399999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.006295, + "name": "entrypointLookup" + }, + { + "ms": 67.684565, + "name": "wasi.start" + }, + { + "ms": 2.2844919999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 81.915952 + }, + "memory": { + "start": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82186, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286691328, + "peakRssBytes": 286838784, + "pssBytes": 288149504, + "virtualBytes": 8327368704, + "minorFaults": 82260, + "majorFaults": 0 + }, + "end": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82260, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 83.25460999999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.600174, + "firstHostCallMs": 0.010124, + "firstOutputMs": 63.134675, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0012389999999999999, + "name": "Engine" + }, + { + "ms": 0.112162, + "name": "canonicalPreopens" + }, + { + "ms": 6.604002, + "name": "moduleRead" + }, + { + "ms": 3.8784639999999997, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007545, + "name": "importValidation" + }, + { + "ms": 0.20246, + "name": "Linker" + }, + { + "ms": 0.017452000000000002, + "name": "Store" + }, + { + "ms": 0.975579, + "name": "Instance" + }, + { + "ms": 0.040944, + "name": "signalMaskInit" + }, + { + "ms": 0.003464, + "name": "entrypointLookup" + }, + { + "ms": 50.813099, + "name": "wasi.start" + }, + { + "ms": 0.927772, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 64.222954 + }, + "memory": { + "start": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287514624, + "virtualBytes": 3962912768, + "minorFaults": 82260, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286691328, + "peakRssBytes": 286838784, + "pssBytes": 288149504, + "virtualBytes": 8327368704, + "minorFaults": 82334, + "majorFaults": 0 + }, + "end": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287513600, + "virtualBytes": 3962912768, + "minorFaults": 82334, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 4053.303215, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3594.711488, + "firstHostCallMs": 0.014464000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001248, + "name": "Engine" + }, + { + "ms": 0.222681, + "name": "canonicalPreopens" + }, + { + "ms": 13.310842, + "name": "moduleRead" + }, + { + "ms": 12.609458, + "name": "profileValidation" + }, + { + "ms": 3563.618907, + "name": "moduleCompile" + }, + { + "ms": 0.009181, + "name": "importValidation" + }, + { + "ms": 0.178978, + "name": "Linker" + }, + { + "ms": 0.01799, + "name": "Store" + }, + { + "ms": 2.963383, + "name": "Instance" + }, + { + "ms": 0.062347999999999994, + "name": "signalMaskInit" + }, + { + "ms": 0.004916, + "name": "entrypointLookup" + }, + { + "ms": 428.453633, + "name": "wasi.start" + }, + { + "ms": 0.05253, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 4022.991245 + }, + "memory": { + "start": { + "rssBytes": 286093312, + "peakRssBytes": 286838784, + "pssBytes": 287513600, + "virtualBytes": 3962912768, + "minorFaults": 82334, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417562624, + "peakRssBytes": 417890304, + "pssBytes": 420155392, + "virtualBytes": 12866441216, + "minorFaults": 122288, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122288, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1222759, + "wasmtimeProcessRetainedRssBytes": 286093312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 87.09601699999985, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 25.944233, + "firstHostCallMs": 0.016235999999999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001247, + "name": "Engine" + }, + { + "ms": 0.12402600000000001, + "name": "canonicalPreopens" + }, + { + "ms": 11.718153000000001, + "name": "moduleRead" + }, + { + "ms": 12.287126, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010967000000000001, + "name": "importValidation" + }, + { + "ms": 0.217071, + "name": "Linker" + }, + { + "ms": 0.021797, + "name": "Store" + }, + { + "ms": 0.048061, + "name": "Instance" + }, + { + "ms": 0.078529, + "name": "signalMaskInit" + }, + { + "ms": 0.004534, + "name": "entrypointLookup" + }, + { + "ms": 30.804907, + "name": "wasi.start" + }, + { + "ms": 0.043944, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 56.798404 + }, + "memory": { + "start": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122288, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 420372480, + "virtualBytes": 12866441216, + "minorFaults": 122377, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122377, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 103.95621500000198, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.232818, + "firstHostCallMs": 0.01216, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001182, + "name": "Engine" + }, + { + "ms": 0.13408499999999998, + "name": "canonicalPreopens" + }, + { + "ms": 12.465016, + "name": "moduleRead" + }, + { + "ms": 12.842893, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010426, + "name": "importValidation" + }, + { + "ms": 0.213165, + "name": "Linker" + }, + { + "ms": 0.021292, + "name": "Store" + }, + { + "ms": 0.927038, + "name": "Instance" + }, + { + "ms": 0.07319099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.005527, + "name": "entrypointLookup" + }, + { + "ms": 43.971584, + "name": "wasi.start" + }, + { + "ms": 3.615579, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 75.739006 + }, + "memory": { + "start": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122377, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 420368384, + "virtualBytes": 12866441216, + "minorFaults": 122466, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122466, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 98.46111600000222, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 28.772250999999997, + "firstHostCallMs": 0.018667, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001502, + "name": "Engine" + }, + { + "ms": 0.128853, + "name": "canonicalPreopens" + }, + { + "ms": 13.065398, + "name": "moduleRead" + }, + { + "ms": 12.400646, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011736000000000002, + "name": "importValidation" + }, + { + "ms": 0.212595, + "name": "Linker" + }, + { + "ms": 0.017633, + "name": "Store" + }, + { + "ms": 1.267419, + "name": "Instance" + }, + { + "ms": 0.083748, + "name": "signalMaskInit" + }, + { + "ms": 0.003864, + "name": "entrypointLookup" + }, + { + "ms": 39.476941000000004, + "name": "wasi.start" + }, + { + "ms": 0.051293, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 68.19431800000001 + }, + "memory": { + "start": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419524608, + "virtualBytes": 4137529344, + "minorFaults": 122466, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 420371456, + "virtualBytes": 12866441216, + "minorFaults": 122555, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419523584, + "virtualBytes": 4137529344, + "minorFaults": 122555, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 104.23217099999965, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.310132, + "firstHostCallMs": 0.022167, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.002233, + "name": "Engine" + }, + { + "ms": 0.191781, + "name": "canonicalPreopens" + }, + { + "ms": 13.867174, + "name": "moduleRead" + }, + { + "ms": 13.243701999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013037, + "name": "importValidation" + }, + { + "ms": 0.208615, + "name": "Linker" + }, + { + "ms": 0.021827999999999997, + "name": "Store" + }, + { + "ms": 0.188357, + "name": "Instance" + }, + { + "ms": 0.092486, + "name": "signalMaskInit" + }, + { + "ms": 0.003235, + "name": "entrypointLookup" + }, + { + "ms": 44.644985999999996, + "name": "wasi.start" + }, + { + "ms": 0.333849, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 74.31316699999999 + }, + "memory": { + "start": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419523584, + "virtualBytes": 4137529344, + "minorFaults": 122555, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 420371456, + "virtualBytes": 12866441216, + "minorFaults": 122644, + "majorFaults": 0 + }, + "end": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419523584, + "virtualBytes": 4137529344, + "minorFaults": 122644, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1626.9836600000017, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1596.282047, + "firstHostCallMs": 0.034106000000000004, + "firstOutputMs": 1603.8111050000002, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.002187, + "name": "Engine" + }, + { + "ms": 0.181646, + "name": "canonicalPreopens" + }, + { + "ms": 8.114999, + "name": "moduleRead" + }, + { + "ms": 6.42186, + "name": "profileValidation" + }, + { + "ms": 1580.236709, + "name": "moduleCompile" + }, + { + "ms": 0.019554, + "name": "importValidation" + }, + { + "ms": 0.203761, + "name": "Linker" + }, + { + "ms": 0.018859, + "name": "Store" + }, + { + "ms": 0.171122, + "name": "Instance" + }, + { + "ms": 0.087118, + "name": "signalMaskInit" + }, + { + "ms": 0.003126, + "name": "entrypointLookup" + }, + { + "ms": 7.835481, + "name": "wasi.start" + }, + { + "ms": 1.669836, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 1605.823801 + }, + "memory": { + "start": { + "rssBytes": 417112064, + "peakRssBytes": 417890304, + "pssBytes": 419523584, + "virtualBytes": 4137529344, + "minorFaults": 122644, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424669184, + "peakRssBytes": 425160704, + "pssBytes": 427417600, + "virtualBytes": 8509382656, + "minorFaults": 124547, + "majorFaults": 0 + }, + "end": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427417600, + "virtualBytes": 4144926720, + "minorFaults": 124547, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5181028, + "wasmtimeProcessRetainedRssBytes": 417112064, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 49.130716000003304, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.742406, + "firstHostCallMs": 0.023142, + "firstOutputMs": 23.615902000000002, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001717, + "name": "Engine" + }, + { + "ms": 0.110914, + "name": "canonicalPreopens" + }, + { + "ms": 6.967059, + "name": "moduleRead" + }, + { + "ms": 6.301022, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014052, + "name": "importValidation" + }, + { + "ms": 0.20474900000000001, + "name": "Linker" + }, + { + "ms": 0.018321, + "name": "Store" + }, + { + "ms": 1.26972, + "name": "Instance" + }, + { + "ms": 0.07693499999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.003, + "name": "entrypointLookup" + }, + { + "ms": 8.109724, + "name": "wasi.start" + }, + { + "ms": 1.789832, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 25.67061 + }, + "memory": { + "start": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427417600, + "virtualBytes": 4144926720, + "minorFaults": 124547, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427491328, + "virtualBytes": 8509382656, + "minorFaults": 124600, + "majorFaults": 0 + }, + "end": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427433984, + "virtualBytes": 4144926720, + "minorFaults": 124600, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 46.379775999997946, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.699640000000002, + "firstHostCallMs": 0.016649, + "firstOutputMs": 23.261774, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001372, + "name": "Engine" + }, + { + "ms": 0.15048199999999998, + "name": "canonicalPreopens" + }, + { + "ms": 7.42042, + "name": "moduleRead" + }, + { + "ms": 7.31827, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014066, + "name": "importValidation" + }, + { + "ms": 0.21504, + "name": "Linker" + }, + { + "ms": 0.024604, + "name": "Store" + }, + { + "ms": 0.6251819999999999, + "name": "Instance" + }, + { + "ms": 0.13402299999999998, + "name": "signalMaskInit" + }, + { + "ms": 0.0066619999999999995, + "name": "entrypointLookup" + }, + { + "ms": 6.746237000000001, + "name": "wasi.start" + }, + { + "ms": 0.787469, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.221878 + }, + "memory": { + "start": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427433984, + "virtualBytes": 4144926720, + "minorFaults": 124600, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427974656, + "virtualBytes": 8509382656, + "minorFaults": 124655, + "majorFaults": 0 + }, + "end": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427520000, + "virtualBytes": 4144926720, + "minorFaults": 124655, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 48.47748799999681, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.539390000000001, + "firstHostCallMs": 0.013944000000000002, + "firstOutputMs": 23.185732, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001181, + "name": "Engine" + }, + { + "ms": 0.112489, + "name": "canonicalPreopens" + }, + { + "ms": 6.990366, + "name": "moduleRead" + }, + { + "ms": 6.282273, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013517, + "name": "importValidation" + }, + { + "ms": 0.20477599999999999, + "name": "Linker" + }, + { + "ms": 0.018201000000000002, + "name": "Store" + }, + { + "ms": 1.089729, + "name": "Instance" + }, + { + "ms": 0.055513, + "name": "signalMaskInit" + }, + { + "ms": 0.003431, + "name": "entrypointLookup" + }, + { + "ms": 7.824452000000001, + "name": "wasi.start" + }, + { + "ms": 2.230745, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 25.611061 + }, + "memory": { + "start": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427520000, + "virtualBytes": 4144926720, + "minorFaults": 124655, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427888640, + "virtualBytes": 8509382656, + "minorFaults": 124706, + "majorFaults": 0 + }, + "end": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427528192, + "virtualBytes": 4144926720, + "minorFaults": 124706, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 57.53015500000038, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.191558, + "firstHostCallMs": 0.013739, + "firstOutputMs": 24.342157999999998, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001033, + "name": "Engine" + }, + { + "ms": 0.149245, + "name": "canonicalPreopens" + }, + { + "ms": 6.975603, + "name": "moduleRead" + }, + { + "ms": 6.484786000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012469999999999998, + "name": "importValidation" + }, + { + "ms": 0.19852499999999998, + "name": "Linker" + }, + { + "ms": 0.019143999999999998, + "name": "Store" + }, + { + "ms": 0.504776, + "name": "Instance" + }, + { + "ms": 0.06301699999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.005988, + "name": "entrypointLookup" + }, + { + "ms": 9.394647, + "name": "wasi.start" + }, + { + "ms": 0.044990999999999996, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.63628 + }, + "memory": { + "start": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427527168, + "virtualBytes": 4144926720, + "minorFaults": 124706, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 428052480, + "virtualBytes": 8509382656, + "minorFaults": 124755, + "majorFaults": 0 + }, + "end": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427528192, + "virtualBytes": 4144926720, + "minorFaults": 124755, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1295.2616340000022, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1260.8552829999999, + "firstHostCallMs": 0.013770000000000001, + "firstOutputMs": 1262.7319850000001, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001051, + "name": "Engine" + }, + { + "ms": 0.152745, + "name": "canonicalPreopens" + }, + { + "ms": 4.212994999999999, + "name": "moduleRead" + }, + { + "ms": 4.783201, + "name": "profileValidation" + }, + { + "ms": 1249.518785, + "name": "moduleCompile" + }, + { + "ms": 0.008743, + "name": "importValidation" + }, + { + "ms": 0.17679499999999998, + "name": "Linker" + }, + { + "ms": 0.016622, + "name": "Store" + }, + { + "ms": 1.483159, + "name": "Instance" + }, + { + "ms": 0.057894, + "name": "signalMaskInit" + }, + { + "ms": 0.0026680000000000002, + "name": "entrypointLookup" + }, + { + "ms": 1.952311, + "name": "wasi.start" + }, + { + "ms": 0.051808, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 1262.9468049999998 + }, + "memory": { + "start": { + "rssBytes": 424509440, + "peakRssBytes": 425160704, + "pssBytes": 427528192, + "virtualBytes": 4144926720, + "minorFaults": 124755, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150407168, + "minorFaults": 126170, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126170, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6761265, + "wasmtimeProcessRetainedRssBytes": 424509440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 45.607340000002296, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.173266, + "firstHostCallMs": 0.056693, + "firstOutputMs": 13.798366, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001959, + "name": "Engine" + }, + { + "ms": 0.12640099999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.34248, + "name": "moduleRead" + }, + { + "ms": 4.909117, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010637, + "name": "importValidation" + }, + { + "ms": 0.24856599999999998, + "name": "Linker" + }, + { + "ms": 0.019681, + "name": "Store" + }, + { + "ms": 0.941324, + "name": "Instance" + }, + { + "ms": 0.0824, + "name": "signalMaskInit" + }, + { + "ms": 0.004803, + "name": "entrypointLookup" + }, + { + "ms": 1.696623, + "name": "wasi.start" + }, + { + "ms": 0.034197, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.925921 + }, + "memory": { + "start": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126170, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 433070080, + "virtualBytes": 4150407168, + "minorFaults": 126220, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126220, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 43.906511000001046, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.96758, + "firstHostCallMs": 0.020779, + "firstOutputMs": 14.782569, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001771, + "name": "Engine" + }, + { + "ms": 0.11583099999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.889875, + "name": "moduleRead" + }, + { + "ms": 4.826551, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010994, + "name": "importValidation" + }, + { + "ms": 0.220653, + "name": "Linker" + }, + { + "ms": 0.020828000000000003, + "name": "Store" + }, + { + "ms": 0.352407, + "name": "Instance" + }, + { + "ms": 0.075032, + "name": "signalMaskInit" + }, + { + "ms": 0.003284, + "name": "entrypointLookup" + }, + { + "ms": 2.924612, + "name": "wasi.start" + }, + { + "ms": 1.6291159999999998, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 16.565301 + }, + "memory": { + "start": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126220, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 433070080, + "virtualBytes": 8514850816, + "minorFaults": 126270, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126270, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 52.40747099999862, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.48567, + "firstHostCallMs": 0.01954, + "firstOutputMs": 16.343817, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0016539999999999999, + "name": "Engine" + }, + { + "ms": 0.13761900000000002, + "name": "canonicalPreopens" + }, + { + "ms": 6.091411, + "name": "moduleRead" + }, + { + "ms": 5.047607, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.018976, + "name": "importValidation" + }, + { + "ms": 0.2831, + "name": "Linker" + }, + { + "ms": 0.031906000000000004, + "name": "Store" + }, + { + "ms": 0.600871, + "name": "Instance" + }, + { + "ms": 0.106554, + "name": "signalMaskInit" + }, + { + "ms": 0.009063, + "name": "entrypointLookup" + }, + { + "ms": 3.61212, + "name": "wasi.start" + }, + { + "ms": 1.3248540000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 17.849418 + }, + "memory": { + "start": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432996352, + "virtualBytes": 4150394880, + "minorFaults": 126270, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 433069056, + "virtualBytes": 8514850816, + "minorFaults": 126320, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432995328, + "virtualBytes": 4150394880, + "minorFaults": 126320, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 67.44644899999912, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.235600999999999, + "firstHostCallMs": 0.029006, + "firstOutputMs": 18.164865, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.002058, + "name": "Engine" + }, + { + "ms": 0.7161379999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.533727, + "name": "moduleRead" + }, + { + "ms": 5.199017, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013215, + "name": "importValidation" + }, + { + "ms": 0.223959, + "name": "Linker" + }, + { + "ms": 0.026889, + "name": "Store" + }, + { + "ms": 2.946844, + "name": "Instance" + }, + { + "ms": 0.09984699999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.005662, + "name": "entrypointLookup" + }, + { + "ms": 3.026781, + "name": "wasi.start" + }, + { + "ms": 3.09559, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 21.41173 + }, + "memory": { + "start": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432995328, + "virtualBytes": 4150394880, + "minorFaults": 126320, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 433069056, + "virtualBytes": 8514850816, + "minorFaults": 126370, + "majorFaults": 0 + }, + "end": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432995328, + "virtualBytes": 4150394880, + "minorFaults": 126370, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3991.8115499999985, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3957.155899, + "firstHostCallMs": 0.023493999999999998, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.0019520000000000002, + "name": "Engine" + }, + { + "ms": 0.19741499999999998, + "name": "canonicalPreopens" + }, + { + "ms": 13.5974, + "name": "moduleRead" + }, + { + "ms": 22.484016999999998, + "name": "profileValidation" + }, + { + "ms": 3916.167583, + "name": "moduleCompile" + }, + { + "ms": 0.013148, + "name": "importValidation" + }, + { + "ms": 0.187235, + "name": "Linker" + }, + { + "ms": 0.023139, + "name": "Store" + }, + { + "ms": 2.712341, + "name": "Instance" + }, + { + "ms": 0.074744, + "name": "signalMaskInit" + }, + { + "ms": 0.007843, + "name": "entrypointLookup" + }, + { + "ms": 10.052916, + "name": "wasi.start" + }, + { + "ms": 0.06975200000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 3966.9881 + }, + "memory": { + "start": { + "rssBytes": 429977600, + "peakRssBytes": 430383104, + "pssBytes": 432995328, + "virtualBytes": 4150394880, + "minorFaults": 126370, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 447098880, + "peakRssBytes": 447139840, + "pssBytes": 450092032, + "virtualBytes": 8531267584, + "minorFaults": 127439, + "majorFaults": 0 + }, + "end": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127439, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8010783, + "wasmtimeProcessRetainedRssBytes": 429977600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 67.71884800000043, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.573876, + "firstHostCallMs": 0.040116, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001095, + "name": "Engine" + }, + { + "ms": 0.109283, + "name": "canonicalPreopens" + }, + { + "ms": 11.200669999999999, + "name": "moduleRead" + }, + { + "ms": 14.738741, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012104, + "name": "importValidation" + }, + { + "ms": 0.20529, + "name": "Linker" + }, + { + "ms": 0.017152, + "name": "Store" + }, + { + "ms": 1.84955, + "name": "Instance" + }, + { + "ms": 0.04952, + "name": "signalMaskInit" + }, + { + "ms": 0.004938, + "name": "entrypointLookup" + }, + { + "ms": 12.372358, + "name": "wasi.start" + }, + { + "ms": 0.45742, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 42.387099 + }, + "memory": { + "start": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127439, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 446947328, + "peakRssBytes": 447139840, + "pssBytes": 449489920, + "virtualBytes": 8531267584, + "minorFaults": 127536, + "majorFaults": 0 + }, + "end": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127536, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 82.38879700000325, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.429601, + "firstHostCallMs": 0.010909, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.000947, + "name": "Engine" + }, + { + "ms": 0.107876, + "name": "canonicalPreopens" + }, + { + "ms": 11.433198, + "name": "moduleRead" + }, + { + "ms": 16.617198, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013186999999999999, + "name": "importValidation" + }, + { + "ms": 0.205164, + "name": "Linker" + }, + { + "ms": 0.018405, + "name": "Store" + }, + { + "ms": 1.576132, + "name": "Instance" + }, + { + "ms": 0.08005599999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004126, + "name": "entrypointLookup" + }, + { + "ms": 19.213213, + "name": "wasi.start" + }, + { + "ms": 0.053844, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 50.665488 + }, + "memory": { + "start": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127536, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 446947328, + "peakRssBytes": 447139840, + "pssBytes": 450071552, + "virtualBytes": 8531267584, + "minorFaults": 127633, + "majorFaults": 0 + }, + "end": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127633, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 572.5858059999991, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.631559, + "firstHostCallMs": 0.020117, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001336, + "name": "Engine" + }, + { + "ms": 0.142281, + "name": "canonicalPreopens" + }, + { + "ms": 11.346820000000001, + "name": "moduleRead" + }, + { + "ms": 15.292188999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012608, + "name": "importValidation" + }, + { + "ms": 0.204458, + "name": "Linker" + }, + { + "ms": 0.017643, + "name": "Store" + }, + { + "ms": 1.176931, + "name": "Instance" + }, + { + "ms": 0.06971, + "name": "signalMaskInit" + }, + { + "ms": 0.0040539999999999994, + "name": "entrypointLookup" + }, + { + "ms": 21.375021, + "name": "wasi.start" + }, + { + "ms": 0.699426, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 51.694476 + }, + "memory": { + "start": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449416192, + "virtualBytes": 4166811648, + "minorFaults": 127633, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 446947328, + "peakRssBytes": 447139840, + "pssBytes": 450055168, + "virtualBytes": 8531267584, + "minorFaults": 127730, + "majorFaults": 0 + }, + "end": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449415168, + "virtualBytes": 4166811648, + "minorFaults": 127730, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 84.87784800000009, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.510457, + "firstHostCallMs": 0.019020000000000002, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001852, + "name": "Engine" + }, + { + "ms": 0.125558, + "name": "canonicalPreopens" + }, + { + "ms": 11.76854, + "name": "moduleRead" + }, + { + "ms": 14.656124, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013023, + "name": "importValidation" + }, + { + "ms": 0.218705, + "name": "Linker" + }, + { + "ms": 0.021847, + "name": "Store" + }, + { + "ms": 3.220733, + "name": "Instance" + }, + { + "ms": 0.071311, + "name": "signalMaskInit" + }, + { + "ms": 0.009160999999999999, + "name": "entrypointLookup" + }, + { + "ms": 23.395096000000002, + "name": "wasi.start" + }, + { + "ms": 1.549009, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 56.434673 + }, + "memory": { + "start": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449415168, + "virtualBytes": 4166811648, + "minorFaults": 127730, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 446947328, + "peakRssBytes": 447139840, + "pssBytes": 450058240, + "virtualBytes": 8531267584, + "minorFaults": 127827, + "majorFaults": 0 + }, + "end": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449415168, + "virtualBytes": 4166811648, + "minorFaults": 127827, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3926.064602000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3893.653365, + "firstHostCallMs": 0.015279000000000001, + "firstOutputMs": 3895.549109, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0011489999999999998, + "name": "Engine" + }, + { + "ms": 0.158556, + "name": "canonicalPreopens" + }, + { + "ms": 12.213194, + "name": "moduleRead" + }, + { + "ms": 17.613259, + "name": "profileValidation" + }, + { + "ms": 3861.566618, + "name": "moduleCompile" + }, + { + "ms": 0.015290000000000002, + "name": "importValidation" + }, + { + "ms": 0.180718, + "name": "Linker" + }, + { + "ms": 0.016497, + "name": "Store" + }, + { + "ms": 0.214322, + "name": "Instance" + }, + { + "ms": 0.09046599999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.003714, + "name": "entrypointLookup" + }, + { + "ms": 2.119337, + "name": "wasi.start" + }, + { + "ms": 1.3085550000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 3897.114178 + }, + "memory": { + "start": { + "rssBytes": 446394368, + "peakRssBytes": 447139840, + "pssBytes": 449415168, + "virtualBytes": 4166811648, + "minorFaults": 127827, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 474165248, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 8549863424, + "minorFaults": 134144, + "majorFaults": 0 + }, + "end": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134144, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11926951, + "wasmtimeProcessRetainedRssBytes": 446394368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 63.04460800000379, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.283851000000002, + "firstHostCallMs": 0.054235, + "firstOutputMs": 32.854653, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001513, + "name": "Engine" + }, + { + "ms": 0.123821, + "name": "canonicalPreopens" + }, + { + "ms": 13.229637, + "name": "moduleRead" + }, + { + "ms": 15.806585000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.016458999999999998, + "name": "importValidation" + }, + { + "ms": 0.209477, + "name": "Linker" + }, + { + "ms": 0.020754, + "name": "Store" + }, + { + "ms": 0.199464, + "name": "Instance" + }, + { + "ms": 0.060044, + "name": "signalMaskInit" + }, + { + "ms": 0.003811, + "name": "entrypointLookup" + }, + { + "ms": 1.724161, + "name": "wasi.start" + }, + { + "ms": 2.908146, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 35.925865 + }, + "memory": { + "start": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134144, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477154304, + "virtualBytes": 8549863424, + "minorFaults": 134188, + "majorFaults": 0 + }, + "end": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134188, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 61.6401079999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 33.781524, + "firstHostCallMs": 0.013644, + "firstOutputMs": 35.811513000000005, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0010359999999999998, + "name": "Engine" + }, + { + "ms": 0.10700399999999999, + "name": "canonicalPreopens" + }, + { + "ms": 12.854403, + "name": "moduleRead" + }, + { + "ms": 15.738495000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.016937999999999998, + "name": "importValidation" + }, + { + "ms": 0.21410600000000002, + "name": "Linker" + }, + { + "ms": 0.023111, + "name": "Store" + }, + { + "ms": 2.520942, + "name": "Instance" + }, + { + "ms": 0.075218, + "name": "signalMaskInit" + }, + { + "ms": 0.0051, + "name": "entrypointLookup" + }, + { + "ms": 2.836126, + "name": "wasi.start" + }, + { + "ms": 1.4414740000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 37.428945999999996 + }, + "memory": { + "start": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134188, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477154304, + "virtualBytes": 8549863424, + "minorFaults": 134232, + "majorFaults": 0 + }, + "end": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134232, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 658.2945529999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 34.615928, + "firstHostCallMs": 0.020924, + "firstOutputMs": 37.003092, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001012, + "name": "Engine" + }, + { + "ms": 0.11849900000000001, + "name": "canonicalPreopens" + }, + { + "ms": 13.429841, + "name": "moduleRead" + }, + { + "ms": 16.636505, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015899, + "name": "importValidation" + }, + { + "ms": 0.21674500000000002, + "name": "Linker" + }, + { + "ms": 0.017881, + "name": "Store" + }, + { + "ms": 2.139537, + "name": "Instance" + }, + { + "ms": 0.102462, + "name": "signalMaskInit" + }, + { + "ms": 0.008537, + "name": "entrypointLookup" + }, + { + "ms": 2.879991, + "name": "wasi.start" + }, + { + "ms": 0.041108, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 37.221534 + }, + "memory": { + "start": { + "rssBytes": 474058752, + "peakRssBytes": 474791936, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134232, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 474132480, + "peakRssBytes": 476762112, + "pssBytes": 477153280, + "virtualBytes": 4185686016, + "minorFaults": 134276, + "majorFaults": 0 + }, + "end": { + "rssBytes": 474058752, + "peakRssBytes": 476762112, + "pssBytes": 477079552, + "virtualBytes": 4185407488, + "minorFaults": 134276, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 70.34671600000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 33.850328999999995, + "firstHostCallMs": 0.029819000000000002, + "firstOutputMs": 36.542370999999996, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001442, + "name": "Engine" + }, + { + "ms": 0.1817, + "name": "canonicalPreopens" + }, + { + "ms": 14.828335, + "name": "moduleRead" + }, + { + "ms": 16.375794, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014964, + "name": "importValidation" + }, + { + "ms": 0.215651, + "name": "Linker" + }, + { + "ms": 0.018625, + "name": "Store" + }, + { + "ms": 0.27752400000000005, + "name": "Instance" + }, + { + "ms": 0.11559499999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004351000000000001, + "name": "entrypointLookup" + }, + { + "ms": 3.108668, + "name": "wasi.start" + }, + { + "ms": 1.008955, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 37.763572 + }, + "memory": { + "start": { + "rssBytes": 474058752, + "peakRssBytes": 476762112, + "pssBytes": 477079552, + "virtualBytes": 4185407488, + "minorFaults": 134276, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 474132480, + "peakRssBytes": 476762112, + "pssBytes": 477153280, + "virtualBytes": 8547495936, + "minorFaults": 134320, + "majorFaults": 0 + }, + "end": { + "rssBytes": 474058752, + "peakRssBytes": 476762112, + "pssBytes": 477079552, + "virtualBytes": 4185407488, + "minorFaults": 134320, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 705.496779000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 638.2197779999999, + "firstHostCallMs": 0.018904, + "firstOutputMs": 681.931745, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001245, + "name": "Engine" + }, + { + "ms": 0.153631, + "name": "canonicalPreopens" + }, + { + "ms": 7.424831, + "name": "moduleRead" + }, + { + "ms": 2.470124, + "name": "profileValidation" + }, + { + "ms": 624.807938, + "name": "moduleCompile" + }, + { + "ms": 0.0071389999999999995, + "name": "importValidation" + }, + { + "ms": 0.180197, + "name": "Linker" + }, + { + "ms": 0.019131, + "name": "Store" + }, + { + "ms": 1.8617650000000001, + "name": "Instance" + }, + { + "ms": 0.108926, + "name": "signalMaskInit" + }, + { + "ms": 0.005605, + "name": "entrypointLookup" + }, + { + "ms": 44.276545, + "name": "wasi.start" + }, + { + "ms": 1.713334, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 683.804456 + }, + "memory": { + "start": { + "rssBytes": 474058752, + "peakRssBytes": 476762112, + "pssBytes": 477080576, + "virtualBytes": 4185407488, + "minorFaults": 134320, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478109696, + "peakRssBytes": 478441472, + "pssBytes": 481471488, + "virtualBytes": 8553725952, + "minorFaults": 135343, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135343, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15788517, + "wasmtimeProcessRetainedRssBytes": 474058752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 97.00000300000102, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.034735, + "firstHostCallMs": 0.017518000000000002, + "firstOutputMs": 64.12267100000001, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001071, + "name": "Engine" + }, + { + "ms": 0.10743899999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.743884, + "name": "moduleRead" + }, + { + "ms": 2.547523, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007655, + "name": "importValidation" + }, + { + "ms": 0.207328, + "name": "Linker" + }, + { + "ms": 0.019009, + "name": "Store" + }, + { + "ms": 1.574911, + "name": "Instance" + }, + { + "ms": 0.06661299999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.00281, + "name": "entrypointLookup" + }, + { + "ms": 52.304889, + "name": "wasi.start" + }, + { + "ms": 2.482686, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 66.796258 + }, + "memory": { + "start": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135343, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 481348608, + "virtualBytes": 8553725952, + "minorFaults": 135393, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480942080, + "virtualBytes": 4189270016, + "minorFaults": 135393, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 84.51421899999696, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.194421, + "firstHostCallMs": 0.021079, + "firstOutputMs": 58.019594, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.00137, + "name": "Engine" + }, + { + "ms": 0.116698, + "name": "canonicalPreopens" + }, + { + "ms": 6.617899, + "name": "moduleRead" + }, + { + "ms": 2.560583, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007796, + "name": "importValidation" + }, + { + "ms": 0.206264, + "name": "Linker" + }, + { + "ms": 0.016429000000000003, + "name": "Store" + }, + { + "ms": 0.851182, + "name": "Instance" + }, + { + "ms": 0.049713, + "name": "signalMaskInit" + }, + { + "ms": 0.0032389999999999997, + "name": "entrypointLookup" + }, + { + "ms": 47.038783, + "name": "wasi.start" + }, + { + "ms": 1.327907, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 59.49389 + }, + "memory": { + "start": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135393, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 481348608, + "virtualBytes": 8553725952, + "minorFaults": 135443, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135443, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 75.9941289999988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.623557, + "firstHostCallMs": 0.017165, + "firstOutputMs": 51.872681, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001769, + "name": "Engine" + }, + { + "ms": 0.106943, + "name": "canonicalPreopens" + }, + { + "ms": 6.9919709999999995, + "name": "moduleRead" + }, + { + "ms": 2.548721, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009334, + "name": "importValidation" + }, + { + "ms": 0.222777, + "name": "Linker" + }, + { + "ms": 0.025908999999999998, + "name": "Store" + }, + { + "ms": 0.8861490000000001, + "name": "Instance" + }, + { + "ms": 0.056131999999999994, + "name": "signalMaskInit" + }, + { + "ms": 0.003112, + "name": "entrypointLookup" + }, + { + "ms": 40.467646, + "name": "wasi.start" + }, + { + "ms": 2.220722, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 54.23278 + }, + "memory": { + "start": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135443, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 481348608, + "virtualBytes": 8553725952, + "minorFaults": 135493, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135493, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 85.02203700000246, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.538113, + "firstHostCallMs": 0.018419, + "firstOutputMs": 56.259195, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.000897, + "name": "Engine" + }, + { + "ms": 0.126079, + "name": "canonicalPreopens" + }, + { + "ms": 6.670197999999999, + "name": "moduleRead" + }, + { + "ms": 2.4700249999999997, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008046000000000001, + "name": "importValidation" + }, + { + "ms": 0.20562799999999998, + "name": "Linker" + }, + { + "ms": 0.016906, + "name": "Store" + }, + { + "ms": 1.115675, + "name": "Instance" + }, + { + "ms": 0.047691000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.003871, + "name": "entrypointLookup" + }, + { + "ms": 45.037400999999996, + "name": "wasi.start" + }, + { + "ms": 1.432556, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 57.830618 + }, + "memory": { + "start": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135493, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 481348608, + "virtualBytes": 8553725952, + "minorFaults": 135543, + "majorFaults": 0 + }, + "end": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135543, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1437.8297190000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1400.479357, + "firstHostCallMs": 0.015339, + "firstOutputMs": 1403.9807950000002, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0010860000000000002, + "name": "Engine" + }, + { + "ms": 0.110288, + "name": "canonicalPreopens" + }, + { + "ms": 6.790479, + "name": "moduleRead" + }, + { + "ms": 4.392049, + "name": "profileValidation" + }, + { + "ms": 1385.365838, + "name": "moduleCompile" + }, + { + "ms": 0.00849, + "name": "importValidation" + }, + { + "ms": 0.18971600000000002, + "name": "Linker" + }, + { + "ms": 0.019466, + "name": "Store" + }, + { + "ms": 2.784915, + "name": "Instance" + }, + { + "ms": 0.07822899999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004284, + "name": "entrypointLookup" + }, + { + "ms": 4.832523, + "name": "wasi.start" + }, + { + "ms": 1.0424069999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 1406.405913 + }, + "memory": { + "start": { + "rssBytes": 477921280, + "peakRssBytes": 478441472, + "pssBytes": 480943104, + "virtualBytes": 4189270016, + "minorFaults": 135543, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 512991232, + "peakRssBytes": 516235264, + "pssBytes": 511151104, + "virtualBytes": 8567250944, + "minorFaults": 157415, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157415, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16413325, + "wasmtimeProcessRetainedRssBytes": 477921280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 55.77443800000037, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.317021, + "firstHostCallMs": 0.016631, + "firstOutputMs": 20.033321, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001061, + "name": "Engine" + }, + { + "ms": 0.106322, + "name": "canonicalPreopens" + }, + { + "ms": 7.4761239999999995, + "name": "moduleRead" + }, + { + "ms": 4.859087, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010272, + "name": "importValidation" + }, + { + "ms": 0.211451, + "name": "Linker" + }, + { + "ms": 0.018878000000000002, + "name": "Store" + }, + { + "ms": 2.769531, + "name": "Instance" + }, + { + "ms": 0.08578, + "name": "signalMaskInit" + }, + { + "ms": 0.007283, + "name": "entrypointLookup" + }, + { + "ms": 5.510508, + "name": "wasi.start" + }, + { + "ms": 1.518432, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 23.333304 + }, + "memory": { + "start": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157415, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497424384, + "virtualBytes": 8567250944, + "minorFaults": 157448, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157448, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 56.675605000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.432984, + "firstHostCallMs": 0.022212, + "firstOutputMs": 20.049312999999998, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001322, + "name": "Engine" + }, + { + "ms": 0.127168, + "name": "canonicalPreopens" + }, + { + "ms": 6.726782, + "name": "moduleRead" + }, + { + "ms": 4.458963, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010271, + "name": "importValidation" + }, + { + "ms": 0.212369, + "name": "Linker" + }, + { + "ms": 0.018912, + "name": "Store" + }, + { + "ms": 2.051867, + "name": "Instance" + }, + { + "ms": 0.069023, + "name": "signalMaskInit" + }, + { + "ms": 0.005687, + "name": "entrypointLookup" + }, + { + "ms": 7.679486, + "name": "wasi.start" + }, + { + "ms": 0.256073, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 22.386772 + }, + "memory": { + "start": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157448, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497424384, + "virtualBytes": 8567250944, + "minorFaults": 157481, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157481, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 42.87288400000398, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.557157, + "firstHostCallMs": 0.02678, + "firstOutputMs": 14.931595000000002, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.002182, + "name": "Engine" + }, + { + "ms": 0.161309, + "name": "canonicalPreopens" + }, + { + "ms": 5.842318, + "name": "moduleRead" + }, + { + "ms": 4.4090240000000005, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009874, + "name": "importValidation" + }, + { + "ms": 0.19994, + "name": "Linker" + }, + { + "ms": 0.018416000000000002, + "name": "Store" + }, + { + "ms": 0.10392799999999999, + "name": "Instance" + }, + { + "ms": 0.054386, + "name": "signalMaskInit" + }, + { + "ms": 0.003104, + "name": "entrypointLookup" + }, + { + "ms": 4.754426, + "name": "wasi.start" + }, + { + "ms": 0.49994799999999995, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 16.827858000000003 + }, + "memory": { + "start": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497350656, + "virtualBytes": 4202795008, + "minorFaults": 157481, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497424384, + "virtualBytes": 8567250944, + "minorFaults": 157514, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497351680, + "virtualBytes": 4202795008, + "minorFaults": 157514, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 48.408339999994496, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.630129, + "firstHostCallMs": 0.017753, + "firstOutputMs": 16.892333999999998, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001403, + "name": "Engine" + }, + { + "ms": 0.174084, + "name": "canonicalPreopens" + }, + { + "ms": 6.649985, + "name": "moduleRead" + }, + { + "ms": 4.543976, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009007000000000001, + "name": "importValidation" + }, + { + "ms": 0.200903, + "name": "Linker" + }, + { + "ms": 0.017099, + "name": "Store" + }, + { + "ms": 0.228719, + "name": "Instance" + }, + { + "ms": 0.062440999999999997, + "name": "signalMaskInit" + }, + { + "ms": 0.002616, + "name": "entrypointLookup" + }, + { + "ms": 5.659714, + "name": "wasi.start" + }, + { + "ms": 0.077808, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 18.382341 + }, + "memory": { + "start": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497351680, + "virtualBytes": 4202795008, + "minorFaults": 157514, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497425408, + "virtualBytes": 8567250944, + "minorFaults": 157547, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494329856, + "peakRssBytes": 516235264, + "pssBytes": 497351680, + "virtualBytes": 4202795008, + "minorFaults": 157547, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17798691, + "wasmtimeProcessRetainedRssBytes": 494329856, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 1, + "vmSetupMs": 460.88630600000033, + "fixtureSetupMs": 405.5117070000051, + "baseline": { + "rssBytes": 241242112, + "peakRssBytes": 246636544, + "pssBytes": 242308096, + "virtualBytes": 3885953024, + "minorFaults": 66428, + "majorFaults": 1 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387117056, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 403136512, + "peakRssBytes": 511717376, + "pssBytes": 404839424, + "virtualBytes": 4120133632, + "minorFaults": 1651502, + "majorFaults": 1 + }, + "retainedDelta": { + "rssBytes": 161894400, + "peakRssBytes": 265080832, + "pssBytes": 162531328, + "virtualBytes": 234180608, + "minorFaults": 1585074, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 88.16289200000028, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 2.824146 + }, + { + "name": "WebAssembly.Module", + "ms": 0.183378 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.067632 + }, + { + "name": "wasi.start", + "ms": 0.089489 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 241242112, + "peakRssBytes": 246636544, + "pssBytes": 242316288, + "virtualBytes": 3888066560, + "minorFaults": 66430, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 268869632, + "peakRssBytes": 269447168, + "pssBytes": 257803264, + "virtualBytes": 4640018432, + "minorFaults": 76473, + "majorFaults": 1 + }, + "end": { + "rssBytes": 256565248, + "peakRssBytes": 269447168, + "pssBytes": 257803264, + "virtualBytes": 3955175424, + "minorFaults": 76473, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 241242112, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 256565248, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 68.24932699999772, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.175634 + }, + { + "name": "WebAssembly.Module", + "ms": 0.184233 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.063734 + }, + { + "name": "wasi.start", + "ms": 0.085043 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 256565248, + "peakRssBytes": 269447168, + "pssBytes": 257803264, + "virtualBytes": 3955175424, + "minorFaults": 76473, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 274137088, + "peakRssBytes": 274968576, + "pssBytes": 262748160, + "virtualBytes": 4640280576, + "minorFaults": 83936, + "majorFaults": 1 + }, + "end": { + "rssBytes": 261459968, + "peakRssBytes": 274968576, + "pssBytes": 262748160, + "virtualBytes": 3955175424, + "minorFaults": 83936, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 256565248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261459968, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 312.37073899999814, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.563899 + }, + { + "name": "WebAssembly.Module", + "ms": 0.182391 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.077606 + }, + { + "name": "wasi.start", + "ms": 0.092274 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261459968, + "peakRssBytes": 274968576, + "pssBytes": 262748160, + "virtualBytes": 3955175424, + "minorFaults": 83936, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 273821696, + "peakRssBytes": 274968576, + "pssBytes": 262947840, + "virtualBytes": 4639494144, + "minorFaults": 90246, + "majorFaults": 1 + }, + "end": { + "rssBytes": 261697536, + "peakRssBytes": 274968576, + "pssBytes": 262947840, + "virtualBytes": 3955175424, + "minorFaults": 90246, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261459968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261697536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 88.02421600000525, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.102584 + }, + { + "name": "WebAssembly.Module", + "ms": 0.154043 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.067257 + }, + { + "name": "wasi.start", + "ms": 0.125381 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261697536, + "peakRssBytes": 274968576, + "pssBytes": 262947840, + "virtualBytes": 3955175424, + "minorFaults": 90246, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 274292736, + "peakRssBytes": 274968576, + "pssBytes": 275706880, + "virtualBytes": 4640280576, + "minorFaults": 96510, + "majorFaults": 1 + }, + "end": { + "rssBytes": 261615616, + "peakRssBytes": 274968576, + "pssBytes": 262960128, + "virtualBytes": 3955175424, + "minorFaults": 96510, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261697536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261615616, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 78.85961800000223, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.135509 + }, + { + "name": "WebAssembly.Module", + "ms": 0.167921 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.066397 + }, + { + "name": "wasi.start", + "ms": 0.093336 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261615616, + "peakRssBytes": 274968576, + "pssBytes": 262960128, + "virtualBytes": 3955175424, + "minorFaults": 96510, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 274255872, + "peakRssBytes": 274968576, + "pssBytes": 275564544, + "virtualBytes": 4642381824, + "minorFaults": 102797, + "majorFaults": 1 + }, + "end": { + "rssBytes": 261615616, + "peakRssBytes": 274968576, + "pssBytes": 263067648, + "virtualBytes": 3957276672, + "minorFaults": 102797, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261615616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261615616, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 228.58471599999757, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.113615 + }, + { + "name": "WebAssembly.Module", + "ms": 2.587896 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.290779 + }, + { + "name": "wasi.start", + "ms": 95.080825 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 261615616, + "peakRssBytes": 274968576, + "pssBytes": 263067648, + "virtualBytes": 3957276672, + "minorFaults": 102797, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 331825152, + "peakRssBytes": 331825152, + "pssBytes": 326793216, + "virtualBytes": 4696059904, + "minorFaults": 132422, + "majorFaults": 1 + }, + "end": { + "rssBytes": 282726400, + "peakRssBytes": 331825152, + "pssBytes": 114087936, + "virtualBytes": 3957276672, + "minorFaults": 132422, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 261615616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282996736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 247.697196000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.384035 + }, + { + "name": "WebAssembly.Module", + "ms": 3.14278 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.845879 + }, + { + "name": "wasi.start", + "ms": 98.905569 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 282996736, + "peakRssBytes": 331825152, + "pssBytes": 284321792, + "virtualBytes": 3957276672, + "minorFaults": 132441, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 339046400, + "peakRssBytes": 339587072, + "pssBytes": 336517120, + "virtualBytes": 4696178688, + "minorFaults": 157806, + "majorFaults": 1 + }, + "end": { + "rssBytes": 290861056, + "peakRssBytes": 339587072, + "pssBytes": 291735552, + "virtualBytes": 3957276672, + "minorFaults": 157806, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282996736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290861056, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 221.40927999999985, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.958061 + }, + { + "name": "WebAssembly.Module", + "ms": 1.245694 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.499861 + }, + { + "name": "wasi.start", + "ms": 85.480338 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 290861056, + "peakRssBytes": 339587072, + "pssBytes": 291735552, + "virtualBytes": 3957276672, + "minorFaults": 157812, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 343306240, + "peakRssBytes": 343306240, + "pssBytes": 344610816, + "virtualBytes": 4695797760, + "minorFaults": 181936, + "majorFaults": 1 + }, + "end": { + "rssBytes": 290734080, + "peakRssBytes": 343306240, + "pssBytes": 291944448, + "virtualBytes": 3957276672, + "minorFaults": 181936, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290861056, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290734080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 255.9923789999957, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.816113 + }, + { + "name": "WebAssembly.Module", + "ms": 2.500773 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.499767 + }, + { + "name": "wasi.start", + "ms": 102.414208 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 290734080, + "peakRssBytes": 343306240, + "pssBytes": 291944448, + "virtualBytes": 3957276672, + "minorFaults": 181936, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 343334912, + "peakRssBytes": 343605248, + "pssBytes": 344672256, + "virtualBytes": 4695654400, + "minorFaults": 206048, + "majorFaults": 1 + }, + "end": { + "rssBytes": 290967552, + "peakRssBytes": 343605248, + "pssBytes": 292124672, + "virtualBytes": 3957276672, + "minorFaults": 206048, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290734080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290967552, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 241.8861409999954, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.058117 + }, + { + "name": "WebAssembly.Module", + "ms": 1.390824 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.468193 + }, + { + "name": "wasi.start", + "ms": 99.169076 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 290967552, + "peakRssBytes": 343605248, + "pssBytes": 292124672, + "virtualBytes": 3957276672, + "minorFaults": 206048, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 358264832, + "peakRssBytes": 359886848, + "pssBytes": 355186688, + "virtualBytes": 4763287552, + "minorFaults": 235000, + "majorFaults": 1 + }, + "end": { + "rssBytes": 310988800, + "peakRssBytes": 359886848, + "pssBytes": 231826432, + "virtualBytes": 4024385536, + "minorFaults": 235000, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 290967552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 311259136, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 384.20413800000097, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.236424 + }, + { + "name": "WebAssembly.Module", + "ms": 4.674983 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.82882 + }, + { + "name": "wasi.start", + "ms": 116.330213 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 311259136, + "peakRssBytes": 359886848, + "pssBytes": 231035904, + "virtualBytes": 4024385536, + "minorFaults": 235059, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 424009728, + "peakRssBytes": 424349696, + "pssBytes": 425578496, + "virtualBytes": 5471125504, + "minorFaults": 298217, + "majorFaults": 1 + }, + "end": { + "rssBytes": 337301504, + "peakRssBytes": 424349696, + "pssBytes": 338827264, + "virtualBytes": 4028760064, + "minorFaults": 298217, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 311259136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337301504, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 436.8377959999998, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 73.131825 + }, + { + "name": "WebAssembly.Module", + "ms": 4.039591 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.8953 + }, + { + "name": "wasi.start", + "ms": 161.317434 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 337301504, + "peakRssBytes": 424349696, + "pssBytes": 338827264, + "virtualBytes": 4028760064, + "minorFaults": 298217, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 436527104, + "peakRssBytes": 436752384, + "pssBytes": 437638144, + "virtualBytes": 5472964608, + "minorFaults": 356706, + "majorFaults": 1 + }, + "end": { + "rssBytes": 343388160, + "peakRssBytes": 436752384, + "pssBytes": 344590336, + "virtualBytes": 4030861312, + "minorFaults": 356706, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 337301504, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343388160, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 405.26831899999524, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 95.475306 + }, + { + "name": "WebAssembly.Module", + "ms": 5.617584 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.030833 + }, + { + "name": "wasi.start", + "ms": 110.897875 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343388160, + "peakRssBytes": 436752384, + "pssBytes": 344590336, + "virtualBytes": 4030861312, + "minorFaults": 356706, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435695616, + "peakRssBytes": 436752384, + "pssBytes": 437281792, + "virtualBytes": 5472702464, + "minorFaults": 413233, + "majorFaults": 1 + }, + "end": { + "rssBytes": 343400448, + "peakRssBytes": 436752384, + "pssBytes": 344770560, + "virtualBytes": 4030861312, + "minorFaults": 413233, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343388160, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343400448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 366.92876600000454, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.715184 + }, + { + "name": "WebAssembly.Module", + "ms": 5.812172 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.897078 + }, + { + "name": "wasi.start", + "ms": 128.987206 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 343400448, + "peakRssBytes": 436752384, + "pssBytes": 344770560, + "virtualBytes": 4030861312, + "minorFaults": 413233, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 437018624, + "peakRssBytes": 437260288, + "pssBytes": 438543360, + "virtualBytes": 5540073472, + "minorFaults": 472350, + "majorFaults": 1 + }, + "end": { + "rssBytes": 351973376, + "peakRssBytes": 437260288, + "pssBytes": 353428480, + "virtualBytes": 4097970176, + "minorFaults": 472350, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 343400448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351973376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 373.33007799999905, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 60.767401 + }, + { + "name": "WebAssembly.Module", + "ms": 3.292338 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.556756 + }, + { + "name": "wasi.start", + "ms": 140.872308 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 351973376, + "peakRssBytes": 437260288, + "pssBytes": 353428480, + "virtualBytes": 4097970176, + "minorFaults": 472350, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 457891840, + "peakRssBytes": 457940992, + "pssBytes": 458934272, + "virtualBytes": 5540478976, + "minorFaults": 532566, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365039616, + "peakRssBytes": 457940992, + "pssBytes": 366556160, + "virtualBytes": 4097970176, + "minorFaults": 532566, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 351973376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365039616, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 200.91326999999728, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.710423 + }, + { + "name": "WebAssembly.Module", + "ms": 1.410053 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.457102 + }, + { + "name": "wasi.start", + "ms": 24.42796 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365039616, + "peakRssBytes": 457940992, + "pssBytes": 366557184, + "virtualBytes": 4097970176, + "minorFaults": 532566, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 431718400, + "peakRssBytes": 457940992, + "pssBytes": 433693696, + "virtualBytes": 4855787520, + "minorFaults": 564337, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365047808, + "peakRssBytes": 457940992, + "pssBytes": 367028224, + "virtualBytes": 4097970176, + "minorFaults": 564337, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365039616, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365047808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 191.3590980000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 36.502791 + }, + { + "name": "WebAssembly.Module", + "ms": 1.481926 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.258801 + }, + { + "name": "wasi.start", + "ms": 22.719971 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365047808, + "peakRssBytes": 457940992, + "pssBytes": 367028224, + "virtualBytes": 4097970176, + "minorFaults": 564337, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 431726592, + "peakRssBytes": 457940992, + "pssBytes": 433702912, + "virtualBytes": 4855787520, + "minorFaults": 596099, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365056000, + "peakRssBytes": 457940992, + "pssBytes": 367036416, + "virtualBytes": 4097970176, + "minorFaults": 596099, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365047808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365056000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 206.64967700000125, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 46.625166 + }, + { + "name": "WebAssembly.Module", + "ms": 3.077584 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.80814 + }, + { + "name": "wasi.start", + "ms": 28.550263 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365056000, + "peakRssBytes": 457940992, + "pssBytes": 367036416, + "virtualBytes": 4097970176, + "minorFaults": 596099, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 431706112, + "peakRssBytes": 457940992, + "pssBytes": 433743872, + "virtualBytes": 4855644160, + "minorFaults": 628373, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365142016, + "peakRssBytes": 457940992, + "pssBytes": 367044608, + "virtualBytes": 4097970176, + "minorFaults": 628373, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365056000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365142016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 187.99272899999778, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.403997 + }, + { + "name": "WebAssembly.Module", + "ms": 3.024191 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.278255 + }, + { + "name": "wasi.start", + "ms": 20.789188 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365142016, + "peakRssBytes": 457940992, + "pssBytes": 367044608, + "virtualBytes": 4097970176, + "minorFaults": 628373, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 431796224, + "peakRssBytes": 457940992, + "pssBytes": 433747968, + "virtualBytes": 4855644160, + "minorFaults": 660646, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365223936, + "peakRssBytes": 457940992, + "pssBytes": 367048704, + "virtualBytes": 4097970176, + "minorFaults": 660646, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365142016, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365223936, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 204.94029800000135, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.816492 + }, + { + "name": "WebAssembly.Module", + "ms": 3.652465 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.614742 + }, + { + "name": "wasi.start", + "ms": 22.638931 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365223936, + "peakRssBytes": 457940992, + "pssBytes": 367048704, + "virtualBytes": 4097970176, + "minorFaults": 660646, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 431964160, + "peakRssBytes": 457940992, + "pssBytes": 433641472, + "virtualBytes": 4855787520, + "minorFaults": 692918, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365207552, + "peakRssBytes": 457940992, + "pssBytes": 367052800, + "virtualBytes": 4097970176, + "minorFaults": 692918, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365223936, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365207552, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 132.1042890000026, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.577324 + }, + { + "name": "WebAssembly.Module", + "ms": 3.150945 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.635571 + }, + { + "name": "wasi.start", + "ms": 14.260463 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365207552, + "peakRssBytes": 457940992, + "pssBytes": 367052800, + "virtualBytes": 4097970176, + "minorFaults": 692918, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 404172800, + "peakRssBytes": 457940992, + "pssBytes": 404174848, + "virtualBytes": 4825341952, + "minorFaults": 712523, + "majorFaults": 1 + }, + "end": { + "rssBytes": 371761152, + "peakRssBytes": 457940992, + "pssBytes": 290998272, + "virtualBytes": 4097970176, + "minorFaults": 712523, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365207552, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372301824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 149.9740380000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.417613 + }, + { + "name": "WebAssembly.Module", + "ms": 1.343801 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.35629 + }, + { + "name": "wasi.start", + "ms": 14.196115 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 372572160, + "peakRssBytes": 457940992, + "pssBytes": 109209600, + "virtualBytes": 4097970176, + "minorFaults": 712728, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 426049536, + "peakRssBytes": 457940992, + "pssBytes": 404715520, + "virtualBytes": 4825079808, + "minorFaults": 738264, + "majorFaults": 1 + }, + "end": { + "rssBytes": 375062528, + "peakRssBytes": 457940992, + "pssBytes": 219550720, + "virtualBytes": 4097970176, + "minorFaults": 738264, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372301824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 376143872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 160.62619899999845, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.847162 + }, + { + "name": "WebAssembly.Module", + "ms": 2.345779 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.32701 + }, + { + "name": "wasi.start", + "ms": 15.706572 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 376414208, + "peakRssBytes": 457940992, + "pssBytes": 321985536, + "virtualBytes": 4097970176, + "minorFaults": 738634, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 430129152, + "peakRssBytes": 457940992, + "pssBytes": 420177920, + "virtualBytes": 4825341952, + "minorFaults": 766712, + "majorFaults": 1 + }, + "end": { + "rssBytes": 389033984, + "peakRssBytes": 457940992, + "pssBytes": 391948288, + "virtualBytes": 4097970176, + "minorFaults": 766712, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 376414208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390115328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 182.21136999999726, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.448459 + }, + { + "name": "WebAssembly.Module", + "ms": 2.979541 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.03835 + }, + { + "name": "wasi.start", + "ms": 14.931027 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 390115328, + "peakRssBytes": 457940992, + "pssBytes": 391952384, + "virtualBytes": 4097970176, + "minorFaults": 766965, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 426262528, + "peakRssBytes": 457940992, + "pssBytes": 408785920, + "virtualBytes": 4824961024, + "minorFaults": 787150, + "majorFaults": 1 + }, + "end": { + "rssBytes": 370552832, + "peakRssBytes": 457940992, + "pssBytes": 372168704, + "virtualBytes": 4097970176, + "minorFaults": 787150, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390115328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 370552832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 143.4593789999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 18.913621 + }, + { + "name": "WebAssembly.Module", + "ms": 3.915126 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.157748 + }, + { + "name": "wasi.start", + "ms": 14.093556 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 370552832, + "peakRssBytes": 457940992, + "pssBytes": 372168704, + "virtualBytes": 4097970176, + "minorFaults": 787150, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 406376448, + "peakRssBytes": 457940992, + "pssBytes": 405092352, + "virtualBytes": 4825223168, + "minorFaults": 815220, + "majorFaults": 1 + }, + "end": { + "rssBytes": 382943232, + "peakRssBytes": 457940992, + "pssBytes": 386000896, + "virtualBytes": 4097970176, + "minorFaults": 815220, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 370552832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 384294912, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 299.1098409999977, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.686555 + }, + { + "name": "WebAssembly.Module", + "ms": 4.612751 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.646042 + }, + { + "name": "wasi.start", + "ms": 30.859589 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 384294912, + "peakRssBytes": 457940992, + "pssBytes": 386369536, + "virtualBytes": 4097970176, + "minorFaults": 815541, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 459923456, + "peakRssBytes": 460136448, + "pssBytes": 461871104, + "virtualBytes": 4836626432, + "minorFaults": 864590, + "majorFaults": 1 + }, + "end": { + "rssBytes": 371773440, + "peakRssBytes": 460136448, + "pssBytes": 373626880, + "virtualBytes": 4097970176, + "minorFaults": 864590, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 384294912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371773440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 279.4649879999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.602341 + }, + { + "name": "WebAssembly.Module", + "ms": 4.226536 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.007249 + }, + { + "name": "wasi.start", + "ms": 33.844718 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 371773440, + "peakRssBytes": 460136448, + "pssBytes": 373626880, + "virtualBytes": 4097970176, + "minorFaults": 864590, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 463073280, + "peakRssBytes": 463343616, + "pssBytes": 465164288, + "virtualBytes": 4836769792, + "minorFaults": 909907, + "majorFaults": 1 + }, + "end": { + "rssBytes": 372322304, + "peakRssBytes": 463343616, + "pssBytes": 374339584, + "virtualBytes": 4097970176, + "minorFaults": 909907, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371773440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372322304, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 295.0437479999964, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.658344 + }, + { + "name": "WebAssembly.Module", + "ms": 3.467736 + }, + { + "name": "WebAssembly.Instance", + "ms": 7.244198 + }, + { + "name": "wasi.start", + "ms": 37.637465 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 372322304, + "peakRssBytes": 463343616, + "pssBytes": 374338560, + "virtualBytes": 4097970176, + "minorFaults": 909907, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 464916480, + "peakRssBytes": 465104896, + "pssBytes": 462296064, + "virtualBytes": 4888928256, + "minorFaults": 954572, + "majorFaults": 1 + }, + "end": { + "rssBytes": 372428800, + "peakRssBytes": 465104896, + "pssBytes": 374409216, + "virtualBytes": 4097970176, + "minorFaults": 954572, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372322304, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372428800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 296.7384930000044, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 77.515073 + }, + { + "name": "WebAssembly.Module", + "ms": 5.460358 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.488015 + }, + { + "name": "wasi.start", + "ms": 48.867623 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 372428800, + "peakRssBytes": 465104896, + "pssBytes": 374409216, + "virtualBytes": 4097970176, + "minorFaults": 954572, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 465006592, + "peakRssBytes": 465125376, + "pssBytes": 467269632, + "virtualBytes": 4836769792, + "minorFaults": 999664, + "majorFaults": 1 + }, + "end": { + "rssBytes": 374972416, + "peakRssBytes": 465125376, + "pssBytes": 285795328, + "virtualBytes": 4642717696, + "minorFaults": 999664, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 372428800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374841344, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 507.95485899999767, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 72.549314 + }, + { + "name": "WebAssembly.Module", + "ms": 4.463765 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.835237 + }, + { + "name": "wasi.start", + "ms": 57.086665 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374841344, + "peakRssBytes": 465125376, + "pssBytes": 376738816, + "virtualBytes": 4097970176, + "minorFaults": 999664, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 467685376, + "peakRssBytes": 467759104, + "pssBytes": 469602304, + "virtualBytes": 4889452544, + "minorFaults": 1044826, + "majorFaults": 1 + }, + "end": { + "rssBytes": 375042048, + "peakRssBytes": 467759104, + "pssBytes": 376845312, + "virtualBytes": 4097970176, + "minorFaults": 1044826, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374841344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375042048, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 325.3107860000018, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 96.621737 + }, + { + "name": "WebAssembly.Module", + "ms": 9.984288 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.432522 + }, + { + "name": "wasi.start", + "ms": 7.204563 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375042048, + "peakRssBytes": 467759104, + "pssBytes": 376844288, + "virtualBytes": 4097970176, + "minorFaults": 1044826, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 468291584, + "peakRssBytes": 468312064, + "pssBytes": 466017280, + "virtualBytes": 4879753216, + "minorFaults": 1108705, + "majorFaults": 1 + }, + "end": { + "rssBytes": 378507264, + "peakRssBytes": 468312064, + "pssBytes": 380278784, + "virtualBytes": 4099571712, + "minorFaults": 1108705, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375042048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 378507264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 308.2085120000047, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 90.091262 + }, + { + "name": "WebAssembly.Module", + "ms": 4.486345 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.511371 + }, + { + "name": "wasi.start", + "ms": 5.071775 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 378507264, + "peakRssBytes": 468312064, + "pssBytes": 380278784, + "virtualBytes": 4099571712, + "minorFaults": 1108705, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 470941696, + "peakRssBytes": 471154688, + "pssBytes": 469810176, + "virtualBytes": 4880003072, + "minorFaults": 1173268, + "majorFaults": 1 + }, + "end": { + "rssBytes": 380674048, + "peakRssBytes": 471154688, + "pssBytes": 382307328, + "virtualBytes": 4099964928, + "minorFaults": 1173268, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 378507264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 380674048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 318.8781649999946, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 86.424529 + }, + { + "name": "WebAssembly.Module", + "ms": 4.017072 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.697466 + }, + { + "name": "wasi.start", + "ms": 6.908464 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 380674048, + "peakRssBytes": 471154688, + "pssBytes": 382307328, + "virtualBytes": 4099964928, + "minorFaults": 1173268, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 511455232, + "peakRssBytes": 511717376, + "pssBytes": 513251328, + "virtualBytes": 4880003072, + "minorFaults": 1237579, + "majorFaults": 1 + }, + "end": { + "rssBytes": 381702144, + "peakRssBytes": 511717376, + "pssBytes": 383314944, + "virtualBytes": 4099964928, + "minorFaults": 1237579, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 380674048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381702144, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 297.9121410000007, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 85.520773 + }, + { + "name": "WebAssembly.Module", + "ms": 3.635135 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.990226 + }, + { + "name": "wasi.start", + "ms": 6.259169 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 381702144, + "peakRssBytes": 511717376, + "pssBytes": 383314944, + "virtualBytes": 4099964928, + "minorFaults": 1237579, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 511676416, + "peakRssBytes": 511717376, + "pssBytes": 513219584, + "virtualBytes": 4880003072, + "minorFaults": 1301642, + "majorFaults": 1 + }, + "end": { + "rssBytes": 381431808, + "peakRssBytes": 511717376, + "pssBytes": 383313920, + "virtualBytes": 4099964928, + "minorFaults": 1301642, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381702144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381431808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 419.5652610000034, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 93.395791 + }, + { + "name": "WebAssembly.Module", + "ms": 5.96766 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.539486 + }, + { + "name": "wasi.start", + "ms": 5.083364 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 381431808, + "peakRssBytes": 511717376, + "pssBytes": 383313920, + "virtualBytes": 4099964928, + "minorFaults": 1301642, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 471937024, + "peakRssBytes": 511717376, + "pssBytes": 469965824, + "virtualBytes": 4880003072, + "minorFaults": 1365706, + "majorFaults": 1 + }, + "end": { + "rssBytes": 381407232, + "peakRssBytes": 511717376, + "pssBytes": 383313920, + "virtualBytes": 4099964928, + "minorFaults": 1365706, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381431808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381407232, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 246.036334000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 30.906895 + }, + { + "name": "WebAssembly.Module", + "ms": 1.161552 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.509619 + }, + { + "name": "wasi.start", + "ms": 78.155971 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 381407232, + "peakRssBytes": 511717376, + "pssBytes": 383313920, + "virtualBytes": 4099964928, + "minorFaults": 1365706, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435081216, + "peakRssBytes": 511717376, + "pssBytes": 433093632, + "virtualBytes": 4837986304, + "minorFaults": 1390624, + "majorFaults": 1 + }, + "end": { + "rssBytes": 385486848, + "peakRssBytes": 511717376, + "pssBytes": 387185664, + "virtualBytes": 4100923392, + "minorFaults": 1390624, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 381407232, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385486848, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 262.3751970000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.586923 + }, + { + "name": "WebAssembly.Module", + "ms": 4.153511 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.271231 + }, + { + "name": "wasi.start", + "ms": 90.188238 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 385486848, + "peakRssBytes": 511717376, + "pssBytes": 387185664, + "virtualBytes": 4100923392, + "minorFaults": 1390624, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435462144, + "peakRssBytes": 511717376, + "pssBytes": 437401600, + "virtualBytes": 4838653952, + "minorFaults": 1414628, + "majorFaults": 1 + }, + "end": { + "rssBytes": 385187840, + "peakRssBytes": 511717376, + "pssBytes": 387315712, + "virtualBytes": 4100923392, + "minorFaults": 1414628, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385486848, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385187840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 220.63501700000052, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 28.864853 + }, + { + "name": "WebAssembly.Module", + "ms": 2.820871 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.385478 + }, + { + "name": "wasi.start", + "ms": 72.632886 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 385187840, + "peakRssBytes": 511717376, + "pssBytes": 387315712, + "virtualBytes": 4100923392, + "minorFaults": 1414628, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435499008, + "peakRssBytes": 511717376, + "pssBytes": 437382144, + "virtualBytes": 4838916096, + "minorFaults": 1438602, + "majorFaults": 1 + }, + "end": { + "rssBytes": 385449984, + "peakRssBytes": 511717376, + "pssBytes": 387324928, + "virtualBytes": 4100923392, + "minorFaults": 1438602, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385187840, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385449984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 227.84548899999936, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.263878 + }, + { + "name": "WebAssembly.Module", + "ms": 3.063183 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.676139 + }, + { + "name": "wasi.start", + "ms": 76.549924 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 385449984, + "peakRssBytes": 511717376, + "pssBytes": 387324928, + "virtualBytes": 4100923392, + "minorFaults": 1438602, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435507200, + "peakRssBytes": 511717376, + "pssBytes": 437471232, + "virtualBytes": 4837986304, + "minorFaults": 1462574, + "majorFaults": 1 + }, + "end": { + "rssBytes": 385454080, + "peakRssBytes": 511717376, + "pssBytes": 387323904, + "virtualBytes": 4100923392, + "minorFaults": 1462574, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385449984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385454080, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 230.45646300000226, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.645011 + }, + { + "name": "WebAssembly.Module", + "ms": 3.057737 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.560671 + }, + { + "name": "wasi.start", + "ms": 75.902215 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 385454080, + "peakRssBytes": 511717376, + "pssBytes": 387323904, + "virtualBytes": 4100923392, + "minorFaults": 1462574, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 435445760, + "peakRssBytes": 511717376, + "pssBytes": 437373952, + "virtualBytes": 4838653952, + "minorFaults": 1486547, + "majorFaults": 1 + }, + "end": { + "rssBytes": 385388544, + "peakRssBytes": 511717376, + "pssBytes": 387324928, + "virtualBytes": 4100923392, + "minorFaults": 1486547, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385454080, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385388544, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 415.5584929999968, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.546536 + }, + { + "name": "WebAssembly.Module", + "ms": 1.411252 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.768425 + }, + { + "name": "wasi.start", + "ms": 17.094253 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 385388544, + "peakRssBytes": 511717376, + "pssBytes": 387324928, + "virtualBytes": 4100923392, + "minorFaults": 1486547, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 452984832, + "peakRssBytes": 511717376, + "pssBytes": 454659072, + "virtualBytes": 4858560512, + "minorFaults": 1519021, + "majorFaults": 1 + }, + "end": { + "rssBytes": 387031040, + "peakRssBytes": 511717376, + "pssBytes": 388725760, + "virtualBytes": 4102430720, + "minorFaults": 1519021, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 385388544, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387031040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 178.70123700000113, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.468802 + }, + { + "name": "WebAssembly.Module", + "ms": 1.292243 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.957836 + }, + { + "name": "wasi.start", + "ms": 17.605015 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 387031040, + "peakRssBytes": 511717376, + "pssBytes": 388725760, + "virtualBytes": 4102430720, + "minorFaults": 1519021, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 452976640, + "peakRssBytes": 511717376, + "pssBytes": 454876160, + "virtualBytes": 4859494400, + "minorFaults": 1551221, + "majorFaults": 1 + }, + "end": { + "rssBytes": 387006464, + "peakRssBytes": 511717376, + "pssBytes": 388995072, + "virtualBytes": 4102696960, + "minorFaults": 1551221, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387031040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387006464, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 197.95611800000188, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 39.163304 + }, + { + "name": "WebAssembly.Module", + "ms": 3.166119 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.784544 + }, + { + "name": "wasi.start", + "ms": 13.932456 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 387006464, + "peakRssBytes": 511717376, + "pssBytes": 388995072, + "virtualBytes": 4102696960, + "minorFaults": 1551221, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 453197824, + "peakRssBytes": 511717376, + "pssBytes": 454851584, + "virtualBytes": 4859088896, + "minorFaults": 1583355, + "majorFaults": 1 + }, + "end": { + "rssBytes": 387194880, + "peakRssBytes": 511717376, + "pssBytes": 388996096, + "virtualBytes": 4102696960, + "minorFaults": 1583355, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387006464, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387194880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 185.7811789999978, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.283581 + }, + { + "name": "WebAssembly.Module", + "ms": 2.300215 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.357341 + }, + { + "name": "wasi.start", + "ms": 17.112614 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 387194880, + "peakRssBytes": 511717376, + "pssBytes": 388996096, + "virtualBytes": 4102696960, + "minorFaults": 1583355, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 453357568, + "peakRssBytes": 511717376, + "pssBytes": 455068672, + "virtualBytes": 4858970112, + "minorFaults": 1615486, + "majorFaults": 1 + }, + "end": { + "rssBytes": 387133440, + "peakRssBytes": 511717376, + "pssBytes": 388996096, + "virtualBytes": 4102696960, + "minorFaults": 1615486, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387194880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387133440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 227.22711100000015, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624860708586110/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.641975 + }, + { + "name": "WebAssembly.Module", + "ms": 4.207339 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.990306 + }, + { + "name": "wasi.start", + "ms": 21.501787 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 387133440, + "peakRssBytes": 511717376, + "pssBytes": 388996096, + "virtualBytes": 4102696960, + "minorFaults": 1615486, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 453083136, + "peakRssBytes": 511717376, + "pssBytes": 455075840, + "virtualBytes": 4858826752, + "minorFaults": 1647620, + "majorFaults": 1 + }, + "end": { + "rssBytes": 387117056, + "peakRssBytes": 511717376, + "pssBytes": 388995072, + "virtualBytes": 4102696960, + "minorFaults": 1647620, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387133440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 387117056, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 1, + "vmSetupMs": 447.31380699999863, + "fixtureSetupMs": 402.2440159999969, + "baseline": { + "rssBytes": 240857088, + "peakRssBytes": 246292480, + "pssBytes": 242402304, + "virtualBytes": 3885953024, + "minorFaults": 67444, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 496205824, + "peakRssBytes": 529338368, + "pssBytes": 498275328, + "virtualBytes": 4195414016, + "minorFaults": 131993, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 255348736, + "peakRssBytes": 283045888, + "pssBytes": 255873024, + "virtualBytes": 309460992, + "minorFaults": 64549, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 67.98965999999928, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.080082, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.073989, + "name": "Engine" + }, + { + "ms": 0.10629, + "name": "canonicalPreopens" + }, + { + "ms": 3.46279, + "name": "moduleRead" + }, + { + "ms": 0.21092, + "name": "profileValidation" + }, + { + "ms": 48.04254, + "name": "moduleCompile" + }, + { + "ms": 0.003205, + "name": "importValidation" + }, + { + "ms": 0.181915, + "name": "Linker" + }, + { + "ms": 0.016353, + "name": "Store" + }, + { + "ms": 0.048933, + "name": "Instance" + }, + { + "ms": 0.043248, + "name": "signalMaskInit" + }, + { + "ms": 0.002475, + "name": "entrypointLookup" + }, + { + "ms": 0.019842000000000002, + "name": "wasi.start" + }, + { + "ms": 0.017363999999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 52.292012 + }, + "memory": { + "start": { + "rssBytes": 240857088, + "peakRssBytes": 246292480, + "pssBytes": 242410496, + "virtualBytes": 3888066560, + "minorFaults": 67446, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253084672, + "virtualBytes": 3957465088, + "minorFaults": 68589, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253084672, + "virtualBytes": 3957465088, + "minorFaults": 68589, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240857088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 25.262886999997136, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010421000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.0024389999999999998, + "name": "Engine" + }, + { + "ms": 0.103116, + "name": "canonicalPreopens" + }, + { + "ms": 3.3028760000000004, + "name": "moduleRead" + }, + { + "ms": 0.24259999999999998, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0031149999999999997, + "name": "importValidation" + }, + { + "ms": 0.187689, + "name": "Linker" + }, + { + "ms": 0.015274999999999999, + "name": "Store" + }, + { + "ms": 0.031017, + "name": "Instance" + }, + { + "ms": 0.624761, + "name": "signalMaskInit" + }, + { + "ms": 0.006794, + "name": "entrypointLookup" + }, + { + "ms": 0.037846000000000005, + "name": "wasi.start" + }, + { + "ms": 0.015353, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 4.636508 + }, + "memory": { + "start": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253084672, + "virtualBytes": 3957465088, + "minorFaults": 68589, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253084672, + "virtualBytes": 3957465088, + "minorFaults": 68611, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68611, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 28.144391000001633, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.020016, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.002128, + "name": "Engine" + }, + { + "ms": 0.120182, + "name": "canonicalPreopens" + }, + { + "ms": 3.325273, + "name": "moduleRead" + }, + { + "ms": 0.23948, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003828, + "name": "importValidation" + }, + { + "ms": 0.205852, + "name": "Linker" + }, + { + "ms": 0.014008, + "name": "Store" + }, + { + "ms": 0.028506, + "name": "Instance" + }, + { + "ms": 0.540982, + "name": "signalMaskInit" + }, + { + "ms": 0.003799, + "name": "entrypointLookup" + }, + { + "ms": 2.480002, + "name": "wasi.start" + }, + { + "ms": 0.026633999999999998, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 7.086459 + }, + "memory": { + "start": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68611, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253084672, + "virtualBytes": 8319553536, + "minorFaults": 68633, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68633, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 24.14887599999929, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.013731, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.000973, + "name": "Engine" + }, + { + "ms": 0.10053000000000001, + "name": "canonicalPreopens" + }, + { + "ms": 3.328171, + "name": "moduleRead" + }, + { + "ms": 0.24216200000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003484, + "name": "importValidation" + }, + { + "ms": 0.196608, + "name": "Linker" + }, + { + "ms": 0.015787, + "name": "Store" + }, + { + "ms": 0.032214, + "name": "Instance" + }, + { + "ms": 0.959734, + "name": "signalMaskInit" + }, + { + "ms": 0.012578, + "name": "entrypointLookup" + }, + { + "ms": 0.031819999999999994, + "name": "wasi.start" + }, + { + "ms": 0.018286, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.027291 + }, + "memory": { + "start": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68633, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68655, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68655, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 23.503891000000294, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010611, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001577, + "name": "Engine" + }, + { + "ms": 0.10495299999999999, + "name": "canonicalPreopens" + }, + { + "ms": 3.3721240000000003, + "name": "moduleRead" + }, + { + "ms": 0.228747, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0030529999999999997, + "name": "importValidation" + }, + { + "ms": 0.185032, + "name": "Linker" + }, + { + "ms": 0.014945, + "name": "Store" + }, + { + "ms": 0.033795, + "name": "Instance" + }, + { + "ms": 0.618313, + "name": "signalMaskInit" + }, + { + "ms": 0.007637, + "name": "entrypointLookup" + }, + { + "ms": 0.654684, + "name": "wasi.start" + }, + { + "ms": 0.020533000000000003, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.325644 + }, + "memory": { + "start": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68655, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 8319553536, + "minorFaults": 68677, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68677, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1132.8213069999983, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1063.252583, + "firstHostCallMs": 0.016762, + "firstOutputMs": 1110.569892, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.002518, + "name": "Engine" + }, + { + "ms": 0.126706, + "name": "canonicalPreopens" + }, + { + "ms": 5.597691, + "name": "moduleRead" + }, + { + "ms": 3.991886, + "name": "profileValidation" + }, + { + "ms": 1052.328916, + "name": "moduleCompile" + }, + { + "ms": 0.006888, + "name": "importValidation" + }, + { + "ms": 0.174604, + "name": "Linker" + }, + { + "ms": 0.016654, + "name": "Store" + }, + { + "ms": 0.181602, + "name": "Instance" + }, + { + "ms": 0.071881, + "name": "signalMaskInit" + }, + { + "ms": 0.00334, + "name": "entrypointLookup" + }, + { + "ms": 47.559421, + "name": "wasi.start" + }, + { + "ms": 1.178351, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 1111.860522 + }, + "memory": { + "start": { + "rssBytes": 251412480, + "peakRssBytes": 251617280, + "pssBytes": 253083648, + "virtualBytes": 3957465088, + "minorFaults": 68677, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289542144, + "peakRssBytes": 289656832, + "pssBytes": 291406848, + "virtualBytes": 8327368704, + "minorFaults": 80191, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80191, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 48042, + "wasmtimeProcessRetainedRssBytes": 251412480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 81.10191599999962, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.545637, + "firstHostCallMs": 0.009665, + "firstOutputMs": 59.076128, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.00117, + "name": "Engine" + }, + { + "ms": 0.12286099999999998, + "name": "canonicalPreopens" + }, + { + "ms": 6.569153, + "name": "moduleRead" + }, + { + "ms": 3.8166290000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007329, + "name": "importValidation" + }, + { + "ms": 0.18812700000000002, + "name": "Linker" + }, + { + "ms": 0.01746, + "name": "Store" + }, + { + "ms": 0.043546, + "name": "Instance" + }, + { + "ms": 0.028998, + "name": "signalMaskInit" + }, + { + "ms": 0.0033060000000000003, + "name": "entrypointLookup" + }, + { + "ms": 47.784084, + "name": "wasi.start" + }, + { + "ms": 0.047338, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 59.230422999999995 + }, + "memory": { + "start": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80191, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289509376, + "peakRssBytes": 289656832, + "pssBytes": 291283968, + "virtualBytes": 8327368704, + "minorFaults": 80265, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80265, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 79.65995699999621, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.701625, + "firstHostCallMs": 0.012256, + "firstOutputMs": 59.479133, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001429, + "name": "Engine" + }, + { + "ms": 0.113088, + "name": "canonicalPreopens" + }, + { + "ms": 6.658156999999999, + "name": "moduleRead" + }, + { + "ms": 3.876039, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007524, + "name": "importValidation" + }, + { + "ms": 0.19337200000000002, + "name": "Linker" + }, + { + "ms": 0.017318, + "name": "Store" + }, + { + "ms": 0.039255000000000005, + "name": "Instance" + }, + { + "ms": 0.050535000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.003349, + "name": "entrypointLookup" + }, + { + "ms": 48.036439, + "name": "wasi.start" + }, + { + "ms": 1.838714, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 61.473224 + }, + "memory": { + "start": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80265, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289509376, + "peakRssBytes": 289656832, + "pssBytes": 291283968, + "virtualBytes": 8327368704, + "minorFaults": 80339, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80339, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 86.73017600000458, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.564623000000001, + "firstHostCallMs": 0.012785000000000001, + "firstOutputMs": 66.58851200000001, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001353, + "name": "Engine" + }, + { + "ms": 0.11234, + "name": "canonicalPreopens" + }, + { + "ms": 7.126816, + "name": "moduleRead" + }, + { + "ms": 4.195078, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008031, + "name": "importValidation" + }, + { + "ms": 0.19696899999999998, + "name": "Linker" + }, + { + "ms": 0.017316, + "name": "Store" + }, + { + "ms": 0.046971, + "name": "Instance" + }, + { + "ms": 0.082373, + "name": "signalMaskInit" + }, + { + "ms": 0.004643, + "name": "entrypointLookup" + }, + { + "ms": 54.286820999999996, + "name": "wasi.start" + }, + { + "ms": 0.044727, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 66.73736299999999 + }, + "memory": { + "start": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80339, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289509376, + "peakRssBytes": 289656832, + "pssBytes": 291282944, + "virtualBytes": 8327368704, + "minorFaults": 80413, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290648064, + "virtualBytes": 3962912768, + "minorFaults": 80413, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 86.97417100000166, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.072799999999999, + "firstHostCallMs": 0.015252, + "firstOutputMs": 66.23459199999999, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001993, + "name": "Engine" + }, + { + "ms": 0.17758100000000002, + "name": "canonicalPreopens" + }, + { + "ms": 6.66612, + "name": "moduleRead" + }, + { + "ms": 4.179324, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008844999999999999, + "name": "importValidation" + }, + { + "ms": 0.199208, + "name": "Linker" + }, + { + "ms": 0.01912, + "name": "Store" + }, + { + "ms": 0.040917, + "name": "Instance" + }, + { + "ms": 0.028299, + "name": "signalMaskInit" + }, + { + "ms": 0.003094, + "name": "entrypointLookup" + }, + { + "ms": 54.408907, + "name": "wasi.start" + }, + { + "ms": 0.6756279999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 67.02003099999999 + }, + "memory": { + "start": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290648064, + "virtualBytes": 3962912768, + "minorFaults": 80413, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 289509376, + "peakRssBytes": 289656832, + "pssBytes": 291282944, + "virtualBytes": 8327368704, + "minorFaults": 80487, + "majorFaults": 0 + }, + "end": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290648064, + "virtualBytes": 3962912768, + "minorFaults": 80487, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3901.201423000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3440.367139, + "firstHostCallMs": 0.00965, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001061, + "name": "Engine" + }, + { + "ms": 0.139045, + "name": "canonicalPreopens" + }, + { + "ms": 10.575546, + "name": "moduleRead" + }, + { + "ms": 12.662398, + "name": "profileValidation" + }, + { + "ms": 3412.543914, + "name": "moduleCompile" + }, + { + "ms": 0.009779999999999999, + "name": "importValidation" + }, + { + "ms": 0.211902, + "name": "Linker" + }, + { + "ms": 0.017625000000000002, + "name": "Store" + }, + { + "ms": 2.613136, + "name": "Instance" + }, + { + "ms": 0.112984, + "name": "signalMaskInit" + }, + { + "ms": 0.005677, + "name": "entrypointLookup" + }, + { + "ms": 431.936624, + "name": "wasi.start" + }, + { + "ms": 0.058266, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 3872.333527 + }, + "memory": { + "start": { + "rssBytes": 288911360, + "peakRssBytes": 289656832, + "pssBytes": 290649088, + "virtualBytes": 3962912768, + "minorFaults": 80487, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420622336, + "peakRssBytes": 420835328, + "pssBytes": 423486464, + "virtualBytes": 12866445312, + "minorFaults": 120459, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120459, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1100371, + "wasmtimeProcessRetainedRssBytes": 288911360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 97.13445800000045, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 28.600991, + "firstHostCallMs": 0.018817, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0018750000000000001, + "name": "Engine" + }, + { + "ms": 0.176363, + "name": "canonicalPreopens" + }, + { + "ms": 12.873558000000001, + "name": "moduleRead" + }, + { + "ms": 12.71135, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00989, + "name": "importValidation" + }, + { + "ms": 0.202093, + "name": "Linker" + }, + { + "ms": 0.01754, + "name": "Store" + }, + { + "ms": 1.071505, + "name": "Instance" + }, + { + "ms": 0.090206, + "name": "signalMaskInit" + }, + { + "ms": 0.0043170000000000005, + "name": "entrypointLookup" + }, + { + "ms": 35.496844, + "name": "wasi.start" + }, + { + "ms": 0.062737, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 64.191132 + }, + "memory": { + "start": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120459, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420130816, + "peakRssBytes": 420835328, + "pssBytes": 423629824, + "virtualBytes": 12866445312, + "minorFaults": 120548, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120548, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 101.58639099999709, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 27.943264999999997, + "firstHostCallMs": 0.010829, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0010350000000000001, + "name": "Engine" + }, + { + "ms": 0.111537, + "name": "canonicalPreopens" + }, + { + "ms": 11.618939, + "name": "moduleRead" + }, + { + "ms": 12.788102, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012935, + "name": "importValidation" + }, + { + "ms": 0.2334, + "name": "Linker" + }, + { + "ms": 0.018787, + "name": "Store" + }, + { + "ms": 0.321847, + "name": "Instance" + }, + { + "ms": 0.067994, + "name": "signalMaskInit" + }, + { + "ms": 0.00437, + "name": "entrypointLookup" + }, + { + "ms": 41.181229, + "name": "wasi.start" + }, + { + "ms": 0.053528, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 67.88617199999999 + }, + "memory": { + "start": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120548, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420130816, + "peakRssBytes": 420835328, + "pssBytes": 423628800, + "virtualBytes": 12866445312, + "minorFaults": 120637, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422785024, + "virtualBytes": 4137533440, + "minorFaults": 120637, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 108.72462200000155, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.212611000000003, + "firstHostCallMs": 0.024758, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.00226, + "name": "Engine" + }, + { + "ms": 0.220728, + "name": "canonicalPreopens" + }, + { + "ms": 12.371773000000001, + "name": "moduleRead" + }, + { + "ms": 12.994182, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010434, + "name": "importValidation" + }, + { + "ms": 0.207062, + "name": "Linker" + }, + { + "ms": 0.017956, + "name": "Store" + }, + { + "ms": 3.572334, + "name": "Instance" + }, + { + "ms": 0.123994, + "name": "signalMaskInit" + }, + { + "ms": 0.010524, + "name": "entrypointLookup" + }, + { + "ms": 46.402215, + "name": "wasi.start" + }, + { + "ms": 0.6532789999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 78.072767 + }, + "memory": { + "start": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422785024, + "virtualBytes": 4137533440, + "minorFaults": 120637, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 423628800, + "virtualBytes": 12866445312, + "minorFaults": 120726, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120726, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 82.48570000000473, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 26.100337, + "firstHostCallMs": 0.015466, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0012410000000000001, + "name": "Engine" + }, + { + "ms": 0.125288, + "name": "canonicalPreopens" + }, + { + "ms": 11.661487999999999, + "name": "moduleRead" + }, + { + "ms": 12.528973, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010421000000000001, + "name": "importValidation" + }, + { + "ms": 0.203003, + "name": "Linker" + }, + { + "ms": 0.018202, + "name": "Store" + }, + { + "ms": 0.045841, + "name": "Instance" + }, + { + "ms": 0.067969, + "name": "signalMaskInit" + }, + { + "ms": 0.0029579999999999997, + "name": "entrypointLookup" + }, + { + "ms": 30.651434000000002, + "name": "wasi.start" + }, + { + "ms": 0.04562, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 56.803643 + }, + "memory": { + "start": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120726, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 423629824, + "virtualBytes": 12866445312, + "minorFaults": 120815, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120815, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1583.730942999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1551.1273970000002, + "firstHostCallMs": 0.013852, + "firstOutputMs": 1558.379051, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001414, + "name": "Engine" + }, + { + "ms": 0.10679, + "name": "canonicalPreopens" + }, + { + "ms": 6.88417, + "name": "moduleRead" + }, + { + "ms": 6.2358530000000005, + "name": "profileValidation" + }, + { + "ms": 1536.62799, + "name": "moduleCompile" + }, + { + "ms": 0.011704, + "name": "importValidation" + }, + { + "ms": 0.181203, + "name": "Linker" + }, + { + "ms": 0.017479, + "name": "Store" + }, + { + "ms": 0.25421699999999997, + "name": "Instance" + }, + { + "ms": 0.052392, + "name": "signalMaskInit" + }, + { + "ms": 0.0026070000000000004, + "name": "entrypointLookup" + }, + { + "ms": 7.413816, + "name": "wasi.start" + }, + { + "ms": 0.048529, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 1558.625806 + }, + "memory": { + "start": { + "rssBytes": 420057088, + "peakRssBytes": 420835328, + "pssBytes": 422786048, + "virtualBytes": 4137533440, + "minorFaults": 120815, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427622400, + "peakRssBytes": 428183552, + "pssBytes": 430748672, + "virtualBytes": 8509386752, + "minorFaults": 122733, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430747648, + "virtualBytes": 4144930816, + "minorFaults": 122733, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4899621, + "wasmtimeProcessRetainedRssBytes": 420057088, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 58.696011, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.309957, + "firstHostCallMs": 0.036715000000000005, + "firstOutputMs": 28.335803, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001338, + "name": "Engine" + }, + { + "ms": 0.135897, + "name": "canonicalPreopens" + }, + { + "ms": 6.922039, + "name": "moduleRead" + }, + { + "ms": 6.317647, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.022385, + "name": "importValidation" + }, + { + "ms": 0.233509, + "name": "Linker" + }, + { + "ms": 0.01867, + "name": "Store" + }, + { + "ms": 1.758156, + "name": "Instance" + }, + { + "ms": 0.069132, + "name": "signalMaskInit" + }, + { + "ms": 0.004928999999999999, + "name": "entrypointLookup" + }, + { + "ms": 12.403325, + "name": "wasi.start" + }, + { + "ms": 1.975441, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 30.73011 + }, + "memory": { + "start": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430747648, + "virtualBytes": 4144930816, + "minorFaults": 122733, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 431296512, + "virtualBytes": 8509386752, + "minorFaults": 122785, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430759936, + "virtualBytes": 4144930816, + "minorFaults": 122785, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 70.89306299999589, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.91727, + "firstHostCallMs": 0.016186, + "firstOutputMs": 33.259825, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001624, + "name": "Engine" + }, + { + "ms": 0.115247, + "name": "canonicalPreopens" + }, + { + "ms": 6.970879, + "name": "moduleRead" + }, + { + "ms": 8.033362, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014573, + "name": "importValidation" + }, + { + "ms": 0.223717, + "name": "Linker" + }, + { + "ms": 0.019045000000000003, + "name": "Store" + }, + { + "ms": 1.621937, + "name": "Instance" + }, + { + "ms": 0.09920999999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.007862000000000001, + "name": "entrypointLookup" + }, + { + "ms": 15.569462, + "name": "wasi.start" + }, + { + "ms": 2.48519, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 35.987798 + }, + "memory": { + "start": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430759936, + "virtualBytes": 4144930816, + "minorFaults": 122785, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 431300608, + "virtualBytes": 8509386752, + "minorFaults": 122835, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430764032, + "virtualBytes": 4144930816, + "minorFaults": 122835, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 68.17730200000369, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.933212, + "firstHostCallMs": 0.023474, + "firstOutputMs": 31.3916, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.00217, + "name": "Engine" + }, + { + "ms": 0.161677, + "name": "canonicalPreopens" + }, + { + "ms": 7.237975, + "name": "moduleRead" + }, + { + "ms": 6.465507, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014581, + "name": "importValidation" + }, + { + "ms": 0.219778, + "name": "Linker" + }, + { + "ms": 0.019999, + "name": "Store" + }, + { + "ms": 2.90263, + "name": "Instance" + }, + { + "ms": 0.10036400000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.006171, + "name": "entrypointLookup" + }, + { + "ms": 13.659792000000001, + "name": "wasi.start" + }, + { + "ms": 2.986968, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 34.600227 + }, + "memory": { + "start": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430764032, + "virtualBytes": 4144930816, + "minorFaults": 122835, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 431326208, + "virtualBytes": 8509386752, + "minorFaults": 122891, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430789632, + "virtualBytes": 4144930816, + "minorFaults": 122891, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 60.90697999999975, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.275114000000002, + "firstHostCallMs": 0.046551999999999996, + "firstOutputMs": 30.713762, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001725, + "name": "Engine" + }, + { + "ms": 0.13122, + "name": "canonicalPreopens" + }, + { + "ms": 7.172561, + "name": "moduleRead" + }, + { + "ms": 6.3659040000000005, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014334, + "name": "importValidation" + }, + { + "ms": 0.215261, + "name": "Linker" + }, + { + "ms": 0.018559, + "name": "Store" + }, + { + "ms": 0.051962, + "name": "Instance" + }, + { + "ms": 0.061436, + "name": "signalMaskInit" + }, + { + "ms": 0.003962, + "name": "entrypointLookup" + }, + { + "ms": 16.057002, + "name": "wasi.start" + }, + { + "ms": 2.322828, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 33.240567 + }, + "memory": { + "start": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430789632, + "virtualBytes": 4144930816, + "minorFaults": 122891, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 431342592, + "virtualBytes": 8509386752, + "minorFaults": 122945, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430806016, + "virtualBytes": 4144930816, + "minorFaults": 122945, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1340.506143999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1309.22256, + "firstHostCallMs": 0.01222, + "firstOutputMs": 1311.341571, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001212, + "name": "Engine" + }, + { + "ms": 0.11158, + "name": "canonicalPreopens" + }, + { + "ms": 5.356016, + "name": "moduleRead" + }, + { + "ms": 6.167739, + "name": "profileValidation" + }, + { + "ms": 1296.7183129999999, + "name": "moduleCompile" + }, + { + "ms": 0.009109, + "name": "importValidation" + }, + { + "ms": 0.189115, + "name": "Linker" + }, + { + "ms": 0.024868, + "name": "Store" + }, + { + "ms": 0.10381800000000001, + "name": "Instance" + }, + { + "ms": 0.092401, + "name": "signalMaskInit" + }, + { + "ms": 0.005366999999999999, + "name": "entrypointLookup" + }, + { + "ms": 2.184109, + "name": "wasi.start" + }, + { + "ms": 1.6949619999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 1313.1575870000001 + }, + "memory": { + "start": { + "rssBytes": 427532288, + "peakRssBytes": 428183552, + "pssBytes": 430806016, + "virtualBytes": 4144930816, + "minorFaults": 122945, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433057792, + "peakRssBytes": 435499008, + "pssBytes": 436273152, + "virtualBytes": 8514854912, + "minorFaults": 123340, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436273152, + "virtualBytes": 4150398976, + "minorFaults": 123340, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6436249, + "wasmtimeProcessRetainedRssBytes": 427532288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 43.70844300000317, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.13335, + "firstHostCallMs": 0.038152000000000005, + "firstOutputMs": 14.079898, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0011489999999999998, + "name": "Engine" + }, + { + "ms": 0.102675, + "name": "canonicalPreopens" + }, + { + "ms": 5.2099470000000005, + "name": "moduleRead" + }, + { + "ms": 4.809137, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010048, + "name": "importValidation" + }, + { + "ms": 0.199194, + "name": "Linker" + }, + { + "ms": 0.016736, + "name": "Store" + }, + { + "ms": 1.2606819999999999, + "name": "Instance" + }, + { + "ms": 0.054333000000000006, + "name": "signalMaskInit" + }, + { + "ms": 0.00333, + "name": "entrypointLookup" + }, + { + "ms": 2.0161480000000003, + "name": "wasi.start" + }, + { + "ms": 0.04024, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 14.212819 + }, + "memory": { + "start": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123340, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433074176, + "peakRssBytes": 435499008, + "pssBytes": 436347904, + "virtualBytes": 4150677504, + "minorFaults": 123390, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123390, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 41.244144999996934, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.544324999999999, + "firstHostCallMs": 0.016984, + "firstOutputMs": 13.46027, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001204, + "name": "Engine" + }, + { + "ms": 0.111, + "name": "canonicalPreopens" + }, + { + "ms": 5.285451, + "name": "moduleRead" + }, + { + "ms": 4.8578909999999995, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009824, + "name": "importValidation" + }, + { + "ms": 0.20036500000000002, + "name": "Linker" + }, + { + "ms": 0.017761000000000002, + "name": "Store" + }, + { + "ms": 0.568366, + "name": "Instance" + }, + { + "ms": 0.04884, + "name": "signalMaskInit" + }, + { + "ms": 0.003954, + "name": "entrypointLookup" + }, + { + "ms": 1.9776630000000002, + "name": "wasi.start" + }, + { + "ms": 0.040437, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.587716 + }, + "memory": { + "start": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123390, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433074176, + "peakRssBytes": 435499008, + "pssBytes": 436347904, + "virtualBytes": 4150677504, + "minorFaults": 123440, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123440, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 40.97710999999981, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.462227, + "firstHostCallMs": 0.017655999999999998, + "firstOutputMs": 13.291606, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.002075, + "name": "Engine" + }, + { + "ms": 0.110386, + "name": "canonicalPreopens" + }, + { + "ms": 5.133225, + "name": "moduleRead" + }, + { + "ms": 4.825762, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.01055, + "name": "importValidation" + }, + { + "ms": 0.19700499999999999, + "name": "Linker" + }, + { + "ms": 0.016404000000000002, + "name": "Store" + }, + { + "ms": 0.671974, + "name": "Instance" + }, + { + "ms": 0.048983, + "name": "signalMaskInit" + }, + { + "ms": 0.003378, + "name": "entrypointLookup" + }, + { + "ms": 1.892431, + "name": "wasi.start" + }, + { + "ms": 0.038287999999999996, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.418615 + }, + "memory": { + "start": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123440, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436347904, + "virtualBytes": 4150411264, + "minorFaults": 123490, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123490, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 167.80803600000218, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.914231000000001, + "firstHostCallMs": 0.014807, + "firstOutputMs": 16.619696, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001751, + "name": "Engine" + }, + { + "ms": 0.129033, + "name": "canonicalPreopens" + }, + { + "ms": 5.429578, + "name": "moduleRead" + }, + { + "ms": 5.586016, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00915, + "name": "importValidation" + }, + { + "ms": 0.194698, + "name": "Linker" + }, + { + "ms": 0.01696, + "name": "Store" + }, + { + "ms": 1.77139, + "name": "Instance" + }, + { + "ms": 0.07930000000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.003591, + "name": "entrypointLookup" + }, + { + "ms": 3.027776, + "name": "wasi.start" + }, + { + "ms": 0.039916, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 16.752688000000003 + }, + "memory": { + "start": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436274176, + "virtualBytes": 4150398976, + "minorFaults": 123490, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436347904, + "virtualBytes": 4150411264, + "minorFaults": 123540, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436273152, + "virtualBytes": 4150398976, + "minorFaults": 123540, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3690.876639000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3650.8631339999997, + "firstHostCallMs": 0.016021, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.0012180000000000001, + "name": "Engine" + }, + { + "ms": 0.143402, + "name": "canonicalPreopens" + }, + { + "ms": 12.5737, + "name": "moduleRead" + }, + { + "ms": 14.626926000000001, + "name": "profileValidation" + }, + { + "ms": 3619.712989, + "name": "moduleCompile" + }, + { + "ms": 0.013889, + "name": "importValidation" + }, + { + "ms": 0.25223599999999996, + "name": "Linker" + }, + { + "ms": 0.020988, + "name": "Store" + }, + { + "ms": 1.9798580000000001, + "name": "Instance" + }, + { + "ms": 0.060067, + "name": "signalMaskInit" + }, + { + "ms": 0.005596, + "name": "entrypointLookup" + }, + { + "ms": 7.574016, + "name": "wasi.start" + }, + { + "ms": 2.640637, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 3661.0457629999996 + }, + "memory": { + "start": { + "rssBytes": 433000448, + "peakRssBytes": 435499008, + "pssBytes": 436273152, + "virtualBytes": 4150398976, + "minorFaults": 123540, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450142208, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 8531271680, + "minorFaults": 124604, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124604, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7732968, + "wasmtimeProcessRetainedRssBytes": 433000448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 73.79316799999651, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.180059, + "firstHostCallMs": 0.02112, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001557, + "name": "Engine" + }, + { + "ms": 0.136686, + "name": "canonicalPreopens" + }, + { + "ms": 12.374092999999998, + "name": "moduleRead" + }, + { + "ms": 14.501194, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012972999999999998, + "name": "importValidation" + }, + { + "ms": 0.20863500000000001, + "name": "Linker" + }, + { + "ms": 0.017471, + "name": "Store" + }, + { + "ms": 1.451277, + "name": "Instance" + }, + { + "ms": 0.09034600000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.005378, + "name": "entrypointLookup" + }, + { + "ms": 12.068092, + "name": "wasi.start" + }, + { + "ms": 0.046256, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 42.265223 + }, + "memory": { + "start": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124604, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449970176, + "peakRssBytes": 450162688, + "pssBytes": 453411840, + "virtualBytes": 8531271680, + "minorFaults": 124701, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124701, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 248.54324499999348, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.069892, + "firstHostCallMs": 0.022736, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001384, + "name": "Engine" + }, + { + "ms": 0.21868100000000001, + "name": "canonicalPreopens" + }, + { + "ms": 11.522915999999999, + "name": "moduleRead" + }, + { + "ms": 15.095597, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.096787, + "name": "importValidation" + }, + { + "ms": 0.242144, + "name": "Linker" + }, + { + "ms": 0.019785, + "name": "Store" + }, + { + "ms": 0.376071, + "name": "Instance" + }, + { + "ms": 0.086585, + "name": "signalMaskInit" + }, + { + "ms": 0.004856, + "name": "entrypointLookup" + }, + { + "ms": 22.109068999999998, + "name": "wasi.start" + }, + { + "ms": 1.365312, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 52.512313 + }, + "memory": { + "start": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124701, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449970176, + "peakRssBytes": 450162688, + "pssBytes": 453399552, + "virtualBytes": 8531271680, + "minorFaults": 124798, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124798, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 77.15401200001361, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 27.529936000000003, + "firstHostCallMs": 0.022674999999999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.00128, + "name": "Engine" + }, + { + "ms": 0.136439, + "name": "canonicalPreopens" + }, + { + "ms": 11.251704, + "name": "moduleRead" + }, + { + "ms": 14.411827, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013517, + "name": "importValidation" + }, + { + "ms": 0.216398, + "name": "Linker" + }, + { + "ms": 0.018012999999999998, + "name": "Store" + }, + { + "ms": 0.05116, + "name": "Instance" + }, + { + "ms": 0.039403, + "name": "signalMaskInit" + }, + { + "ms": 0.0039, + "name": "entrypointLookup" + }, + { + "ms": 18.726671, + "name": "wasi.start" + }, + { + "ms": 0.666846, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 46.937250999999996 + }, + "memory": { + "start": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124798, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449970176, + "peakRssBytes": 450162688, + "pssBytes": 453395456, + "virtualBytes": 8531271680, + "minorFaults": 124895, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124895, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 77.7686570000078, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.684171, + "firstHostCallMs": 0.027833, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.009368999999999999, + "name": "Engine" + }, + { + "ms": 0.118258, + "name": "canonicalPreopens" + }, + { + "ms": 11.245008, + "name": "moduleRead" + }, + { + "ms": 14.431497, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014131999999999999, + "name": "importValidation" + }, + { + "ms": 0.20687599999999998, + "name": "Linker" + }, + { + "ms": 0.017942, + "name": "Store" + }, + { + "ms": 2.167221, + "name": "Instance" + }, + { + "ms": 0.07841799999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004543, + "name": "entrypointLookup" + }, + { + "ms": 19.144135, + "name": "wasi.start" + }, + { + "ms": 1.1457030000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 49.942256 + }, + "memory": { + "start": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124895, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449970176, + "peakRssBytes": 450162688, + "pssBytes": 453399552, + "virtualBytes": 8531271680, + "minorFaults": 124992, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124992, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3946.587942000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3914.031645, + "firstHostCallMs": 0.016574000000000002, + "firstOutputMs": 3915.31487, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001093, + "name": "Engine" + }, + { + "ms": 0.115371, + "name": "canonicalPreopens" + }, + { + "ms": 12.806583999999999, + "name": "moduleRead" + }, + { + "ms": 15.94982, + "name": "profileValidation" + }, + { + "ms": 3880.74049, + "name": "moduleCompile" + }, + { + "ms": 0.013712, + "name": "importValidation" + }, + { + "ms": 0.178493, + "name": "Linker" + }, + { + "ms": 0.017438000000000002, + "name": "Store" + }, + { + "ms": 2.154811, + "name": "Instance" + }, + { + "ms": 0.099714, + "name": "signalMaskInit" + }, + { + "ms": 0.006386, + "name": "entrypointLookup" + }, + { + "ms": 1.7775340000000002, + "name": "wasi.start" + }, + { + "ms": 0.048396, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 3915.539537 + }, + "memory": { + "start": { + "rssBytes": 449417216, + "peakRssBytes": 450162688, + "pssBytes": 452756480, + "virtualBytes": 4166815744, + "minorFaults": 124992, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479666176, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185423872, + "minorFaults": 126275, + "majorFaults": 0 + }, + "end": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126275, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11352681, + "wasmtimeProcessRetainedRssBytes": 449417216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 60.83197199999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 32.203634, + "firstHostCallMs": 0.086481, + "firstOutputMs": 34.54755, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.002427, + "name": "Engine" + }, + { + "ms": 0.130097, + "name": "canonicalPreopens" + }, + { + "ms": 12.927114, + "name": "moduleRead" + }, + { + "ms": 15.815375, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015267000000000001, + "name": "importValidation" + }, + { + "ms": 0.208001, + "name": "Linker" + }, + { + "ms": 0.017838, + "name": "Store" + }, + { + "ms": 1.100564, + "name": "Instance" + }, + { + "ms": 0.047246, + "name": "signalMaskInit" + }, + { + "ms": 0.00423, + "name": "entrypointLookup" + }, + { + "ms": 2.774636, + "name": "wasi.start" + }, + { + "ms": 0.621924, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 35.345092 + }, + "memory": { + "start": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126275, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 481583104, + "peakRssBytes": 482312192, + "pssBytes": 482898944, + "virtualBytes": 8549867520, + "minorFaults": 126319, + "majorFaults": 0 + }, + "end": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126319, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 60.072443999990355, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.191439000000003, + "firstHostCallMs": 0.014556, + "firstOutputMs": 32.481787, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001311, + "name": "Engine" + }, + { + "ms": 0.114016, + "name": "canonicalPreopens" + }, + { + "ms": 12.753765, + "name": "moduleRead" + }, + { + "ms": 15.93145, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.016874, + "name": "importValidation" + }, + { + "ms": 0.21231399999999997, + "name": "Linker" + }, + { + "ms": 0.016979, + "name": "Store" + }, + { + "ms": 0.200018, + "name": "Instance" + }, + { + "ms": 0.105647, + "name": "signalMaskInit" + }, + { + "ms": 0.004390000000000001, + "name": "entrypointLookup" + }, + { + "ms": 1.6680760000000001, + "name": "wasi.start" + }, + { + "ms": 0.932813, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 33.581592 + }, + "memory": { + "start": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126319, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 481583104, + "peakRssBytes": 482312192, + "pssBytes": 482898944, + "virtualBytes": 8549867520, + "minorFaults": 126363, + "majorFaults": 0 + }, + "end": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126363, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 62.59875399999146, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 33.01225, + "firstHostCallMs": 0.029744, + "firstOutputMs": 34.27666, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0021490000000000003, + "name": "Engine" + }, + { + "ms": 0.15081, + "name": "canonicalPreopens" + }, + { + "ms": 13.935202, + "name": "moduleRead" + }, + { + "ms": 16.661260000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014780999999999999, + "name": "importValidation" + }, + { + "ms": 0.21193800000000002, + "name": "Linker" + }, + { + "ms": 0.015979, + "name": "Store" + }, + { + "ms": 0.054189, + "name": "Instance" + }, + { + "ms": 0.07289200000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.003033, + "name": "entrypointLookup" + }, + { + "ms": 1.716923, + "name": "wasi.start" + }, + { + "ms": 1.814514, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 36.253190999999994 + }, + "memory": { + "start": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126363, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482898944, + "virtualBytes": 8549867520, + "minorFaults": 126407, + "majorFaults": 0 + }, + "end": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126407, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 68.66979299999366, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 32.090925999999996, + "firstHostCallMs": 0.011569000000000001, + "firstOutputMs": 34.460446, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.000931, + "name": "Engine" + }, + { + "ms": 0.1118, + "name": "canonicalPreopens" + }, + { + "ms": 12.833901, + "name": "moduleRead" + }, + { + "ms": 16.516686999999997, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.017196, + "name": "importValidation" + }, + { + "ms": 0.21895199999999998, + "name": "Linker" + }, + { + "ms": 0.021591, + "name": "Store" + }, + { + "ms": 0.365587, + "name": "Instance" + }, + { + "ms": 0.062313, + "name": "signalMaskInit" + }, + { + "ms": 0.0055119999999999995, + "name": "entrypointLookup" + }, + { + "ms": 2.879991, + "name": "wasi.start" + }, + { + "ms": 1.497449, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 36.123273999999995 + }, + "memory": { + "start": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482825216, + "virtualBytes": 4185411584, + "minorFaults": 126407, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 481583104, + "peakRssBytes": 482312192, + "pssBytes": 482897920, + "virtualBytes": 8549867520, + "minorFaults": 126451, + "majorFaults": 0 + }, + "end": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482824192, + "virtualBytes": 4185411584, + "minorFaults": 126451, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 709.448522000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 642.2689790000001, + "firstHostCallMs": 0.018054999999999998, + "firstOutputMs": 685.828805, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001712, + "name": "Engine" + }, + { + "ms": 0.119053, + "name": "canonicalPreopens" + }, + { + "ms": 7.327445999999999, + "name": "moduleRead" + }, + { + "ms": 2.53898, + "name": "profileValidation" + }, + { + "ms": 628.673951, + "name": "moduleCompile" + }, + { + "ms": 0.0070079999999999995, + "name": "importValidation" + }, + { + "ms": 0.203124, + "name": "Linker" + }, + { + "ms": 0.019364999999999997, + "name": "Store" + }, + { + "ms": 2.4828889999999997, + "name": "Instance" + }, + { + "ms": 0.075343, + "name": "signalMaskInit" + }, + { + "ms": 0.006364000000000001, + "name": "entrypointLookup" + }, + { + "ms": 43.815264, + "name": "wasi.start" + }, + { + "ms": 1.300625, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 687.3016250000001 + }, + "memory": { + "start": { + "rssBytes": 479485952, + "peakRssBytes": 482312192, + "pssBytes": 482824192, + "virtualBytes": 4185411584, + "minorFaults": 126451, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483467264, + "peakRssBytes": 483868672, + "pssBytes": 487216128, + "virtualBytes": 8553730048, + "minorFaults": 126963, + "majorFaults": 0 + }, + "end": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 126963, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15233421, + "wasmtimeProcessRetainedRssBytes": 479485952, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 81.55167700001039, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.164868, + "firstHostCallMs": 0.056871, + "firstOutputMs": 53.264019000000005, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0015290000000000002, + "name": "Engine" + }, + { + "ms": 0.12514199999999998, + "name": "canonicalPreopens" + }, + { + "ms": 6.602272, + "name": "moduleRead" + }, + { + "ms": 2.420741, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007995, + "name": "importValidation" + }, + { + "ms": 0.222773, + "name": "Linker" + }, + { + "ms": 0.017071999999999997, + "name": "Store" + }, + { + "ms": 0.863908, + "name": "Instance" + }, + { + "ms": 0.052296, + "name": "signalMaskInit" + }, + { + "ms": 0.002951, + "name": "entrypointLookup" + }, + { + "ms": 42.354561, + "name": "wasi.start" + }, + { + "ms": 2.175852, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 55.587914000000005 + }, + "memory": { + "start": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 126963, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 487093248, + "virtualBytes": 8553730048, + "minorFaults": 127013, + "majorFaults": 0 + }, + "end": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127013, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 72.01720000000205, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.331782, + "firstHostCallMs": 0.018002, + "firstOutputMs": 51.188223, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001986, + "name": "Engine" + }, + { + "ms": 0.12045800000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.706888999999999, + "name": "moduleRead" + }, + { + "ms": 2.468092, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007944999999999999, + "name": "importValidation" + }, + { + "ms": 0.201971, + "name": "Linker" + }, + { + "ms": 0.016253, + "name": "Store" + }, + { + "ms": 0.9332659999999999, + "name": "Instance" + }, + { + "ms": 0.051821, + "name": "signalMaskInit" + }, + { + "ms": 0.003437, + "name": "entrypointLookup" + }, + { + "ms": 40.120413, + "name": "wasi.start" + }, + { + "ms": 0.852662, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 52.169891 + }, + "memory": { + "start": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127013, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 487093248, + "virtualBytes": 8553730048, + "minorFaults": 127063, + "majorFaults": 0 + }, + "end": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127063, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 90.73135600000387, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.868390999999999, + "firstHostCallMs": 0.018431, + "firstOutputMs": 59.614753, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001714, + "name": "Engine" + }, + { + "ms": 0.111095, + "name": "canonicalPreopens" + }, + { + "ms": 6.981995, + "name": "moduleRead" + }, + { + "ms": 3.797058, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.01298, + "name": "importValidation" + }, + { + "ms": 0.31095199999999995, + "name": "Linker" + }, + { + "ms": 0.023969, + "name": "Store" + }, + { + "ms": 0.755011, + "name": "Instance" + }, + { + "ms": 0.046168, + "name": "signalMaskInit" + }, + { + "ms": 0.0061979999999999995, + "name": "entrypointLookup" + }, + { + "ms": 47.021979, + "name": "wasi.start" + }, + { + "ms": 0.163876, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 59.919094 + }, + "memory": { + "start": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127063, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 487092224, + "virtualBytes": 8553730048, + "minorFaults": 127113, + "majorFaults": 0 + }, + "end": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486686720, + "virtualBytes": 4189274112, + "minorFaults": 127113, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 84.42175500000303, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.760801, + "firstHostCallMs": 0.021358000000000002, + "firstOutputMs": 59.613269, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.00174, + "name": "Engine" + }, + { + "ms": 0.120278, + "name": "canonicalPreopens" + }, + { + "ms": 6.95637, + "name": "moduleRead" + }, + { + "ms": 2.558501, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014433999999999999, + "name": "importValidation" + }, + { + "ms": 0.315756, + "name": "Linker" + }, + { + "ms": 0.024403, + "name": "Store" + }, + { + "ms": 1.902293, + "name": "Instance" + }, + { + "ms": 0.051051000000000006, + "name": "signalMaskInit" + }, + { + "ms": 0.0089, + "name": "entrypointLookup" + }, + { + "ms": 47.092776, + "name": "wasi.start" + }, + { + "ms": 1.5698459999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 61.320372 + }, + "memory": { + "start": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486686720, + "virtualBytes": 4189274112, + "minorFaults": 127113, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 487093248, + "virtualBytes": 8553730048, + "minorFaults": 127163, + "majorFaults": 0 + }, + "end": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127163, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1443.8645760000072, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1402.093728, + "firstHostCallMs": 0.014270000000000001, + "firstOutputMs": 1407.332042, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001031, + "name": "Engine" + }, + { + "ms": 0.108329, + "name": "canonicalPreopens" + }, + { + "ms": 6.745188, + "name": "moduleRead" + }, + { + "ms": 4.401873, + "name": "profileValidation" + }, + { + "ms": 1389.55249, + "name": "moduleCompile" + }, + { + "ms": 0.008308000000000001, + "name": "importValidation" + }, + { + "ms": 0.184226, + "name": "Linker" + }, + { + "ms": 0.016861, + "name": "Store" + }, + { + "ms": 0.27505999999999997, + "name": "Instance" + }, + { + "ms": 0.052450000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.004095, + "name": "entrypointLookup" + }, + { + "ms": 6.692215, + "name": "wasi.start" + }, + { + "ms": 3.866685, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 1412.700624 + }, + "memory": { + "start": { + "rssBytes": 483348480, + "peakRssBytes": 483868672, + "pssBytes": 486687744, + "virtualBytes": 4189274112, + "minorFaults": 127163, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 526635008, + "peakRssBytes": 529338368, + "pssBytes": 531501056, + "virtualBytes": 8560050176, + "minorFaults": 131859, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131859, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15862095, + "wasmtimeProcessRetainedRssBytes": 483348480, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 46.54298499999277, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.791304, + "firstHostCallMs": 0.068233, + "firstOutputMs": 16.063622, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001124, + "name": "Engine" + }, + { + "ms": 0.135014, + "name": "canonicalPreopens" + }, + { + "ms": 7.115232, + "name": "moduleRead" + }, + { + "ms": 4.4193039999999995, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008592, + "name": "importValidation" + }, + { + "ms": 0.19781200000000002, + "name": "Linker" + }, + { + "ms": 0.016775, + "name": "Store" + }, + { + "ms": 0.043300000000000005, + "name": "Instance" + }, + { + "ms": 0.06357, + "name": "signalMaskInit" + }, + { + "ms": 0.003108, + "name": "entrypointLookup" + }, + { + "ms": 4.5923869999999996, + "name": "wasi.start" + }, + { + "ms": 0.30473700000000004, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 17.700827999999998 + }, + "memory": { + "start": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131859, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498398208, + "virtualBytes": 8560050176, + "minorFaults": 131892, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131892, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 44.3182259999885, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.79283, + "firstHostCallMs": 0.015169, + "firstOutputMs": 16.175983000000002, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0009680000000000001, + "name": "Engine" + }, + { + "ms": 0.107829, + "name": "canonicalPreopens" + }, + { + "ms": 7.179303, + "name": "moduleRead" + }, + { + "ms": 4.388149, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008417, + "name": "importValidation" + }, + { + "ms": 0.20845, + "name": "Linker" + }, + { + "ms": 0.016929999999999997, + "name": "Store" + }, + { + "ms": 0.046437000000000006, + "name": "Instance" + }, + { + "ms": 0.055822000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.002971, + "name": "entrypointLookup" + }, + { + "ms": 4.713337, + "name": "wasi.start" + }, + { + "ms": 0.45169, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 18.001337999999997 + }, + "memory": { + "start": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131892, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498398208, + "virtualBytes": 8560050176, + "minorFaults": 131925, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131925, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 47.88408200000413, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.934536, + "firstHostCallMs": 0.018757, + "firstOutputMs": 16.319942, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001269, + "name": "Engine" + }, + { + "ms": 0.111442, + "name": "canonicalPreopens" + }, + { + "ms": 6.892472000000001, + "name": "moduleRead" + }, + { + "ms": 4.622395, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009693, + "name": "importValidation" + }, + { + "ms": 0.21225000000000002, + "name": "Linker" + }, + { + "ms": 0.019408, + "name": "Store" + }, + { + "ms": 0.27652299999999996, + "name": "Instance" + }, + { + "ms": 0.043206, + "name": "signalMaskInit" + }, + { + "ms": 0.003468, + "name": "entrypointLookup" + }, + { + "ms": 4.785608, + "name": "wasi.start" + }, + { + "ms": 0.035181000000000004, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 17.760528 + }, + "memory": { + "start": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131925, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498627584, + "virtualBytes": 8560050176, + "minorFaults": 131958, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131958, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 42.53827400000591, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.468010999999999, + "firstHostCallMs": 0.017195000000000002, + "firstOutputMs": 15.862258, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001333, + "name": "Engine" + }, + { + "ms": 0.108974, + "name": "canonicalPreopens" + }, + { + "ms": 6.8512759999999995, + "name": "moduleRead" + }, + { + "ms": 4.424348, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008941, + "name": "importValidation" + }, + { + "ms": 0.20411400000000002, + "name": "Linker" + }, + { + "ms": 0.016884, + "name": "Store" + }, + { + "ms": 0.049675000000000004, + "name": "Instance" + }, + { + "ms": 0.053433, + "name": "signalMaskInit" + }, + { + "ms": 0.002955, + "name": "entrypointLookup" + }, + { + "ms": 4.70865, + "name": "wasi.start" + }, + { + "ms": 0.037149, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 17.220355 + }, + "memory": { + "start": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131958, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498627584, + "virtualBytes": 8560050176, + "minorFaults": 131991, + "majorFaults": 0 + }, + "end": { + "rssBytes": 494985216, + "peakRssBytes": 529338368, + "pssBytes": 498324480, + "virtualBytes": 4195594240, + "minorFaults": 131991, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17251648, + "wasmtimeProcessRetainedRssBytes": 494985216, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 2, + "vmSetupMs": 446.34975900000427, + "fixtureSetupMs": 381.010552000007, + "baseline": { + "rssBytes": 239030272, + "peakRssBytes": 246394880, + "pssBytes": 240112640, + "virtualBytes": 3885957120, + "minorFaults": 55073, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402698240, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 418889728, + "peakRssBytes": 528896000, + "pssBytes": 420934656, + "virtualBytes": 4119547904, + "minorFaults": 1149584, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 179859456, + "peakRssBytes": 282501120, + "pssBytes": 180822016, + "virtualBytes": 233590784, + "minorFaults": 1094511, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 65.16563499999756, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.091913 + }, + { + "name": "WebAssembly.Module", + "ms": 0.14758 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.058757 + }, + { + "name": "wasi.start", + "ms": 0.090129 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 239030272, + "peakRssBytes": 246394880, + "pssBytes": 240120832, + "virtualBytes": 3888070656, + "minorFaults": 55075, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 274690048, + "peakRssBytes": 275271680, + "pssBytes": 261604352, + "virtualBytes": 4640808960, + "minorFaults": 66043, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260321280, + "peakRssBytes": 275271680, + "pssBytes": 261604352, + "virtualBytes": 3955179520, + "minorFaults": 66043, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 239030272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260321280, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 66.81254400000034, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 0.996957 + }, + { + "name": "WebAssembly.Module", + "ms": 0.170485 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.057606 + }, + { + "name": "wasi.start", + "ms": 0.086269 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260321280, + "peakRssBytes": 275271680, + "pssBytes": 261604352, + "virtualBytes": 3955179520, + "minorFaults": 66043, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275312640, + "peakRssBytes": 275542016, + "pssBytes": 261661696, + "virtualBytes": 4640546816, + "minorFaults": 72306, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260595712, + "peakRssBytes": 275542016, + "pssBytes": 261661696, + "virtualBytes": 3955179520, + "minorFaults": 72306, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260321280, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260595712, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 64.10543899999175, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.057941 + }, + { + "name": "WebAssembly.Module", + "ms": 0.138562 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.099986 + }, + { + "name": "wasi.start", + "ms": 0.119395 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260595712, + "peakRssBytes": 275542016, + "pssBytes": 261661696, + "virtualBytes": 3955179520, + "minorFaults": 72306, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275333120, + "peakRssBytes": 275578880, + "pssBytes": 264408064, + "virtualBytes": 4640546816, + "minorFaults": 78574, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260632576, + "peakRssBytes": 275578880, + "pssBytes": 261743616, + "virtualBytes": 3955179520, + "minorFaults": 78574, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260595712, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260632576, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 64.96620699999039, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.663559 + }, + { + "name": "WebAssembly.Module", + "ms": 0.128367 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.058815 + }, + { + "name": "wasi.start", + "ms": 0.084721 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260632576, + "peakRssBytes": 275578880, + "pssBytes": 261743616, + "virtualBytes": 3955179520, + "minorFaults": 78574, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275345408, + "peakRssBytes": 275578880, + "pssBytes": 276554752, + "virtualBytes": 4640284672, + "minorFaults": 84822, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260603904, + "peakRssBytes": 275578880, + "pssBytes": 261743616, + "virtualBytes": 3955179520, + "minorFaults": 84822, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260632576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260603904, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 63.21864900000219, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.111951 + }, + { + "name": "WebAssembly.Module", + "ms": 0.136278 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.059492 + }, + { + "name": "wasi.start", + "ms": 0.088885 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260603904, + "peakRssBytes": 275578880, + "pssBytes": 261743616, + "virtualBytes": 3955179520, + "minorFaults": 84822, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 275349504, + "peakRssBytes": 275619840, + "pssBytes": 261850112, + "virtualBytes": 4640284672, + "minorFaults": 91096, + "majorFaults": 0 + }, + "end": { + "rssBytes": 260669440, + "peakRssBytes": 275619840, + "pssBytes": 261850112, + "virtualBytes": 3955179520, + "minorFaults": 91096, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260603904, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260669440, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 224.2568570000003, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.057201 + }, + { + "name": "WebAssembly.Module", + "ms": 1.503742 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.314179 + }, + { + "name": "wasi.start", + "ms": 98.235106 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 260669440, + "peakRssBytes": 275619840, + "pssBytes": 261850112, + "virtualBytes": 3955179520, + "minorFaults": 91096, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 331517952, + "peakRssBytes": 332058624, + "pssBytes": 329180160, + "virtualBytes": 4694343680, + "minorFaults": 109253, + "majorFaults": 0 + }, + "end": { + "rssBytes": 279986176, + "peakRssBytes": 332058624, + "pssBytes": 280998912, + "virtualBytes": 3955179520, + "minorFaults": 109253, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 260669440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 279986176, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 200.46537000000535, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.765506 + }, + { + "name": "WebAssembly.Module", + "ms": 1.040708 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.281361 + }, + { + "name": "wasi.start", + "ms": 83.626183 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 279986176, + "peakRssBytes": 332058624, + "pssBytes": 280998912, + "virtualBytes": 3955179520, + "minorFaults": 109253, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 336613376, + "peakRssBytes": 336883712, + "pssBytes": 337912832, + "virtualBytes": 4693819392, + "minorFaults": 124196, + "majorFaults": 0 + }, + "end": { + "rssBytes": 282189824, + "peakRssBytes": 336883712, + "pssBytes": 283550720, + "virtualBytes": 3955179520, + "minorFaults": 124196, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 279986176, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282189824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 232.1845200000098, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.17237 + }, + { + "name": "WebAssembly.Module", + "ms": 2.163339 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.315285 + }, + { + "name": "wasi.start", + "ms": 109.831118 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 282189824, + "peakRssBytes": 336883712, + "pssBytes": 283550720, + "virtualBytes": 3955179520, + "minorFaults": 124196, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 344281088, + "peakRssBytes": 344821760, + "pssBytes": 343680000, + "virtualBytes": 4695920640, + "minorFaults": 141428, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292073472, + "peakRssBytes": 344821760, + "pssBytes": 293381120, + "virtualBytes": 3957280768, + "minorFaults": 141428, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282189824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 292073472, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 207.34026299999095, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.188833 + }, + { + "name": "WebAssembly.Module", + "ms": 2.359304 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.754983 + }, + { + "name": "wasi.start", + "ms": 85.38542 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 292073472, + "peakRssBytes": 344821760, + "pssBytes": 293381120, + "virtualBytes": 3957280768, + "minorFaults": 141428, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 351039488, + "peakRssBytes": 351580160, + "pssBytes": 349242368, + "virtualBytes": 4695658496, + "minorFaults": 158379, + "majorFaults": 0 + }, + "end": { + "rssBytes": 299241472, + "peakRssBytes": 351580160, + "pssBytes": 300545024, + "virtualBytes": 3957280768, + "minorFaults": 158379, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 292073472, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 299241472, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 238.428608000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.910426 + }, + { + "name": "WebAssembly.Module", + "ms": 1.415357 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.661044 + }, + { + "name": "wasi.start", + "ms": 107.847636 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 299241472, + "peakRssBytes": 351580160, + "pssBytes": 300545024, + "virtualBytes": 3957280768, + "minorFaults": 158379, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 359829504, + "peakRssBytes": 360099840, + "pssBytes": 357986304, + "virtualBytes": 4695658496, + "minorFaults": 176769, + "majorFaults": 0 + }, + "end": { + "rssBytes": 308891648, + "peakRssBytes": 360099840, + "pssBytes": 310076416, + "virtualBytes": 3957280768, + "minorFaults": 176769, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 299241472, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 308891648, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 335.79073900000367, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.545749 + }, + { + "name": "WebAssembly.Module", + "ms": 3.79674 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.981857 + }, + { + "name": "wasi.start", + "ms": 126.807482 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 308891648, + "peakRssBytes": 360099840, + "pssBytes": 310076416, + "virtualBytes": 3957280768, + "minorFaults": 176769, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 413278208, + "peakRssBytes": 413462528, + "pssBytes": 414602240, + "virtualBytes": 5472702464, + "minorFaults": 217746, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333185024, + "peakRssBytes": 413462528, + "pssBytes": 335716352, + "virtualBytes": 4030861312, + "minorFaults": 217746, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 308891648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333185024, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 347.80153000001155, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.072467 + }, + { + "name": "WebAssembly.Module", + "ms": 4.051285 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.870132 + }, + { + "name": "wasi.start", + "ms": 117.064683 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333185024, + "peakRssBytes": 413462528, + "pssBytes": 335716352, + "virtualBytes": 4030861312, + "minorFaults": 217746, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428949504, + "peakRssBytes": 429219840, + "pssBytes": 431608832, + "virtualBytes": 5473107968, + "minorFaults": 254239, + "majorFaults": 0 + }, + "end": { + "rssBytes": 333582336, + "peakRssBytes": 429219840, + "pssBytes": 336045056, + "virtualBytes": 4030861312, + "minorFaults": 254239, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333185024, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333582336, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 381.81688899999426, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 60.410478 + }, + { + "name": "WebAssembly.Module", + "ms": 4.03369 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.950681 + }, + { + "name": "wasi.start", + "ms": 119.44204 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 333582336, + "peakRssBytes": 429219840, + "pssBytes": 336045056, + "virtualBytes": 4030861312, + "minorFaults": 254239, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 429129728, + "peakRssBytes": 429219840, + "pssBytes": 431264768, + "virtualBytes": 5540335616, + "minorFaults": 290795, + "majorFaults": 0 + }, + "end": { + "rssBytes": 342077440, + "peakRssBytes": 429219840, + "pssBytes": 344679424, + "virtualBytes": 4097970176, + "minorFaults": 290795, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 333582336, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342077440, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 379.8099530000036, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.816005 + }, + { + "name": "WebAssembly.Module", + "ms": 4.958767 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.945252 + }, + { + "name": "wasi.start", + "ms": 136.735963 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 342077440, + "peakRssBytes": 429219840, + "pssBytes": 344679424, + "virtualBytes": 4097970176, + "minorFaults": 290795, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450113536, + "peakRssBytes": 450138112, + "pssBytes": 452440064, + "virtualBytes": 5540335616, + "minorFaults": 330435, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355586048, + "peakRssBytes": 450138112, + "pssBytes": 357770240, + "virtualBytes": 4097970176, + "minorFaults": 330435, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 342077440, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355586048, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 379.66825800000515, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 64.762156 + }, + { + "name": "WebAssembly.Module", + "ms": 5.264231 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.802244 + }, + { + "name": "wasi.start", + "ms": 138.273248 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355586048, + "peakRssBytes": 450138112, + "pssBytes": 357770240, + "virtualBytes": 4097970176, + "minorFaults": 330435, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 451391488, + "peakRssBytes": 451637248, + "pssBytes": 454193152, + "virtualBytes": 5539811328, + "minorFaults": 369100, + "majorFaults": 0 + }, + "end": { + "rssBytes": 355078144, + "peakRssBytes": 451637248, + "pssBytes": 357876736, + "virtualBytes": 4097970176, + "minorFaults": 369100, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355586048, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355078144, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 181.7958409999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.579971 + }, + { + "name": "WebAssembly.Module", + "ms": 1.780844 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.314501 + }, + { + "name": "wasi.start", + "ms": 30.469491 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 355078144, + "peakRssBytes": 451637248, + "pssBytes": 357876736, + "virtualBytes": 4097970176, + "minorFaults": 369100, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424767488, + "peakRssBytes": 451637248, + "pssBytes": 427361280, + "virtualBytes": 4855382016, + "minorFaults": 386030, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356278272, + "peakRssBytes": 451637248, + "pssBytes": 358675456, + "virtualBytes": 4097970176, + "minorFaults": 386030, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 355078144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356278272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 193.0474319999921, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.364709 + }, + { + "name": "WebAssembly.Module", + "ms": 4.158937 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.500126 + }, + { + "name": "wasi.start", + "ms": 29.634676 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356278272, + "peakRssBytes": 451637248, + "pssBytes": 358675456, + "virtualBytes": 4097970176, + "minorFaults": 386030, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 426225664, + "peakRssBytes": 451637248, + "pssBytes": 428449792, + "virtualBytes": 4855787520, + "minorFaults": 401168, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356679680, + "peakRssBytes": 451637248, + "pssBytes": 358678528, + "virtualBytes": 4097970176, + "minorFaults": 401168, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356278272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356679680, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 189.6865770000004, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.798074 + }, + { + "name": "WebAssembly.Module", + "ms": 2.887989 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.978154 + }, + { + "name": "wasi.start", + "ms": 25.865822 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356679680, + "peakRssBytes": 451637248, + "pssBytes": 358678528, + "virtualBytes": 4097970176, + "minorFaults": 401168, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425050112, + "peakRssBytes": 451637248, + "pssBytes": 427373568, + "virtualBytes": 4855906304, + "minorFaults": 418598, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356556800, + "peakRssBytes": 451637248, + "pssBytes": 358683648, + "virtualBytes": 4097970176, + "minorFaults": 418598, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356679680, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356556800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 184.0208330000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.597092 + }, + { + "name": "WebAssembly.Module", + "ms": 2.466866 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.343971 + }, + { + "name": "wasi.start", + "ms": 23.563376 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356556800, + "peakRssBytes": 451637248, + "pssBytes": 358683648, + "virtualBytes": 4097970176, + "minorFaults": 418598, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425578496, + "peakRssBytes": 451637248, + "pssBytes": 427372544, + "virtualBytes": 4855787520, + "minorFaults": 437559, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356786176, + "peakRssBytes": 451637248, + "pssBytes": 358686720, + "virtualBytes": 4097970176, + "minorFaults": 437559, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356556800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356786176, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 198.91201800000272, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.576277 + }, + { + "name": "WebAssembly.Module", + "ms": 2.28624 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.938361 + }, + { + "name": "wasi.start", + "ms": 25.28622 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356786176, + "peakRssBytes": 451637248, + "pssBytes": 358686720, + "virtualBytes": 4097970176, + "minorFaults": 437559, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 425308160, + "peakRssBytes": 451637248, + "pssBytes": 427381760, + "virtualBytes": 4855525376, + "minorFaults": 457033, + "majorFaults": 0 + }, + "end": { + "rssBytes": 356646912, + "peakRssBytes": 451637248, + "pssBytes": 358691840, + "virtualBytes": 4097970176, + "minorFaults": 457033, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356786176, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356646912, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 150.87885399999504, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 17.628883 + }, + { + "name": "WebAssembly.Module", + "ms": 1.351119 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.541491 + }, + { + "name": "wasi.start", + "ms": 16.922872 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 356646912, + "peakRssBytes": 451637248, + "pssBytes": 358691840, + "virtualBytes": 4097970176, + "minorFaults": 457033, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 401383424, + "peakRssBytes": 451637248, + "pssBytes": 403747840, + "virtualBytes": 4825079808, + "minorFaults": 471288, + "majorFaults": 0 + }, + "end": { + "rssBytes": 368852992, + "peakRssBytes": 451637248, + "pssBytes": 161181696, + "virtualBytes": 4097970176, + "minorFaults": 471288, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 356646912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 369123328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 205.80199500000163, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.591546 + }, + { + "name": "WebAssembly.Module", + "ms": 2.049079 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.719685 + }, + { + "name": "wasi.start", + "ms": 18.201124 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 369664000, + "peakRssBytes": 451637248, + "pssBytes": 118404096, + "virtualBytes": 4097970176, + "minorFaults": 471489, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 424001536, + "peakRssBytes": 451637248, + "pssBytes": 413995008, + "virtualBytes": 4825079808, + "minorFaults": 494417, + "majorFaults": 0 + }, + "end": { + "rssBytes": 374919168, + "peakRssBytes": 451637248, + "pssBytes": 363911168, + "virtualBytes": 4097970176, + "minorFaults": 494417, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 369123328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375730176, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 158.45915300000343, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.722237 + }, + { + "name": "WebAssembly.Module", + "ms": 2.500085 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.378731 + }, + { + "name": "wasi.start", + "ms": 15.380645 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 375730176, + "peakRssBytes": 451637248, + "pssBytes": 263864320, + "virtualBytes": 4097970176, + "minorFaults": 494584, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432676864, + "peakRssBytes": 451637248, + "pssBytes": 425985024, + "virtualBytes": 4825341952, + "minorFaults": 516515, + "majorFaults": 0 + }, + "end": { + "rssBytes": 389292032, + "peakRssBytes": 451637248, + "pssBytes": 392778752, + "virtualBytes": 4097970176, + "minorFaults": 516515, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 375730176, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390643712, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 159.43923299999733, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.622078 + }, + { + "name": "WebAssembly.Module", + "ms": 1.08837 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.868209 + }, + { + "name": "wasi.start", + "ms": 19.103097 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 390643712, + "peakRssBytes": 451637248, + "pssBytes": 392778752, + "virtualBytes": 4097970176, + "minorFaults": 516819, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435937280, + "peakRssBytes": 451637248, + "pssBytes": 425426944, + "virtualBytes": 4825485312, + "minorFaults": 537348, + "majorFaults": 0 + }, + "end": { + "rssBytes": 389156864, + "peakRssBytes": 451637248, + "pssBytes": 178996224, + "virtualBytes": 4097970176, + "minorFaults": 537348, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390643712, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391589888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 239.35671599999478, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 40.647961 + }, + { + "name": "WebAssembly.Module", + "ms": 1.664683 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.56132 + }, + { + "name": "wasi.start", + "ms": 17.911696 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 392130560, + "peakRssBytes": 451637248, + "pssBytes": 67821568, + "virtualBytes": 4097970176, + "minorFaults": 538066, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 442212352, + "peakRssBytes": 451637248, + "pssBytes": 434716672, + "virtualBytes": 4825079808, + "minorFaults": 559676, + "majorFaults": 0 + }, + "end": { + "rssBytes": 398614528, + "peakRssBytes": 451637248, + "pssBytes": 401515520, + "virtualBytes": 4097970176, + "minorFaults": 559676, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 392130560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 399695872, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 270.2340569999942, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.768837 + }, + { + "name": "WebAssembly.Module", + "ms": 4.578035 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.829832 + }, + { + "name": "wasi.start", + "ms": 22.445748 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 399695872, + "peakRssBytes": 451637248, + "pssBytes": 402121728, + "virtualBytes": 4097970176, + "minorFaults": 559953, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480903168, + "peakRssBytes": 481046528, + "pssBytes": 483431424, + "virtualBytes": 4836364288, + "minorFaults": 599041, + "majorFaults": 0 + }, + "end": { + "rssBytes": 390647808, + "peakRssBytes": 481046528, + "pssBytes": 393065472, + "virtualBytes": 4097970176, + "minorFaults": 599041, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 399695872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390647808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 286.20724200000404, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 81.409703 + }, + { + "name": "WebAssembly.Module", + "ms": 5.063338 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.808511 + }, + { + "name": "wasi.start", + "ms": 30.058953 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 390647808, + "peakRssBytes": 481046528, + "pssBytes": 393065472, + "virtualBytes": 4097970176, + "minorFaults": 599041, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 483614720, + "peakRssBytes": 483614720, + "pssBytes": 485928960, + "virtualBytes": 4836769792, + "minorFaults": 633429, + "majorFaults": 0 + }, + "end": { + "rssBytes": 390692864, + "peakRssBytes": 483614720, + "pssBytes": 393065472, + "virtualBytes": 4097970176, + "minorFaults": 633429, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390647808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390692864, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 262.61806599999545, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.41829 + }, + { + "name": "WebAssembly.Module", + "ms": 6.174258 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.47195 + }, + { + "name": "wasi.start", + "ms": 41.287031 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 390692864, + "peakRssBytes": 483614720, + "pssBytes": 393065472, + "virtualBytes": 4097970176, + "minorFaults": 633429, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 484192256, + "peakRssBytes": 484204544, + "pssBytes": 486528000, + "virtualBytes": 4836769792, + "minorFaults": 668312, + "majorFaults": 0 + }, + "end": { + "rssBytes": 391315456, + "peakRssBytes": 484204544, + "pssBytes": 393666560, + "virtualBytes": 4097970176, + "minorFaults": 668312, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 390692864, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391315456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 276.44784799999616, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 56.401798 + }, + { + "name": "WebAssembly.Module", + "ms": 4.480208 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.453672 + }, + { + "name": "wasi.start", + "ms": 45.259459 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 391315456, + "peakRssBytes": 484204544, + "pssBytes": 393666560, + "virtualBytes": 4097970176, + "minorFaults": 668312, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 485036032, + "peakRssBytes": 485093376, + "pssBytes": 486532096, + "virtualBytes": 4888666112, + "minorFaults": 703170, + "majorFaults": 0 + }, + "end": { + "rssBytes": 391217152, + "peakRssBytes": 485093376, + "pssBytes": 393671680, + "virtualBytes": 4097970176, + "minorFaults": 703170, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391315456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391217152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 286.2810979999922, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 78.336792 + }, + { + "name": "WebAssembly.Module", + "ms": 5.445249 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.888973 + }, + { + "name": "wasi.start", + "ms": 45.589303 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 391217152, + "peakRssBytes": 485093376, + "pssBytes": 393671680, + "virtualBytes": 4097970176, + "minorFaults": 703170, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 484114432, + "peakRssBytes": 485093376, + "pssBytes": 486523904, + "virtualBytes": 4836626432, + "minorFaults": 739357, + "majorFaults": 0 + }, + "end": { + "rssBytes": 391262208, + "peakRssBytes": 485093376, + "pssBytes": 393670656, + "virtualBytes": 4097970176, + "minorFaults": 739357, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391217152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391262208, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 273.39911199999915, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.474442 + }, + { + "name": "WebAssembly.Module", + "ms": 3.793295 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.656839 + }, + { + "name": "wasi.start", + "ms": 5.775742 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 391262208, + "peakRssBytes": 485093376, + "pssBytes": 393670656, + "virtualBytes": 4097970176, + "minorFaults": 739357, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 524361728, + "peakRssBytes": 524546048, + "pssBytes": 526623744, + "virtualBytes": 4878725120, + "minorFaults": 789236, + "majorFaults": 0 + }, + "end": { + "rssBytes": 394563584, + "peakRssBytes": 524546048, + "pssBytes": 396366848, + "virtualBytes": 4098949120, + "minorFaults": 789236, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 391262208, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 394563584, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 423.58581600000616, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 88.583303 + }, + { + "name": "WebAssembly.Module", + "ms": 4.217761 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.743445 + }, + { + "name": "wasi.start", + "ms": 4.619381 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 394563584, + "peakRssBytes": 524546048, + "pssBytes": 396366848, + "virtualBytes": 4098949120, + "minorFaults": 789236, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488058880, + "peakRssBytes": 524546048, + "pssBytes": 489686016, + "virtualBytes": 4880003072, + "minorFaults": 830437, + "majorFaults": 0 + }, + "end": { + "rssBytes": 395153408, + "peakRssBytes": 524546048, + "pssBytes": 396993536, + "virtualBytes": 4099964928, + "minorFaults": 830437, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 394563584, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 395153408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 319.6258150000067, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 103.347932 + }, + { + "name": "WebAssembly.Module", + "ms": 5.919845 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.09413 + }, + { + "name": "wasi.start", + "ms": 5.442308 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 395153408, + "peakRssBytes": 524546048, + "pssBytes": 396993536, + "virtualBytes": 4099964928, + "minorFaults": 830437, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 528556032, + "peakRssBytes": 528707584, + "pssBytes": 530604032, + "virtualBytes": 4879884288, + "minorFaults": 881603, + "majorFaults": 0 + }, + "end": { + "rssBytes": 396644352, + "peakRssBytes": 528707584, + "pssBytes": 398627840, + "virtualBytes": 4099964928, + "minorFaults": 881603, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 395153408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396644352, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 301.40420900000026, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 102.390208 + }, + { + "name": "WebAssembly.Module", + "ms": 4.196222 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.190712 + }, + { + "name": "wasi.start", + "ms": 7.989918 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 396644352, + "peakRssBytes": 528707584, + "pssBytes": 398627840, + "virtualBytes": 4099964928, + "minorFaults": 881603, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 528633856, + "peakRssBytes": 528896000, + "pssBytes": 530629632, + "virtualBytes": 4880146432, + "minorFaults": 928791, + "majorFaults": 0 + }, + "end": { + "rssBytes": 396828672, + "peakRssBytes": 528896000, + "pssBytes": 398626816, + "virtualBytes": 4099964928, + "minorFaults": 928791, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396644352, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396828672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 321.4110509999882, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 96.683716 + }, + { + "name": "WebAssembly.Module", + "ms": 5.778268 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.955017 + }, + { + "name": "wasi.start", + "ms": 5.5122 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 396828672, + "peakRssBytes": 528896000, + "pssBytes": 398626816, + "virtualBytes": 4099964928, + "minorFaults": 928791, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 528633856, + "peakRssBytes": 528896000, + "pssBytes": 530605056, + "virtualBytes": 4880146432, + "minorFaults": 974385, + "majorFaults": 0 + }, + "end": { + "rssBytes": 396857344, + "peakRssBytes": 528896000, + "pssBytes": 398627840, + "virtualBytes": 4099964928, + "minorFaults": 974385, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396828672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396857344, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 268.67901600000914, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.524613 + }, + { + "name": "WebAssembly.Module", + "ms": 4.520698 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.422472 + }, + { + "name": "wasi.start", + "ms": 100.429382 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 396857344, + "peakRssBytes": 528896000, + "pssBytes": 398627840, + "virtualBytes": 4099964928, + "minorFaults": 974385, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 453914624, + "peakRssBytes": 528896000, + "pssBytes": 450207744, + "virtualBytes": 4838043648, + "minorFaults": 991067, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402771968, + "peakRssBytes": 528896000, + "pssBytes": 404504576, + "virtualBytes": 4100194304, + "minorFaults": 991067, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 396857344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402771968, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 220.0860410000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 31.048327 + }, + { + "name": "WebAssembly.Module", + "ms": 3.174507 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.43383 + }, + { + "name": "wasi.start", + "ms": 78.292384 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402771968, + "peakRssBytes": 528896000, + "pssBytes": 404504576, + "virtualBytes": 4100194304, + "minorFaults": 991067, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 454541312, + "peakRssBytes": 528896000, + "pssBytes": 456582144, + "virtualBytes": 4838043648, + "minorFaults": 1005291, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402608128, + "peakRssBytes": 528896000, + "pssBytes": 404509696, + "virtualBytes": 4100194304, + "minorFaults": 1005291, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402771968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402608128, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 236.6401130000013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.490972 + }, + { + "name": "WebAssembly.Module", + "ms": 1.544531 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.394256 + }, + { + "name": "wasi.start", + "ms": 83.996159 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402608128, + "peakRssBytes": 528896000, + "pssBytes": 404509696, + "virtualBytes": 4100194304, + "minorFaults": 1005291, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 454688768, + "peakRssBytes": 528896000, + "pssBytes": 456601600, + "virtualBytes": 4837257216, + "minorFaults": 1020025, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402804736, + "peakRssBytes": 528896000, + "pssBytes": 404512768, + "virtualBytes": 4100194304, + "minorFaults": 1020025, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402608128, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402804736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 215.05499200000486, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.318474 + }, + { + "name": "WebAssembly.Module", + "ms": 1.028754 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.205609 + }, + { + "name": "wasi.start", + "ms": 74.623491 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402804736, + "peakRssBytes": 528896000, + "pssBytes": 404512768, + "virtualBytes": 4100194304, + "minorFaults": 1020025, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 454533120, + "peakRssBytes": 528896000, + "pssBytes": 456606720, + "virtualBytes": 4836995072, + "minorFaults": 1034246, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402698240, + "peakRssBytes": 528896000, + "pssBytes": 404513792, + "virtualBytes": 4100194304, + "minorFaults": 1034246, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402804736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402698240, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 219.6801470000064, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.4016 + }, + { + "name": "WebAssembly.Module", + "ms": 0.857301 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.827303 + }, + { + "name": "wasi.start", + "ms": 76.789229 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402698240, + "peakRssBytes": 528896000, + "pssBytes": 404513792, + "virtualBytes": 4100194304, + "minorFaults": 1034246, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 454787072, + "peakRssBytes": 528896000, + "pssBytes": 456605696, + "virtualBytes": 4837257216, + "minorFaults": 1048978, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402771968, + "peakRssBytes": 528896000, + "pssBytes": 404513792, + "virtualBytes": 4100194304, + "minorFaults": 1048978, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402698240, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402771968, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 181.86370799998986, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 39.998362 + }, + { + "name": "WebAssembly.Module", + "ms": 2.155167 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.505707 + }, + { + "name": "wasi.start", + "ms": 16.43549 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402771968, + "peakRssBytes": 528896000, + "pssBytes": 404512768, + "virtualBytes": 4100194304, + "minorFaults": 1048978, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470446080, + "peakRssBytes": 528896000, + "pssBytes": 472585216, + "virtualBytes": 4857835520, + "minorFaults": 1066259, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402432000, + "peakRssBytes": 528896000, + "pssBytes": 404550656, + "virtualBytes": 4101705728, + "minorFaults": 1066259, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402771968, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402432000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 167.8013859999919, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.659956 + }, + { + "name": "WebAssembly.Module", + "ms": 2.812227 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.361304 + }, + { + "name": "wasi.start", + "ms": 13.418137 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402432000, + "peakRssBytes": 528896000, + "pssBytes": 404550656, + "virtualBytes": 4101705728, + "minorFaults": 1066259, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470818816, + "peakRssBytes": 528896000, + "pssBytes": 472851456, + "virtualBytes": 4858101760, + "minorFaults": 1085640, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402874368, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1085640, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402432000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402874368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 187.3069780000078, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 42.471344 + }, + { + "name": "WebAssembly.Module", + "ms": 1.317226 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.475456 + }, + { + "name": "wasi.start", + "ms": 15.491749 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402874368, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1085640, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470708224, + "peakRssBytes": 528896000, + "pssBytes": 472855552, + "virtualBytes": 4858101760, + "minorFaults": 1105469, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402731008, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1105469, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402874368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402731008, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 167.53960400000506, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.393746 + }, + { + "name": "WebAssembly.Module", + "ms": 3.338242 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.295858 + }, + { + "name": "wasi.start", + "ms": 18.168439 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402731008, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1105469, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470810624, + "peakRssBytes": 528896000, + "pssBytes": 472736768, + "virtualBytes": 4858101760, + "minorFaults": 1126317, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402849792, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1126317, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402731008, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402849792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 177.12018800000078, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624895243199074/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.541097 + }, + { + "name": "WebAssembly.Module", + "ms": 2.57099 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.753088 + }, + { + "name": "wasi.start", + "ms": 14.15172 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402849792, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1126317, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 470663168, + "peakRssBytes": 528896000, + "pssBytes": 472855552, + "virtualBytes": 4858507264, + "minorFaults": 1147679, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402698240, + "peakRssBytes": 528896000, + "pssBytes": 404816896, + "virtualBytes": 4101971968, + "minorFaults": 1147679, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402849792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402698240, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 2, + "vmSetupMs": 439.32796500000404, + "fixtureSetupMs": 411.0048659999884, + "baseline": { + "rssBytes": 238481408, + "peakRssBytes": 245596160, + "pssBytes": 240338944, + "virtualBytes": 3885953024, + "minorFaults": 53092, + "majorFaults": 2 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 498651136, + "peakRssBytes": 516812800, + "pssBytes": 501092352, + "virtualBytes": 4202618880, + "minorFaults": 135146, + "majorFaults": 2 + }, + "retainedDelta": { + "rssBytes": 260169728, + "peakRssBytes": 271216640, + "pssBytes": 260753408, + "virtualBytes": 316665856, + "minorFaults": 82054, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 68.44208799999615, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.07716300000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.07042799999999999, + "name": "Engine" + }, + { + "ms": 0.09785600000000001, + "name": "canonicalPreopens" + }, + { + "ms": 3.521115, + "name": "moduleRead" + }, + { + "ms": 0.212216, + "name": "profileValidation" + }, + { + "ms": 47.345505, + "name": "moduleCompile" + }, + { + "ms": 0.0028729999999999997, + "name": "importValidation" + }, + { + "ms": 0.18329199999999998, + "name": "Linker" + }, + { + "ms": 0.016572999999999997, + "name": "Store" + }, + { + "ms": 0.046083, + "name": "Instance" + }, + { + "ms": 0.041276, + "name": "signalMaskInit" + }, + { + "ms": 0.004045, + "name": "entrypointLookup" + }, + { + "ms": 0.035086, + "name": "wasi.start" + }, + { + "ms": 0.020658000000000003, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 51.662907000000004 + }, + "memory": { + "start": { + "rssBytes": 238481408, + "peakRssBytes": 245596160, + "pssBytes": 240347136, + "virtualBytes": 3888066560, + "minorFaults": 53094, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54244, + "majorFaults": 2 + }, + "end": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54244, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 238481408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 21.35185300000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.007592000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.000841, + "name": "Engine" + }, + { + "ms": 0.101103, + "name": "canonicalPreopens" + }, + { + "ms": 3.380905, + "name": "moduleRead" + }, + { + "ms": 0.232074, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003484, + "name": "importValidation" + }, + { + "ms": 0.1915, + "name": "Linker" + }, + { + "ms": 0.014603999999999999, + "name": "Store" + }, + { + "ms": 0.041083, + "name": "Instance" + }, + { + "ms": 0.6044400000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.006895999999999999, + "name": "entrypointLookup" + }, + { + "ms": 0.95748, + "name": "wasi.start" + }, + { + "ms": 0.017297, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.621608999999999 + }, + "memory": { + "start": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54244, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 8319557632, + "minorFaults": 54266, + "majorFaults": 2 + }, + "end": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54266, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 20.826832000006107, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010856000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001209, + "name": "Engine" + }, + { + "ms": 0.10234399999999999, + "name": "canonicalPreopens" + }, + { + "ms": 3.348332, + "name": "moduleRead" + }, + { + "ms": 0.23724099999999998, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0031820000000000004, + "name": "importValidation" + }, + { + "ms": 0.188946, + "name": "Linker" + }, + { + "ms": 0.015007999999999999, + "name": "Store" + }, + { + "ms": 0.033442, + "name": "Instance" + }, + { + "ms": 0.627687, + "name": "signalMaskInit" + }, + { + "ms": 0.010058, + "name": "entrypointLookup" + }, + { + "ms": 1.194833, + "name": "wasi.start" + }, + { + "ms": 0.027676, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.88509 + }, + "memory": { + "start": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54266, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 8319557632, + "minorFaults": 54288, + "majorFaults": 2 + }, + "end": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54288, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 23.18759400000272, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.008672, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001051, + "name": "Engine" + }, + { + "ms": 0.09949400000000001, + "name": "canonicalPreopens" + }, + { + "ms": 3.374839, + "name": "moduleRead" + }, + { + "ms": 0.24363100000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.004378, + "name": "importValidation" + }, + { + "ms": 0.195006, + "name": "Linker" + }, + { + "ms": 0.016245000000000002, + "name": "Store" + }, + { + "ms": 0.033095, + "name": "Instance" + }, + { + "ms": 0.562227, + "name": "signalMaskInit" + }, + { + "ms": 0.0036669999999999997, + "name": "entrypointLookup" + }, + { + "ms": 1.216374, + "name": "wasi.start" + }, + { + "ms": 0.019229, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.851821 + }, + "memory": { + "start": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54288, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 8319557632, + "minorFaults": 54310, + "majorFaults": 2 + }, + "end": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54310, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 21.12463299999945, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.008218, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.0013030000000000001, + "name": "Engine" + }, + { + "ms": 0.169914, + "name": "canonicalPreopens" + }, + { + "ms": 1.245142, + "name": "moduleRead" + }, + { + "ms": 0.2243, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003739, + "name": "importValidation" + }, + { + "ms": 0.19623000000000002, + "name": "Linker" + }, + { + "ms": 0.015336, + "name": "Store" + }, + { + "ms": 0.031394, + "name": "Instance" + }, + { + "ms": 0.579217, + "name": "signalMaskInit" + }, + { + "ms": 0.005462000000000001, + "name": "entrypointLookup" + }, + { + "ms": 0.053841999999999994, + "name": "wasi.start" + }, + { + "ms": 0.019655000000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 2.617229 + }, + "memory": { + "start": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54310, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54332, + "majorFaults": 2 + }, + "end": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54332, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1139.881355000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1070.4925819999999, + "firstHostCallMs": 0.007763, + "firstOutputMs": 1119.953305, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.000853, + "name": "Engine" + }, + { + "ms": 0.099981, + "name": "canonicalPreopens" + }, + { + "ms": 6.455074, + "name": "moduleRead" + }, + { + "ms": 3.843426, + "name": "profileValidation" + }, + { + "ms": 1058.732715, + "name": "moduleCompile" + }, + { + "ms": 0.011212999999999999, + "name": "importValidation" + }, + { + "ms": 0.240854, + "name": "Linker" + }, + { + "ms": 0.017884, + "name": "Store" + }, + { + "ms": 0.18959, + "name": "Instance" + }, + { + "ms": 0.088406, + "name": "signalMaskInit" + }, + { + "ms": 0.00618, + "name": "entrypointLookup" + }, + { + "ms": 49.777099, + "name": "wasi.start" + }, + { + "ms": 0.066824, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 1120.156366 + }, + "memory": { + "start": { + "rssBytes": 249040896, + "peakRssBytes": 249245696, + "pssBytes": 251109376, + "virtualBytes": 3957469184, + "minorFaults": 54332, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289746944, + "virtualBytes": 8327372800, + "minorFaults": 64906, + "majorFaults": 2 + }, + "end": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 64906, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47345, + "wasmtimeProcessRetainedRssBytes": 249040896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 80.57973800000036, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.705782, + "firstHostCallMs": 0.008046999999999999, + "firstOutputMs": 58.847584000000005, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.00128, + "name": "Engine" + }, + { + "ms": 0.139485, + "name": "canonicalPreopens" + }, + { + "ms": 6.586355999999999, + "name": "moduleRead" + }, + { + "ms": 3.882179, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008311, + "name": "importValidation" + }, + { + "ms": 0.197327, + "name": "Linker" + }, + { + "ms": 0.017893999999999997, + "name": "Store" + }, + { + "ms": 0.053520000000000005, + "name": "Instance" + }, + { + "ms": 0.06430799999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.003579, + "name": "entrypointLookup" + }, + { + "ms": 47.397208000000006, + "name": "wasi.start" + }, + { + "ms": 0.052558, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 59.004098 + }, + "memory": { + "start": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 64906, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 287453184, + "peakRssBytes": 287600640, + "pssBytes": 289689600, + "virtualBytes": 8327372800, + "minorFaults": 64980, + "majorFaults": 2 + }, + "end": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 64980, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 80.31804699999338, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.584335, + "firstHostCallMs": 0.012143000000000001, + "firstOutputMs": 60.487815, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001152, + "name": "Engine" + }, + { + "ms": 0.12195, + "name": "canonicalPreopens" + }, + { + "ms": 6.5597900000000005, + "name": "moduleRead" + }, + { + "ms": 3.8723859999999997, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007947000000000001, + "name": "importValidation" + }, + { + "ms": 0.192189, + "name": "Linker" + }, + { + "ms": 0.015134, + "name": "Store" + }, + { + "ms": 0.038940999999999996, + "name": "Instance" + }, + { + "ms": 0.026601000000000003, + "name": "signalMaskInit" + }, + { + "ms": 0.002704, + "name": "entrypointLookup" + }, + { + "ms": 49.146706, + "name": "wasi.start" + }, + { + "ms": 0.729557, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 61.322241999999996 + }, + "memory": { + "start": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 64980, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 287453184, + "peakRssBytes": 287600640, + "pssBytes": 289689600, + "virtualBytes": 8327372800, + "minorFaults": 65054, + "majorFaults": 2 + }, + "end": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 65054, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 78.76135800000338, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.616312, + "firstHostCallMs": 0.010949, + "firstOutputMs": 60.207328, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001084, + "name": "Engine" + }, + { + "ms": 0.10714699999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.583519000000001, + "name": "moduleRead" + }, + { + "ms": 3.881528, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00707, + "name": "importValidation" + }, + { + "ms": 0.20522300000000002, + "name": "Linker" + }, + { + "ms": 0.016696, + "name": "Store" + }, + { + "ms": 0.044641, + "name": "Instance" + }, + { + "ms": 0.023063, + "name": "signalMaskInit" + }, + { + "ms": 0.0028710000000000003, + "name": "entrypointLookup" + }, + { + "ms": 48.855158, + "name": "wasi.start" + }, + { + "ms": 0.841228, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 61.211572 + }, + "memory": { + "start": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 65054, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 287453184, + "peakRssBytes": 287600640, + "pssBytes": 289689600, + "virtualBytes": 8327372800, + "minorFaults": 65128, + "majorFaults": 2 + }, + "end": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 65128, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 89.4567790000001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.362697, + "firstHostCallMs": 0.010509999999999999, + "firstOutputMs": 63.66065699999999, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.000989, + "name": "Engine" + }, + { + "ms": 0.114565, + "name": "canonicalPreopens" + }, + { + "ms": 6.612816, + "name": "moduleRead" + }, + { + "ms": 4.132842, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010515, + "name": "importValidation" + }, + { + "ms": 0.22530799999999998, + "name": "Linker" + }, + { + "ms": 0.018991, + "name": "Store" + }, + { + "ms": 0.282897, + "name": "Instance" + }, + { + "ms": 0.06503500000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.004207, + "name": "entrypointLookup" + }, + { + "ms": 51.682954, + "name": "wasi.start" + }, + { + "ms": 0.051941999999999995, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 63.828258 + }, + "memory": { + "start": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289054720, + "virtualBytes": 3962916864, + "minorFaults": 65128, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 287453184, + "peakRssBytes": 287600640, + "pssBytes": 289688576, + "virtualBytes": 8327372800, + "minorFaults": 65202, + "majorFaults": 2 + }, + "end": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289053696, + "virtualBytes": 3962916864, + "minorFaults": 65202, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3900.4119800000044, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3450.345419, + "firstHostCallMs": 0.014202000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001505, + "name": "Engine" + }, + { + "ms": 0.113923, + "name": "canonicalPreopens" + }, + { + "ms": 11.891962000000001, + "name": "moduleRead" + }, + { + "ms": 12.641917000000001, + "name": "profileValidation" + }, + { + "ms": 3420.678433, + "name": "moduleCompile" + }, + { + "ms": 0.009255000000000001, + "name": "importValidation" + }, + { + "ms": 0.179188, + "name": "Linker" + }, + { + "ms": 0.015858999999999998, + "name": "Store" + }, + { + "ms": 3.292467, + "name": "Instance" + }, + { + "ms": 0.076895, + "name": "signalMaskInit" + }, + { + "ms": 0.006248, + "name": "entrypointLookup" + }, + { + "ms": 425.939489, + "name": "wasi.start" + }, + { + "ms": 0.075039, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 3876.405031 + }, + "memory": { + "start": { + "rssBytes": 286855168, + "peakRssBytes": 287600640, + "pssBytes": 289053696, + "virtualBytes": 3962916864, + "minorFaults": 65202, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 418258944, + "peakRssBytes": 418455552, + "pssBytes": 421568512, + "virtualBytes": 12866449408, + "minorFaults": 95797, + "majorFaults": 2 + }, + "end": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 95797, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1106078, + "wasmtimeProcessRetainedRssBytes": 286855168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 82.69085300000734, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 26.126963999999997, + "firstHostCallMs": 0.033969, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.00098, + "name": "Engine" + }, + { + "ms": 0.121366, + "name": "canonicalPreopens" + }, + { + "ms": 11.917586, + "name": "moduleRead" + }, + { + "ms": 12.258225, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011895000000000001, + "name": "importValidation" + }, + { + "ms": 0.20752199999999998, + "name": "Linker" + }, + { + "ms": 0.017752, + "name": "Store" + }, + { + "ms": 0.05416, + "name": "Instance" + }, + { + "ms": 0.067728, + "name": "signalMaskInit" + }, + { + "ms": 0.004283, + "name": "entrypointLookup" + }, + { + "ms": 29.636894, + "name": "wasi.start" + }, + { + "ms": 0.043057, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 55.815045 + }, + "memory": { + "start": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 95797, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 421654528, + "virtualBytes": 12866449408, + "minorFaults": 95886, + "majorFaults": 2 + }, + "end": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 95886, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 88.49471400000039, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 26.476031, + "firstHostCallMs": 0.013653, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0009620000000000001, + "name": "Engine" + }, + { + "ms": 0.11400199999999999, + "name": "canonicalPreopens" + }, + { + "ms": 12.241017999999999, + "name": "moduleRead" + }, + { + "ms": 12.323571, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011798, + "name": "importValidation" + }, + { + "ms": 0.212772, + "name": "Linker" + }, + { + "ms": 0.016821, + "name": "Store" + }, + { + "ms": 0.048704, + "name": "Instance" + }, + { + "ms": 0.060407, + "name": "signalMaskInit" + }, + { + "ms": 0.0029579999999999997, + "name": "entrypointLookup" + }, + { + "ms": 31.401605999999997, + "name": "wasi.start" + }, + { + "ms": 0.037887, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 57.913706000000005 + }, + "memory": { + "start": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 95886, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 421650432, + "virtualBytes": 12866449408, + "minorFaults": 95975, + "majorFaults": 2 + }, + "end": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420805632, + "virtualBytes": 4137537536, + "minorFaults": 95975, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 94.44075900000462, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 28.122518, + "firstHostCallMs": 0.012298, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.000984, + "name": "Engine" + }, + { + "ms": 0.114873, + "name": "canonicalPreopens" + }, + { + "ms": 11.629657, + "name": "moduleRead" + }, + { + "ms": 13.23064, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011307000000000001, + "name": "importValidation" + }, + { + "ms": 0.21595899999999998, + "name": "Linker" + }, + { + "ms": 0.01862, + "name": "Store" + }, + { + "ms": 1.400109, + "name": "Instance" + }, + { + "ms": 0.056134, + "name": "signalMaskInit" + }, + { + "ms": 0.0037589999999999998, + "name": "entrypointLookup" + }, + { + "ms": 42.266698999999996, + "name": "wasi.start" + }, + { + "ms": 0.052329, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 70.472402 + }, + "memory": { + "start": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420805632, + "virtualBytes": 4137537536, + "minorFaults": 95975, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 421653504, + "virtualBytes": 12866449408, + "minorFaults": 96064, + "majorFaults": 2 + }, + "end": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420805632, + "virtualBytes": 4137537536, + "minorFaults": 96064, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 90.5766089999961, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.997907, + "firstHostCallMs": 0.03505, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.003502, + "name": "Engine" + }, + { + "ms": 0.19964900000000002, + "name": "canonicalPreopens" + }, + { + "ms": 11.180664, + "name": "moduleRead" + }, + { + "ms": 14.757781, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014461, + "name": "importValidation" + }, + { + "ms": 0.22075099999999998, + "name": "Linker" + }, + { + "ms": 0.018074, + "name": "Store" + }, + { + "ms": 3.041642, + "name": "Instance" + }, + { + "ms": 0.06905499999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.0089, + "name": "entrypointLookup" + }, + { + "ms": 32.841708999999994, + "name": "wasi.start" + }, + { + "ms": 2.874135, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 66.701019 + }, + "memory": { + "start": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420805632, + "virtualBytes": 4137537536, + "minorFaults": 96064, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 421649408, + "virtualBytes": 12866449408, + "minorFaults": 96153, + "majorFaults": 2 + }, + "end": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 96153, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1609.063089999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1575.584229, + "firstHostCallMs": 0.014450000000000001, + "firstOutputMs": 1580.6776810000001, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001153, + "name": "Engine" + }, + { + "ms": 0.10638500000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.867833, + "name": "moduleRead" + }, + { + "ms": 6.264063, + "name": "profileValidation" + }, + { + "ms": 1561.102601, + "name": "moduleCompile" + }, + { + "ms": 0.010772, + "name": "importValidation" + }, + { + "ms": 0.18409899999999998, + "name": "Linker" + }, + { + "ms": 0.015744, + "name": "Store" + }, + { + "ms": 0.181588, + "name": "Instance" + }, + { + "ms": 0.082574, + "name": "signalMaskInit" + }, + { + "ms": 0.004202, + "name": "entrypointLookup" + }, + { + "ms": 5.2743199999999995, + "name": "wasi.start" + }, + { + "ms": 0.055889, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 1580.945792 + }, + "memory": { + "start": { + "rssBytes": 417677312, + "peakRssBytes": 418455552, + "pssBytes": 420806656, + "virtualBytes": 4137537536, + "minorFaults": 96153, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 425172992, + "peakRssBytes": 425734144, + "pssBytes": 428888064, + "virtualBytes": 8509390848, + "minorFaults": 97545, + "majorFaults": 2 + }, + "end": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428830720, + "virtualBytes": 4144934912, + "minorFaults": 97545, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4923165, + "wasmtimeProcessRetainedRssBytes": 417677312, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 45.41370999999344, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.342958, + "firstHostCallMs": 0.019818, + "firstOutputMs": 22.722160000000002, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.00162, + "name": "Engine" + }, + { + "ms": 0.128996, + "name": "canonicalPreopens" + }, + { + "ms": 6.936747, + "name": "moduleRead" + }, + { + "ms": 6.25023, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011783, + "name": "importValidation" + }, + { + "ms": 0.20517300000000002, + "name": "Linker" + }, + { + "ms": 0.017703, + "name": "Store" + }, + { + "ms": 2.928045, + "name": "Instance" + }, + { + "ms": 0.072489, + "name": "signalMaskInit" + }, + { + "ms": 0.004939, + "name": "entrypointLookup" + }, + { + "ms": 5.564832, + "name": "wasi.start" + }, + { + "ms": 0.758398, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 23.657427000000002 + }, + "memory": { + "start": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428830720, + "virtualBytes": 4144934912, + "minorFaults": 97545, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 429342720, + "virtualBytes": 8509390848, + "minorFaults": 97595, + "majorFaults": 2 + }, + "end": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428834816, + "virtualBytes": 4144934912, + "minorFaults": 97595, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 52.72330800000054, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.096207, + "firstHostCallMs": 0.018423, + "firstOutputMs": 22.898591999999997, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001091, + "name": "Engine" + }, + { + "ms": 0.118787, + "name": "canonicalPreopens" + }, + { + "ms": 6.962913, + "name": "moduleRead" + }, + { + "ms": 6.226303, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012397, + "name": "importValidation" + }, + { + "ms": 0.208654, + "name": "Linker" + }, + { + "ms": 0.016949, + "name": "Store" + }, + { + "ms": 1.738164, + "name": "Instance" + }, + { + "ms": 0.043868000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.003257, + "name": "entrypointLookup" + }, + { + "ms": 7.00415, + "name": "wasi.start" + }, + { + "ms": 0.580152, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 23.697311 + }, + "memory": { + "start": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428834816, + "virtualBytes": 4144934912, + "minorFaults": 97595, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 429383680, + "virtualBytes": 8509390848, + "minorFaults": 97652, + "majorFaults": 2 + }, + "end": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428928000, + "virtualBytes": 4144934912, + "minorFaults": 97652, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 52.53513399999065, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.574697999999998, + "firstHostCallMs": 0.015265999999999998, + "firstOutputMs": 23.140622999999998, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001095, + "name": "Engine" + }, + { + "ms": 0.13819599999999999, + "name": "canonicalPreopens" + }, + { + "ms": 7.025821, + "name": "moduleRead" + }, + { + "ms": 6.453261, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011987999999999999, + "name": "importValidation" + }, + { + "ms": 0.217254, + "name": "Linker" + }, + { + "ms": 0.018458, + "name": "Store" + }, + { + "ms": 1.859288, + "name": "Instance" + }, + { + "ms": 0.076263, + "name": "signalMaskInit" + }, + { + "ms": 0.005311, + "name": "entrypointLookup" + }, + { + "ms": 6.741458, + "name": "wasi.start" + }, + { + "ms": 0.708309, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.030303 + }, + "memory": { + "start": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428928000, + "virtualBytes": 4144934912, + "minorFaults": 97652, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 429456384, + "virtualBytes": 8509390848, + "minorFaults": 97702, + "majorFaults": 2 + }, + "end": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428932096, + "virtualBytes": 4144934912, + "minorFaults": 97702, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 48.87428499999805, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.254024999999999, + "firstHostCallMs": 0.022014, + "firstOutputMs": 23.309177000000002, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.0017549999999999998, + "name": "Engine" + }, + { + "ms": 0.193955, + "name": "canonicalPreopens" + }, + { + "ms": 6.940723, + "name": "moduleRead" + }, + { + "ms": 6.43385, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012912999999999999, + "name": "importValidation" + }, + { + "ms": 0.207761, + "name": "Linker" + }, + { + "ms": 0.020405, + "name": "Store" + }, + { + "ms": 0.578243, + "name": "Instance" + }, + { + "ms": 0.080231, + "name": "signalMaskInit" + }, + { + "ms": 0.0034869999999999996, + "name": "entrypointLookup" + }, + { + "ms": 8.254097, + "name": "wasi.start" + }, + { + "ms": 0.8216, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.342261 + }, + "memory": { + "start": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428932096, + "virtualBytes": 4144934912, + "minorFaults": 97702, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 429460480, + "virtualBytes": 8509390848, + "minorFaults": 97751, + "majorFaults": 2 + }, + "end": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428932096, + "virtualBytes": 4144934912, + "minorFaults": 97751, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1279.9080850000028, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1248.0401940000002, + "firstHostCallMs": 0.012777, + "firstOutputMs": 1250.1782699999999, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001193, + "name": "Engine" + }, + { + "ms": 0.108048, + "name": "canonicalPreopens" + }, + { + "ms": 5.187355999999999, + "name": "moduleRead" + }, + { + "ms": 4.851607, + "name": "profileValidation" + }, + { + "ms": 1236.947881, + "name": "moduleCompile" + }, + { + "ms": 0.009120000000000001, + "name": "importValidation" + }, + { + "ms": 0.191971, + "name": "Linker" + }, + { + "ms": 0.018574, + "name": "Store" + }, + { + "ms": 0.227633, + "name": "Instance" + }, + { + "ms": 0.06012, + "name": "signalMaskInit" + }, + { + "ms": 0.002615, + "name": "entrypointLookup" + }, + { + "ms": 2.201483, + "name": "wasi.start" + }, + { + "ms": 2.383, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 1252.6777499999998 + }, + "memory": { + "start": { + "rssBytes": 425082880, + "peakRssBytes": 425734144, + "pssBytes": 428932096, + "virtualBytes": 4144934912, + "minorFaults": 97751, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 432783360, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 8514859008, + "minorFaults": 98655, + "majorFaults": 2 + }, + "end": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98655, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6484267, + "wasmtimeProcessRetainedRssBytes": 425082880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 37.541293999995105, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.524698, + "firstHostCallMs": 0.042665, + "firstOutputMs": 14.162819, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001144, + "name": "Engine" + }, + { + "ms": 0.105538, + "name": "canonicalPreopens" + }, + { + "ms": 5.330961, + "name": "moduleRead" + }, + { + "ms": 5.491951, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010339000000000001, + "name": "importValidation" + }, + { + "ms": 0.20549800000000001, + "name": "Linker" + }, + { + "ms": 0.017554, + "name": "Store" + }, + { + "ms": 0.853749, + "name": "Instance" + }, + { + "ms": 0.037842999999999995, + "name": "signalMaskInit" + }, + { + "ms": 0.0035039999999999997, + "name": "entrypointLookup" + }, + { + "ms": 1.698701, + "name": "wasi.start" + }, + { + "ms": 2.102712, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 16.360689 + }, + "memory": { + "start": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98655, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434475008, + "virtualBytes": 8514859008, + "minorFaults": 98705, + "majorFaults": 2 + }, + "end": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98705, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 43.085986000005505, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.712372, + "firstHostCallMs": 0.020422, + "firstOutputMs": 13.727803999999999, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.000956, + "name": "Engine" + }, + { + "ms": 0.101948, + "name": "canonicalPreopens" + }, + { + "ms": 5.305095000000001, + "name": "moduleRead" + }, + { + "ms": 4.920441, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00958, + "name": "importValidation" + }, + { + "ms": 0.215943, + "name": "Linker" + }, + { + "ms": 0.024516, + "name": "Store" + }, + { + "ms": 0.624816, + "name": "Instance" + }, + { + "ms": 0.061543, + "name": "signalMaskInit" + }, + { + "ms": 0.003077, + "name": "entrypointLookup" + }, + { + "ms": 2.111397, + "name": "wasi.start" + }, + { + "ms": 0.07127399999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.978925 + }, + "memory": { + "start": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98705, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 432648192, + "peakRssBytes": 433049600, + "pssBytes": 434475008, + "virtualBytes": 8514859008, + "minorFaults": 98755, + "majorFaults": 2 + }, + "end": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98755, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 53.47755199999665, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.712703, + "firstHostCallMs": 0.030482000000000002, + "firstOutputMs": 16.852995999999997, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0033060000000000003, + "name": "Engine" + }, + { + "ms": 0.141589, + "name": "canonicalPreopens" + }, + { + "ms": 5.421826, + "name": "moduleRead" + }, + { + "ms": 4.974245000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011183, + "name": "importValidation" + }, + { + "ms": 0.214339, + "name": "Linker" + }, + { + "ms": 0.020511, + "name": "Store" + }, + { + "ms": 2.342285, + "name": "Instance" + }, + { + "ms": 0.101103, + "name": "signalMaskInit" + }, + { + "ms": 0.005716, + "name": "entrypointLookup" + }, + { + "ms": 3.280449, + "name": "wasi.start" + }, + { + "ms": 2.6363100000000004, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 19.752843 + }, + "memory": { + "start": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98755, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434473984, + "virtualBytes": 8514859008, + "minorFaults": 98805, + "majorFaults": 2 + }, + "end": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434401280, + "virtualBytes": 4150403072, + "minorFaults": 98805, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 42.88672899999074, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.858049000000001, + "firstHostCallMs": 0.012393, + "firstOutputMs": 14.03944, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0010890000000000001, + "name": "Engine" + }, + { + "ms": 0.149371, + "name": "canonicalPreopens" + }, + { + "ms": 4.749421, + "name": "moduleRead" + }, + { + "ms": 4.941534, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010628, + "name": "importValidation" + }, + { + "ms": 0.202766, + "name": "Linker" + }, + { + "ms": 0.015937, + "name": "Store" + }, + { + "ms": 1.217141, + "name": "Instance" + }, + { + "ms": 0.080108, + "name": "signalMaskInit" + }, + { + "ms": 0.004981, + "name": "entrypointLookup" + }, + { + "ms": 2.280808, + "name": "wasi.start" + }, + { + "ms": 2.584382, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 16.732184 + }, + "memory": { + "start": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434400256, + "virtualBytes": 4150403072, + "minorFaults": 98805, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 432648192, + "peakRssBytes": 433049600, + "pssBytes": 434473984, + "virtualBytes": 8514859008, + "minorFaults": 98855, + "majorFaults": 2 + }, + "end": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434400256, + "virtualBytes": 4150403072, + "minorFaults": 98855, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3711.566989999992, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3680.868275, + "firstHostCallMs": 0.028203000000000002, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001756, + "name": "Engine" + }, + { + "ms": 0.15103, + "name": "canonicalPreopens" + }, + { + "ms": 10.977598, + "name": "moduleRead" + }, + { + "ms": 16.197066, + "name": "profileValidation" + }, + { + "ms": 3649.451143, + "name": "moduleCompile" + }, + { + "ms": 0.012664, + "name": "importValidation" + }, + { + "ms": 0.178484, + "name": "Linker" + }, + { + "ms": 0.017426, + "name": "Store" + }, + { + "ms": 2.429281, + "name": "Instance" + }, + { + "ms": 0.057358, + "name": "signalMaskInit" + }, + { + "ms": 0.004455, + "name": "entrypointLookup" + }, + { + "ms": 7.0776140000000005, + "name": "wasi.start" + }, + { + "ms": 0.580195, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 3688.5438019999997 + }, + "memory": { + "start": { + "rssBytes": 430551040, + "peakRssBytes": 433049600, + "pssBytes": 434400256, + "virtualBytes": 4150403072, + "minorFaults": 98855, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 447627264, + "peakRssBytes": 447713280, + "pssBytes": 451502080, + "virtualBytes": 8531275776, + "minorFaults": 102983, + "majorFaults": 2 + }, + "end": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 102983, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7721215, + "wasmtimeProcessRetainedRssBytes": 430551040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 76.92460900000879, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.961055, + "firstHostCallMs": 0.06857100000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001034, + "name": "Engine" + }, + { + "ms": 0.13672399999999998, + "name": "canonicalPreopens" + }, + { + "ms": 11.586236999999999, + "name": "moduleRead" + }, + { + "ms": 14.426936, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.016577, + "name": "importValidation" + }, + { + "ms": 0.216635, + "name": "Linker" + }, + { + "ms": 0.016566, + "name": "Store" + }, + { + "ms": 2.0497520000000002, + "name": "Instance" + }, + { + "ms": 0.091957, + "name": "signalMaskInit" + }, + { + "ms": 0.003511, + "name": "entrypointLookup" + }, + { + "ms": 15.923231, + "name": "wasi.start" + }, + { + "ms": 0.05115, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 45.919682 + }, + "memory": { + "start": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 102983, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 447520768, + "peakRssBytes": 447713280, + "pssBytes": 451477504, + "virtualBytes": 8531275776, + "minorFaults": 103080, + "majorFaults": 2 + }, + "end": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103080, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 73.26333300000988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 28.905635999999998, + "firstHostCallMs": 0.013674, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.0009299999999999999, + "name": "Engine" + }, + { + "ms": 0.112425, + "name": "canonicalPreopens" + }, + { + "ms": 11.45195, + "name": "moduleRead" + }, + { + "ms": 14.363315, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014258, + "name": "importValidation" + }, + { + "ms": 0.208668, + "name": "Linker" + }, + { + "ms": 0.017502, + "name": "Store" + }, + { + "ms": 1.304714, + "name": "Instance" + }, + { + "ms": 0.054638, + "name": "signalMaskInit" + }, + { + "ms": 0.005592, + "name": "entrypointLookup" + }, + { + "ms": 19.808111999999998, + "name": "wasi.start" + }, + { + "ms": 1.9207370000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 50.639390999999996 + }, + "memory": { + "start": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103080, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 447520768, + "peakRssBytes": 447713280, + "pssBytes": 451461120, + "virtualBytes": 8531275776, + "minorFaults": 103177, + "majorFaults": 2 + }, + "end": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103177, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 71.5298119999934, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 27.749996999999997, + "firstHostCallMs": 0.014617000000000002, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.00088, + "name": "Engine" + }, + { + "ms": 0.114252, + "name": "canonicalPreopens" + }, + { + "ms": 11.391564, + "name": "moduleRead" + }, + { + "ms": 14.493246, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013278, + "name": "importValidation" + }, + { + "ms": 0.207813, + "name": "Linker" + }, + { + "ms": 0.018146, + "name": "Store" + }, + { + "ms": 0.052562000000000005, + "name": "Instance" + }, + { + "ms": 0.059741, + "name": "signalMaskInit" + }, + { + "ms": 0.003878, + "name": "entrypointLookup" + }, + { + "ms": 18.943952, + "name": "wasi.start" + }, + { + "ms": 0.047643, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 46.690537 + }, + "memory": { + "start": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103177, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 447520768, + "peakRssBytes": 447713280, + "pssBytes": 451477504, + "virtualBytes": 8531275776, + "minorFaults": 103274, + "majorFaults": 2 + }, + "end": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103274, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 337.7979170000035, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.02053, + "firstHostCallMs": 0.02525, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001315, + "name": "Engine" + }, + { + "ms": 0.152227, + "name": "canonicalPreopens" + }, + { + "ms": 11.454847000000001, + "name": "moduleRead" + }, + { + "ms": 14.483187, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013392, + "name": "importValidation" + }, + { + "ms": 0.21751, + "name": "Linker" + }, + { + "ms": 0.017495, + "name": "Store" + }, + { + "ms": 1.1937499999999999, + "name": "Instance" + }, + { + "ms": 0.052045, + "name": "signalMaskInit" + }, + { + "ms": 0.003974, + "name": "entrypointLookup" + }, + { + "ms": 21.166809, + "name": "wasi.start" + }, + { + "ms": 1.338875, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 51.456880000000005 + }, + "memory": { + "start": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103274, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 447520768, + "peakRssBytes": 447713280, + "pssBytes": 451461120, + "virtualBytes": 8531275776, + "minorFaults": 103371, + "majorFaults": 2 + }, + "end": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103371, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3975.9366819999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3944.3027869999996, + "firstHostCallMs": 0.019485000000000002, + "firstOutputMs": 3946.153186, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001299, + "name": "Engine" + }, + { + "ms": 0.128047, + "name": "canonicalPreopens" + }, + { + "ms": 12.861828000000001, + "name": "moduleRead" + }, + { + "ms": 15.833112999999999, + "name": "profileValidation" + }, + { + "ms": 3913.4003079999998, + "name": "moduleCompile" + }, + { + "ms": 0.013913, + "name": "importValidation" + }, + { + "ms": 0.178515, + "name": "Linker" + }, + { + "ms": 0.018394999999999998, + "name": "Store" + }, + { + "ms": 0.214838, + "name": "Instance" + }, + { + "ms": 0.076746, + "name": "signalMaskInit" + }, + { + "ms": 0.004758, + "name": "entrypointLookup" + }, + { + "ms": 2.058683, + "name": "wasi.start" + }, + { + "ms": 1.6423290000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 3948.050092 + }, + "memory": { + "start": { + "rssBytes": 446967808, + "peakRssBytes": 447713280, + "pssBytes": 450818048, + "virtualBytes": 4166819840, + "minorFaults": 103371, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 475500544, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 8549871616, + "minorFaults": 110315, + "majorFaults": 2 + }, + "end": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 4185415680, + "minorFaults": 110315, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11370666, + "wasmtimeProcessRetainedRssBytes": 446967808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 68.6130179999891, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 34.093726999999994, + "firstHostCallMs": 0.085355, + "firstOutputMs": 35.664441, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0014320000000000001, + "name": "Engine" + }, + { + "ms": 0.21515299999999998, + "name": "canonicalPreopens" + }, + { + "ms": 13.241641, + "name": "moduleRead" + }, + { + "ms": 16.565814000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014756, + "name": "importValidation" + }, + { + "ms": 0.204536, + "name": "Linker" + }, + { + "ms": 0.016835, + "name": "Store" + }, + { + "ms": 2.0767960000000003, + "name": "Instance" + }, + { + "ms": 0.085591, + "name": "signalMaskInit" + }, + { + "ms": 0.004857, + "name": "entrypointLookup" + }, + { + "ms": 1.748661, + "name": "wasi.start" + }, + { + "ms": 0.035078, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 35.861965000000005 + }, + "memory": { + "start": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478956544, + "virtualBytes": 4185415680, + "minorFaults": 110315, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 479030272, + "virtualBytes": 4185427968, + "minorFaults": 110359, + "majorFaults": 2 + }, + "end": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478956544, + "virtualBytes": 4185415680, + "minorFaults": 110359, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 65.0076490000065, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 34.816983, + "firstHostCallMs": 0.017828, + "firstOutputMs": 36.385189999999994, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0009490000000000001, + "name": "Engine" + }, + { + "ms": 0.129304, + "name": "canonicalPreopens" + }, + { + "ms": 13.052002, + "name": "moduleRead" + }, + { + "ms": 17.405914000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015037, + "name": "importValidation" + }, + { + "ms": 0.209063, + "name": "Linker" + }, + { + "ms": 0.035962, + "name": "Store" + }, + { + "ms": 2.2937779999999997, + "name": "Instance" + }, + { + "ms": 0.07541300000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.007456, + "name": "entrypointLookup" + }, + { + "ms": 1.733876, + "name": "wasi.start" + }, + { + "ms": 0.036693, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 36.594224000000004 + }, + "memory": { + "start": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478956544, + "virtualBytes": 4185415680, + "minorFaults": 110359, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 479031296, + "virtualBytes": 4185427968, + "minorFaults": 110403, + "majorFaults": 2 + }, + "end": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478956544, + "virtualBytes": 4185415680, + "minorFaults": 110403, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 216.1557199999952, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.534312000000003, + "firstHostCallMs": 0.024943, + "firstOutputMs": 33.064997999999996, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0018050000000000002, + "name": "Engine" + }, + { + "ms": 0.156037, + "name": "canonicalPreopens" + }, + { + "ms": 11.970312, + "name": "moduleRead" + }, + { + "ms": 16.093099, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.019179, + "name": "importValidation" + }, + { + "ms": 0.336697, + "name": "Linker" + }, + { + "ms": 0.02586, + "name": "Store" + }, + { + "ms": 1.235222, + "name": "Instance" + }, + { + "ms": 0.07245599999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.006309, + "name": "entrypointLookup" + }, + { + "ms": 1.719702, + "name": "wasi.start" + }, + { + "ms": 1.40896, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 34.67416 + }, + "memory": { + "start": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478956544, + "virtualBytes": 4185415680, + "minorFaults": 110403, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 479031296, + "virtualBytes": 8549871616, + "minorFaults": 110447, + "majorFaults": 2 + }, + "end": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 4185415680, + "minorFaults": 110447, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 56.04536000000371, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.281241, + "firstHostCallMs": 0.017694, + "firstOutputMs": 33.673342000000005, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001085, + "name": "Engine" + }, + { + "ms": 0.11998, + "name": "canonicalPreopens" + }, + { + "ms": 12.810352, + "name": "moduleRead" + }, + { + "ms": 15.800878, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015837, + "name": "importValidation" + }, + { + "ms": 0.217827, + "name": "Linker" + }, + { + "ms": 0.01883, + "name": "Store" + }, + { + "ms": 0.053572999999999996, + "name": "Instance" + }, + { + "ms": 0.077998, + "name": "signalMaskInit" + }, + { + "ms": 0.0041930000000000005, + "name": "entrypointLookup" + }, + { + "ms": 3.130187, + "name": "wasi.start" + }, + { + "ms": 0.030667000000000003, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 33.909295 + }, + "memory": { + "start": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 4185415680, + "minorFaults": 110447, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 479031296, + "virtualBytes": 8546934784, + "minorFaults": 110491, + "majorFaults": 2 + }, + "end": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 4185415680, + "minorFaults": 110491, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 710.8036649999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 638.484359, + "firstHostCallMs": 0.014659, + "firstOutputMs": 683.6221409999999, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0010789999999999999, + "name": "Engine" + }, + { + "ms": 0.144907, + "name": "canonicalPreopens" + }, + { + "ms": 6.450229, + "name": "moduleRead" + }, + { + "ms": 2.428557, + "name": "profileValidation" + }, + { + "ms": 628.1260440000001, + "name": "moduleCompile" + }, + { + "ms": 0.007103, + "name": "importValidation" + }, + { + "ms": 0.199606, + "name": "Linker" + }, + { + "ms": 0.017266999999999998, + "name": "Store" + }, + { + "ms": 0.283651, + "name": "Instance" + }, + { + "ms": 0.051014000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.002823, + "name": "entrypointLookup" + }, + { + "ms": 45.335559999999994, + "name": "wasi.start" + }, + { + "ms": 0.865841, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 684.648512 + }, + "memory": { + "start": { + "rssBytes": 475107328, + "peakRssBytes": 475840512, + "pssBytes": 478957568, + "virtualBytes": 4185415680, + "minorFaults": 110491, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 479166464, + "peakRssBytes": 479490048, + "pssBytes": 483348480, + "virtualBytes": 8553734144, + "minorFaults": 111514, + "majorFaults": 2 + }, + "end": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482819072, + "virtualBytes": 4189278208, + "minorFaults": 111514, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15284067, + "wasmtimeProcessRetainedRssBytes": 475107328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 89.94559699999809, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.628538, + "firstHostCallMs": 0.023604999999999998, + "firstOutputMs": 59.175399, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0015680000000000002, + "name": "Engine" + }, + { + "ms": 0.173292, + "name": "canonicalPreopens" + }, + { + "ms": 5.725512, + "name": "moduleRead" + }, + { + "ms": 3.7532490000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011127, + "name": "importValidation" + }, + { + "ms": 0.291268, + "name": "Linker" + }, + { + "ms": 0.021587, + "name": "Store" + }, + { + "ms": 0.783478, + "name": "Instance" + }, + { + "ms": 0.040621000000000004, + "name": "signalMaskInit" + }, + { + "ms": 0.004919, + "name": "entrypointLookup" + }, + { + "ms": 47.818253, + "name": "wasi.start" + }, + { + "ms": 0.180005, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 59.48585 + }, + "memory": { + "start": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482819072, + "virtualBytes": 4189278208, + "minorFaults": 111514, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 479043584, + "peakRssBytes": 479490048, + "pssBytes": 483224576, + "virtualBytes": 8553734144, + "minorFaults": 111564, + "majorFaults": 2 + }, + "end": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111564, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 79.20810000000347, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.152557999999999, + "firstHostCallMs": 0.021646, + "firstOutputMs": 56.309487, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001023, + "name": "Engine" + }, + { + "ms": 0.11452, + "name": "canonicalPreopens" + }, + { + "ms": 6.682085, + "name": "moduleRead" + }, + { + "ms": 2.462513, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007965000000000002, + "name": "importValidation" + }, + { + "ms": 0.206591, + "name": "Linker" + }, + { + "ms": 0.016054, + "name": "Store" + }, + { + "ms": 0.830022, + "name": "Instance" + }, + { + "ms": 0.056215999999999995, + "name": "signalMaskInit" + }, + { + "ms": 0.0032689999999999998, + "name": "entrypointLookup" + }, + { + "ms": 45.404720000000005, + "name": "wasi.start" + }, + { + "ms": 1.367566, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 57.848113 + }, + "memory": { + "start": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111564, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 483225600, + "virtualBytes": 8553734144, + "minorFaults": 111614, + "majorFaults": 2 + }, + "end": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111614, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 76.08970599999884, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 10.718245, + "firstHostCallMs": 0.017791, + "firstOutputMs": 54.564859, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001076, + "name": "Engine" + }, + { + "ms": 0.116538, + "name": "canonicalPreopens" + }, + { + "ms": 6.733903000000001, + "name": "moduleRead" + }, + { + "ms": 2.4622960000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00872, + "name": "importValidation" + }, + { + "ms": 0.205161, + "name": "Linker" + }, + { + "ms": 0.016819, + "name": "Store" + }, + { + "ms": 0.300862, + "name": "Instance" + }, + { + "ms": 0.063566, + "name": "signalMaskInit" + }, + { + "ms": 0.0039380000000000005, + "name": "entrypointLookup" + }, + { + "ms": 44.095829, + "name": "wasi.start" + }, + { + "ms": 1.065874, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 55.769424 + }, + "memory": { + "start": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111614, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 483225600, + "virtualBytes": 8553734144, + "minorFaults": 111664, + "majorFaults": 2 + }, + "end": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111664, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 82.31007300000056, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.386220000000002, + "firstHostCallMs": 0.023527, + "firstOutputMs": 52.151841, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0010979999999999998, + "name": "Engine" + }, + { + "ms": 0.156637, + "name": "canonicalPreopens" + }, + { + "ms": 6.682548, + "name": "moduleRead" + }, + { + "ms": 2.434058, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008029000000000001, + "name": "importValidation" + }, + { + "ms": 0.20590499999999998, + "name": "Linker" + }, + { + "ms": 0.018066, + "name": "Store" + }, + { + "ms": 0.8432459999999999, + "name": "Instance" + }, + { + "ms": 0.040188999999999996, + "name": "signalMaskInit" + }, + { + "ms": 0.005109, + "name": "entrypointLookup" + }, + { + "ms": 41.195071999999996, + "name": "wasi.start" + }, + { + "ms": 0.40103, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 52.685158 + }, + "memory": { + "start": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111664, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 479043584, + "peakRssBytes": 479490048, + "pssBytes": 483225600, + "virtualBytes": 8553734144, + "minorFaults": 111714, + "majorFaults": 2 + }, + "end": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111714, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1451.1446200000064, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1415.978511, + "firstHostCallMs": 0.013363, + "firstOutputMs": 1419.3090459999999, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.00087, + "name": "Engine" + }, + { + "ms": 0.10679, + "name": "canonicalPreopens" + }, + { + "ms": 6.859734, + "name": "moduleRead" + }, + { + "ms": 4.376836, + "name": "profileValidation" + }, + { + "ms": 1403.256087, + "name": "moduleCompile" + }, + { + "ms": 0.008459, + "name": "importValidation" + }, + { + "ms": 0.24452600000000002, + "name": "Linker" + }, + { + "ms": 0.021476, + "name": "Store" + }, + { + "ms": 0.25729, + "name": "Instance" + }, + { + "ms": 0.086336, + "name": "signalMaskInit" + }, + { + "ms": 0.013234000000000001, + "name": "entrypointLookup" + }, + { + "ms": 4.653792, + "name": "wasi.start" + }, + { + "ms": 0.065622, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 1420.7235090000001 + }, + "memory": { + "start": { + "rssBytes": 478969856, + "peakRssBytes": 479490048, + "pssBytes": 482820096, + "virtualBytes": 4189278208, + "minorFaults": 111714, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 514109440, + "peakRssBytes": 516812800, + "pssBytes": 512352256, + "virtualBytes": 8567255040, + "minorFaults": 135012, + "majorFaults": 2 + }, + "end": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501140480, + "virtualBytes": 4202799104, + "minorFaults": 135012, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15912193, + "wasmtimeProcessRetainedRssBytes": 478969856, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 51.16390699999465, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.045847, + "firstHostCallMs": 0.021039, + "firstOutputMs": 15.361364, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001285, + "name": "Engine" + }, + { + "ms": 0.168028, + "name": "canonicalPreopens" + }, + { + "ms": 5.9319489999999995, + "name": "moduleRead" + }, + { + "ms": 4.376651, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009677, + "name": "importValidation" + }, + { + "ms": 0.20755200000000001, + "name": "Linker" + }, + { + "ms": 0.020046, + "name": "Store" + }, + { + "ms": 0.5259750000000001, + "name": "Instance" + }, + { + "ms": 0.057431, + "name": "signalMaskInit" + }, + { + "ms": 0.003353, + "name": "entrypointLookup" + }, + { + "ms": 5.156682, + "name": "wasi.start" + }, + { + "ms": 0.330828, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 17.632074 + }, + "memory": { + "start": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501140480, + "virtualBytes": 4202799104, + "minorFaults": 135012, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501214208, + "virtualBytes": 8567255040, + "minorFaults": 135045, + "majorFaults": 2 + }, + "end": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135045, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 54.675832000008086, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.394337999999998, + "firstHostCallMs": 0.019500999999999998, + "firstOutputMs": 22.958159, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001094, + "name": "Engine" + }, + { + "ms": 0.16186, + "name": "canonicalPreopens" + }, + { + "ms": 6.505889, + "name": "moduleRead" + }, + { + "ms": 6.781131, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014861000000000001, + "name": "importValidation" + }, + { + "ms": 0.297888, + "name": "Linker" + }, + { + "ms": 0.025212, + "name": "Store" + }, + { + "ms": 2.785683, + "name": "Instance" + }, + { + "ms": 0.058368, + "name": "signalMaskInit" + }, + { + "ms": 0.007142000000000001, + "name": "entrypointLookup" + }, + { + "ms": 8.328163, + "name": "wasi.start" + }, + { + "ms": 0.664255, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 26.400974 + }, + "memory": { + "start": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135045, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501444608, + "virtualBytes": 8567255040, + "minorFaults": 135078, + "majorFaults": 2 + }, + "end": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135078, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 48.755312000008416, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.361713, + "firstHostCallMs": 0.014273999999999998, + "firstOutputMs": 15.64399, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001044, + "name": "Engine" + }, + { + "ms": 0.108805, + "name": "canonicalPreopens" + }, + { + "ms": 6.827446, + "name": "moduleRead" + }, + { + "ms": 4.392343, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009120000000000001, + "name": "importValidation" + }, + { + "ms": 0.199807, + "name": "Linker" + }, + { + "ms": 0.015879, + "name": "Store" + }, + { + "ms": 0.043088, + "name": "Instance" + }, + { + "ms": 0.020959000000000002, + "name": "signalMaskInit" + }, + { + "ms": 0.0031190000000000002, + "name": "entrypointLookup" + }, + { + "ms": 4.972792, + "name": "wasi.start" + }, + { + "ms": 0.089914, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 17.435185999999998 + }, + "memory": { + "start": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135078, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501415936, + "virtualBytes": 8567255040, + "minorFaults": 135111, + "majorFaults": 2 + }, + "end": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135111, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 48.567032999999356, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.522713, + "firstHostCallMs": 0.021121, + "firstOutputMs": 16.54027, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0012419999999999998, + "name": "Engine" + }, + { + "ms": 0.12166400000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.811552, + "name": "moduleRead" + }, + { + "ms": 4.372181, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010761999999999999, + "name": "importValidation" + }, + { + "ms": 0.215019, + "name": "Linker" + }, + { + "ms": 0.023264999999999997, + "name": "Store" + }, + { + "ms": 0.054097, + "name": "Instance" + }, + { + "ms": 0.08928900000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.00583, + "name": "entrypointLookup" + }, + { + "ms": 5.813381000000001, + "name": "wasi.start" + }, + { + "ms": 0.149454, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 18.510168 + }, + "memory": { + "start": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135111, + "majorFaults": 2 + }, + "peak": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501215232, + "virtualBytes": 8567255040, + "minorFaults": 135144, + "majorFaults": 2 + }, + "end": { + "rssBytes": 497291264, + "peakRssBytes": 516812800, + "pssBytes": 501141504, + "virtualBytes": 4202799104, + "minorFaults": 135144, + "majorFaults": 2 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17315449, + "wasmtimeProcessRetainedRssBytes": 497291264, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 3, + "vmSetupMs": 442.7994820000022, + "fixtureSetupMs": 390.23293999998714, + "baseline": { + "rssBytes": 241262592, + "peakRssBytes": 248086528, + "pssBytes": 242476032, + "virtualBytes": 3885953024, + "minorFaults": 56009, + "majorFaults": 1 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365985792, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 381493248, + "peakRssBytes": 488992768, + "pssBytes": 383474688, + "virtualBytes": 4052439040, + "minorFaults": 1525935, + "majorFaults": 1 + }, + "retainedDelta": { + "rssBytes": 140230656, + "peakRssBytes": 240906240, + "pssBytes": 140998656, + "virtualBytes": 166486016, + "minorFaults": 1469926, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 63.93654700000479, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.082087 + }, + { + "name": "WebAssembly.Module", + "ms": 0.168349 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.064948 + }, + { + "name": "wasi.start", + "ms": 0.096139 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 243359744, + "peakRssBytes": 248086528, + "pssBytes": 244573184, + "virtualBytes": 3888066560, + "minorFaults": 56010, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 278970368, + "peakRssBytes": 279552000, + "pssBytes": 265839616, + "virtualBytes": 4640280576, + "minorFaults": 66925, + "majorFaults": 1 + }, + "end": { + "rssBytes": 264605696, + "peakRssBytes": 279552000, + "pssBytes": 265839616, + "virtualBytes": 3955175424, + "minorFaults": 66925, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 243359744, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264605696, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 68.28622599999653, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.111866 + }, + { + "name": "WebAssembly.Module", + "ms": 0.152124 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.06209 + }, + { + "name": "wasi.start", + "ms": 0.085753 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 264605696, + "peakRssBytes": 279552000, + "pssBytes": 265839616, + "virtualBytes": 3955175424, + "minorFaults": 66925, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 279367680, + "peakRssBytes": 279666688, + "pssBytes": 268144640, + "virtualBytes": 4640280576, + "minorFaults": 73213, + "majorFaults": 1 + }, + "end": { + "rssBytes": 264720384, + "peakRssBytes": 279666688, + "pssBytes": 266003456, + "virtualBytes": 3955175424, + "minorFaults": 73213, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264605696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264720384, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 64.05036599999585, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.074839 + }, + { + "name": "WebAssembly.Module", + "ms": 0.142729 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.056829 + }, + { + "name": "wasi.start", + "ms": 0.086139 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 264720384, + "peakRssBytes": 279666688, + "pssBytes": 266003456, + "virtualBytes": 3955175424, + "minorFaults": 73213, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 279433216, + "peakRssBytes": 279666688, + "pssBytes": 270471168, + "virtualBytes": 4640423936, + "minorFaults": 79465, + "majorFaults": 1 + }, + "end": { + "rssBytes": 264695808, + "peakRssBytes": 279666688, + "pssBytes": 266019840, + "virtualBytes": 3955175424, + "minorFaults": 79465, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264720384, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264695808, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 63.10325200000079, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.029744 + }, + { + "name": "WebAssembly.Module", + "ms": 0.160996 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.059573 + }, + { + "name": "wasi.start", + "ms": 0.087562 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 264695808, + "peakRssBytes": 279666688, + "pssBytes": 266019840, + "virtualBytes": 3955175424, + "minorFaults": 79465, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 279441408, + "peakRssBytes": 279666688, + "pssBytes": 280912896, + "virtualBytes": 4640804864, + "minorFaults": 85736, + "majorFaults": 1 + }, + "end": { + "rssBytes": 264708096, + "peakRssBytes": 279666688, + "pssBytes": 266109952, + "virtualBytes": 3955175424, + "minorFaults": 85736, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264695808, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264708096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 69.24974199999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.516143 + }, + { + "name": "WebAssembly.Module", + "ms": 0.185313 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.089012 + }, + { + "name": "wasi.start", + "ms": 0.123926 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 264708096, + "peakRssBytes": 279666688, + "pssBytes": 266109952, + "virtualBytes": 3955175424, + "minorFaults": 85736, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 279465984, + "peakRssBytes": 279695360, + "pssBytes": 280970240, + "virtualBytes": 4640280576, + "minorFaults": 92004, + "majorFaults": 1 + }, + "end": { + "rssBytes": 264749056, + "peakRssBytes": 279695360, + "pssBytes": 266191872, + "virtualBytes": 3955175424, + "minorFaults": 92004, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264708096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264749056, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 208.54451100000006, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.167489 + }, + { + "name": "WebAssembly.Module", + "ms": 2.30157 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.332358 + }, + { + "name": "wasi.start", + "ms": 85.966572 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 264749056, + "peakRssBytes": 279695360, + "pssBytes": 266191872, + "virtualBytes": 3955175424, + "minorFaults": 92004, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 332595200, + "peakRssBytes": 333946880, + "pssBytes": 332141568, + "virtualBytes": 4693815296, + "minorFaults": 111304, + "majorFaults": 1 + }, + "end": { + "rssBytes": 283336704, + "peakRssBytes": 333946880, + "pssBytes": 209593344, + "virtualBytes": 3955175424, + "minorFaults": 111304, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 264749056, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283607040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 213.48089999999502, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 19.81717 + }, + { + "name": "WebAssembly.Module", + "ms": 1.811546 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.315931 + }, + { + "name": "wasi.start", + "ms": 96.040926 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 283607040, + "peakRssBytes": 333946880, + "pssBytes": 214656000, + "virtualBytes": 3955175424, + "minorFaults": 111334, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 344039424, + "peakRssBytes": 344039424, + "pssBytes": 341902336, + "virtualBytes": 4694482944, + "minorFaults": 128043, + "majorFaults": 1 + }, + "end": { + "rssBytes": 293150720, + "peakRssBytes": 344039424, + "pssBytes": 294937600, + "virtualBytes": 3955175424, + "minorFaults": 128043, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 283607040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293150720, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 249.67719000000216, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.126813 + }, + { + "name": "WebAssembly.Module", + "ms": 1.194966 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.862488 + }, + { + "name": "wasi.start", + "ms": 90.725027 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293150720, + "peakRssBytes": 344039424, + "pssBytes": 294937600, + "virtualBytes": 3955175424, + "minorFaults": 128043, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 348160000, + "peakRssBytes": 348160000, + "pssBytes": 349778944, + "virtualBytes": 4696059904, + "minorFaults": 143961, + "majorFaults": 1 + }, + "end": { + "rssBytes": 293699584, + "peakRssBytes": 348160000, + "pssBytes": 295203840, + "virtualBytes": 3957276672, + "minorFaults": 143961, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293150720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293699584, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 208.82356099999743, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.599969 + }, + { + "name": "WebAssembly.Module", + "ms": 2.543309 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.290033 + }, + { + "name": "wasi.start", + "ms": 85.40591 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 293699584, + "peakRssBytes": 348160000, + "pssBytes": 295203840, + "virtualBytes": 3957276672, + "minorFaults": 143961, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 353906688, + "peakRssBytes": 354988032, + "pssBytes": 351667200, + "virtualBytes": 4696440832, + "minorFaults": 162654, + "majorFaults": 1 + }, + "end": { + "rssBytes": 303349760, + "peakRssBytes": 354988032, + "pssBytes": 304735232, + "virtualBytes": 3957276672, + "minorFaults": 162654, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 293699584, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303349760, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 233.65275599999586, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.640074 + }, + { + "name": "WebAssembly.Module", + "ms": 3.6428 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.627161 + }, + { + "name": "wasi.start", + "ms": 92.019888 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 303349760, + "peakRssBytes": 354988032, + "pssBytes": 304735232, + "virtualBytes": 3957276672, + "minorFaults": 162654, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 358289408, + "peakRssBytes": 358289408, + "pssBytes": 359281664, + "virtualBytes": 4695797760, + "minorFaults": 178039, + "majorFaults": 1 + }, + "end": { + "rssBytes": 303759360, + "peakRssBytes": 358289408, + "pssBytes": 304911360, + "virtualBytes": 3957276672, + "minorFaults": 178039, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303349760, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303759360, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 379.7415260000125, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 58.473368 + }, + { + "name": "WebAssembly.Module", + "ms": 4.387874 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.499919 + }, + { + "name": "wasi.start", + "ms": 164.811038 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 303759360, + "peakRssBytes": 358289408, + "pssBytes": 304911360, + "virtualBytes": 3957276672, + "minorFaults": 178039, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 419467264, + "peakRssBytes": 419491840, + "pssBytes": 420770816, + "virtualBytes": 5472579584, + "minorFaults": 230784, + "majorFaults": 1 + }, + "end": { + "rssBytes": 328044544, + "peakRssBytes": 419491840, + "pssBytes": 330671104, + "virtualBytes": 4030857216, + "minorFaults": 230784, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 303759360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328044544, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 340.479164999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 75.128957 + }, + { + "name": "WebAssembly.Module", + "ms": 4.357444 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.5742 + }, + { + "name": "wasi.start", + "ms": 113.830483 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 328044544, + "peakRssBytes": 419491840, + "pssBytes": 330671104, + "virtualBytes": 4030857216, + "minorFaults": 230784, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 429797376, + "peakRssBytes": 429846528, + "pssBytes": 432235520, + "virtualBytes": 5472960512, + "minorFaults": 281734, + "majorFaults": 1 + }, + "end": { + "rssBytes": 336982016, + "peakRssBytes": 429846528, + "pssBytes": 339526656, + "virtualBytes": 4030857216, + "minorFaults": 281734, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 328044544, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336982016, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 406.6546110000054, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 65.718842 + }, + { + "name": "WebAssembly.Module", + "ms": 5.061278 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.448658 + }, + { + "name": "wasi.start", + "ms": 152.837691 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 336982016, + "peakRssBytes": 429846528, + "pssBytes": 339526656, + "virtualBytes": 4030857216, + "minorFaults": 281734, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 433119232, + "peakRssBytes": 433225728, + "pssBytes": 435590144, + "virtualBytes": 5472841728, + "minorFaults": 333502, + "majorFaults": 1 + }, + "end": { + "rssBytes": 345288704, + "peakRssBytes": 433225728, + "pssBytes": 347796480, + "virtualBytes": 4030857216, + "minorFaults": 333502, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 336982016, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345288704, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 414.3087210000085, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 84.41778 + }, + { + "name": "WebAssembly.Module", + "ms": 4.432591 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.675263 + }, + { + "name": "wasi.start", + "ms": 125.021951 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 345288704, + "peakRssBytes": 433225728, + "pssBytes": 347796480, + "virtualBytes": 4030857216, + "minorFaults": 333502, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 438054912, + "peakRssBytes": 438059008, + "pssBytes": 440377344, + "virtualBytes": 5472698368, + "minorFaults": 385841, + "majorFaults": 1 + }, + "end": { + "rssBytes": 345178112, + "peakRssBytes": 438059008, + "pssBytes": 347897856, + "virtualBytes": 4030857216, + "minorFaults": 385841, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345288704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345178112, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 463.72539099999995, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 92.505468 + }, + { + "name": "WebAssembly.Module", + "ms": 8.375322 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.499287 + }, + { + "name": "wasi.start", + "ms": 168.928814 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 345178112, + "peakRssBytes": 438059008, + "pssBytes": 347897856, + "virtualBytes": 4030857216, + "minorFaults": 385841, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 438169600, + "peakRssBytes": 438169600, + "pssBytes": 440811520, + "virtualBytes": 5472698368, + "minorFaults": 440720, + "majorFaults": 1 + }, + "end": { + "rssBytes": 345169920, + "peakRssBytes": 438169600, + "pssBytes": 347918336, + "virtualBytes": 4030857216, + "minorFaults": 440720, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345178112, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345169920, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 235.7850829999952, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 42.945041 + }, + { + "name": "WebAssembly.Module", + "ms": 2.493846 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.89966 + }, + { + "name": "wasi.start", + "ms": 36.464897 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 345169920, + "peakRssBytes": 438169600, + "pssBytes": 347918336, + "virtualBytes": 4030857216, + "minorFaults": 440720, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 415141888, + "peakRssBytes": 438169600, + "pssBytes": 417070080, + "virtualBytes": 4788531200, + "minorFaults": 470930, + "majorFaults": 1 + }, + "end": { + "rssBytes": 346583040, + "peakRssBytes": 438169600, + "pssBytes": 348384256, + "virtualBytes": 4030857216, + "minorFaults": 470930, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 345169920, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346583040, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 238.36613399999624, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 54.799057 + }, + { + "name": "WebAssembly.Module", + "ms": 3.730833 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.464826 + }, + { + "name": "wasi.start", + "ms": 27.839736 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346583040, + "peakRssBytes": 438169600, + "pssBytes": 348384256, + "virtualBytes": 4030857216, + "minorFaults": 470930, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 415174656, + "peakRssBytes": 438169600, + "pssBytes": 416919552, + "virtualBytes": 4788674560, + "minorFaults": 499086, + "majorFaults": 1 + }, + "end": { + "rssBytes": 346484736, + "peakRssBytes": 438169600, + "pssBytes": 348389376, + "virtualBytes": 4030857216, + "minorFaults": 499086, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346583040, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346484736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 211.1024860000034, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 42.384387 + }, + { + "name": "WebAssembly.Module", + "ms": 1.648943 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.359241 + }, + { + "name": "wasi.start", + "ms": 27.302616 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346484736, + "peakRssBytes": 438169600, + "pssBytes": 348389376, + "virtualBytes": 4030857216, + "minorFaults": 499086, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 414932992, + "peakRssBytes": 438169600, + "pssBytes": 416997376, + "virtualBytes": 4788793344, + "minorFaults": 529779, + "majorFaults": 1 + }, + "end": { + "rssBytes": 346341376, + "peakRssBytes": 438169600, + "pssBytes": 348397568, + "virtualBytes": 4030857216, + "minorFaults": 529779, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346484736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346341376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 249.26199199999974, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.34925 + }, + { + "name": "WebAssembly.Module", + "ms": 2.389541 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.012721 + }, + { + "name": "wasi.start", + "ms": 28.097549 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346341376, + "peakRssBytes": 438169600, + "pssBytes": 348397568, + "virtualBytes": 4030857216, + "minorFaults": 529779, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 415207424, + "peakRssBytes": 438169600, + "pssBytes": 417083392, + "virtualBytes": 4788531200, + "minorFaults": 556915, + "majorFaults": 1 + }, + "end": { + "rssBytes": 346529792, + "peakRssBytes": 438169600, + "pssBytes": 348397568, + "virtualBytes": 4030857216, + "minorFaults": 556915, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346341376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346529792, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 203.98934899999585, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.605366 + }, + { + "name": "WebAssembly.Module", + "ms": 2.153528 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.960527 + }, + { + "name": "wasi.start", + "ms": 25.286142 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346529792, + "peakRssBytes": 438169600, + "pssBytes": 348397568, + "virtualBytes": 4030857216, + "minorFaults": 556915, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 415096832, + "peakRssBytes": 438169600, + "pssBytes": 417087488, + "virtualBytes": 4788674560, + "minorFaults": 580987, + "majorFaults": 1 + }, + "end": { + "rssBytes": 346464256, + "peakRssBytes": 438169600, + "pssBytes": 348401664, + "virtualBytes": 4030857216, + "minorFaults": 580987, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346529792, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346464256, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 202.70167000000947, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 37.798894 + }, + { + "name": "WebAssembly.Module", + "ms": 4.619023 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.631806 + }, + { + "name": "wasi.start", + "ms": 24.784198 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346464256, + "peakRssBytes": 438169600, + "pssBytes": 348401664, + "virtualBytes": 4030857216, + "minorFaults": 580987, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 386801664, + "peakRssBytes": 438169600, + "pssBytes": 387063808, + "virtualBytes": 4758228992, + "minorFaults": 598891, + "majorFaults": 1 + }, + "end": { + "rssBytes": 360890368, + "peakRssBytes": 438169600, + "pssBytes": 300947456, + "virtualBytes": 4030857216, + "minorFaults": 598891, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346464256, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361701376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 160.90202999999747, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.591297 + }, + { + "name": "WebAssembly.Module", + "ms": 1.197625 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.611167 + }, + { + "name": "wasi.start", + "ms": 16.588831 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 361971712, + "peakRssBytes": 438169600, + "pssBytes": 254393344, + "virtualBytes": 4030857216, + "minorFaults": 599145, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 413614080, + "peakRssBytes": 438169600, + "pssBytes": 399929344, + "virtualBytes": 4758228992, + "minorFaults": 623166, + "majorFaults": 1 + }, + "end": { + "rssBytes": 372867072, + "peakRssBytes": 438169600, + "pssBytes": 375754752, + "virtualBytes": 4030857216, + "minorFaults": 623166, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 361701376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 373948416, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 173.33684799999173, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.514563 + }, + { + "name": "WebAssembly.Module", + "ms": 1.270449 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.026134 + }, + { + "name": "wasi.start", + "ms": 17.044604 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 373948416, + "peakRssBytes": 438169600, + "pssBytes": 376999936, + "virtualBytes": 4030857216, + "minorFaults": 623472, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 425308160, + "peakRssBytes": 438169600, + "pssBytes": 419110912, + "virtualBytes": 4757848064, + "minorFaults": 644950, + "majorFaults": 1 + }, + "end": { + "rssBytes": 364298240, + "peakRssBytes": 438169600, + "pssBytes": 285948928, + "virtualBytes": 4030857216, + "minorFaults": 644950, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 373948416, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364298240, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 188.8375970000052, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 21.009264 + }, + { + "name": "WebAssembly.Module", + "ms": 1.700078 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.392494 + }, + { + "name": "wasi.start", + "ms": 21.247377 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364568576, + "peakRssBytes": 438169600, + "pssBytes": 274607104, + "virtualBytes": 4030857216, + "minorFaults": 644979, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 417234944, + "peakRssBytes": 438169600, + "pssBytes": 409776128, + "virtualBytes": 4757966848, + "minorFaults": 671429, + "majorFaults": 1 + }, + "end": { + "rssBytes": 370765824, + "peakRssBytes": 438169600, + "pssBytes": 373362688, + "virtualBytes": 4030857216, + "minorFaults": 671429, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364568576, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371847168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 193.44301899999846, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.807821 + }, + { + "name": "WebAssembly.Module", + "ms": 2.387532 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.174179 + }, + { + "name": "wasi.start", + "ms": 22.34376 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 371847168, + "peakRssBytes": 438169600, + "pssBytes": 373362688, + "virtualBytes": 4030857216, + "minorFaults": 671683, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 403611648, + "peakRssBytes": 438169600, + "pssBytes": 398270464, + "virtualBytes": 4757966848, + "minorFaults": 694690, + "majorFaults": 1 + }, + "end": { + "rssBytes": 369770496, + "peakRssBytes": 438169600, + "pssBytes": 371863552, + "virtualBytes": 4030857216, + "minorFaults": 694690, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371847168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371392512, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 288.03180700000667, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.293493 + }, + { + "name": "WebAssembly.Module", + "ms": 4.989825 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.340566 + }, + { + "name": "wasi.start", + "ms": 42.390923 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 371392512, + "peakRssBytes": 438169600, + "pssBytes": 372899840, + "virtualBytes": 4030857216, + "minorFaults": 695036, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 446414848, + "peakRssBytes": 446500864, + "pssBytes": 448208896, + "virtualBytes": 4821553152, + "minorFaults": 740610, + "majorFaults": 1 + }, + "end": { + "rssBytes": 352542720, + "peakRssBytes": 446500864, + "pssBytes": 354435072, + "virtualBytes": 4030857216, + "minorFaults": 740610, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 371392512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352542720, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 317.8043820000021, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 98.717921 + }, + { + "name": "WebAssembly.Module", + "ms": 4.440559 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.451703 + }, + { + "name": "wasi.start", + "ms": 40.761435 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352542720, + "peakRssBytes": 446500864, + "pssBytes": 354435072, + "virtualBytes": 4030857216, + "minorFaults": 740610, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 445595648, + "peakRssBytes": 446500864, + "pssBytes": 447307776, + "virtualBytes": 4769513472, + "minorFaults": 784733, + "majorFaults": 1 + }, + "end": { + "rssBytes": 352821248, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 784733, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352542720, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352821248, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 342.88677300000563, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 79.714002 + }, + { + "name": "WebAssembly.Module", + "ms": 7.274228 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.776619 + }, + { + "name": "wasi.start", + "ms": 47.174983 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352821248, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 784733, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 443305984, + "peakRssBytes": 446500864, + "pssBytes": 440737792, + "virtualBytes": 4769513472, + "minorFaults": 829907, + "majorFaults": 1 + }, + "end": { + "rssBytes": 352698368, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 829907, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352821248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352698368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 419.7276520000014, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 117.071159 + }, + { + "name": "WebAssembly.Module", + "ms": 5.254198 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.126009 + }, + { + "name": "wasi.start", + "ms": 52.97654 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352698368, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 829907, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 443453440, + "peakRssBytes": 446500864, + "pssBytes": 445317120, + "virtualBytes": 4769656832, + "minorFaults": 875073, + "majorFaults": 1 + }, + "end": { + "rssBytes": 352612352, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 875073, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352698368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352612352, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 350.7389799999946, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 94.622344 + }, + { + "name": "WebAssembly.Module", + "ms": 4.04913 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.611141 + }, + { + "name": "wasi.start", + "ms": 59.039204 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 352612352, + "peakRssBytes": 446500864, + "pssBytes": 354500608, + "virtualBytes": 4030857216, + "minorFaults": 875073, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 444493824, + "peakRssBytes": 446500864, + "pssBytes": 446427136, + "virtualBytes": 4822077440, + "minorFaults": 920806, + "majorFaults": 1 + }, + "end": { + "rssBytes": 354922496, + "peakRssBytes": 446500864, + "pssBytes": 356876288, + "virtualBytes": 4030857216, + "minorFaults": 920806, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 352612352, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 354922496, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 392.511943000005, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 131.206282 + }, + { + "name": "WebAssembly.Module", + "ms": 3.932797 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.24887 + }, + { + "name": "wasi.start", + "ms": 5.130644 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 354922496, + "peakRssBytes": 446500864, + "pssBytes": 356876288, + "virtualBytes": 4030857216, + "minorFaults": 920806, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 486813696, + "peakRssBytes": 486858752, + "pssBytes": 488718336, + "virtualBytes": 4812021760, + "minorFaults": 985397, + "majorFaults": 1 + }, + "end": { + "rssBytes": 357011456, + "peakRssBytes": 486858752, + "pssBytes": 358989824, + "virtualBytes": 4031840256, + "minorFaults": 985397, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 354922496, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357011456, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 348.67124599999806, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 101.774096 + }, + { + "name": "WebAssembly.Module", + "ms": 6.618884 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.3992 + }, + { + "name": "wasi.start", + "ms": 4.752704 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 357011456, + "peakRssBytes": 486858752, + "pssBytes": 358989824, + "virtualBytes": 4031840256, + "minorFaults": 985397, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 487612416, + "peakRssBytes": 487645184, + "pssBytes": 489738240, + "virtualBytes": 4786446336, + "minorFaults": 1049667, + "majorFaults": 1 + }, + "end": { + "rssBytes": 357638144, + "peakRssBytes": 487645184, + "pssBytes": 359788544, + "virtualBytes": 4032851968, + "minorFaults": 1049667, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357011456, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357638144, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 430.0022839999874, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 124.915144 + }, + { + "name": "WebAssembly.Module", + "ms": 7.933498 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.568821 + }, + { + "name": "wasi.start", + "ms": 6.325324 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 357638144, + "peakRssBytes": 487645184, + "pssBytes": 359788544, + "virtualBytes": 4032851968, + "minorFaults": 1049667, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 488280064, + "peakRssBytes": 488538112, + "pssBytes": 490680320, + "virtualBytes": 4813033472, + "minorFaults": 1113971, + "majorFaults": 1 + }, + "end": { + "rssBytes": 358793216, + "peakRssBytes": 488538112, + "pssBytes": 360734720, + "virtualBytes": 4032851968, + "minorFaults": 1113971, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 357638144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358793216, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 368.47767400000885, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 107.009535 + }, + { + "name": "WebAssembly.Module", + "ms": 6.54034 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.00278 + }, + { + "name": "wasi.start", + "ms": 6.555585 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 358793216, + "peakRssBytes": 488538112, + "pssBytes": 360734720, + "virtualBytes": 4032851968, + "minorFaults": 1113971, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 488853504, + "peakRssBytes": 488873984, + "pssBytes": 490696704, + "virtualBytes": 4813033472, + "minorFaults": 1178046, + "majorFaults": 1 + }, + "end": { + "rssBytes": 358858752, + "peakRssBytes": 488873984, + "pssBytes": 360747008, + "virtualBytes": 4032851968, + "minorFaults": 1178046, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358793216, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358858752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 352.2000979999866, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 109.945857 + }, + { + "name": "WebAssembly.Module", + "ms": 4.528608 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.736694 + }, + { + "name": "wasi.start", + "ms": 5.245867 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 358858752, + "peakRssBytes": 488873984, + "pssBytes": 360747008, + "virtualBytes": 4032851968, + "minorFaults": 1178046, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 488984576, + "peakRssBytes": 488992768, + "pssBytes": 490749952, + "virtualBytes": 4813033472, + "minorFaults": 1242144, + "majorFaults": 1 + }, + "end": { + "rssBytes": 358965248, + "peakRssBytes": 488992768, + "pssBytes": 360841216, + "virtualBytes": 4032851968, + "minorFaults": 1242144, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358858752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358965248, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 327.0719900000113, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 43.15004 + }, + { + "name": "WebAssembly.Module", + "ms": 1.337914 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.869385 + }, + { + "name": "wasi.start", + "ms": 112.065492 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 358965248, + "peakRssBytes": 488992768, + "pssBytes": 360841216, + "virtualBytes": 4032851968, + "minorFaults": 1242144, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 414400512, + "peakRssBytes": 488992768, + "pssBytes": 410529792, + "virtualBytes": 4770410496, + "minorFaults": 1267548, + "majorFaults": 1 + }, + "end": { + "rssBytes": 364736512, + "peakRssBytes": 488992768, + "pssBytes": 366702592, + "virtualBytes": 4033085440, + "minorFaults": 1267548, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 358965248, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364736512, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 246.03889399999753, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.864187 + }, + { + "name": "WebAssembly.Module", + "ms": 3.591615 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.04445 + }, + { + "name": "wasi.start", + "ms": 87.809272 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364736512, + "peakRssBytes": 488992768, + "pssBytes": 366702592, + "virtualBytes": 4033085440, + "minorFaults": 1267548, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 416813056, + "peakRssBytes": 488992768, + "pssBytes": 418770944, + "virtualBytes": 4770553856, + "minorFaults": 1291489, + "majorFaults": 1 + }, + "end": { + "rssBytes": 364818432, + "peakRssBytes": 488992768, + "pssBytes": 366747648, + "virtualBytes": 4033085440, + "minorFaults": 1291489, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364736512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364818432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 273.4999119999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 44.098663 + }, + { + "name": "WebAssembly.Module", + "ms": 2.150481 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.821671 + }, + { + "name": "wasi.start", + "ms": 89.877226 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 364818432, + "peakRssBytes": 488992768, + "pssBytes": 366747648, + "virtualBytes": 4033085440, + "minorFaults": 1291489, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 418263040, + "peakRssBytes": 488992768, + "pssBytes": 419852288, + "virtualBytes": 4770291712, + "minorFaults": 1315683, + "majorFaults": 1 + }, + "end": { + "rssBytes": 366047232, + "peakRssBytes": 488992768, + "pssBytes": 367833088, + "virtualBytes": 4033085440, + "minorFaults": 1315683, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 364818432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366047232, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 274.19154400000116, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 44.689528 + }, + { + "name": "WebAssembly.Module", + "ms": 2.842428 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.024125 + }, + { + "name": "wasi.start", + "ms": 95.707279 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 366047232, + "peakRssBytes": 488992768, + "pssBytes": 367833088, + "virtualBytes": 4033085440, + "minorFaults": 1315683, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 416075776, + "peakRssBytes": 488992768, + "pssBytes": 417910784, + "virtualBytes": 4770553856, + "minorFaults": 1339650, + "majorFaults": 1 + }, + "end": { + "rssBytes": 366043136, + "peakRssBytes": 488992768, + "pssBytes": 367833088, + "virtualBytes": 4033085440, + "minorFaults": 1339650, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366047232, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366043136, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 246.93171900000016, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 36.836302 + }, + { + "name": "WebAssembly.Module", + "ms": 1.842249 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.451719 + }, + { + "name": "wasi.start", + "ms": 90.053451 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 366043136, + "peakRssBytes": 488992768, + "pssBytes": 367833088, + "virtualBytes": 4033085440, + "minorFaults": 1339650, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 417652736, + "peakRssBytes": 488992768, + "pssBytes": 419942400, + "virtualBytes": 4770148352, + "minorFaults": 1363075, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365625344, + "peakRssBytes": 488992768, + "pssBytes": 367849472, + "virtualBytes": 4033085440, + "minorFaults": 1363075, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366043136, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365625344, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 262.3676640000049, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 70.329113 + }, + { + "name": "WebAssembly.Module", + "ms": 4.728008 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.380794 + }, + { + "name": "wasi.start", + "ms": 18.348511 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365625344, + "peakRssBytes": 488992768, + "pssBytes": 367849472, + "virtualBytes": 4033085440, + "minorFaults": 1363075, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432238592, + "peakRssBytes": 488992768, + "pssBytes": 434458624, + "virtualBytes": 4791128064, + "minorFaults": 1395205, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365613056, + "peakRssBytes": 488992768, + "pssBytes": 367853568, + "virtualBytes": 4034592768, + "minorFaults": 1395205, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365625344, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365613056, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 254.54721099999733, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.470011 + }, + { + "name": "WebAssembly.Module", + "ms": 3.029672 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.09835 + }, + { + "name": "wasi.start", + "ms": 19.851435 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365613056, + "peakRssBytes": 488992768, + "pssBytes": 367853568, + "virtualBytes": 4034592768, + "minorFaults": 1395205, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 432283648, + "peakRssBytes": 488992768, + "pssBytes": 433987584, + "virtualBytes": 4791250944, + "minorFaults": 1427381, + "majorFaults": 1 + }, + "end": { + "rssBytes": 366284800, + "peakRssBytes": 488992768, + "pssBytes": 368012288, + "virtualBytes": 4034859008, + "minorFaults": 1427381, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365613056, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366284800, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 283.2830680000043, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.469275 + }, + { + "name": "WebAssembly.Module", + "ms": 1.747844 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.411804 + }, + { + "name": "wasi.start", + "ms": 24.125541 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 366284800, + "peakRssBytes": 488992768, + "pssBytes": 368012288, + "virtualBytes": 4034859008, + "minorFaults": 1427381, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 433786880, + "peakRssBytes": 488992768, + "pssBytes": 435957760, + "virtualBytes": 4791513088, + "minorFaults": 1458971, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365842432, + "peakRssBytes": 488992768, + "pssBytes": 368017408, + "virtualBytes": 4034859008, + "minorFaults": 1458971, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366284800, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365842432, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 298.5255610000022, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.969836 + }, + { + "name": "WebAssembly.Module", + "ms": 1.50565 + }, + { + "name": "WebAssembly.Instance", + "ms": 4.403695 + }, + { + "name": "wasi.start", + "ms": 16.008784 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 365842432, + "peakRssBytes": 488992768, + "pssBytes": 368017408, + "virtualBytes": 4034859008, + "minorFaults": 1458971, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 434106368, + "peakRssBytes": 488992768, + "pssBytes": 436026368, + "virtualBytes": 4791394304, + "minorFaults": 1491070, + "majorFaults": 1 + }, + "end": { + "rssBytes": 366145536, + "peakRssBytes": 488992768, + "pssBytes": 368016384, + "virtualBytes": 4034859008, + "minorFaults": 1491070, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365842432, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366145536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 241.581130999999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624928552967860/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 53.397612 + }, + { + "name": "WebAssembly.Module", + "ms": 1.953165 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.265116 + }, + { + "name": "wasi.start", + "ms": 19.363363 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 366145536, + "peakRssBytes": 488992768, + "pssBytes": 368016384, + "virtualBytes": 4034859008, + "minorFaults": 1491070, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 433905664, + "peakRssBytes": 488992768, + "pssBytes": 436031488, + "virtualBytes": 4791394304, + "minorFaults": 1522147, + "majorFaults": 1 + }, + "end": { + "rssBytes": 365985792, + "peakRssBytes": 488992768, + "pssBytes": 368017408, + "virtualBytes": 4034859008, + "minorFaults": 1522147, + "majorFaults": 1 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366145536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 365985792, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 3, + "vmSetupMs": 511.8188130000053, + "fixtureSetupMs": 446.1168550000002, + "baseline": { + "rssBytes": 241053696, + "peakRssBytes": 246595584, + "pssBytes": 241942528, + "virtualBytes": 3885961216, + "minorFaults": 59344, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 489668608, + "peakRssBytes": 525893632, + "pssBytes": 491354112, + "virtualBytes": 4195422208, + "minorFaults": 150804, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 248614912, + "peakRssBytes": 279298048, + "pssBytes": 249411584, + "virtualBytes": 309460992, + "minorFaults": 91460, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 80.14452299999539, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.114417, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.100646, + "name": "Engine" + }, + { + "ms": 0.224799, + "name": "canonicalPreopens" + }, + { + "ms": 3.3960879999999998, + "name": "moduleRead" + }, + { + "ms": 0.256409, + "name": "profileValidation" + }, + { + "ms": 59.802218999999994, + "name": "moduleCompile" + }, + { + "ms": 0.0030819999999999997, + "name": "importValidation" + }, + { + "ms": 0.185078, + "name": "Linker" + }, + { + "ms": 0.023774999999999998, + "name": "Store" + }, + { + "ms": 0.043803, + "name": "Instance" + }, + { + "ms": 0.07109599999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.001854, + "name": "entrypointLookup" + }, + { + "ms": 0.023665000000000002, + "name": "wasi.start" + }, + { + "ms": 0.018856, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 64.222812 + }, + "memory": { + "start": { + "rssBytes": 241053696, + "peakRssBytes": 246595584, + "pssBytes": 241950720, + "virtualBytes": 3888074752, + "minorFaults": 59346, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60494, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60494, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 241053696, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 26.54120699998748, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010428999999999999, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.0018440000000000002, + "name": "Engine" + }, + { + "ms": 0.150981, + "name": "canonicalPreopens" + }, + { + "ms": 3.2333309999999997, + "name": "moduleRead" + }, + { + "ms": 0.32181499999999996, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.006027, + "name": "importValidation" + }, + { + "ms": 0.29261400000000004, + "name": "Linker" + }, + { + "ms": 0.020373, + "name": "Store" + }, + { + "ms": 0.054438, + "name": "Instance" + }, + { + "ms": 0.37479, + "name": "signalMaskInit" + }, + { + "ms": 0.005273, + "name": "entrypointLookup" + }, + { + "ms": 0.051274, + "name": "wasi.start" + }, + { + "ms": 0.031224, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 4.6312310000000005 + }, + "memory": { + "start": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60494, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60516, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60516, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 21.19786000000022, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.014603000000000001, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001399, + "name": "Engine" + }, + { + "ms": 0.13233799999999998, + "name": "canonicalPreopens" + }, + { + "ms": 3.388483, + "name": "moduleRead" + }, + { + "ms": 0.26219200000000004, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003195, + "name": "importValidation" + }, + { + "ms": 0.203742, + "name": "Linker" + }, + { + "ms": 0.014994, + "name": "Store" + }, + { + "ms": 0.033831, + "name": "Instance" + }, + { + "ms": 0.614323, + "name": "signalMaskInit" + }, + { + "ms": 0.008749999999999999, + "name": "entrypointLookup" + }, + { + "ms": 0.904656, + "name": "wasi.start" + }, + { + "ms": 0.023208, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.681469 + }, + "memory": { + "start": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60516, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 8319561728, + "minorFaults": 60538, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60538, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 21.1714079999947, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.013075999999999999, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001256, + "name": "Engine" + }, + { + "ms": 0.116664, + "name": "canonicalPreopens" + }, + { + "ms": 3.240311, + "name": "moduleRead" + }, + { + "ms": 0.20932, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0035540000000000003, + "name": "importValidation" + }, + { + "ms": 0.194774, + "name": "Linker" + }, + { + "ms": 0.013555, + "name": "Store" + }, + { + "ms": 0.030586999999999996, + "name": "Instance" + }, + { + "ms": 0.604426, + "name": "signalMaskInit" + }, + { + "ms": 0.003578, + "name": "entrypointLookup" + }, + { + "ms": 0.577323, + "name": "wasi.start" + }, + { + "ms": 0.022881, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.099113 + }, + "memory": { + "start": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60538, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 8319561728, + "minorFaults": 60560, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60560, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 21.434760999996797, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010767, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001336, + "name": "Engine" + }, + { + "ms": 0.168664, + "name": "canonicalPreopens" + }, + { + "ms": 3.41638, + "name": "moduleRead" + }, + { + "ms": 0.23070600000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00365, + "name": "importValidation" + }, + { + "ms": 0.187686, + "name": "Linker" + }, + { + "ms": 0.016163, + "name": "Store" + }, + { + "ms": 0.518592, + "name": "Instance" + }, + { + "ms": 0.119762, + "name": "signalMaskInit" + }, + { + "ms": 0.002441, + "name": "entrypointLookup" + }, + { + "ms": 0.024184999999999998, + "name": "wasi.start" + }, + { + "ms": 0.018584999999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 4.775456 + }, + "memory": { + "start": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60560, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957485568, + "minorFaults": 60582, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60582, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1230.034216999993, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1153.084441, + "firstHostCallMs": 0.011647000000000001, + "firstOutputMs": 1211.111977, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.00208, + "name": "Engine" + }, + { + "ms": 0.114428, + "name": "canonicalPreopens" + }, + { + "ms": 6.035881, + "name": "moduleRead" + }, + { + "ms": 3.846052, + "name": "profileValidation" + }, + { + "ms": 1137.3716689999999, + "name": "moduleCompile" + }, + { + "ms": 0.014606999999999998, + "name": "importValidation" + }, + { + "ms": 0.32271999999999995, + "name": "Linker" + }, + { + "ms": 0.035773, + "name": "Store" + }, + { + "ms": 0.385497, + "name": "Instance" + }, + { + "ms": 0.152792, + "name": "signalMaskInit" + }, + { + "ms": 0.009054, + "name": "entrypointLookup" + }, + { + "ms": 62.38619, + "name": "wasi.start" + }, + { + "ms": 1.003538, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 1212.323782 + }, + "memory": { + "start": { + "rssBytes": 251801600, + "peakRssBytes": 252006400, + "pssBytes": 252704768, + "virtualBytes": 3957473280, + "minorFaults": 60582, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292724736, + "peakRssBytes": 292868096, + "pssBytes": 293850112, + "virtualBytes": 8327376896, + "minorFaults": 69450, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69450, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 59802, + "wasmtimeProcessRetainedRssBytes": 251801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 110.25597800000105, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.678459, + "firstHostCallMs": 0.012456, + "firstOutputMs": 86.510869, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.00143, + "name": "Engine" + }, + { + "ms": 0.11792499999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.87735, + "name": "moduleRead" + }, + { + "ms": 5.555122, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008142, + "name": "importValidation" + }, + { + "ms": 0.210225, + "name": "Linker" + }, + { + "ms": 0.019478, + "name": "Store" + }, + { + "ms": 1.052379, + "name": "Instance" + }, + { + "ms": 0.080441, + "name": "signalMaskInit" + }, + { + "ms": 0.0032069999999999998, + "name": "entrypointLookup" + }, + { + "ms": 72.115557, + "name": "wasi.start" + }, + { + "ms": 1.3159210000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 87.992919 + }, + "memory": { + "start": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69450, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292720640, + "peakRssBytes": 292868096, + "pssBytes": 293792768, + "virtualBytes": 8327376896, + "minorFaults": 69524, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293156864, + "virtualBytes": 3962920960, + "minorFaults": 69524, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 96.30972800000745, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.840657, + "firstHostCallMs": 0.030546000000000004, + "firstOutputMs": 76.693411, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.003086, + "name": "Engine" + }, + { + "ms": 0.244021, + "name": "canonicalPreopens" + }, + { + "ms": 7.925438, + "name": "moduleRead" + }, + { + "ms": 4.131721, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008003, + "name": "importValidation" + }, + { + "ms": 0.213121, + "name": "Linker" + }, + { + "ms": 0.017772, + "name": "Store" + }, + { + "ms": 1.443896, + "name": "Instance" + }, + { + "ms": 0.064584, + "name": "signalMaskInit" + }, + { + "ms": 0.005391, + "name": "entrypointLookup" + }, + { + "ms": 62.121024, + "name": "wasi.start" + }, + { + "ms": 0.044656, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 76.853773 + }, + "memory": { + "start": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293156864, + "virtualBytes": 3962920960, + "minorFaults": 69524, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292720640, + "peakRssBytes": 292868096, + "pssBytes": 293791744, + "virtualBytes": 8327376896, + "minorFaults": 69598, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293156864, + "virtualBytes": 3962920960, + "minorFaults": 69598, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 80.6787239999976, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.888628, + "firstHostCallMs": 0.011264, + "firstOutputMs": 63.362612999999996, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0010780000000000002, + "name": "Engine" + }, + { + "ms": 0.11311099999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.712355, + "name": "moduleRead" + }, + { + "ms": 3.9544920000000006, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008198, + "name": "importValidation" + }, + { + "ms": 0.20312300000000003, + "name": "Linker" + }, + { + "ms": 0.018304, + "name": "Store" + }, + { + "ms": 0.044248, + "name": "Instance" + }, + { + "ms": 0.059095999999999996, + "name": "signalMaskInit" + }, + { + "ms": 0.003467, + "name": "entrypointLookup" + }, + { + "ms": 51.745616, + "name": "wasi.start" + }, + { + "ms": 0.35718099999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 63.833896 + }, + "memory": { + "start": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293156864, + "virtualBytes": 3962920960, + "minorFaults": 69598, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292720640, + "peakRssBytes": 292868096, + "pssBytes": 293792768, + "virtualBytes": 8327376896, + "minorFaults": 69672, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69672, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 80.58259499999986, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.620916, + "firstHostCallMs": 0.016558999999999997, + "firstOutputMs": 62.083152000000005, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001535, + "name": "Engine" + }, + { + "ms": 0.12537299999999998, + "name": "canonicalPreopens" + }, + { + "ms": 6.700894, + "name": "moduleRead" + }, + { + "ms": 4.260906, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014195000000000001, + "name": "importValidation" + }, + { + "ms": 0.255984, + "name": "Linker" + }, + { + "ms": 0.032901, + "name": "Store" + }, + { + "ms": 0.2329, + "name": "Instance" + }, + { + "ms": 0.106477, + "name": "signalMaskInit" + }, + { + "ms": 0.005741, + "name": "entrypointLookup" + }, + { + "ms": 49.799307, + "name": "wasi.start" + }, + { + "ms": 0.042973, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 62.236913 + }, + "memory": { + "start": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69672, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 292720640, + "peakRssBytes": 292868096, + "pssBytes": 293792768, + "virtualBytes": 8327376896, + "minorFaults": 69746, + "majorFaults": 0 + }, + "end": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69746, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 4073.150912000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3610.8696179999997, + "firstHostCallMs": 0.009736, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0010240000000000002, + "name": "Engine" + }, + { + "ms": 0.106834, + "name": "canonicalPreopens" + }, + { + "ms": 11.485090999999999, + "name": "moduleRead" + }, + { + "ms": 12.575014000000001, + "name": "profileValidation" + }, + { + "ms": 3584.703055, + "name": "moduleCompile" + }, + { + "ms": 0.009385000000000001, + "name": "importValidation" + }, + { + "ms": 0.187226, + "name": "Linker" + }, + { + "ms": 0.019494, + "name": "Store" + }, + { + "ms": 0.244003, + "name": "Instance" + }, + { + "ms": 0.110622, + "name": "signalMaskInit" + }, + { + "ms": 0.0039759999999999995, + "name": "entrypointLookup" + }, + { + "ms": 431.176483, + "name": "wasi.start" + }, + { + "ms": 2.194446, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 4044.4355029999997 + }, + "memory": { + "start": { + "rssBytes": 292122624, + "peakRssBytes": 292868096, + "pssBytes": 293157888, + "virtualBytes": 3962920960, + "minorFaults": 69746, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 423129088, + "peakRssBytes": 423129088, + "pssBytes": 425462784, + "virtualBytes": 12866453504, + "minorFaults": 108148, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422868992, + "virtualBytes": 4137541632, + "minorFaults": 108148, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1197173, + "wasmtimeProcessRetainedRssBytes": 292122624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 102.4411689999979, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 28.007263000000002, + "firstHostCallMs": 0.033582, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0020959999999999998, + "name": "Engine" + }, + { + "ms": 0.136435, + "name": "canonicalPreopens" + }, + { + "ms": 11.998875000000002, + "name": "moduleRead" + }, + { + "ms": 13.019181, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011589, + "name": "importValidation" + }, + { + "ms": 0.21704600000000002, + "name": "Linker" + }, + { + "ms": 0.023521999999999998, + "name": "Store" + }, + { + "ms": 0.906876, + "name": "Instance" + }, + { + "ms": 0.12589999999999998, + "name": "signalMaskInit" + }, + { + "ms": 0.013144, + "name": "entrypointLookup" + }, + { + "ms": 47.460401, + "name": "wasi.start" + }, + { + "ms": 0.054819, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 75.498146 + }, + "memory": { + "start": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422868992, + "virtualBytes": 4137541632, + "minorFaults": 108148, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422809600, + "peakRssBytes": 423129088, + "pssBytes": 425801728, + "virtualBytes": 12866453504, + "minorFaults": 108236, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422868992, + "virtualBytes": 4137541632, + "minorFaults": 108236, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 87.9605040000024, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.166988, + "firstHostCallMs": 0.022399, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001984, + "name": "Engine" + }, + { + "ms": 0.205591, + "name": "canonicalPreopens" + }, + { + "ms": 12.466167, + "name": "moduleRead" + }, + { + "ms": 13.940674999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010818000000000001, + "name": "importValidation" + }, + { + "ms": 0.20197400000000001, + "name": "Linker" + }, + { + "ms": 0.019517999999999997, + "name": "Store" + }, + { + "ms": 0.621498, + "name": "Instance" + }, + { + "ms": 0.081835, + "name": "signalMaskInit" + }, + { + "ms": 0.004358, + "name": "entrypointLookup" + }, + { + "ms": 36.492404, + "name": "wasi.start" + }, + { + "ms": 2.129624, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 67.68128499999999 + }, + "memory": { + "start": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422868992, + "virtualBytes": 4137541632, + "minorFaults": 108236, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422809600, + "peakRssBytes": 423129088, + "pssBytes": 425802752, + "virtualBytes": 12866453504, + "minorFaults": 108324, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108324, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 130.83952299998782, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 26.95156, + "firstHostCallMs": 0.015691, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001406, + "name": "Engine" + }, + { + "ms": 0.143358, + "name": "canonicalPreopens" + }, + { + "ms": 12.215797, + "name": "moduleRead" + }, + { + "ms": 12.752256, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010647, + "name": "importValidation" + }, + { + "ms": 0.20698, + "name": "Linker" + }, + { + "ms": 0.019134, + "name": "Store" + }, + { + "ms": 0.053728, + "name": "Instance" + }, + { + "ms": 0.08722099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004053, + "name": "entrypointLookup" + }, + { + "ms": 31.840225, + "name": "wasi.start" + }, + { + "ms": 2.043708, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 60.907323 + }, + "memory": { + "start": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108324, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 423713792, + "virtualBytes": 12866453504, + "minorFaults": 108413, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108413, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 86.63976200000616, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 26.09699, + "firstHostCallMs": 0.017876, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001655, + "name": "Engine" + }, + { + "ms": 0.16934, + "name": "canonicalPreopens" + }, + { + "ms": 11.249581999999998, + "name": "moduleRead" + }, + { + "ms": 12.74176, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013383, + "name": "importValidation" + }, + { + "ms": 0.28226100000000004, + "name": "Linker" + }, + { + "ms": 0.020199, + "name": "Store" + }, + { + "ms": 0.047312, + "name": "Instance" + }, + { + "ms": 0.100858, + "name": "signalMaskInit" + }, + { + "ms": 0.012681, + "name": "entrypointLookup" + }, + { + "ms": 37.078768999999994, + "name": "wasi.start" + }, + { + "ms": 0.034992, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 63.205406 + }, + "memory": { + "start": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108413, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 423717888, + "virtualBytes": 12866453504, + "minorFaults": 108502, + "majorFaults": 0 + }, + "end": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108502, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1729.2114819999988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1699.5222700000002, + "firstHostCallMs": 0.022118000000000002, + "firstOutputMs": 1706.8051, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001863, + "name": "Engine" + }, + { + "ms": 0.15847999999999998, + "name": "canonicalPreopens" + }, + { + "ms": 7.085402, + "name": "moduleRead" + }, + { + "ms": 6.4349430000000005, + "name": "profileValidation" + }, + { + "ms": 1682.879379, + "name": "moduleCompile" + }, + { + "ms": 0.012424, + "name": "importValidation" + }, + { + "ms": 0.198471, + "name": "Linker" + }, + { + "ms": 0.018834, + "name": "Store" + }, + { + "ms": 1.7705609999999998, + "name": "Instance" + }, + { + "ms": 0.120055, + "name": "signalMaskInit" + }, + { + "ms": 0.004528, + "name": "entrypointLookup" + }, + { + "ms": 7.535394, + "name": "wasi.start" + }, + { + "ms": 0.061896999999999994, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 1707.113915 + }, + "memory": { + "start": { + "rssBytes": 420712448, + "peakRssBytes": 423129088, + "pssBytes": 422870016, + "virtualBytes": 4137541632, + "minorFaults": 108502, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428453888, + "peakRssBytes": 428761088, + "pssBytes": 431078400, + "virtualBytes": 8509394944, + "minorFaults": 109393, + "majorFaults": 0 + }, + "end": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430885888, + "virtualBytes": 4144939008, + "minorFaults": 109393, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 5173865, + "wasmtimeProcessRetainedRssBytes": 420712448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 44.091539999993984, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.363022, + "firstHostCallMs": 0.046891999999999996, + "firstOutputMs": 22.273979, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001307, + "name": "Engine" + }, + { + "ms": 0.11575500000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.997008, + "name": "moduleRead" + }, + { + "ms": 6.3916509999999995, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014032000000000001, + "name": "importValidation" + }, + { + "ms": 0.209881, + "name": "Linker" + }, + { + "ms": 0.017641, + "name": "Store" + }, + { + "ms": 0.7363489999999999, + "name": "Instance" + }, + { + "ms": 0.074632, + "name": "signalMaskInit" + }, + { + "ms": 0.0042899999999999995, + "name": "entrypointLookup" + }, + { + "ms": 7.083444, + "name": "wasi.start" + }, + { + "ms": 0.038238999999999995, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 22.497456999999997 + }, + "memory": { + "start": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430885888, + "virtualBytes": 4144939008, + "minorFaults": 109393, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430959616, + "virtualBytes": 8509394944, + "minorFaults": 109443, + "majorFaults": 0 + }, + "end": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430889984, + "virtualBytes": 4144939008, + "minorFaults": 109443, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 48.26303900001221, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.520602, + "firstHostCallMs": 0.016519, + "firstOutputMs": 25.801607, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.002703, + "name": "Engine" + }, + { + "ms": 0.140554, + "name": "canonicalPreopens" + }, + { + "ms": 6.840407, + "name": "moduleRead" + }, + { + "ms": 6.327711, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.020202, + "name": "importValidation" + }, + { + "ms": 0.24032799999999999, + "name": "Linker" + }, + { + "ms": 0.028539, + "name": "Store" + }, + { + "ms": 3.008167, + "name": "Instance" + }, + { + "ms": 0.083338, + "name": "signalMaskInit" + }, + { + "ms": 0.007961999999999999, + "name": "entrypointLookup" + }, + { + "ms": 8.48007, + "name": "wasi.start" + }, + { + "ms": 0.035168, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 26.020864 + }, + "memory": { + "start": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430889984, + "virtualBytes": 4144939008, + "minorFaults": 109443, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 431393792, + "virtualBytes": 8509394944, + "minorFaults": 109499, + "majorFaults": 0 + }, + "end": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430918656, + "virtualBytes": 4144939008, + "minorFaults": 109499, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 45.980358000000706, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.648470000000001, + "firstHostCallMs": 0.015471000000000002, + "firstOutputMs": 23.634654, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.00138, + "name": "Engine" + }, + { + "ms": 0.15782400000000002, + "name": "canonicalPreopens" + }, + { + "ms": 6.975238, + "name": "moduleRead" + }, + { + "ms": 6.491085, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012781, + "name": "importValidation" + }, + { + "ms": 0.202419, + "name": "Linker" + }, + { + "ms": 0.019643, + "name": "Store" + }, + { + "ms": 0.967249, + "name": "Instance" + }, + { + "ms": 0.051628, + "name": "signalMaskInit" + }, + { + "ms": 0.00401, + "name": "entrypointLookup" + }, + { + "ms": 8.308302999999999, + "name": "wasi.start" + }, + { + "ms": 0.050776999999999996, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.04906 + }, + "memory": { + "start": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430918656, + "virtualBytes": 4144939008, + "minorFaults": 109499, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430992384, + "virtualBytes": 8509394944, + "minorFaults": 109552, + "majorFaults": 0 + }, + "end": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430935040, + "virtualBytes": 4144939008, + "minorFaults": 109552, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 48.44803100000718, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.881671, + "firstHostCallMs": 0.016944, + "firstOutputMs": 21.8135, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.000933, + "name": "Engine" + }, + { + "ms": 0.15971400000000002, + "name": "canonicalPreopens" + }, + { + "ms": 6.935837, + "name": "moduleRead" + }, + { + "ms": 6.244045, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014323, + "name": "importValidation" + }, + { + "ms": 0.206758, + "name": "Linker" + }, + { + "ms": 0.024436000000000003, + "name": "Store" + }, + { + "ms": 1.439051, + "name": "Instance" + }, + { + "ms": 0.073885, + "name": "signalMaskInit" + }, + { + "ms": 0.006127, + "name": "entrypointLookup" + }, + { + "ms": 6.11587, + "name": "wasi.start" + }, + { + "ms": 0.036079, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 22.035943 + }, + "memory": { + "start": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430935040, + "virtualBytes": 4144939008, + "minorFaults": 109552, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 431008768, + "virtualBytes": 8509394944, + "minorFaults": 109601, + "majorFaults": 0 + }, + "end": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430934016, + "virtualBytes": 4144939008, + "minorFaults": 109601, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1326.3762969999952, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1302.9435760000001, + "firstHostCallMs": 0.015336999999999998, + "firstOutputMs": 1305.32417, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001051, + "name": "Engine" + }, + { + "ms": 0.111391, + "name": "canonicalPreopens" + }, + { + "ms": 5.308495, + "name": "moduleRead" + }, + { + "ms": 5.087998, + "name": "profileValidation" + }, + { + "ms": 1291.623106, + "name": "moduleCompile" + }, + { + "ms": 0.009725, + "name": "importValidation" + }, + { + "ms": 0.19391, + "name": "Linker" + }, + { + "ms": 0.017994, + "name": "Store" + }, + { + "ms": 0.077724, + "name": "Instance" + }, + { + "ms": 0.06776, + "name": "signalMaskInit" + }, + { + "ms": 0.002828, + "name": "entrypointLookup" + }, + { + "ms": 2.4533240000000003, + "name": "wasi.start" + }, + { + "ms": 0.035809, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 1305.475238 + }, + "memory": { + "start": { + "rssBytes": 428109824, + "peakRssBytes": 428761088, + "pssBytes": 430934016, + "virtualBytes": 4144939008, + "minorFaults": 109601, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150419456, + "minorFaults": 111016, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111016, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6856744, + "wasmtimeProcessRetainedRssBytes": 428109824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 35.50483499999973, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.647158, + "firstHostCallMs": 0.015027, + "firstOutputMs": 13.365882000000001, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001044, + "name": "Engine" + }, + { + "ms": 0.10652199999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.476173, + "name": "moduleRead" + }, + { + "ms": 4.816171, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010212, + "name": "importValidation" + }, + { + "ms": 0.204017, + "name": "Linker" + }, + { + "ms": 0.018168999999999998, + "name": "Store" + }, + { + "ms": 0.510922, + "name": "Instance" + }, + { + "ms": 0.056308, + "name": "signalMaskInit" + }, + { + "ms": 0.003603, + "name": "entrypointLookup" + }, + { + "ms": 1.781436, + "name": "wasi.start" + }, + { + "ms": 0.028323, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.486188 + }, + "memory": { + "start": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111016, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436476928, + "virtualBytes": 4150419456, + "minorFaults": 111066, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111066, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 39.186470000000554, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.8623, + "firstHostCallMs": 0.015097, + "firstOutputMs": 13.55264, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001161, + "name": "Engine" + }, + { + "ms": 0.10324799999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.352240999999999, + "name": "moduleRead" + }, + { + "ms": 5.293578, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009771, + "name": "importValidation" + }, + { + "ms": 0.200741, + "name": "Linker" + }, + { + "ms": 0.01852, + "name": "Store" + }, + { + "ms": 0.355185, + "name": "Instance" + }, + { + "ms": 0.079507, + "name": "signalMaskInit" + }, + { + "ms": 0.003316, + "name": "entrypointLookup" + }, + { + "ms": 1.7562229999999999, + "name": "wasi.start" + }, + { + "ms": 0.033867, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.678324 + }, + "memory": { + "start": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111066, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436476928, + "virtualBytes": 4150419456, + "minorFaults": 111116, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111116, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 35.806005000005825, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.387621000000001, + "firstHostCallMs": 0.017530999999999998, + "firstOutputMs": 13.776232, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0014759999999999999, + "name": "Engine" + }, + { + "ms": 0.105862, + "name": "canonicalPreopens" + }, + { + "ms": 5.31611, + "name": "moduleRead" + }, + { + "ms": 4.910696, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00991, + "name": "importValidation" + }, + { + "ms": 0.20282699999999998, + "name": "Linker" + }, + { + "ms": 0.019017000000000003, + "name": "Store" + }, + { + "ms": 0.296831, + "name": "Instance" + }, + { + "ms": 0.076771, + "name": "signalMaskInit" + }, + { + "ms": 0.003109, + "name": "entrypointLookup" + }, + { + "ms": 2.4609829999999997, + "name": "wasi.start" + }, + { + "ms": 0.029833000000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.90083 + }, + "memory": { + "start": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436403200, + "virtualBytes": 4150407168, + "minorFaults": 111116, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436476928, + "virtualBytes": 4150419456, + "minorFaults": 111166, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436402176, + "virtualBytes": 4150407168, + "minorFaults": 111166, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 34.031088000003365, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.477895, + "firstHostCallMs": 0.030879, + "firstOutputMs": 13.224632999999999, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.00283, + "name": "Engine" + }, + { + "ms": 0.13603500000000002, + "name": "canonicalPreopens" + }, + { + "ms": 5.39694, + "name": "moduleRead" + }, + { + "ms": 4.844913999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012388999999999999, + "name": "importValidation" + }, + { + "ms": 0.222527, + "name": "Linker" + }, + { + "ms": 0.022283999999999998, + "name": "Store" + }, + { + "ms": 0.30362500000000003, + "name": "Instance" + }, + { + "ms": 0.06064, + "name": "signalMaskInit" + }, + { + "ms": 0.005089, + "name": "entrypointLookup" + }, + { + "ms": 1.845851, + "name": "wasi.start" + }, + { + "ms": 1.28836, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 14.673983999999999 + }, + "memory": { + "start": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436402176, + "virtualBytes": 4150407168, + "minorFaults": 111166, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436471808, + "virtualBytes": 8514863104, + "minorFaults": 111216, + "majorFaults": 0 + }, + "end": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436402176, + "virtualBytes": 4150407168, + "minorFaults": 111216, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3910.991177999982, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3870.270231, + "firstHostCallMs": 0.018230999999999997, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.00184, + "name": "Engine" + }, + { + "ms": 0.162514, + "name": "canonicalPreopens" + }, + { + "ms": 12.222368000000001, + "name": "moduleRead" + }, + { + "ms": 15.377072, + "name": "profileValidation" + }, + { + "ms": 3840.667725, + "name": "moduleCompile" + }, + { + "ms": 0.012504, + "name": "importValidation" + }, + { + "ms": 0.187705, + "name": "Linker" + }, + { + "ms": 0.018335, + "name": "Store" + }, + { + "ms": 0.183923, + "name": "Instance" + }, + { + "ms": 0.060987, + "name": "signalMaskInit" + }, + { + "ms": 0.004067, + "name": "entrypointLookup" + }, + { + "ms": 9.785381, + "name": "wasi.start" + }, + { + "ms": 0.145678, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 3880.36301 + }, + "memory": { + "start": { + "rssBytes": 433577984, + "peakRssBytes": 433983488, + "pssBytes": 436402176, + "virtualBytes": 4150407168, + "minorFaults": 111216, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450641920, + "peakRssBytes": 450740224, + "pssBytes": 453495808, + "virtualBytes": 8531279872, + "minorFaults": 115347, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452819968, + "virtualBytes": 4166823936, + "minorFaults": 115347, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 8148367, + "wasmtimeProcessRetainedRssBytes": 433577984, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 69.24411100000725, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.066955999999998, + "firstHostCallMs": 0.120682, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.003117, + "name": "Engine" + }, + { + "ms": 0.172179, + "name": "canonicalPreopens" + }, + { + "ms": 12.565483, + "name": "moduleRead" + }, + { + "ms": 14.878432, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.020543, + "name": "importValidation" + }, + { + "ms": 0.27041, + "name": "Linker" + }, + { + "ms": 0.021428, + "name": "Store" + }, + { + "ms": 0.5319740000000001, + "name": "Instance" + }, + { + "ms": 0.083409, + "name": "signalMaskInit" + }, + { + "ms": 0.007958000000000002, + "name": "entrypointLookup" + }, + { + "ms": 13.905251999999999, + "name": "wasi.start" + }, + { + "ms": 0.6520849999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 44.648591 + }, + "memory": { + "start": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452819968, + "virtualBytes": 4166823936, + "minorFaults": 115347, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450547712, + "peakRssBytes": 450740224, + "pssBytes": 453462016, + "virtualBytes": 8531279872, + "minorFaults": 115445, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452884480, + "virtualBytes": 4166823936, + "minorFaults": 115445, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 81.89611599998898, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.95121, + "firstHostCallMs": 0.048865, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.002518, + "name": "Engine" + }, + { + "ms": 0.140143, + "name": "canonicalPreopens" + }, + { + "ms": 13.93106, + "name": "moduleRead" + }, + { + "ms": 15.294436999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013425, + "name": "importValidation" + }, + { + "ms": 0.227963, + "name": "Linker" + }, + { + "ms": 0.019876, + "name": "Store" + }, + { + "ms": 0.7343850000000001, + "name": "Instance" + }, + { + "ms": 0.071246, + "name": "signalMaskInit" + }, + { + "ms": 0.007231, + "name": "entrypointLookup" + }, + { + "ms": 24.695963000000003, + "name": "wasi.start" + }, + { + "ms": 0.06268199999999999, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 56.701744 + }, + "memory": { + "start": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452884480, + "virtualBytes": 4166823936, + "minorFaults": 115445, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450547712, + "peakRssBytes": 450740224, + "pssBytes": 453544960, + "virtualBytes": 8531279872, + "minorFaults": 115542, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115542, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 76.65312899998389, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.956183, + "firstHostCallMs": 0.040401, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.003112, + "name": "Engine" + }, + { + "ms": 0.203437, + "name": "canonicalPreopens" + }, + { + "ms": 12.303955, + "name": "moduleRead" + }, + { + "ms": 15.126003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015559000000000002, + "name": "importValidation" + }, + { + "ms": 0.236044, + "name": "Linker" + }, + { + "ms": 0.020788, + "name": "Store" + }, + { + "ms": 0.575401, + "name": "Instance" + }, + { + "ms": 0.053947999999999996, + "name": "signalMaskInit" + }, + { + "ms": 0.004693, + "name": "entrypointLookup" + }, + { + "ms": 19.762525, + "name": "wasi.start" + }, + { + "ms": 0.50261, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 50.252779999999994 + }, + "memory": { + "start": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115542, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450547712, + "peakRssBytes": 450740224, + "pssBytes": 453524480, + "virtualBytes": 8531279872, + "minorFaults": 115639, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115639, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 73.99589200000628, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.385177, + "firstHostCallMs": 0.033562, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001932, + "name": "Engine" + }, + { + "ms": 0.181078, + "name": "canonicalPreopens" + }, + { + "ms": 12.812894, + "name": "moduleRead" + }, + { + "ms": 15.634749999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.01302, + "name": "importValidation" + }, + { + "ms": 0.223591, + "name": "Linker" + }, + { + "ms": 0.017720999999999997, + "name": "Store" + }, + { + "ms": 0.043915, + "name": "Instance" + }, + { + "ms": 0.076668, + "name": "signalMaskInit" + }, + { + "ms": 0.003376, + "name": "entrypointLookup" + }, + { + "ms": 18.874912000000002, + "name": "wasi.start" + }, + { + "ms": 0.154392, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 49.405615 + }, + "memory": { + "start": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115639, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 450547712, + "peakRssBytes": 450740224, + "pssBytes": 453524480, + "virtualBytes": 8531279872, + "minorFaults": 115736, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115736, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 4264.67938799999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 4231.704501, + "firstHostCallMs": 0.020010999999999998, + "firstOutputMs": 4234.5655830000005, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001374, + "name": "Engine" + }, + { + "ms": 0.11891399999999999, + "name": "canonicalPreopens" + }, + { + "ms": 13.364583, + "name": "moduleRead" + }, + { + "ms": 19.355767999999998, + "name": "profileValidation" + }, + { + "ms": 4196.714782, + "name": "moduleCompile" + }, + { + "ms": 0.018273, + "name": "importValidation" + }, + { + "ms": 0.182166, + "name": "Linker" + }, + { + "ms": 0.017937, + "name": "Store" + }, + { + "ms": 0.257832, + "name": "Instance" + }, + { + "ms": 0.07455099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.005443, + "name": "entrypointLookup" + }, + { + "ms": 3.039866, + "name": "wasi.start" + }, + { + "ms": 1.276211, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 4236.058327000001 + }, + "memory": { + "start": { + "rssBytes": 449994752, + "peakRssBytes": 450740224, + "pssBytes": 452885504, + "virtualBytes": 4166823936, + "minorFaults": 115736, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 472010752, + "peakRssBytes": 472285184, + "pssBytes": 474441728, + "virtualBytes": 8549875712, + "minorFaults": 120567, + "majorFaults": 0 + }, + "end": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474441728, + "virtualBytes": 4185419776, + "minorFaults": 120567, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11989035, + "wasmtimeProcessRetainedRssBytes": 449994752, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 65.98721700001624, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 34.076364, + "firstHostCallMs": 0.148366, + "firstOutputMs": 37.021339, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.002853, + "name": "Engine" + }, + { + "ms": 0.24683399999999997, + "name": "canonicalPreopens" + }, + { + "ms": 13.837564, + "name": "moduleRead" + }, + { + "ms": 17.788979, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.018978000000000002, + "name": "importValidation" + }, + { + "ms": 0.222168, + "name": "Linker" + }, + { + "ms": 0.019205999999999997, + "name": "Store" + }, + { + "ms": 0.13300399999999998, + "name": "Instance" + }, + { + "ms": 0.064122, + "name": "signalMaskInit" + }, + { + "ms": 0.003395, + "name": "entrypointLookup" + }, + { + "ms": 3.1532560000000003, + "name": "wasi.start" + }, + { + "ms": 1.2770240000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 38.523885 + }, + "memory": { + "start": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474441728, + "virtualBytes": 4185419776, + "minorFaults": 120567, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474515456, + "virtualBytes": 8549875712, + "minorFaults": 120611, + "majorFaults": 0 + }, + "end": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474441728, + "virtualBytes": 4185419776, + "minorFaults": 120611, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 74.57065000000875, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 37.197520999999995, + "firstHostCallMs": 0.020453, + "firstOutputMs": 40.107633, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001812, + "name": "Engine" + }, + { + "ms": 0.132404, + "name": "canonicalPreopens" + }, + { + "ms": 16.431533, + "name": "moduleRead" + }, + { + "ms": 16.166949, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.030091, + "name": "importValidation" + }, + { + "ms": 0.327732, + "name": "Linker" + }, + { + "ms": 0.027562, + "name": "Store" + }, + { + "ms": 0.7881980000000001, + "name": "Instance" + }, + { + "ms": 0.071561, + "name": "signalMaskInit" + }, + { + "ms": 0.0066159999999999995, + "name": "entrypointLookup" + }, + { + "ms": 4.622120000000001, + "name": "wasi.start" + }, + { + "ms": 0.053431, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 40.381031 + }, + "memory": { + "start": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474441728, + "virtualBytes": 4185419776, + "minorFaults": 120611, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474515456, + "virtualBytes": 8547508224, + "minorFaults": 120655, + "majorFaults": 0 + }, + "end": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120655, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 177.99036299998988, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 33.323009, + "firstHostCallMs": 0.032777, + "firstOutputMs": 35.261807, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0012619999999999999, + "name": "Engine" + }, + { + "ms": 0.130315, + "name": "canonicalPreopens" + }, + { + "ms": 13.071306, + "name": "moduleRead" + }, + { + "ms": 16.028019, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.020462, + "name": "importValidation" + }, + { + "ms": 0.24563100000000002, + "name": "Linker" + }, + { + "ms": 0.01816, + "name": "Store" + }, + { + "ms": 0.05549, + "name": "Instance" + }, + { + "ms": 0.06403099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004659, + "name": "entrypointLookup" + }, + { + "ms": 4.212952, + "name": "wasi.start" + }, + { + "ms": 0.034454, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 35.508424 + }, + "memory": { + "start": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120655, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474516480, + "virtualBytes": 8549875712, + "minorFaults": 120699, + "majorFaults": 0 + }, + "end": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120699, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 72.11763399999472, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 35.179559999999995, + "firstHostCallMs": 0.055651, + "firstOutputMs": 38.251216, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.0024779999999999997, + "name": "Engine" + }, + { + "ms": 0.149222, + "name": "canonicalPreopens" + }, + { + "ms": 13.44299, + "name": "moduleRead" + }, + { + "ms": 16.423446000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.017828, + "name": "importValidation" + }, + { + "ms": 0.217434, + "name": "Linker" + }, + { + "ms": 0.017119, + "name": "Store" + }, + { + "ms": 3.02157, + "name": "Instance" + }, + { + "ms": 0.137827, + "name": "signalMaskInit" + }, + { + "ms": 0.015316, + "name": "entrypointLookup" + }, + { + "ms": 3.520136, + "name": "wasi.start" + }, + { + "ms": 4.359789, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 43.031939 + }, + "memory": { + "start": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120699, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474516480, + "virtualBytes": 8549875712, + "minorFaults": 120743, + "majorFaults": 0 + }, + "end": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120743, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 767.2871039999882, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 686.4591770000001, + "firstHostCallMs": 0.022467, + "firstOutputMs": 738.8851960000001, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0012929999999999999, + "name": "Engine" + }, + { + "ms": 0.161732, + "name": "canonicalPreopens" + }, + { + "ms": 5.917732, + "name": "moduleRead" + }, + { + "ms": 2.4518940000000002, + "name": "profileValidation" + }, + { + "ms": 672.544364, + "name": "moduleCompile" + }, + { + "ms": 0.012094, + "name": "importValidation" + }, + { + "ms": 0.415778, + "name": "Linker" + }, + { + "ms": 0.032496000000000004, + "name": "Store" + }, + { + "ms": 3.785508, + "name": "Instance" + }, + { + "ms": 0.085195, + "name": "signalMaskInit" + }, + { + "ms": 0.042707999999999996, + "name": "entrypointLookup" + }, + { + "ms": 52.875916000000004, + "name": "wasi.start" + }, + { + "ms": 1.181892, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 740.253512 + }, + "memory": { + "start": { + "rssBytes": 471552000, + "peakRssBytes": 472285184, + "pssBytes": 474442752, + "virtualBytes": 4185419776, + "minorFaults": 120743, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475602944, + "peakRssBytes": 475934720, + "pssBytes": 478832640, + "virtualBytes": 8553738240, + "minorFaults": 121766, + "majorFaults": 0 + }, + "end": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121766, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 16185750, + "wasmtimeProcessRetainedRssBytes": 471552000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 95.92340500000864, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.066811, + "firstHostCallMs": 0.025783, + "firstOutputMs": 68.372212, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001821, + "name": "Engine" + }, + { + "ms": 0.15606499999999998, + "name": "canonicalPreopens" + }, + { + "ms": 6.48838, + "name": "moduleRead" + }, + { + "ms": 3.339168, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011013, + "name": "importValidation" + }, + { + "ms": 0.232077, + "name": "Linker" + }, + { + "ms": 0.022431, + "name": "Store" + }, + { + "ms": 1.77232, + "name": "Instance" + }, + { + "ms": 0.103543, + "name": "signalMaskInit" + }, + { + "ms": 0.011989, + "name": "entrypointLookup" + }, + { + "ms": 55.670341, + "name": "wasi.start" + }, + { + "ms": 1.834366, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 70.428532 + }, + "memory": { + "start": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121766, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478710784, + "virtualBytes": 8553738240, + "minorFaults": 121816, + "majorFaults": 0 + }, + "end": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121816, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 83.42430500002229, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.365658, + "firstHostCallMs": 0.028918, + "firstOutputMs": 61.236388000000005, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001427, + "name": "Engine" + }, + { + "ms": 0.12307299999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.880413, + "name": "moduleRead" + }, + { + "ms": 3.8968230000000004, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015549, + "name": "importValidation" + }, + { + "ms": 0.32499500000000003, + "name": "Linker" + }, + { + "ms": 0.034052, + "name": "Store" + }, + { + "ms": 0.072815, + "name": "Instance" + }, + { + "ms": 0.09385399999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.008903, + "name": "entrypointLookup" + }, + { + "ms": 49.229226000000004, + "name": "wasi.start" + }, + { + "ms": 0.12813200000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 61.586244 + }, + "memory": { + "start": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121816, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478710784, + "virtualBytes": 8553738240, + "minorFaults": 121866, + "majorFaults": 0 + }, + "end": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121866, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 77.1635660000029, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.090403, + "firstHostCallMs": 0.022863, + "firstOutputMs": 56.175134, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001302, + "name": "Engine" + }, + { + "ms": 0.12137300000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.778465, + "name": "moduleRead" + }, + { + "ms": 2.555359, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008087, + "name": "importValidation" + }, + { + "ms": 0.240443, + "name": "Linker" + }, + { + "ms": 0.03294, + "name": "Store" + }, + { + "ms": 1.2731190000000001, + "name": "Instance" + }, + { + "ms": 0.099495, + "name": "signalMaskInit" + }, + { + "ms": 0.045774, + "name": "entrypointLookup" + }, + { + "ms": 44.459885, + "name": "wasi.start" + }, + { + "ms": 0.70241, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 57.025000999999996 + }, + "memory": { + "start": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121866, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478710784, + "virtualBytes": 8553738240, + "minorFaults": 121916, + "majorFaults": 0 + }, + "end": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121916, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 109.70068199999514, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.89033, + "firstHostCallMs": 0.042577000000000004, + "firstOutputMs": 74.176463, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.0033629999999999997, + "name": "Engine" + }, + { + "ms": 0.237503, + "name": "canonicalPreopens" + }, + { + "ms": 7.333552999999999, + "name": "moduleRead" + }, + { + "ms": 2.758519, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.01171, + "name": "importValidation" + }, + { + "ms": 0.23107999999999998, + "name": "Linker" + }, + { + "ms": 0.031687, + "name": "Store" + }, + { + "ms": 3.084334, + "name": "Instance" + }, + { + "ms": 0.12611799999999998, + "name": "signalMaskInit" + }, + { + "ms": 0.014061, + "name": "entrypointLookup" + }, + { + "ms": 59.806913, + "name": "wasi.start" + }, + { + "ms": 2.355787, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 76.803399 + }, + "memory": { + "start": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478305280, + "virtualBytes": 4189282304, + "minorFaults": 121916, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478710784, + "virtualBytes": 8553738240, + "minorFaults": 121966, + "majorFaults": 0 + }, + "end": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478304256, + "virtualBytes": 4189282304, + "minorFaults": 121966, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1616.9195569999865, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1578.5038519999998, + "firstHostCallMs": 0.021667000000000002, + "firstOutputMs": 1583.9969310000001, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.00164, + "name": "Engine" + }, + { + "ms": 0.23328100000000002, + "name": "canonicalPreopens" + }, + { + "ms": 7.046975, + "name": "moduleRead" + }, + { + "ms": 7.138476000000001, + "name": "profileValidation" + }, + { + "ms": 1562.7618519999999, + "name": "moduleCompile" + }, + { + "ms": 0.008551, + "name": "importValidation" + }, + { + "ms": 0.20036, + "name": "Linker" + }, + { + "ms": 0.020224000000000002, + "name": "Store" + }, + { + "ms": 0.271397, + "name": "Instance" + }, + { + "ms": 0.071804, + "name": "signalMaskInit" + }, + { + "ms": 0.004132, + "name": "entrypointLookup" + }, + { + "ms": 7.541634, + "name": "wasi.start" + }, + { + "ms": 0.707927, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 1586.871661 + }, + "memory": { + "start": { + "rssBytes": 475414528, + "peakRssBytes": 475934720, + "pssBytes": 478304256, + "virtualBytes": 4189282304, + "minorFaults": 121966, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 525893632, + "peakRssBytes": 525893632, + "pssBytes": 528907264, + "virtualBytes": 8560058368, + "minorFaults": 150670, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491404288, + "virtualBytes": 4195602432, + "minorFaults": 150670, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 16858294, + "wasmtimeProcessRetainedRssBytes": 475414528, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 366.3740299999772, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.276401, + "firstHostCallMs": 0.033839, + "firstOutputMs": 21.057645, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.002372, + "name": "Engine" + }, + { + "ms": 0.269639, + "name": "canonicalPreopens" + }, + { + "ms": 7.461609, + "name": "moduleRead" + }, + { + "ms": 4.95532, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014022, + "name": "importValidation" + }, + { + "ms": 0.24454, + "name": "Linker" + }, + { + "ms": 0.023456, + "name": "Store" + }, + { + "ms": 0.048841999999999997, + "name": "Instance" + }, + { + "ms": 0.08655600000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.004777, + "name": "entrypointLookup" + }, + { + "ms": 9.599074000000002, + "name": "wasi.start" + }, + { + "ms": 0.061107999999999996, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 23.645122 + }, + "memory": { + "start": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491404288, + "virtualBytes": 4195602432, + "minorFaults": 150670, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491841536, + "virtualBytes": 8560058368, + "minorFaults": 150703, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150703, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 58.39175099998829, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.419544000000002, + "firstHostCallMs": 0.029422, + "firstOutputMs": 20.157398999999998, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0009299999999999999, + "name": "Engine" + }, + { + "ms": 0.150336, + "name": "canonicalPreopens" + }, + { + "ms": 7.809364, + "name": "moduleRead" + }, + { + "ms": 4.568225, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.024757, + "name": "importValidation" + }, + { + "ms": 0.38717599999999996, + "name": "Linker" + }, + { + "ms": 0.041659, + "name": "Store" + }, + { + "ms": 2.4891639999999997, + "name": "Instance" + }, + { + "ms": 0.11349, + "name": "signalMaskInit" + }, + { + "ms": 0.014962, + "name": "entrypointLookup" + }, + { + "ms": 6.11814, + "name": "wasi.start" + }, + { + "ms": 3.249426, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 25.92631 + }, + "memory": { + "start": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150703, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491476992, + "virtualBytes": 8560058368, + "minorFaults": 150736, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150736, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 50.3090460000094, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.178021, + "firstHostCallMs": 0.030445, + "firstOutputMs": 19.980727, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001738, + "name": "Engine" + }, + { + "ms": 0.135863, + "name": "canonicalPreopens" + }, + { + "ms": 7.534363, + "name": "moduleRead" + }, + { + "ms": 5.046163, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014097, + "name": "importValidation" + }, + { + "ms": 0.293463, + "name": "Linker" + }, + { + "ms": 0.025224, + "name": "Store" + }, + { + "ms": 0.063162, + "name": "Instance" + }, + { + "ms": 0.081881, + "name": "signalMaskInit" + }, + { + "ms": 0.004776, + "name": "entrypointLookup" + }, + { + "ms": 7.871563999999999, + "name": "wasi.start" + }, + { + "ms": 1.372031, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 23.265395 + }, + "memory": { + "start": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150736, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491476992, + "virtualBytes": 8560058368, + "minorFaults": 150769, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150769, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 50.38200099999085, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.526917000000001, + "firstHostCallMs": 0.030814, + "firstOutputMs": 17.950708, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0015689999999999999, + "name": "Engine" + }, + { + "ms": 0.18986599999999998, + "name": "canonicalPreopens" + }, + { + "ms": 7.094067, + "name": "moduleRead" + }, + { + "ms": 5.660298999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010383, + "name": "importValidation" + }, + { + "ms": 0.220363, + "name": "Linker" + }, + { + "ms": 0.021925, + "name": "Store" + }, + { + "ms": 0.44614699999999996, + "name": "Instance" + }, + { + "ms": 0.09793099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.003733, + "name": "entrypointLookup" + }, + { + "ms": 4.882159, + "name": "wasi.start" + }, + { + "ms": 0.037752, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 19.458747 + }, + "memory": { + "start": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491403264, + "virtualBytes": 4195602432, + "minorFaults": 150769, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491707392, + "virtualBytes": 8560058368, + "minorFaults": 150802, + "majorFaults": 0 + }, + "end": { + "rssBytes": 488513536, + "peakRssBytes": 525893632, + "pssBytes": 491404288, + "virtualBytes": 4195602432, + "minorFaults": 150802, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 18421056, + "wasmtimeProcessRetainedRssBytes": 488513536, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "v8", + "processIndex": 4, + "vmSetupMs": 508.4677040000097, + "fixtureSetupMs": 416.3627020000131, + "baseline": { + "rssBytes": 250015744, + "peakRssBytes": 255447040, + "pssBytes": 251294720, + "virtualBytes": 3885957120, + "minorFaults": 60406, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410845184, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 426262528, + "peakRssBytes": 534519808, + "pssBytes": 428145664, + "virtualBytes": 4053024768, + "minorFaults": 1454426, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 176246784, + "peakRssBytes": 279072768, + "pssBytes": 176850944, + "virtualBytes": 167067648, + "minorFaults": 1394020, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 74.84915600001113, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 2.928927 + }, + { + "name": "WebAssembly.Module", + "ms": 0.149504 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.068239 + }, + { + "name": "wasi.start", + "ms": 0.090753 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 250015744, + "peakRssBytes": 255447040, + "pssBytes": 251302912, + "virtualBytes": 3888070656, + "minorFaults": 60408, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 285757440, + "peakRssBytes": 286322688, + "pssBytes": 286547968, + "virtualBytes": 4640284672, + "minorFaults": 71321, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271376384, + "peakRssBytes": 286322688, + "pssBytes": 272568320, + "virtualBytes": 3955179520, + "minorFaults": 71321, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 250015744, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271376384, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 86.64847399998689, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 3.182551 + }, + { + "name": "WebAssembly.Module", + "ms": 0.150841 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.068107 + }, + { + "name": "wasi.start", + "ms": 0.098056 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271376384, + "peakRssBytes": 286322688, + "pssBytes": 272568320, + "virtualBytes": 3955179520, + "minorFaults": 71321, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286150656, + "peakRssBytes": 286461952, + "pssBytes": 272736256, + "virtualBytes": 4640284672, + "minorFaults": 77608, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271519744, + "peakRssBytes": 286461952, + "pssBytes": 272736256, + "virtualBytes": 3955179520, + "minorFaults": 77608, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271376384, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271519744, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 76.31700999999885, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.29608 + }, + { + "name": "WebAssembly.Module", + "ms": 0.178446 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.066807 + }, + { + "name": "wasi.start", + "ms": 0.092544 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271519744, + "peakRssBytes": 286461952, + "pssBytes": 272736256, + "virtualBytes": 3955179520, + "minorFaults": 77608, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286261248, + "peakRssBytes": 286507008, + "pssBytes": 287601664, + "virtualBytes": 4640284672, + "minorFaults": 83878, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271560704, + "peakRssBytes": 286507008, + "pssBytes": 272831488, + "virtualBytes": 3955179520, + "minorFaults": 83878, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271519744, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271560704, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 74.31707399999141, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.123235 + }, + { + "name": "WebAssembly.Module", + "ms": 0.140206 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.071709 + }, + { + "name": "wasi.start", + "ms": 0.104602 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271560704, + "peakRssBytes": 286507008, + "pssBytes": 272831488, + "virtualBytes": 3955179520, + "minorFaults": 83878, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286310400, + "peakRssBytes": 286515200, + "pssBytes": 287679488, + "virtualBytes": 4640284672, + "minorFaults": 90134, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271568896, + "peakRssBytes": 286515200, + "pssBytes": 272868352, + "virtualBytes": 3955179520, + "minorFaults": 90134, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271560704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271568896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 73.73135300001013, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/true", + "sourceModuleBytes": 28203, + "moduleBytes": 28205, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 1.107503 + }, + { + "name": "WebAssembly.Module", + "ms": 0.146368 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.07547 + }, + { + "name": "wasi.start", + "ms": 0.09567 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271568896, + "peakRssBytes": 286515200, + "pssBytes": 272868352, + "virtualBytes": 3955179520, + "minorFaults": 90134, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 286285824, + "peakRssBytes": 286515200, + "pssBytes": 287687680, + "virtualBytes": 4640284672, + "minorFaults": 96387, + "majorFaults": 0 + }, + "end": { + "rssBytes": 271560704, + "peakRssBytes": 286515200, + "pssBytes": 272892928, + "virtualBytes": 3955179520, + "minorFaults": 96387, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271568896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271560704, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 250.08101200000965, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 32.590449 + }, + { + "name": "WebAssembly.Module", + "ms": 2.473441 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.643017 + }, + { + "name": "wasi.start", + "ms": 104.937896 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 271560704, + "peakRssBytes": 286515200, + "pssBytes": 272892928, + "virtualBytes": 3955179520, + "minorFaults": 96387, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 335015936, + "peakRssBytes": 335286272, + "pssBytes": 337072128, + "virtualBytes": 4696444928, + "minorFaults": 123127, + "majorFaults": 0 + }, + "end": { + "rssBytes": 282853376, + "peakRssBytes": 335286272, + "pssBytes": 284598272, + "virtualBytes": 3957280768, + "minorFaults": 123127, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 271560704, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282853376, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 250.20249200001126, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.598328 + }, + { + "name": "WebAssembly.Module", + "ms": 1.287047 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.559706 + }, + { + "name": "wasi.start", + "ms": 92.543756 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 282853376, + "peakRssBytes": 335286272, + "pssBytes": 284598272, + "virtualBytes": 3957280768, + "minorFaults": 123127, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 344920064, + "peakRssBytes": 345460736, + "pssBytes": 341025792, + "virtualBytes": 4695920640, + "minorFaults": 149001, + "majorFaults": 0 + }, + "end": { + "rssBytes": 294522880, + "peakRssBytes": 345460736, + "pssBytes": 296338432, + "virtualBytes": 3957280768, + "minorFaults": 149001, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 282853376, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 294522880, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 230.9778309999965, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.12472 + }, + { + "name": "WebAssembly.Module", + "ms": 1.793262 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.434303 + }, + { + "name": "wasi.start", + "ms": 90.486798 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 294522880, + "peakRssBytes": 345460736, + "pssBytes": 296338432, + "virtualBytes": 3957280768, + "minorFaults": 149001, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 349499392, + "peakRssBytes": 349769728, + "pssBytes": 351212544, + "virtualBytes": 4695920640, + "minorFaults": 171593, + "majorFaults": 0 + }, + "end": { + "rssBytes": 295145472, + "peakRssBytes": 349769728, + "pssBytes": 296731648, + "virtualBytes": 3957280768, + "minorFaults": 171593, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 294522880, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 295145472, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 225.51046099999803, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.569677 + }, + { + "name": "WebAssembly.Module", + "ms": 1.734764 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.327761 + }, + { + "name": "wasi.start", + "ms": 90.46323 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 295145472, + "peakRssBytes": 349769728, + "pssBytes": 296731648, + "virtualBytes": 3957280768, + "minorFaults": 171593, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 355008512, + "peakRssBytes": 355819520, + "pssBytes": 350913536, + "virtualBytes": 4695920640, + "minorFaults": 196321, + "majorFaults": 0 + }, + "end": { + "rssBytes": 304373760, + "peakRssBytes": 355819520, + "pssBytes": 230038528, + "virtualBytes": 3957280768, + "minorFaults": 196321, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 295145472, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304644096, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 235.94026999999187, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/ls", + "sourceModuleBytes": 1208565, + "moduleBytes": 1208567, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.659966 + }, + { + "name": "WebAssembly.Module", + "ms": 2.571517 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.337191 + }, + { + "name": "wasi.start", + "ms": 91.452482 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 304644096, + "peakRssBytes": 355819520, + "pssBytes": 262566912, + "virtualBytes": 3957280768, + "minorFaults": 196381, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 359235584, + "peakRssBytes": 359505920, + "pssBytes": 360895488, + "virtualBytes": 4695920640, + "minorFaults": 220974, + "majorFaults": 0 + }, + "end": { + "rssBytes": 304852992, + "peakRssBytes": 359505920, + "pssBytes": 306331648, + "virtualBytes": 3957280768, + "minorFaults": 220974, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304644096, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304852992, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 348.4005869999819, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.672329 + }, + { + "name": "WebAssembly.Module", + "ms": 4.798015 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.687127 + }, + { + "name": "wasi.start", + "ms": 119.71448 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 304852992, + "peakRssBytes": 359505920, + "pssBytes": 306331648, + "virtualBytes": 3957280768, + "minorFaults": 220974, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 411992064, + "peakRssBytes": 412119040, + "pssBytes": 413746176, + "virtualBytes": 5472702464, + "minorFaults": 282879, + "majorFaults": 0 + }, + "end": { + "rssBytes": 329666560, + "peakRssBytes": 412119040, + "pssBytes": 332317696, + "virtualBytes": 4030861312, + "minorFaults": 282879, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 304852992, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329666560, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 407.1533950000012, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 91.174313 + }, + { + "name": "WebAssembly.Module", + "ms": 3.699261 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.494016 + }, + { + "name": "wasi.start", + "ms": 128.35134 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 329666560, + "peakRssBytes": 412119040, + "pssBytes": 332317696, + "virtualBytes": 4030861312, + "minorFaults": 282879, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 422678528, + "peakRssBytes": 422825984, + "pssBytes": 425242624, + "virtualBytes": 5472845824, + "minorFaults": 339475, + "majorFaults": 0 + }, + "end": { + "rssBytes": 330104832, + "peakRssBytes": 422825984, + "pssBytes": 332628992, + "virtualBytes": 4030861312, + "minorFaults": 339475, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 329666560, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330104832, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 351.07378399997833, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.809557 + }, + { + "name": "WebAssembly.Module", + "ms": 3.634423 + }, + { + "name": "WebAssembly.Instance", + "ms": 6.042174 + }, + { + "name": "wasi.start", + "ms": 118.843416 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 330104832, + "peakRssBytes": 422825984, + "pssBytes": 332628992, + "virtualBytes": 4030861312, + "minorFaults": 339475, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432373760, + "peakRssBytes": 432381952, + "pssBytes": 434313216, + "virtualBytes": 5446782976, + "minorFaults": 398018, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338358272, + "peakRssBytes": 432381952, + "pssBytes": 341427200, + "virtualBytes": 4030861312, + "minorFaults": 398018, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 330104832, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338358272, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 389.1344600000011, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 63.422281 + }, + { + "name": "WebAssembly.Module", + "ms": 4.307468 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.584972 + }, + { + "name": "wasi.start", + "ms": 132.294562 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338358272, + "peakRssBytes": 432381952, + "pssBytes": 341427200, + "virtualBytes": 4030861312, + "minorFaults": 398018, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433131520, + "peakRssBytes": 433340416, + "pssBytes": 435872768, + "virtualBytes": 5473226752, + "minorFaults": 452891, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338587648, + "peakRssBytes": 433340416, + "pssBytes": 341430272, + "virtualBytes": 4030861312, + "minorFaults": 452891, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338358272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338587648, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 371.22936900000786, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sh", + "sourceModuleBytes": 3082692, + "moduleBytes": 3082694, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 64.695579 + }, + { + "name": "WebAssembly.Module", + "ms": 4.644118 + }, + { + "name": "WebAssembly.Instance", + "ms": 9.790389 + }, + { + "name": "wasi.start", + "ms": 113.758343 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338587648, + "peakRssBytes": 433340416, + "pssBytes": 341430272, + "virtualBytes": 4030861312, + "minorFaults": 452891, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 431554560, + "peakRssBytes": 433340416, + "pssBytes": 434136064, + "virtualBytes": 5446520832, + "minorFaults": 504654, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338546688, + "peakRssBytes": 433340416, + "pssBytes": 341479424, + "virtualBytes": 4030861312, + "minorFaults": 504654, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338587648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338546688, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 274.0933939999959, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 55.719209 + }, + { + "name": "WebAssembly.Module", + "ms": 2.913917 + }, + { + "name": "WebAssembly.Instance", + "ms": 10.388608 + }, + { + "name": "wasi.start", + "ms": 54.22894 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 338546688, + "peakRssBytes": 433340416, + "pssBytes": 341479424, + "virtualBytes": 4030861312, + "minorFaults": 504654, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 408354816, + "peakRssBytes": 433340416, + "pssBytes": 410293248, + "virtualBytes": 4788678656, + "minorFaults": 533843, + "majorFaults": 0 + }, + "end": { + "rssBytes": 341622784, + "peakRssBytes": 433340416, + "pssBytes": 245066752, + "virtualBytes": 4581900288, + "minorFaults": 533843, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 338546688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 341155840, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 301.4461789999914, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 80.265734 + }, + { + "name": "WebAssembly.Module", + "ms": 2.069892 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.133862 + }, + { + "name": "wasi.start", + "ms": 40.441805 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339574784, + "peakRssBytes": 433340416, + "pssBytes": 341951488, + "virtualBytes": 4030861312, + "minorFaults": 533843, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 406593536, + "peakRssBytes": 433340416, + "pssBytes": 408653824, + "virtualBytes": 4788535296, + "minorFaults": 566114, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339898368, + "peakRssBytes": 433340416, + "pssBytes": 341954560, + "virtualBytes": 4030861312, + "minorFaults": 566114, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339574784, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339898368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 275.4507229999872, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.952018 + }, + { + "name": "WebAssembly.Module", + "ms": 2.748936 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.442083 + }, + { + "name": "wasi.start", + "ms": 29.236214 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339898368, + "peakRssBytes": 433340416, + "pssBytes": 341955584, + "virtualBytes": 4030861312, + "minorFaults": 566114, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 408510464, + "peakRssBytes": 433340416, + "pssBytes": 410555392, + "virtualBytes": 4788678656, + "minorFaults": 597852, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339791872, + "peakRssBytes": 433340416, + "pssBytes": 341959680, + "virtualBytes": 4030861312, + "minorFaults": 597852, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339898368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339791872, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 267.7597489999898, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 48.591811 + }, + { + "name": "WebAssembly.Module", + "ms": 4.574002 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.129241 + }, + { + "name": "wasi.start", + "ms": 26.146541 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339791872, + "peakRssBytes": 433340416, + "pssBytes": 341959680, + "virtualBytes": 4030861312, + "minorFaults": 597852, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 406298624, + "peakRssBytes": 433340416, + "pssBytes": 408662016, + "virtualBytes": 4788535296, + "minorFaults": 630133, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339857408, + "peakRssBytes": 433340416, + "pssBytes": 341962752, + "virtualBytes": 4030861312, + "minorFaults": 630133, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339791872, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339857408, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 197.36079299999983, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/curl", + "sourceModuleBytes": 1561500, + "moduleBytes": 1561502, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.299615 + }, + { + "name": "WebAssembly.Module", + "ms": 3.060786 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.337925 + }, + { + "name": "wasi.start", + "ms": 25.823694 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339857408, + "peakRssBytes": 433340416, + "pssBytes": 341962752, + "virtualBytes": 4030861312, + "minorFaults": 630133, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 406347776, + "peakRssBytes": 433340416, + "pssBytes": 408667136, + "virtualBytes": 4788535296, + "minorFaults": 662412, + "majorFaults": 0 + }, + "end": { + "rssBytes": 339877888, + "peakRssBytes": 433340416, + "pssBytes": 341967872, + "virtualBytes": 4030861312, + "minorFaults": 662412, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339857408, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339877888, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 145.31057400000282, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 22.601848 + }, + { + "name": "WebAssembly.Module", + "ms": 1.411551 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.195961 + }, + { + "name": "wasi.start", + "ms": 15.560706 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 339877888, + "peakRssBytes": 433340416, + "pssBytes": 341967872, + "virtualBytes": 4030861312, + "minorFaults": 662412, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 376627200, + "peakRssBytes": 433340416, + "pssBytes": 379174912, + "virtualBytes": 4757970944, + "minorFaults": 681794, + "majorFaults": 0 + }, + "end": { + "rssBytes": 345620480, + "peakRssBytes": 433340416, + "pssBytes": 101600256, + "virtualBytes": 4030861312, + "minorFaults": 681794, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 339877888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346161152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 195.06303799999296, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.867242 + }, + { + "name": "WebAssembly.Module", + "ms": 1.486575 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.208465 + }, + { + "name": "wasi.start", + "ms": 24.744042 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 346701824, + "peakRssBytes": 433340416, + "pssBytes": 210608128, + "virtualBytes": 4030861312, + "minorFaults": 682060, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 409415680, + "peakRssBytes": 433340416, + "pssBytes": 404978688, + "virtualBytes": 4758233088, + "minorFaults": 711902, + "majorFaults": 0 + }, + "end": { + "rssBytes": 366292992, + "peakRssBytes": 433340416, + "pssBytes": 105206784, + "virtualBytes": 4030861312, + "minorFaults": 711902, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 346701824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 366833664, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 173.44920699999784, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 24.256364 + }, + { + "name": "WebAssembly.Module", + "ms": 1.753975 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.182305 + }, + { + "name": "wasi.start", + "ms": 15.312411 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 367374336, + "peakRssBytes": 433340416, + "pssBytes": 129836032, + "virtualBytes": 4030861312, + "minorFaults": 712119, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419475456, + "peakRssBytes": 433340416, + "pssBytes": 421749760, + "virtualBytes": 4758233088, + "minorFaults": 738298, + "majorFaults": 0 + }, + "end": { + "rssBytes": 372572160, + "peakRssBytes": 433340416, + "pssBytes": 103998464, + "virtualBytes": 4030861312, + "minorFaults": 738298, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 367104000, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374194176, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 185.42722099999082, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 25.309821 + }, + { + "name": "WebAssembly.Module", + "ms": 1.842374 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.521068 + }, + { + "name": "wasi.start", + "ms": 21.705375 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 374734848, + "peakRssBytes": 433340416, + "pssBytes": 340058112, + "virtualBytes": 4030861312, + "minorFaults": 738804, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 433094656, + "peakRssBytes": 433340416, + "pssBytes": 420232192, + "virtualBytes": 4757852160, + "minorFaults": 764922, + "majorFaults": 0 + }, + "end": { + "rssBytes": 378990592, + "peakRssBytes": 433340416, + "pssBytes": 381779968, + "virtualBytes": 4030861312, + "minorFaults": 764922, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 374464512, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 379531264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 182.2041700000118, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sqlite3", + "sourceModuleBytes": 878882, + "moduleBytes": 878884, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 26.751161 + }, + { + "name": "WebAssembly.Module", + "ms": 1.436593 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.554544 + }, + { + "name": "wasi.start", + "ms": 16.955285 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 379801600, + "peakRssBytes": 433340416, + "pssBytes": 385110016, + "virtualBytes": 4030861312, + "minorFaults": 765124, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 435949568, + "peakRssBytes": 436219904, + "pssBytes": 438592512, + "virtualBytes": 4757708800, + "minorFaults": 787321, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397238272, + "peakRssBytes": 436219904, + "pssBytes": 399561728, + "virtualBytes": 4030861312, + "minorFaults": 787321, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 379801600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397238272, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 295.7348579999816, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.778547 + }, + { + "name": "WebAssembly.Module", + "ms": 3.711571 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.750856 + }, + { + "name": "wasi.start", + "ms": 48.17769 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397238272, + "peakRssBytes": 436219904, + "pssBytes": 399561728, + "virtualBytes": 4030861312, + "minorFaults": 787321, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 491233280, + "peakRssBytes": 491233280, + "pssBytes": 493469696, + "virtualBytes": 4822081536, + "minorFaults": 832398, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397357056, + "peakRssBytes": 491233280, + "pssBytes": 399695872, + "virtualBytes": 4030861312, + "minorFaults": 832398, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397238272, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397357056, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 296.78298699998413, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 68.919969 + }, + { + "name": "WebAssembly.Module", + "ms": 4.447175 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.462651 + }, + { + "name": "wasi.start", + "ms": 47.020587 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397357056, + "peakRssBytes": 491233280, + "pssBytes": 399695872, + "virtualBytes": 4030861312, + "minorFaults": 832398, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 491634688, + "peakRssBytes": 491724800, + "pssBytes": 490419200, + "virtualBytes": 4821819392, + "minorFaults": 877503, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397754368, + "peakRssBytes": 491724800, + "pssBytes": 399705088, + "virtualBytes": 4030861312, + "minorFaults": 877503, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397357056, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397754368, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 334.1334189999907, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 78.263577 + }, + { + "name": "WebAssembly.Module", + "ms": 4.096963 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.89653 + }, + { + "name": "wasi.start", + "ms": 48.458792 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397754368, + "peakRssBytes": 491724800, + "pssBytes": 399705088, + "virtualBytes": 4030861312, + "minorFaults": 877503, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 490156032, + "peakRssBytes": 491724800, + "pssBytes": 492408832, + "virtualBytes": 4769517568, + "minorFaults": 922651, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397500416, + "peakRssBytes": 491724800, + "pssBytes": 399705088, + "virtualBytes": 4030861312, + "minorFaults": 922651, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397754368, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397500416, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 345.4228390000062, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 69.597594 + }, + { + "name": "WebAssembly.Module", + "ms": 5.794486 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.462372 + }, + { + "name": "wasi.start", + "ms": 68.020338 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397500416, + "peakRssBytes": 491724800, + "pssBytes": 399705088, + "virtualBytes": 4030861312, + "minorFaults": 922651, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 491397120, + "peakRssBytes": 491724800, + "pssBytes": 493583360, + "virtualBytes": 4821557248, + "minorFaults": 964508, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397574144, + "peakRssBytes": 491724800, + "pssBytes": 399710208, + "virtualBytes": 4030861312, + "minorFaults": 964508, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397500416, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397574144, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 284.3511460000009, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/vim", + "sourceModuleBytes": 2854951, + "moduleBytes": 2854953, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 49.863715 + }, + { + "name": "WebAssembly.Module", + "ms": 2.924744 + }, + { + "name": "WebAssembly.Instance", + "ms": 6.736975 + }, + { + "name": "wasi.start", + "ms": 39.879029 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397574144, + "peakRssBytes": 491724800, + "pssBytes": 399710208, + "virtualBytes": 4030861312, + "minorFaults": 964508, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 490520576, + "peakRssBytes": 491724800, + "pssBytes": 492440576, + "virtualBytes": 4821557248, + "minorFaults": 998388, + "majorFaults": 0 + }, + "end": { + "rssBytes": 397426688, + "peakRssBytes": 491724800, + "pssBytes": 399711232, + "virtualBytes": 4030861312, + "minorFaults": 998388, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397574144, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397426688, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 291.65770700000576, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 95.058131 + }, + { + "name": "WebAssembly.Module", + "ms": 6.354488 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.186125 + }, + { + "name": "wasi.start", + "ms": 5.234067 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 397426688, + "peakRssBytes": 491724800, + "pssBytes": 399711232, + "virtualBytes": 4030861312, + "minorFaults": 998388, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 493551616, + "peakRssBytes": 493789184, + "pssBytes": 495187968, + "virtualBytes": 4813127680, + "minorFaults": 1043895, + "majorFaults": 0 + }, + "end": { + "rssBytes": 401035264, + "peakRssBytes": 493789184, + "pssBytes": 403332096, + "virtualBytes": 4033089536, + "minorFaults": 1043895, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 397426688, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 401035264, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 266.6714829999837, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 71.501077 + }, + { + "name": "WebAssembly.Module", + "ms": 4.521458 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.781231 + }, + { + "name": "wasi.start", + "ms": 6.972944 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 401035264, + "peakRssBytes": 493789184, + "pssBytes": 403332096, + "virtualBytes": 4033089536, + "minorFaults": 1043895, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 493973504, + "peakRssBytes": 493973504, + "pssBytes": 495680512, + "virtualBytes": 4813516800, + "minorFaults": 1084950, + "majorFaults": 0 + }, + "end": { + "rssBytes": 401186816, + "peakRssBytes": 493973504, + "pssBytes": 403351552, + "virtualBytes": 4033478656, + "minorFaults": 1084950, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 401035264, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 401186816, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 301.6060189999989, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 89.661646 + }, + { + "name": "WebAssembly.Module", + "ms": 4.532767 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.84978 + }, + { + "name": "wasi.start", + "ms": 7.4753 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 401186816, + "peakRssBytes": 493973504, + "pssBytes": 403351552, + "virtualBytes": 4033478656, + "minorFaults": 1084950, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 534360064, + "peakRssBytes": 534360064, + "pssBytes": 536529920, + "virtualBytes": 4813398016, + "minorFaults": 1132931, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402292736, + "peakRssBytes": 534360064, + "pssBytes": 404524032, + "virtualBytes": 4033478656, + "minorFaults": 1132931, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 401186816, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402292736, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 303.053012999997, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 96.637198 + }, + { + "name": "WebAssembly.Module", + "ms": 4.845964 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.429183 + }, + { + "name": "wasi.start", + "ms": 5.780871 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402292736, + "peakRssBytes": 534360064, + "pssBytes": 404524032, + "virtualBytes": 4033478656, + "minorFaults": 1132931, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 495091712, + "peakRssBytes": 534360064, + "pssBytes": 493476864, + "virtualBytes": 4813516800, + "minorFaults": 1181666, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402460672, + "peakRssBytes": 534360064, + "pssBytes": 404617216, + "virtualBytes": 4033478656, + "minorFaults": 1181666, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402292736, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402460672, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 351.98723999998765, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/git", + "sourceModuleBytes": 3397393, + "moduleBytes": 3397395, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 118.897364 + }, + { + "name": "WebAssembly.Module", + "ms": 4.340397 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.405569 + }, + { + "name": "wasi.start", + "ms": 8.758288 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402460672, + "peakRssBytes": 534360064, + "pssBytes": 404617216, + "virtualBytes": 4033478656, + "minorFaults": 1181666, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 534310912, + "peakRssBytes": 534519808, + "pssBytes": 536620032, + "virtualBytes": 4813516800, + "minorFaults": 1233451, + "majorFaults": 0 + }, + "end": { + "rssBytes": 402509824, + "peakRssBytes": 534519808, + "pssBytes": 404617216, + "virtualBytes": 4033478656, + "minorFaults": 1233451, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402460672, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402509824, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 268.5235780000221, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 41.542296 + }, + { + "name": "WebAssembly.Module", + "ms": 4.351649 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.972048 + }, + { + "name": "wasi.start", + "ms": 88.468956 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 402509824, + "peakRssBytes": 534519808, + "pssBytes": 404617216, + "virtualBytes": 4033478656, + "minorFaults": 1233451, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 460513280, + "peakRssBytes": 534519808, + "pssBytes": 456768512, + "virtualBytes": 4772286464, + "minorFaults": 1254330, + "majorFaults": 0 + }, + "end": { + "rssBytes": 408649728, + "peakRssBytes": 534519808, + "pssBytes": 410913792, + "virtualBytes": 4034437120, + "minorFaults": 1254330, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 402509824, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 408649728, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 215.2996259999927, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 27.353095 + }, + { + "name": "WebAssembly.Module", + "ms": 3.324184 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.698276 + }, + { + "name": "wasi.start", + "ms": 72.723394 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 408649728, + "peakRssBytes": 534519808, + "pssBytes": 410913792, + "virtualBytes": 4034437120, + "minorFaults": 1254330, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 460926976, + "peakRssBytes": 534519808, + "pssBytes": 463059968, + "virtualBytes": 4771500032, + "minorFaults": 1271156, + "majorFaults": 0 + }, + "end": { + "rssBytes": 409137152, + "peakRssBytes": 534519808, + "pssBytes": 411113472, + "virtualBytes": 4034437120, + "minorFaults": 1271156, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 408649728, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409137152, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 228.04868499998702, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 33.01544 + }, + { + "name": "WebAssembly.Module", + "ms": 1.501964 + }, + { + "name": "WebAssembly.Instance", + "ms": 1.317804 + }, + { + "name": "wasi.start", + "ms": 73.436961 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 409137152, + "peakRssBytes": 534519808, + "pssBytes": 411113472, + "virtualBytes": 4034437120, + "minorFaults": 1271156, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 461230080, + "peakRssBytes": 534519808, + "pssBytes": 463215616, + "virtualBytes": 4771762176, + "minorFaults": 1289979, + "majorFaults": 0 + }, + "end": { + "rssBytes": 409223168, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1289979, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409137152, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409223168, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 202.72369899999467, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 23.836261 + }, + { + "name": "WebAssembly.Module", + "ms": 2.341242 + }, + { + "name": "WebAssembly.Instance", + "ms": 3.795213 + }, + { + "name": "wasi.start", + "ms": 75.791349 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 409223168, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1289979, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 461279232, + "peakRssBytes": 534519808, + "pssBytes": 463154176, + "virtualBytes": 4771762176, + "minorFaults": 1309822, + "majorFaults": 0 + }, + "end": { + "rssBytes": 409382912, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1309822, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409223168, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409382912, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 223.13461800001096, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/sha256sum", + "sourceModuleBytes": 1349812, + "moduleBytes": 1349814, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.557444 + }, + { + "name": "WebAssembly.Module", + "ms": 1.937937 + }, + { + "name": "WebAssembly.Instance", + "ms": 2.546531 + }, + { + "name": "wasi.start", + "ms": 76.541505 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 409382912, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1309822, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 461221888, + "peakRssBytes": 534519808, + "pssBytes": 463223808, + "virtualBytes": 4772286464, + "minorFaults": 1329665, + "majorFaults": 0 + }, + "end": { + "rssBytes": 409243648, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1329665, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409382912, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409243648, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 180.50901599999634, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 36.559707 + }, + { + "name": "WebAssembly.Module", + "ms": 1.954807 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.355205 + }, + { + "name": "wasi.start", + "ms": 15.407574 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 409243648, + "peakRssBytes": 534519808, + "pssBytes": 411118592, + "virtualBytes": 4034437120, + "minorFaults": 1329665, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478486528, + "peakRssBytes": 534519808, + "pssBytes": 480553984, + "virtualBytes": 4792745984, + "minorFaults": 1352390, + "majorFaults": 0 + }, + "end": { + "rssBytes": 410472448, + "peakRssBytes": 534519808, + "pssBytes": 412515328, + "virtualBytes": 4035948544, + "minorFaults": 1352390, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 409243648, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410472448, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 169.14176299999235, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 38.754416 + }, + { + "name": "WebAssembly.Module", + "ms": 3.748869 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.853252 + }, + { + "name": "wasi.start", + "ms": 16.653999 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 410472448, + "peakRssBytes": 534519808, + "pssBytes": 412515328, + "virtualBytes": 4035948544, + "minorFaults": 1352390, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478879744, + "peakRssBytes": 534519808, + "pssBytes": 480823296, + "virtualBytes": 4792868864, + "minorFaults": 1376372, + "majorFaults": 0 + }, + "end": { + "rssBytes": 410906624, + "peakRssBytes": 534519808, + "pssBytes": 412784640, + "virtualBytes": 4036214784, + "minorFaults": 1376372, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410472448, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410906624, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 174.3138500000059, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 29.700224 + }, + { + "name": "WebAssembly.Module", + "ms": 1.384755 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.361871 + }, + { + "name": "wasi.start", + "ms": 14.674967 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 410906624, + "peakRssBytes": 534519808, + "pssBytes": 412784640, + "virtualBytes": 4036214784, + "minorFaults": 1376372, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478814208, + "peakRssBytes": 534519808, + "pssBytes": 480824320, + "virtualBytes": 4792606720, + "minorFaults": 1401310, + "majorFaults": 0 + }, + "end": { + "rssBytes": 410796032, + "peakRssBytes": 534519808, + "pssBytes": 412785664, + "virtualBytes": 4036214784, + "minorFaults": 1401310, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410906624, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410796032, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 173.2074090000242, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 35.716067 + }, + { + "name": "WebAssembly.Module", + "ms": 2.326168 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.373533 + }, + { + "name": "wasi.start", + "ms": 14.986724 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 410796032, + "peakRssBytes": 534519808, + "pssBytes": 412785664, + "virtualBytes": 4036214784, + "minorFaults": 1401310, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478654464, + "peakRssBytes": 534519808, + "pssBytes": 480787456, + "virtualBytes": 4792868864, + "minorFaults": 1427271, + "majorFaults": 0 + }, + "end": { + "rssBytes": 410689536, + "peakRssBytes": 534519808, + "pssBytes": 412785664, + "virtualBytes": 4036214784, + "minorFaults": 1427271, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410796032, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410689536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 187.12015099998098, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "reason": "complete", + "modulePath": "/tmp/agentos-native-sidecar-runtime-vm-1-1784624964951556389/__secure_exec/commands/0/find", + "sourceModuleBytes": 1509578, + "moduleBytes": 1509580, + "phases": [ + { + "name": "enforceMemoryLimit", + "ms": 36.338195 + }, + { + "name": "WebAssembly.Module", + "ms": 1.557905 + }, + { + "name": "WebAssembly.Instance", + "ms": 0.403053 + }, + { + "name": "wasi.start", + "ms": 15.896591 + } + ], + "exitCode": 0 + }, + "memory": { + "start": { + "rssBytes": 410689536, + "peakRssBytes": 534519808, + "pssBytes": 412785664, + "virtualBytes": 4036214784, + "minorFaults": 1427271, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 478797824, + "peakRssBytes": 534519808, + "pssBytes": 480753664, + "virtualBytes": 4792606720, + "minorFaults": 1452210, + "majorFaults": 0 + }, + "end": { + "rssBytes": 410845184, + "peakRssBytes": 534519808, + "pssBytes": 412784640, + "virtualBytes": 4036214784, + "minorFaults": 1452210, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410689536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 410845184, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + }, + { + "backend": "wasmtime", + "processIndex": 4, + "vmSetupMs": 443.88819100000546, + "fixtureSetupMs": 401.24960400001146, + "baseline": { + "rssBytes": 240517120, + "peakRssBytes": 247873536, + "pssBytes": 242058240, + "virtualBytes": 3885457408, + "minorFaults": 55114, + "majorFaults": 0 + }, + "beforeDispose": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + }, + "retained": { + "rssBytes": 504201216, + "peakRssBytes": 518463488, + "pssBytes": 506373120, + "virtualBytes": 4203581440, + "minorFaults": 133086, + "majorFaults": 0 + }, + "retainedDelta": { + "rssBytes": 263684096, + "peakRssBytes": 270589952, + "pssBytes": 264314880, + "virtualBytes": 318124032, + "minorFaults": 77972, + "majorFaults": 0 + }, + "workloads": [ + { + "name": "trivial", + "command": "true", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 67.10170700002345, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.086024, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.079897, + "name": "Engine" + }, + { + "ms": 0.095864, + "name": "canonicalPreopens" + }, + { + "ms": 3.445402, + "name": "moduleRead" + }, + { + "ms": 0.215666, + "name": "profileValidation" + }, + { + "ms": 47.083618, + "name": "moduleCompile" + }, + { + "ms": 0.00276, + "name": "importValidation" + }, + { + "ms": 0.18139100000000002, + "name": "Linker" + }, + { + "ms": 0.015333000000000001, + "name": "Store" + }, + { + "ms": 0.045562000000000005, + "name": "Instance" + }, + { + "ms": 0.081736, + "name": "signalMaskInit" + }, + { + "ms": 0.002281, + "name": "entrypointLookup" + }, + { + "ms": 0.024253999999999998, + "name": "wasi.start" + }, + { + "ms": 0.020424, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 51.356345 + }, + "memory": { + "start": { + "rssBytes": 240517120, + "peakRssBytes": 247873536, + "pssBytes": 242066432, + "virtualBytes": 3887570944, + "minorFaults": 55116, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956981760, + "minorFaults": 56278, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56278, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 0, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 0, + "wasmtimeChargedModuleBytes": 0, + "wasmtimeCompileTimeMicros": 0, + "wasmtimeProcessRetainedRssBytes": 240517120, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 22.676508999982616, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.00713, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.000723, + "name": "Engine" + }, + { + "ms": 0.12792399999999998, + "name": "canonicalPreopens" + }, + { + "ms": 2.203979, + "name": "moduleRead" + }, + { + "ms": 0.24909900000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003431, + "name": "importValidation" + }, + { + "ms": 0.18681, + "name": "Linker" + }, + { + "ms": 0.016778, + "name": "Store" + }, + { + "ms": 0.038013, + "name": "Instance" + }, + { + "ms": 0.586551, + "name": "signalMaskInit" + }, + { + "ms": 0.007262, + "name": "entrypointLookup" + }, + { + "ms": 0.039632, + "name": "wasi.start" + }, + { + "ms": 0.017813, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 3.542824 + }, + "memory": { + "start": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56278, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56300, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56300, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 0, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 19.18946200000937, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.010702, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001009, + "name": "Engine" + }, + { + "ms": 0.10195599999999999, + "name": "canonicalPreopens" + }, + { + "ms": 3.314229, + "name": "moduleRead" + }, + { + "ms": 0.21443800000000002, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003888, + "name": "importValidation" + }, + { + "ms": 0.18116000000000002, + "name": "Linker" + }, + { + "ms": 0.015243, + "name": "Store" + }, + { + "ms": 0.034653, + "name": "Instance" + }, + { + "ms": 0.623031, + "name": "signalMaskInit" + }, + { + "ms": 0.0023049999999999998, + "name": "entrypointLookup" + }, + { + "ms": 0.438343, + "name": "wasi.start" + }, + { + "ms": 0.021626, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 5.0168360000000005 + }, + "memory": { + "start": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56300, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 8319057920, + "minorFaults": 56322, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56322, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 1, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 21.809599999978673, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.013571999999999999, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001287, + "name": "Engine" + }, + { + "ms": 0.09814, + "name": "canonicalPreopens" + }, + { + "ms": 3.339192, + "name": "moduleRead" + }, + { + "ms": 0.222996, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.003692, + "name": "importValidation" + }, + { + "ms": 0.201696, + "name": "Linker" + }, + { + "ms": 0.019673999999999997, + "name": "Store" + }, + { + "ms": 0.03492, + "name": "Instance" + }, + { + "ms": 0.6045929999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.003297, + "name": "entrypointLookup" + }, + { + "ms": 0.030167, + "name": "wasi.start" + }, + { + "ms": 0.018357000000000002, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 4.649414999999999 + }, + "memory": { + "start": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56322, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56344, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56344, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 2, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 19.47299499998917, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": null, + "firstHostCallMs": 0.008436, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1114112, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 28203, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/true", + "phases": [ + { + "ms": 0.001214, + "name": "Engine" + }, + { + "ms": 0.108432, + "name": "canonicalPreopens" + }, + { + "ms": 3.303976, + "name": "moduleRead" + }, + { + "ms": 0.240915, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.0037689999999999998, + "name": "importValidation" + }, + { + "ms": 0.192445, + "name": "Linker" + }, + { + "ms": 0.014086, + "name": "Store" + }, + { + "ms": 0.033245, + "name": "Instance" + }, + { + "ms": 0.601643, + "name": "signalMaskInit" + }, + { + "ms": 0.006852, + "name": "entrypointLookup" + }, + { + "ms": 0.39193300000000003, + "name": "wasi.start" + }, + { + "ms": 0.018423, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 28203, + "totalMs": 4.983355 + }, + "memory": { + "start": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56344, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 8319057920, + "minorFaults": 56366, + "majorFaults": 0 + }, + "end": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56366, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 3, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "coreutils", + "command": "ls", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1142.0087930000154, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1064.895334, + "firstHostCallMs": 0.00956, + "firstOutputMs": 1120.25103, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.000798, + "name": "Engine" + }, + { + "ms": 0.111335, + "name": "canonicalPreopens" + }, + { + "ms": 6.525179, + "name": "moduleRead" + }, + { + "ms": 3.881392, + "name": "profileValidation" + }, + { + "ms": 1052.844717, + "name": "moduleCompile" + }, + { + "ms": 0.008877, + "name": "importValidation" + }, + { + "ms": 0.338387, + "name": "Linker" + }, + { + "ms": 0.024388, + "name": "Store" + }, + { + "ms": 0.23953200000000002, + "name": "Instance" + }, + { + "ms": 0.068122, + "name": "signalMaskInit" + }, + { + "ms": 0.0052569999999999995, + "name": "entrypointLookup" + }, + { + "ms": 55.731590999999995, + "name": "wasi.start" + }, + { + "ms": 0.080665, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 1120.5095119999999 + }, + "memory": { + "start": { + "rssBytes": 251088896, + "peakRssBytes": 251293696, + "pssBytes": 252756992, + "virtualBytes": 3956969472, + "minorFaults": 56366, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 290959360, + "peakRssBytes": 291532800, + "pssBytes": 293278720, + "virtualBytes": 8326873088, + "minorFaults": 63549, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63549, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 1, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 1, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 28203, + "wasmtimeChargedModuleBytes": 1048576, + "wasmtimeCompileTimeMicros": 47083, + "wasmtimeProcessRetainedRssBytes": 251088896, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 77.91669600000023, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.562061, + "firstHostCallMs": 0.008581, + "firstOutputMs": 59.393861, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.001002, + "name": "Engine" + }, + { + "ms": 0.108345, + "name": "canonicalPreopens" + }, + { + "ms": 6.528289, + "name": "moduleRead" + }, + { + "ms": 3.8812379999999997, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008027, + "name": "importValidation" + }, + { + "ms": 0.196012, + "name": "Linker" + }, + { + "ms": 0.016305, + "name": "Store" + }, + { + "ms": 0.043924000000000005, + "name": "Instance" + }, + { + "ms": 0.02533, + "name": "signalMaskInit" + }, + { + "ms": 0.00269, + "name": "entrypointLookup" + }, + { + "ms": 48.086974, + "name": "wasi.start" + }, + { + "ms": 1.7810000000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 61.300342 + }, + "memory": { + "start": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63549, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291385344, + "peakRssBytes": 291532800, + "pssBytes": 293221376, + "virtualBytes": 8326873088, + "minorFaults": 63623, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63623, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 4, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 82.29307100002188, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.432371, + "firstHostCallMs": 0.01131, + "firstOutputMs": 62.329268, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0012729999999999998, + "name": "Engine" + }, + { + "ms": 0.145711, + "name": "canonicalPreopens" + }, + { + "ms": 7.3907110000000005, + "name": "moduleRead" + }, + { + "ms": 3.8117110000000003, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.00775, + "name": "importValidation" + }, + { + "ms": 0.192449, + "name": "Linker" + }, + { + "ms": 0.016062999999999997, + "name": "Store" + }, + { + "ms": 0.041881, + "name": "Instance" + }, + { + "ms": 0.061872, + "name": "signalMaskInit" + }, + { + "ms": 0.003565, + "name": "entrypointLookup" + }, + { + "ms": 50.153527000000004, + "name": "wasi.start" + }, + { + "ms": 0.050982, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 62.487556 + }, + "memory": { + "start": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63623, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291385344, + "peakRssBytes": 291532800, + "pssBytes": 293221376, + "virtualBytes": 8326873088, + "minorFaults": 63697, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63697, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 5, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 79.1046609999903, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.071553, + "firstHostCallMs": 0.011296, + "firstOutputMs": 60.604676000000005, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.000979, + "name": "Engine" + }, + { + "ms": 0.116448, + "name": "canonicalPreopens" + }, + { + "ms": 6.580284, + "name": "moduleRead" + }, + { + "ms": 5.258221000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007645999999999999, + "name": "importValidation" + }, + { + "ms": 0.19090900000000002, + "name": "Linker" + }, + { + "ms": 0.015453, + "name": "Store" + }, + { + "ms": 0.039087000000000004, + "name": "Instance" + }, + { + "ms": 0.109833, + "name": "signalMaskInit" + }, + { + "ms": 0.003561, + "name": "entrypointLookup" + }, + { + "ms": 47.798065, + "name": "wasi.start" + }, + { + "ms": 0.0679, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 60.826337 + }, + "memory": { + "start": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63697, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291385344, + "peakRssBytes": 291532800, + "pssBytes": 293221376, + "virtualBytes": 8326873088, + "minorFaults": 63771, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63771, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 6, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 84.72650900000008, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 3694, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.620236, + "firstHostCallMs": 0.010817, + "firstOutputMs": 63.312952, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1208565, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/ls", + "phases": [ + { + "ms": 0.0011790000000000001, + "name": "Engine" + }, + { + "ms": 0.11310200000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.553078, + "name": "moduleRead" + }, + { + "ms": 3.851654, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.007635, + "name": "importValidation" + }, + { + "ms": 0.196194, + "name": "Linker" + }, + { + "ms": 0.016721, + "name": "Store" + }, + { + "ms": 0.053689, + "name": "Instance" + }, + { + "ms": 0.050555, + "name": "signalMaskInit" + }, + { + "ms": 0.003785, + "name": "entrypointLookup" + }, + { + "ms": 51.985387, + "name": "wasi.start" + }, + { + "ms": 0.064731, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1208565, + "totalMs": 63.53596 + }, + "memory": { + "start": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292586496, + "virtualBytes": 3962417152, + "minorFaults": 63771, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 291385344, + "peakRssBytes": 291532800, + "pssBytes": 293220352, + "virtualBytes": 8326873088, + "minorFaults": 63845, + "majorFaults": 0 + }, + "end": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292585472, + "virtualBytes": 3962417152, + "minorFaults": 63845, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 7, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "shell", + "command": "sh", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3928.6977999999945, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3480.089621, + "firstHostCallMs": 0.024292, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.00249, + "name": "Engine" + }, + { + "ms": 0.475021, + "name": "canonicalPreopens" + }, + { + "ms": 13.239419, + "name": "moduleRead" + }, + { + "ms": 13.791292, + "name": "profileValidation" + }, + { + "ms": 3449.202434, + "name": "moduleCompile" + }, + { + "ms": 0.010229, + "name": "importValidation" + }, + { + "ms": 0.190579, + "name": "Linker" + }, + { + "ms": 0.018338999999999998, + "name": "Store" + }, + { + "ms": 1.6305180000000001, + "name": "Instance" + }, + { + "ms": 0.08348699999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.004889, + "name": "entrypointLookup" + }, + { + "ms": 423.071148, + "name": "wasi.start" + }, + { + "ms": 0.056969000000000006, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 3903.242233 + }, + "memory": { + "start": { + "rssBytes": 290787328, + "peakRssBytes": 291532800, + "pssBytes": 292585472, + "virtualBytes": 3962417152, + "minorFaults": 63845, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 420253696, + "peakRssBytes": 420491264, + "pssBytes": 423235584, + "virtualBytes": 12865949696, + "minorFaults": 100398, + "majorFaults": 0 + }, + "end": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100398, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 2, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 2, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 1236768, + "wasmtimeChargedModuleBytes": 10717096, + "wasmtimeCompileTimeMicros": 1099928, + "wasmtimeProcessRetainedRssBytes": 290787328, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 80.97056700001121, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 25.950181999999998, + "firstHostCallMs": 0.030794, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.0010240000000000002, + "name": "Engine" + }, + { + "ms": 0.113764, + "name": "canonicalPreopens" + }, + { + "ms": 11.293552, + "name": "moduleRead" + }, + { + "ms": 12.540195, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010807, + "name": "importValidation" + }, + { + "ms": 0.20938400000000001, + "name": "Linker" + }, + { + "ms": 0.019892, + "name": "Store" + }, + { + "ms": 0.17602500000000001, + "name": "Instance" + }, + { + "ms": 0.08004, + "name": "signalMaskInit" + }, + { + "ms": 0.005148, + "name": "entrypointLookup" + }, + { + "ms": 29.800263, + "name": "wasi.start" + }, + { + "ms": 0.055311, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 55.802471 + }, + "memory": { + "start": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100398, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 423383040, + "virtualBytes": 12865949696, + "minorFaults": 100487, + "majorFaults": 0 + }, + "end": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100487, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 8, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 97.67350400000578, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 25.835658000000002, + "firstHostCallMs": 0.015179, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001085, + "name": "Engine" + }, + { + "ms": 0.116038, + "name": "canonicalPreopens" + }, + { + "ms": 11.618516000000001, + "name": "moduleRead" + }, + { + "ms": 12.305292000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010995000000000001, + "name": "importValidation" + }, + { + "ms": 0.199607, + "name": "Linker" + }, + { + "ms": 0.018240000000000003, + "name": "Store" + }, + { + "ms": 0.047703, + "name": "Instance" + }, + { + "ms": 0.07637100000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.004245, + "name": "entrypointLookup" + }, + { + "ms": 47.96283, + "name": "wasi.start" + }, + { + "ms": 0.055194, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 73.86295 + }, + "memory": { + "start": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100487, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 423383040, + "virtualBytes": 12865949696, + "minorFaults": 100576, + "majorFaults": 0 + }, + "end": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100576, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 10, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 97.13123999998788, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.075196, + "firstHostCallMs": 0.027225000000000003, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.002246, + "name": "Engine" + }, + { + "ms": 0.14049599999999998, + "name": "canonicalPreopens" + }, + { + "ms": 11.930223, + "name": "moduleRead" + }, + { + "ms": 14.734603, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010793, + "name": "importValidation" + }, + { + "ms": 0.201547, + "name": "Linker" + }, + { + "ms": 0.017716000000000003, + "name": "Store" + }, + { + "ms": 1.256947, + "name": "Instance" + }, + { + "ms": 0.085203, + "name": "signalMaskInit" + }, + { + "ms": 0.004009, + "name": "entrypointLookup" + }, + { + "ms": 39.455364, + "name": "wasi.start" + }, + { + "ms": 0.053556, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 69.347861 + }, + "memory": { + "start": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100576, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 423383040, + "virtualBytes": 12865949696, + "minorFaults": 100665, + "majorFaults": 0 + }, + "end": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100665, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 12, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 89.70928599999752, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 5, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 29.875539999999997, + "firstHostCallMs": 0.020059, + "firstOutputMs": null, + "guestLinearMemoryBytes": 1769472, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3082692, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sh", + "phases": [ + { + "ms": 0.001962, + "name": "Engine" + }, + { + "ms": 0.134084, + "name": "canonicalPreopens" + }, + { + "ms": 12.748692, + "name": "moduleRead" + }, + { + "ms": 13.244957999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011062, + "name": "importValidation" + }, + { + "ms": 0.206548, + "name": "Linker" + }, + { + "ms": 0.01797, + "name": "Store" + }, + { + "ms": 1.9652469999999997, + "name": "Instance" + }, + { + "ms": 0.08022, + "name": "signalMaskInit" + }, + { + "ms": 0.005881, + "name": "entrypointLookup" + }, + { + "ms": 31.259546, + "name": "wasi.start" + }, + { + "ms": 0.039273, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3082692, + "totalMs": 61.174464 + }, + "memory": { + "start": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422539264, + "virtualBytes": 4137037824, + "minorFaults": 100665, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 423384064, + "virtualBytes": 12865949696, + "minorFaults": 100754, + "majorFaults": 0 + }, + "end": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422540288, + "virtualBytes": 4137037824, + "minorFaults": 100754, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 14, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "curl", + "command": "curl", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1590.64798899999, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1561.6386069999999, + "firstHostCallMs": 0.014070000000000001, + "firstOutputMs": 1570.0364339999999, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.0011149999999999999, + "name": "Engine" + }, + { + "ms": 0.148712, + "name": "canonicalPreopens" + }, + { + "ms": 6.796758, + "name": "moduleRead" + }, + { + "ms": 6.947304, + "name": "profileValidation" + }, + { + "ms": 1546.530175, + "name": "moduleCompile" + }, + { + "ms": 0.011628, + "name": "importValidation" + }, + { + "ms": 0.187032, + "name": "Linker" + }, + { + "ms": 0.017748, + "name": "Store" + }, + { + "ms": 0.168904, + "name": "Instance" + }, + { + "ms": 0.06647, + "name": "signalMaskInit" + }, + { + "ms": 0.0033770000000000002, + "name": "entrypointLookup" + }, + { + "ms": 8.614834, + "name": "wasi.start" + }, + { + "ms": 0.055319, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 1570.3450559999999 + }, + "memory": { + "start": { + "rssBytes": 419749888, + "peakRssBytes": 420491264, + "pssBytes": 422540288, + "virtualBytes": 4137037824, + "minorFaults": 100754, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427245568, + "peakRssBytes": 427802624, + "pssBytes": 430583808, + "virtualBytes": 8508891136, + "minorFaults": 102146, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430563328, + "virtualBytes": 4144435200, + "minorFaults": 102146, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 4, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 4, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 4618526, + "wasmtimeChargedModuleBytes": 37771160, + "wasmtimeCompileTimeMicros": 4939805, + "wasmtimeProcessRetainedRssBytes": 419749888, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 50.246343999984674, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.002405, + "firstHostCallMs": 0.030369, + "firstOutputMs": 23.193234, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001434, + "name": "Engine" + }, + { + "ms": 0.10401, + "name": "canonicalPreopens" + }, + { + "ms": 6.947184, + "name": "moduleRead" + }, + { + "ms": 6.25182, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013507, + "name": "importValidation" + }, + { + "ms": 0.204885, + "name": "Linker" + }, + { + "ms": 0.018306, + "name": "Store" + }, + { + "ms": 1.578166, + "name": "Instance" + }, + { + "ms": 0.061271000000000006, + "name": "signalMaskInit" + }, + { + "ms": 0.004019, + "name": "entrypointLookup" + }, + { + "ms": 7.403545, + "name": "wasi.start" + }, + { + "ms": 1.387893, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 24.762394 + }, + "memory": { + "start": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430563328, + "virtualBytes": 4144435200, + "minorFaults": 102146, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430698496, + "virtualBytes": 8508891136, + "minorFaults": 102198, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430698496, + "virtualBytes": 4144435200, + "minorFaults": 102198, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 16, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 58.69871999998577, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.091202, + "firstHostCallMs": 0.015201000000000001, + "firstOutputMs": 27.352411, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001244, + "name": "Engine" + }, + { + "ms": 0.119773, + "name": "canonicalPreopens" + }, + { + "ms": 7.355042, + "name": "moduleRead" + }, + { + "ms": 6.675061, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013784999999999999, + "name": "importValidation" + }, + { + "ms": 0.25839100000000004, + "name": "Linker" + }, + { + "ms": 0.022083, + "name": "Store" + }, + { + "ms": 0.748382, + "name": "Instance" + }, + { + "ms": 0.062111, + "name": "signalMaskInit" + }, + { + "ms": 0.006431, + "name": "entrypointLookup" + }, + { + "ms": 11.530129, + "name": "wasi.start" + }, + { + "ms": 2.506209, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 30.15634 + }, + "memory": { + "start": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430698496, + "virtualBytes": 4144435200, + "minorFaults": 102198, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 431255552, + "virtualBytes": 8508891136, + "minorFaults": 102252, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430718976, + "virtualBytes": 4144435200, + "minorFaults": 102252, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 17, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 47.63124700001208, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 16.863903, + "firstHostCallMs": 0.023645000000000003, + "firstOutputMs": 21.605072999999997, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.001313, + "name": "Engine" + }, + { + "ms": 0.152392, + "name": "canonicalPreopens" + }, + { + "ms": 6.993407, + "name": "moduleRead" + }, + { + "ms": 8.546163, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.018636, + "name": "importValidation" + }, + { + "ms": 0.21035399999999999, + "name": "Linker" + }, + { + "ms": 0.017841, + "name": "Store" + }, + { + "ms": 0.050397, + "name": "Instance" + }, + { + "ms": 0.066744, + "name": "signalMaskInit" + }, + { + "ms": 0.0037589999999999998, + "name": "entrypointLookup" + }, + { + "ms": 4.9057900000000005, + "name": "wasi.start" + }, + { + "ms": 1.867023, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 23.654984 + }, + "memory": { + "start": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430718976, + "virtualBytes": 4144435200, + "minorFaults": 102252, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 431226880, + "virtualBytes": 8508891136, + "minorFaults": 102301, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430718976, + "virtualBytes": 4144435200, + "minorFaults": 102301, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 18, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 54.51222400000552, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 27, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 17.033475, + "firstHostCallMs": 0.017255, + "firstOutputMs": 27.312202, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1561500, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/curl", + "phases": [ + { + "ms": 0.00159, + "name": "Engine" + }, + { + "ms": 0.12240800000000002, + "name": "canonicalPreopens" + }, + { + "ms": 7.044436, + "name": "moduleRead" + }, + { + "ms": 6.907496, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015649, + "name": "importValidation" + }, + { + "ms": 0.230611, + "name": "Linker" + }, + { + "ms": 0.027151, + "name": "Store" + }, + { + "ms": 1.809392, + "name": "Instance" + }, + { + "ms": 0.08769099999999999, + "name": "signalMaskInit" + }, + { + "ms": 0.007462, + "name": "entrypointLookup" + }, + { + "ms": 10.453895, + "name": "wasi.start" + }, + { + "ms": 0.04185, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1561500, + "totalMs": 27.533343 + }, + "memory": { + "start": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430718976, + "virtualBytes": 4144435200, + "minorFaults": 102301, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 431247360, + "virtualBytes": 8508891136, + "minorFaults": 102351, + "majorFaults": 0 + }, + "end": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430723072, + "virtualBytes": 4144435200, + "minorFaults": 102351, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 19, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "sqlite", + "command": "sqlite3", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1272.6397069999948, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1245.3927760000001, + "firstHostCallMs": 0.012648, + "firstOutputMs": 1247.0371010000001, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001026, + "name": "Engine" + }, + { + "ms": 0.106989, + "name": "canonicalPreopens" + }, + { + "ms": 5.171218, + "name": "moduleRead" + }, + { + "ms": 4.800961, + "name": "profileValidation" + }, + { + "ms": 1234.529632, + "name": "moduleCompile" + }, + { + "ms": 0.008686, + "name": "importValidation" + }, + { + "ms": 0.180626, + "name": "Linker" + }, + { + "ms": 0.016181, + "name": "Store" + }, + { + "ms": 0.08494, + "name": "Instance" + }, + { + "ms": 0.055875999999999995, + "name": "signalMaskInit" + }, + { + "ms": 0.003334, + "name": "entrypointLookup" + }, + { + "ms": 1.7061030000000001, + "name": "wasi.start" + }, + { + "ms": 0.044646, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 1247.182612 + }, + "memory": { + "start": { + "rssBytes": 427151360, + "peakRssBytes": 427802624, + "pssBytes": 430723072, + "virtualBytes": 4144435200, + "minorFaults": 102351, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432816128, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4150181888, + "minorFaults": 103255, + "majorFaults": 0 + }, + "end": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103255, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 5, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 5, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 6180026, + "wasmtimeChargedModuleBytes": 50263160, + "wasmtimeCompileTimeMicros": 6486335, + "wasmtimeProcessRetainedRssBytes": 427151360, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 36.88527999998769, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.50301, + "firstHostCallMs": 0.016208, + "firstOutputMs": 13.161168, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.0016530000000000002, + "name": "Engine" + }, + { + "ms": 0.10450699999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.333036, + "name": "moduleRead" + }, + { + "ms": 4.967782, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010074, + "name": "importValidation" + }, + { + "ms": 0.20089700000000002, + "name": "Linker" + }, + { + "ms": 0.016146999999999998, + "name": "Store" + }, + { + "ms": 0.375643, + "name": "Instance" + }, + { + "ms": 0.049198, + "name": "signalMaskInit" + }, + { + "ms": 0.002707, + "name": "entrypointLookup" + }, + { + "ms": 1.7237019999999998, + "name": "wasi.start" + }, + { + "ms": 1.728551, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 14.997948000000001 + }, + "memory": { + "start": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103255, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436264960, + "virtualBytes": 8514359296, + "minorFaults": 103305, + "majorFaults": 0 + }, + "end": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103305, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 20, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 35.881025999988196, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.462649, + "firstHostCallMs": 0.014754, + "firstOutputMs": 13.043579, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001312, + "name": "Engine" + }, + { + "ms": 0.11740199999999999, + "name": "canonicalPreopens" + }, + { + "ms": 5.278378, + "name": "moduleRead" + }, + { + "ms": 4.837860999999999, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008988, + "name": "importValidation" + }, + { + "ms": 0.201799, + "name": "Linker" + }, + { + "ms": 0.017772, + "name": "Store" + }, + { + "ms": 0.532623, + "name": "Instance" + }, + { + "ms": 0.023212, + "name": "signalMaskInit" + }, + { + "ms": 0.002781, + "name": "entrypointLookup" + }, + { + "ms": 1.642159, + "name": "wasi.start" + }, + { + "ms": 0.036083, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 13.168024 + }, + "memory": { + "start": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103305, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436264960, + "virtualBytes": 4149915648, + "minorFaults": 103355, + "majorFaults": 0 + }, + "end": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103355, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 21, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 36.5673700000043, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.450347, + "firstHostCallMs": 0.016087, + "firstOutputMs": 14.049629, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.001108, + "name": "Engine" + }, + { + "ms": 0.107421, + "name": "canonicalPreopens" + }, + { + "ms": 5.109695, + "name": "moduleRead" + }, + { + "ms": 4.854896, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010243, + "name": "importValidation" + }, + { + "ms": 0.20607199999999998, + "name": "Linker" + }, + { + "ms": 0.017317, + "name": "Store" + }, + { + "ms": 1.615777, + "name": "Instance" + }, + { + "ms": 0.080425, + "name": "signalMaskInit" + }, + { + "ms": 0.003513, + "name": "entrypointLookup" + }, + { + "ms": 1.662397, + "name": "wasi.start" + }, + { + "ms": 0.036180000000000004, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 14.174978 + }, + "memory": { + "start": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103355, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436264960, + "virtualBytes": 4149915648, + "minorFaults": 103405, + "majorFaults": 0 + }, + "end": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103405, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 22, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 40.392848999996204, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 7, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.538501, + "firstHostCallMs": 0.015472, + "firstOutputMs": 17.370331, + "guestLinearMemoryBytes": 16777216, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 878882, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sqlite3", + "phases": [ + { + "ms": 0.00134, + "name": "Engine" + }, + { + "ms": 0.11597800000000001, + "name": "canonicalPreopens" + }, + { + "ms": 5.209011, + "name": "moduleRead" + }, + { + "ms": 7.125563, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.014702999999999999, + "name": "importValidation" + }, + { + "ms": 0.282017, + "name": "Linker" + }, + { + "ms": 0.022036999999999998, + "name": "Store" + }, + { + "ms": 0.060318000000000004, + "name": "Instance" + }, + { + "ms": 0.0522, + "name": "signalMaskInit" + }, + { + "ms": 0.005743, + "name": "entrypointLookup" + }, + { + "ms": 4.127446, + "name": "wasi.start" + }, + { + "ms": 0.044326000000000004, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 878882, + "totalMs": 17.539778 + }, + "memory": { + "start": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103405, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436264960, + "virtualBytes": 8511991808, + "minorFaults": 103455, + "majorFaults": 0 + }, + "end": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103455, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 23, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "vim", + "command": "vim", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3667.212870999996, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3633.6671589999996, + "firstHostCallMs": 0.0144, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.00091, + "name": "Engine" + }, + { + "ms": 0.107042, + "name": "canonicalPreopens" + }, + { + "ms": 12.075961, + "name": "moduleRead" + }, + { + "ms": 14.461165999999999, + "name": "profileValidation" + }, + { + "ms": 3603.434174, + "name": "moduleCompile" + }, + { + "ms": 0.012558000000000001, + "name": "importValidation" + }, + { + "ms": 0.184014, + "name": "Linker" + }, + { + "ms": 0.020206, + "name": "Store" + }, + { + "ms": 1.931121, + "name": "Instance" + }, + { + "ms": 0.08468300000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.004432, + "name": "entrypointLookup" + }, + { + "ms": 7.310025, + "name": "wasi.start" + }, + { + "ms": 0.051203, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 3641.043538 + }, + "memory": { + "start": { + "rssBytes": 432619520, + "peakRssBytes": 433025024, + "pssBytes": 436191232, + "virtualBytes": 4149903360, + "minorFaults": 103455, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449695744, + "peakRssBytes": 449781760, + "pssBytes": 453328896, + "virtualBytes": 8530776064, + "minorFaults": 107583, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107583, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 6, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 6, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 7058908, + "wasmtimeChargedModuleBytes": 57294216, + "wasmtimeCompileTimeMicros": 7720865, + "wasmtimeProcessRetainedRssBytes": 432619520, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 74.18457899999339, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.475016, + "firstHostCallMs": 0.018235, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001128, + "name": "Engine" + }, + { + "ms": 0.129544, + "name": "canonicalPreopens" + }, + { + "ms": 11.321667, + "name": "moduleRead" + }, + { + "ms": 14.632121, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.013579, + "name": "importValidation" + }, + { + "ms": 0.209816, + "name": "Linker" + }, + { + "ms": 0.017025000000000002, + "name": "Store" + }, + { + "ms": 2.686293, + "name": "Instance" + }, + { + "ms": 0.081954, + "name": "signalMaskInit" + }, + { + "ms": 0.004967, + "name": "entrypointLookup" + }, + { + "ms": 13.058703, + "name": "wasi.start" + }, + { + "ms": 1.896715, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 45.403881 + }, + "memory": { + "start": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107583, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449589248, + "peakRssBytes": 449781760, + "pssBytes": 453251072, + "virtualBytes": 8530776064, + "minorFaults": 107680, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107680, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 24, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 71.66153599999961, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 27.611138, + "firstHostCallMs": 0.014962, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.000909, + "name": "Engine" + }, + { + "ms": 0.112161, + "name": "canonicalPreopens" + }, + { + "ms": 11.214607999999998, + "name": "moduleRead" + }, + { + "ms": 14.469421, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.016441, + "name": "importValidation" + }, + { + "ms": 0.224405, + "name": "Linker" + }, + { + "ms": 0.01683, + "name": "Store" + }, + { + "ms": 0.050252, + "name": "Instance" + }, + { + "ms": 0.11178700000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.0037960000000000003, + "name": "entrypointLookup" + }, + { + "ms": 21.215225, + "name": "wasi.start" + }, + { + "ms": 1.705424, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 50.510130000000004 + }, + "memory": { + "start": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107680, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449589248, + "peakRssBytes": 449781760, + "pssBytes": 453251072, + "virtualBytes": 8530776064, + "minorFaults": 107777, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107777, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 25, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 81.08445900000515, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.456601, + "firstHostCallMs": 0.029575, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.002326, + "name": "Engine" + }, + { + "ms": 0.175965, + "name": "canonicalPreopens" + }, + { + "ms": 11.300762, + "name": "moduleRead" + }, + { + "ms": 17.106336, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012916, + "name": "importValidation" + }, + { + "ms": 0.205837, + "name": "Linker" + }, + { + "ms": 0.017922, + "name": "Store" + }, + { + "ms": 0.201602, + "name": "Instance" + }, + { + "ms": 0.051487, + "name": "signalMaskInit" + }, + { + "ms": 0.002762, + "name": "entrypointLookup" + }, + { + "ms": 20.806552999999997, + "name": "wasi.start" + }, + { + "ms": 0.09873, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 51.342154 + }, + "memory": { + "start": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107777, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449589248, + "peakRssBytes": 449781760, + "pssBytes": 453267456, + "virtualBytes": 8530776064, + "minorFaults": 107874, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107874, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 26, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 274.49729100000695, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 0, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.445418000000004, + "firstHostCallMs": 0.031313, + "firstOutputMs": null, + "guestLinearMemoryBytes": 720896, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 2854951, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/vim", + "phases": [ + { + "ms": 0.001461, + "name": "Engine" + }, + { + "ms": 0.167108, + "name": "canonicalPreopens" + }, + { + "ms": 11.712041, + "name": "moduleRead" + }, + { + "ms": 16.002515, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015012, + "name": "importValidation" + }, + { + "ms": 0.23194399999999998, + "name": "Linker" + }, + { + "ms": 0.017399, + "name": "Store" + }, + { + "ms": 1.671739, + "name": "Instance" + }, + { + "ms": 0.225114, + "name": "signalMaskInit" + }, + { + "ms": 0.005446, + "name": "entrypointLookup" + }, + { + "ms": 19.398617, + "name": "wasi.start" + }, + { + "ms": 0.761094, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 2854951, + "totalMs": 51.595328 + }, + "memory": { + "start": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107874, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 449589248, + "peakRssBytes": 449781760, + "pssBytes": 453246976, + "virtualBytes": 8530776064, + "minorFaults": 107971, + "majorFaults": 0 + }, + "end": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107971, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 27, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "large-module", + "command": "git", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 3953.44120500001, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 3921.897523, + "firstHostCallMs": 0.014718, + "firstOutputMs": 3923.411713, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001154, + "name": "Engine" + }, + { + "ms": 0.15768, + "name": "canonicalPreopens" + }, + { + "ms": 13.624697, + "name": "moduleRead" + }, + { + "ms": 16.104647999999997, + "name": "profileValidation" + }, + { + "ms": 3889.91505, + "name": "moduleCompile" + }, + { + "ms": 0.01429, + "name": "importValidation" + }, + { + "ms": 0.176176, + "name": "Linker" + }, + { + "ms": 0.016491, + "name": "Store" + }, + { + "ms": 0.23311400000000002, + "name": "Instance" + }, + { + "ms": 0.082778, + "name": "signalMaskInit" + }, + { + "ms": 0.003518, + "name": "entrypointLookup" + }, + { + "ms": 1.662226, + "name": "wasi.start" + }, + { + "ms": 0.042555, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 3923.616634 + }, + "memory": { + "start": { + "rssBytes": 449036288, + "peakRssBytes": 449781760, + "pssBytes": 452608000, + "virtualBytes": 4166320128, + "minorFaults": 107971, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 476336128, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184928256, + "minorFaults": 114699, + "majorFaults": 0 + }, + "end": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114699, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 7, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 7, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 9913859, + "wasmtimeChargedModuleBytes": 80133824, + "wasmtimeCompileTimeMicros": 11324299, + "wasmtimeProcessRetainedRssBytes": 449036288, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 59.968914000026416, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.507182, + "firstHostCallMs": 0.059039999999999995, + "firstOutputMs": 33.055065, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001687, + "name": "Engine" + }, + { + "ms": 0.11660999999999999, + "name": "canonicalPreopens" + }, + { + "ms": 12.875829, + "name": "moduleRead" + }, + { + "ms": 15.975900000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.015711, + "name": "importValidation" + }, + { + "ms": 0.204453, + "name": "Linker" + }, + { + "ms": 0.017584, + "name": "Store" + }, + { + "ms": 0.586469, + "name": "Instance" + }, + { + "ms": 0.080594, + "name": "signalMaskInit" + }, + { + "ms": 0.0038449999999999995, + "name": "entrypointLookup" + }, + { + "ms": 1.704087, + "name": "wasi.start" + }, + { + "ms": 0.032654999999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 33.25945 + }, + "memory": { + "start": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114699, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479937536, + "virtualBytes": 4184928256, + "minorFaults": 114743, + "majorFaults": 0 + }, + "end": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114743, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 28, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 73.63681900000665, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 34.447773, + "firstHostCallMs": 0.019318000000000002, + "firstOutputMs": 35.713962, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.000837, + "name": "Engine" + }, + { + "ms": 0.110638, + "name": "canonicalPreopens" + }, + { + "ms": 13.491389, + "name": "moduleRead" + }, + { + "ms": 16.23556, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.018981, + "name": "importValidation" + }, + { + "ms": 0.224784, + "name": "Linker" + }, + { + "ms": 0.018071999999999998, + "name": "Store" + }, + { + "ms": 2.350933, + "name": "Instance" + }, + { + "ms": 0.080427, + "name": "signalMaskInit" + }, + { + "ms": 0.005625000000000001, + "name": "entrypointLookup" + }, + { + "ms": 1.7182650000000002, + "name": "wasi.start" + }, + { + "ms": 0.040506, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 35.918235 + }, + "memory": { + "start": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114743, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479936512, + "virtualBytes": 4184928256, + "minorFaults": 114787, + "majorFaults": 0 + }, + "end": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479862784, + "virtualBytes": 4184915968, + "minorFaults": 114787, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 29, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 320.0608240000147, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 30.995702, + "firstHostCallMs": 0.018377, + "firstOutputMs": 33.620501999999995, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001063, + "name": "Engine" + }, + { + "ms": 0.163104, + "name": "canonicalPreopens" + }, + { + "ms": 12.889559, + "name": "moduleRead" + }, + { + "ms": 15.796569, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.017762999999999998, + "name": "importValidation" + }, + { + "ms": 0.21346299999999999, + "name": "Linker" + }, + { + "ms": 0.018876999999999998, + "name": "Store" + }, + { + "ms": 0.24058200000000002, + "name": "Instance" + }, + { + "ms": 0.07076400000000001, + "name": "signalMaskInit" + }, + { + "ms": 0.003974, + "name": "entrypointLookup" + }, + { + "ms": 2.776734, + "name": "wasi.start" + }, + { + "ms": 0.922242, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 34.709617 + }, + "memory": { + "start": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479862784, + "virtualBytes": 4184915968, + "minorFaults": 114787, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479937536, + "virtualBytes": 8549371904, + "minorFaults": 114831, + "majorFaults": 0 + }, + "end": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114831, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 30, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 63.54172199999448, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 19, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 31.431369, + "firstHostCallMs": 0.015965999999999998, + "firstOutputMs": 34.074435, + "guestLinearMemoryBytes": 9371648, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 3397393, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/git", + "phases": [ + { + "ms": 0.001107, + "name": "Engine" + }, + { + "ms": 0.152247, + "name": "canonicalPreopens" + }, + { + "ms": 12.989651, + "name": "moduleRead" + }, + { + "ms": 16.110736, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.017444, + "name": "importValidation" + }, + { + "ms": 0.22614900000000002, + "name": "Linker" + }, + { + "ms": 0.019778, + "name": "Store" + }, + { + "ms": 0.254383, + "name": "Instance" + }, + { + "ms": 0.059869, + "name": "signalMaskInit" + }, + { + "ms": 0.004213, + "name": "entrypointLookup" + }, + { + "ms": 2.807512, + "name": "wasi.start" + }, + { + "ms": 0.42026800000000003, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 3397393, + "totalMs": 34.663275 + }, + "memory": { + "start": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114831, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479937536, + "virtualBytes": 8549105664, + "minorFaults": 114875, + "majorFaults": 0 + }, + "end": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479863808, + "virtualBytes": 4184915968, + "minorFaults": 114875, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 31, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "compute-heavy", + "command": "sha256sum", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 721.775582000002, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 645.442094, + "firstHostCallMs": 0.017855, + "firstOutputMs": 689.720676, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001184, + "name": "Engine" + }, + { + "ms": 0.17199, + "name": "canonicalPreopens" + }, + { + "ms": 7.338688, + "name": "moduleRead" + }, + { + "ms": 2.61496, + "name": "profileValidation" + }, + { + "ms": 633.957896, + "name": "moduleCompile" + }, + { + "ms": 0.007781999999999999, + "name": "importValidation" + }, + { + "ms": 0.188964, + "name": "Linker" + }, + { + "ms": 0.017142, + "name": "Store" + }, + { + "ms": 0.306551, + "name": "Instance" + }, + { + "ms": 0.069036, + "name": "signalMaskInit" + }, + { + "ms": 0.003061, + "name": "entrypointLookup" + }, + { + "ms": 44.485205, + "name": "wasi.start" + }, + { + "ms": 2.0771409999999997, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 692.0113230000001 + }, + "memory": { + "start": { + "rssBytes": 476291072, + "peakRssBytes": 477024256, + "pssBytes": 479862784, + "virtualBytes": 4184915968, + "minorFaults": 114875, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480342016, + "peakRssBytes": 480673792, + "pssBytes": 484254720, + "virtualBytes": 8553234432, + "minorFaults": 115898, + "majorFaults": 0 + }, + "end": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 115898, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 8, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 13311252, + "wasmtimeChargedModuleBytes": 107312968, + "wasmtimeCompileTimeMicros": 15214214, + "wasmtimeProcessRetainedRssBytes": 476291072, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 100.19057999999495, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.165735, + "firstHostCallMs": 0.054272, + "firstOutputMs": 61.039634, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.002538, + "name": "Engine" + }, + { + "ms": 0.198405, + "name": "canonicalPreopens" + }, + { + "ms": 6.89021, + "name": "moduleRead" + }, + { + "ms": 2.767977, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.012284, + "name": "importValidation" + }, + { + "ms": 0.2376, + "name": "Linker" + }, + { + "ms": 0.023414, + "name": "Store" + }, + { + "ms": 3.120022, + "name": "Instance" + }, + { + "ms": 0.071046, + "name": "signalMaskInit" + }, + { + "ms": 0.0044410000000000005, + "name": "entrypointLookup" + }, + { + "ms": 47.121551, + "name": "wasi.start" + }, + { + "ms": 0.464758, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 61.679935 + }, + "memory": { + "start": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 115898, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480227328, + "peakRssBytes": 480673792, + "pssBytes": 484131840, + "virtualBytes": 8553234432, + "minorFaults": 115948, + "majorFaults": 0 + }, + "end": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 115948, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 32, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 82.93450800000574, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 13.159557000000001, + "firstHostCallMs": 0.018746, + "firstOutputMs": 56.458228999999996, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001166, + "name": "Engine" + }, + { + "ms": 0.11619499999999999, + "name": "canonicalPreopens" + }, + { + "ms": 6.885312, + "name": "moduleRead" + }, + { + "ms": 2.72199, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008308000000000001, + "name": "importValidation" + }, + { + "ms": 0.21343600000000001, + "name": "Linker" + }, + { + "ms": 0.019622, + "name": "Store" + }, + { + "ms": 2.331496, + "name": "Instance" + }, + { + "ms": 0.07242, + "name": "signalMaskInit" + }, + { + "ms": 0.005079, + "name": "entrypointLookup" + }, + { + "ms": 43.508933999999996, + "name": "wasi.start" + }, + { + "ms": 0.39142400000000005, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 56.991121 + }, + "memory": { + "start": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 115948, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 484131840, + "virtualBytes": 8553234432, + "minorFaults": 116000, + "majorFaults": 0 + }, + "end": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 116000, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 33, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 86.37503799999831, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.2012, + "firstHostCallMs": 0.017481, + "firstOutputMs": 56.62697, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001065, + "name": "Engine" + }, + { + "ms": 0.107099, + "name": "canonicalPreopens" + }, + { + "ms": 6.745862, + "name": "moduleRead" + }, + { + "ms": 2.945755, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011253, + "name": "importValidation" + }, + { + "ms": 0.29858100000000004, + "name": "Linker" + }, + { + "ms": 0.015773, + "name": "Store" + }, + { + "ms": 1.181449, + "name": "Instance" + }, + { + "ms": 0.117864, + "name": "signalMaskInit" + }, + { + "ms": 0.004659, + "name": "entrypointLookup" + }, + { + "ms": 44.635875, + "name": "wasi.start" + }, + { + "ms": 0.886112, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 57.659053 + }, + "memory": { + "start": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 116000, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 484130816, + "virtualBytes": 8553234432, + "minorFaults": 116050, + "majorFaults": 0 + }, + "end": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483725312, + "virtualBytes": 4188778496, + "minorFaults": 116050, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 34, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 81.57364300001063, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 98, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 11.802305, + "firstHostCallMs": 0.017663, + "firstOutputMs": 57.140139, + "guestLinearMemoryBytes": 2097152, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1349812, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/sha256sum", + "phases": [ + { + "ms": 0.001315, + "name": "Engine" + }, + { + "ms": 0.10908000000000001, + "name": "canonicalPreopens" + }, + { + "ms": 6.688765999999999, + "name": "moduleRead" + }, + { + "ms": 2.579374, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.008056, + "name": "importValidation" + }, + { + "ms": 0.201437, + "name": "Linker" + }, + { + "ms": 0.016308, + "name": "Store" + }, + { + "ms": 1.3827710000000002, + "name": "Instance" + }, + { + "ms": 0.047481, + "name": "signalMaskInit" + }, + { + "ms": 0.00301, + "name": "entrypointLookup" + }, + { + "ms": 45.552009, + "name": "wasi.start" + }, + { + "ms": 0.11260200000000001, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1349812, + "totalMs": 57.393489 + }, + "memory": { + "start": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 116050, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 484131840, + "virtualBytes": 8553234432, + "minorFaults": 116100, + "majorFaults": 0 + }, + "end": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 116100, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 35, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + } + } + ] + }, + { + "name": "host-call-heavy", + "command": "find", + "samples": [ + { + "index": 0, + "cacheState": "fresh", + "durationMs": 1430.9679920000199, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 1392.837599, + "firstHostCallMs": 0.016998, + "firstOutputMs": 1396.313912, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": false, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001236, + "name": "Engine" + }, + { + "ms": 0.150477, + "name": "canonicalPreopens" + }, + { + "ms": 7.156795, + "name": "moduleRead" + }, + { + "ms": 5.038145, + "name": "profileValidation" + }, + { + "ms": 1379.182998, + "name": "moduleCompile" + }, + { + "ms": 0.008698000000000001, + "name": "importValidation" + }, + { + "ms": 0.198326, + "name": "Linker" + }, + { + "ms": 0.018287, + "name": "Store" + }, + { + "ms": 0.25487699999999996, + "name": "Instance" + }, + { + "ms": 0.081419, + "name": "signalMaskInit" + }, + { + "ms": 0.00341, + "name": "entrypointLookup" + }, + { + "ms": 4.873064, + "name": "wasi.start" + }, + { + "ms": 0.055890999999999996, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 1397.820447 + }, + "memory": { + "start": { + "rssBytes": 480153600, + "peakRssBytes": 480673792, + "pssBytes": 483726336, + "virtualBytes": 4188778496, + "minorFaults": 116100, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 513073152, + "peakRssBytes": 518463488, + "pssBytes": 516838400, + "virtualBytes": 8568217600, + "minorFaults": 132952, + "majorFaults": 0 + }, + "end": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506421248, + "virtualBytes": 4203761664, + "minorFaults": 132952, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 9, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 14661064, + "wasmtimeChargedModuleBytes": 118111464, + "wasmtimeCompileTimeMicros": 15848172, + "wasmtimeProcessRetainedRssBytes": 480153600, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 1, + "cacheState": "warm", + "durationMs": 56.71630699999514, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 18.612844, + "firstHostCallMs": 0.02018, + "firstOutputMs": 22.379592, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.0010040000000000001, + "name": "Engine" + }, + { + "ms": 0.114555, + "name": "canonicalPreopens" + }, + { + "ms": 6.922270999999999, + "name": "moduleRead" + }, + { + "ms": 5.435166, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.011635, + "name": "importValidation" + }, + { + "ms": 0.219724, + "name": "Linker" + }, + { + "ms": 0.022257, + "name": "Store" + }, + { + "ms": 2.4692070000000004, + "name": "Instance" + }, + { + "ms": 0.08634, + "name": "signalMaskInit" + }, + { + "ms": 0.008444, + "name": "entrypointLookup" + }, + { + "ms": 8.728943, + "name": "wasi.start" + }, + { + "ms": 0.627164, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 25.448869 + }, + "memory": { + "start": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506421248, + "virtualBytes": 4203761664, + "minorFaults": 132952, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506494976, + "virtualBytes": 8568217600, + "minorFaults": 132985, + "majorFaults": 0 + }, + "end": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 132985, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 36, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 2, + "cacheState": "warm", + "durationMs": 42.20804399999906, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 12.508543000000001, + "firstHostCallMs": 0.019060999999999998, + "firstOutputMs": 16.960805, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001169, + "name": "Engine" + }, + { + "ms": 0.115675, + "name": "canonicalPreopens" + }, + { + "ms": 6.779757, + "name": "moduleRead" + }, + { + "ms": 4.441261, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010838, + "name": "importValidation" + }, + { + "ms": 0.20794100000000001, + "name": "Linker" + }, + { + "ms": 0.017886000000000003, + "name": "Store" + }, + { + "ms": 0.131123, + "name": "Instance" + }, + { + "ms": 0.054971, + "name": "signalMaskInit" + }, + { + "ms": 0.0033220000000000003, + "name": "entrypointLookup" + }, + { + "ms": 5.871391, + "name": "wasi.start" + }, + { + "ms": 1.772421, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 20.169465000000002 + }, + "memory": { + "start": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 132985, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506725376, + "virtualBytes": 8568217600, + "minorFaults": 133018, + "majorFaults": 0 + }, + "end": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 133018, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 37, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 3, + "cacheState": "warm", + "durationMs": 49.50890800001798, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 14.952143, + "firstHostCallMs": 0.042112, + "firstOutputMs": 18.876705, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001933, + "name": "Engine" + }, + { + "ms": 0.126609, + "name": "canonicalPreopens" + }, + { + "ms": 6.9353419999999995, + "name": "moduleRead" + }, + { + "ms": 4.558933000000001, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.010872, + "name": "importValidation" + }, + { + "ms": 0.209208, + "name": "Linker" + }, + { + "ms": 0.018040999999999998, + "name": "Store" + }, + { + "ms": 2.249, + "name": "Instance" + }, + { + "ms": 0.055316, + "name": "signalMaskInit" + }, + { + "ms": 0.0074789999999999995, + "name": "entrypointLookup" + }, + { + "ms": 5.4861889999999995, + "name": "wasi.start" + }, + { + "ms": 0.99501, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 21.456754 + }, + "memory": { + "start": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 133018, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506496000, + "virtualBytes": 8568217600, + "minorFaults": 133051, + "majorFaults": 0 + }, + "end": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 133051, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 38, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + } + }, + { + "index": 4, + "cacheState": "warm", + "durationMs": 48.86860899999738, + "exitCode": 0, + "passed": true, + "validationError": null, + "stdoutBytes": 2176, + "stderrBytes": 0, + "phase": { + "asyncStackBytes": 2097152, + "backend": "wasmtime", + "firstGuestHostCallMs": 15.623051, + "firstHostCallMs": 0.023102, + "firstOutputMs": 19.089790999999998, + "guestLinearMemoryBytes": 1835008, + "memoryAllocation": "on-demand", + "memoryInitCow": true, + "memoryInitializationIncludedInPhase": "Instance", + "moduleBytes": 1509578, + "moduleCacheHit": true, + "modulePath": "agentos-trusted-initial:/__secure_exec/commands/0/find", + "phases": [ + { + "ms": 0.001853, + "name": "Engine" + }, + { + "ms": 0.12395099999999999, + "name": "canonicalPreopens" + }, + { + "ms": 7.016472, + "name": "moduleRead" + }, + { + "ms": 5.169467, + "name": "profileValidation" + }, + { + "ms": 0, + "name": "moduleCompile" + }, + { + "ms": 0.009694999999999999, + "name": "importValidation" + }, + { + "ms": 0.205485, + "name": "Linker" + }, + { + "ms": 0.016931, + "name": "Store" + }, + { + "ms": 0.044267999999999995, + "name": "Instance" + }, + { + "ms": 0.077656, + "name": "signalMaskInit" + }, + { + "ms": 0.0034879999999999998, + "name": "entrypointLookup" + }, + { + "ms": 7.00908, + "name": "wasi.start" + }, + { + "ms": 1.129394, + "name": "Store.teardown" + } + ], + "reason": "completed", + "reservedStoreBytes": 144314880, + "sourceModuleBytes": 1509578, + "totalMs": 21.571066 + }, + "memory": { + "start": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 133051, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506496000, + "virtualBytes": 8568217600, + "minorFaults": 133084, + "majorFaults": 0 + }, + "end": { + "rssBytes": 502849536, + "peakRssBytes": 518463488, + "pssBytes": 506422272, + "virtualBytes": 4203761664, + "minorFaults": 133084, + "majorFaults": 0 + } + }, + "resourceBefore": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 39, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + }, + "resourceAfter": { + "runningProcesses": 0, + "openFds": 0, + "pipes": 0, + "pipeBufferedBytes": 0, + "ptys": 0, + "ptyBufferedInputBytes": 0, + "ptyBufferedOutputBytes": 0, + "sockets": 0, + "socketBufferedBytes": 0, + "socketDatagramQueueLen": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 10, + "wasmtimeModuleCacheHits": 40, + "wasmtimeModuleCacheMisses": 10, + "wasmtimeModuleCacheEvictions": 0, + "wasmtimeCompiledSourceBytes": 16170642, + "wasmtimeChargedModuleBytes": 130188088, + "wasmtimeCompileTimeMicros": 17227355, + "wasmtimeProcessRetainedRssBytes": 502849536, + "kernelBufferedBytes": 0 + } + } + ] + } + ] + } + ], + "concurrency": [ + { + "backend": "v8", + "levels": [ + { + "level": 1, + "mode": "repeated", + "durationMs": 72.93038700000034, + "throughputPerSecond": 13.711705657067133, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 260198400, + "peakRssBytes": 372658176, + "pssBytes": 261588992, + "virtualBytes": 3888070656, + "minorFaults": 181213, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 287027200, + "peakRssBytes": 372658176, + "pssBytes": 273508352, + "virtualBytes": 4640292864, + "minorFaults": 189857, + "majorFaults": 1 + }, + "end": { + "rssBytes": 272084992, + "peakRssBytes": 372658176, + "pssBytes": 273508352, + "virtualBytes": 3955179520, + "minorFaults": 189857, + "majorFaults": 1 + } + }, + "drainMs": 25.58856299999752 + }, + { + "level": 1, + "mode": "diverse", + "durationMs": 75.2183500000101, + "throughputPerSecond": 13.29462823898511, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 272084992, + "peakRssBytes": 372658176, + "pssBytes": 273508352, + "virtualBytes": 3955179520, + "minorFaults": 189857, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 287072256, + "peakRssBytes": 372658176, + "pssBytes": 273598464, + "virtualBytes": 4640546816, + "minorFaults": 196124, + "majorFaults": 1 + }, + "end": { + "rssBytes": 272404480, + "peakRssBytes": 372658176, + "pssBytes": 273598464, + "virtualBytes": 3955179520, + "minorFaults": 196124, + "majorFaults": 1 + } + }, + "drainMs": 25.574350000009872 + }, + { + "level": 10, + "mode": "repeated", + "durationMs": 494.0117540000065, + "throughputPerSecond": 20.24243334096838, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 272404480, + "peakRssBytes": 372658176, + "pssBytes": 273598464, + "virtualBytes": 3955179520, + "minorFaults": 196124, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 548810752, + "peakRssBytes": 550567936, + "pssBytes": 449795072, + "virtualBytes": 10123689984, + "minorFaults": 290514, + "majorFaults": 1 + }, + "end": { + "rssBytes": 368627712, + "peakRssBytes": 550567936, + "pssBytes": 316666880, + "virtualBytes": 4022964224, + "minorFaults": 290514, + "majorFaults": 1 + } + }, + "drainMs": 25.343617000005906 + }, + { + "level": 10, + "mode": "diverse", + "durationMs": 1164.4927169999864, + "throughputPerSecond": 8.587430263851205, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 373956608, + "peakRssBytes": 550567936, + "pssBytes": 375269376, + "virtualBytes": 4438962176, + "minorFaults": 295381, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 789872640, + "peakRssBytes": 790548480, + "pssBytes": 698016768, + "virtualBytes": 10909097984, + "minorFaults": 476158, + "majorFaults": 1 + }, + "end": { + "rssBytes": 493977600, + "peakRssBytes": 790548480, + "pssBytes": 495371264, + "virtualBytes": 4644073472, + "minorFaults": 476158, + "majorFaults": 1 + } + }, + "drainMs": 25.456953999993857 + }, + { + "level": 50, + "mode": "repeated", + "durationMs": 2468.3105530000175, + "throughputPerSecond": 7.697572729212354, + "fulfilled": 50, + "successful": 19, + "failedExitCodes": 31, + "failureExamples": [ + "sidecar rejected request 931: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 932: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 933: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 493977600, + "peakRssBytes": 790548480, + "pssBytes": 495371264, + "virtualBytes": 4644073472, + "minorFaults": 476158, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 992911360, + "peakRssBytes": 995287040, + "pssBytes": 814490624, + "virtualBytes": 18968473600, + "minorFaults": 670901, + "majorFaults": 1 + }, + "end": { + "rssBytes": 911233024, + "peakRssBytes": 995287040, + "pssBytes": 338146304, + "virtualBytes": 17522765824, + "minorFaults": 670901, + "majorFaults": 1 + } + }, + "drainMs": 25.601284999982454 + }, + { + "level": 50, + "mode": "diverse", + "durationMs": 4783.606188999984, + "throughputPerSecond": 3.971898866526879, + "fulfilled": 50, + "successful": 19, + "failedExitCodes": 31, + "failureExamples": [ + "sidecar rejected request 1023: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1024: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1025: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 670023680, + "peakRssBytes": 995287040, + "pssBytes": 671486976, + "virtualBytes": 5326888960, + "minorFaults": 670972, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 1439965184, + "peakRssBytes": 1440219136, + "pssBytes": 1429283840, + "virtualBytes": 18673664000, + "minorFaults": 1148481, + "majorFaults": 1 + }, + "end": { + "rssBytes": 792100864, + "peakRssBytes": 1440219136, + "pssBytes": 733411328, + "virtualBytes": 5982474240, + "minorFaults": 1148481, + "majorFaults": 1 + } + }, + "drainMs": 25.575372999999672 + }, + { + "level": 100, + "mode": "repeated", + "durationMs": 3300.23242700001, + "throughputPerSecond": 5.757170266117122, + "fulfilled": 100, + "successful": 19, + "failedExitCodes": 81, + "failureExamples": [ + "sidecar rejected request 1137: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1138: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1139: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 757727232, + "peakRssBytes": 1440219136, + "pssBytes": 759048192, + "virtualBytes": 5418311680, + "minorFaults": 1148481, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 1047351296, + "peakRssBytes": 1440219136, + "pssBytes": 1004057600, + "virtualBytes": 18287108096, + "minorFaults": 1322466, + "majorFaults": 1 + }, + "end": { + "rssBytes": 1007652864, + "peakRssBytes": 1440219136, + "pssBytes": 860201984, + "virtualBytes": 16924880896, + "minorFaults": 1322466, + "majorFaults": 1 + } + }, + "drainMs": 25.491763000027277 + }, + { + "level": 100, + "mode": "diverse", + "durationMs": 6183.880968999991, + "throughputPerSecond": 3.0725041596446725, + "fulfilled": 100, + "successful": 19, + "failedExitCodes": 81, + "failureExamples": [ + "sidecar rejected request 1278: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1279: execution_error: failed to start WebAssembly warmup runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms", + "sidecar rejected request 1280: execution_error: failed to start guest WebAssembly runtime: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 session executors reached manager-local limit of 20 (active=20); raise the embedded V8 max_concurrency without exceeding runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 789848064, + "peakRssBytes": 1440219136, + "pssBytes": 791213056, + "virtualBytes": 5418311680, + "minorFaults": 1322466, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 1520328704, + "peakRssBytes": 1520463872, + "pssBytes": 1514570752, + "virtualBytes": 18761375744, + "minorFaults": 1961201, + "majorFaults": 1 + }, + "end": { + "rssBytes": 895852544, + "peakRssBytes": 1520463872, + "pssBytes": 804972544, + "virtualBytes": 6693285888, + "minorFaults": 1961201, + "majorFaults": 1 + } + }, + "drainMs": 25.387867000012193 + }, + { + "level": 200, + "mode": "repeated", + "durationMs": 4648.612484999991, + "throughputPerSecond": 0, + "fulfilled": 200, + "successful": 0, + "failedExitCodes": 200, + "failureExamples": [ + "sidecar rejected request 1626: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1628: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1630: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 830971904, + "peakRssBytes": 1520463872, + "pssBytes": 832206848, + "virtualBytes": 5418311680, + "minorFaults": 1961201, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 1129259008, + "peakRssBytes": 1520463872, + "pssBytes": 1039541248, + "virtualBytes": 17199341568, + "minorFaults": 2144390, + "majorFaults": 1 + }, + "end": { + "rssBytes": 1104211968, + "peakRssBytes": 1520463872, + "pssBytes": 329929728, + "virtualBytes": 16512143360, + "minorFaults": 2144390, + "majorFaults": 1 + } + }, + "drainMs": 25.772266999993008 + }, + { + "level": 200, + "mode": "diverse", + "durationMs": 7538.050300000003, + "throughputPerSecond": 0, + "fulfilled": 200, + "successful": 0, + "failedExitCodes": 200, + "failureExamples": [ + "sidecar rejected request 1867: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1869: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1871: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 874336256, + "peakRssBytes": 1520463872, + "pssBytes": 606524416, + "virtualBytes": 4871098368, + "minorFaults": 2146556, + "majorFaults": 1 + }, + "peak": { + "rssBytes": 1558843392, + "peakRssBytes": 1558843392, + "pssBytes": 1555040256, + "virtualBytes": 19036639232, + "minorFaults": 2873877, + "majorFaults": 1 + }, + "end": { + "rssBytes": 1380614144, + "peakRssBytes": 1558843392, + "pssBytes": 1121072128, + "virtualBytes": 17338396672, + "minorFaults": 2873877, + "majorFaults": 1 + } + }, + "drainMs": 78.59171599999536 + } + ] + }, + { + "backend": "wasmtime", + "levels": [ + { + "level": 1, + "mode": "repeated", + "durationMs": 33.30400700002792, + "throughputPerSecond": 30.026416941335665, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046438400, + "minorFaults": 109948, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046450688, + "minorFaults": 109970, + "majorFaults": 0 + }, + "end": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046438400, + "minorFaults": 109970, + "majorFaults": 0 + } + }, + "drainMs": 25.919685000000754 + }, + { + "level": 1, + "mode": "diverse", + "durationMs": 25.288888999988558, + "throughputPerSecond": 39.543057822763686, + "fulfilled": 1, + "successful": 1, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046438400, + "minorFaults": 109970, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046450688, + "minorFaults": 109992, + "majorFaults": 0 + }, + "end": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046438400, + "minorFaults": 109992, + "majorFaults": 0 + } + }, + "drainMs": 25.373664999991888 + }, + { + "level": 10, + "mode": "repeated", + "durationMs": 36.742918000003556, + "throughputPerSecond": 272.16129105475596, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 323567616, + "peakRssBytes": 383168512, + "pssBytes": 326033408, + "virtualBytes": 4046438400, + "minorFaults": 109992, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 325287936, + "peakRssBytes": 383168512, + "pssBytes": 327856128, + "virtualBytes": 35250601984, + "minorFaults": 110657, + "majorFaults": 0 + }, + "end": { + "rssBytes": 325287936, + "peakRssBytes": 383168512, + "pssBytes": 327856128, + "virtualBytes": 4700741632, + "minorFaults": 110657, + "majorFaults": 0 + } + }, + "drainMs": 24.672474999999395 + }, + { + "level": 10, + "mode": "diverse", + "durationMs": 62.74195100000361, + "throughputPerSecond": 159.38299400347663, + "fulfilled": 10, + "successful": 10, + "failedExitCodes": 0, + "failureExamples": [], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 325287936, + "peakRssBytes": 383168512, + "pssBytes": 327856128, + "virtualBytes": 4700741632, + "minorFaults": 110657, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 345276416, + "peakRssBytes": 383168512, + "pssBytes": 346726400, + "virtualBytes": 30896697344, + "minorFaults": 115179, + "majorFaults": 0 + }, + "end": { + "rssBytes": 344375296, + "peakRssBytes": 383168512, + "pssBytes": 346726400, + "virtualBytes": 4709937152, + "minorFaults": 115179, + "majorFaults": 0 + } + }, + "drainMs": 25.459656999999424 + }, + { + "level": 50, + "mode": "repeated", + "durationMs": 45.86288000000059, + "throughputPerSecond": 436.0825137889235, + "fulfilled": 50, + "successful": 20, + "failedExitCodes": 30, + "failureExamples": [ + "sidecar rejected request 993: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 994: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 995: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 344375296, + "peakRssBytes": 383168512, + "pssBytes": 346726400, + "virtualBytes": 4709937152, + "minorFaults": 115179, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 346562560, + "peakRssBytes": 383168512, + "pssBytes": 348164096, + "virtualBytes": 5441089536, + "minorFaults": 116116, + "majorFaults": 0 + }, + "end": { + "rssBytes": 338866176, + "peakRssBytes": 383168512, + "pssBytes": 341057536, + "virtualBytes": 5376339968, + "minorFaults": 116116, + "majorFaults": 0 + } + }, + "drainMs": 24.92494299999089 + }, + { + "level": 50, + "mode": "diverse", + "durationMs": 124.78484100001515, + "throughputPerSecond": 160.27587838171442, + "fulfilled": 50, + "successful": 20, + "failedExitCodes": 30, + "failureExamples": [ + "sidecar rejected request 1086: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1087: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1088: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 338866176, + "peakRssBytes": 383168512, + "pssBytes": 341057536, + "virtualBytes": 5376339968, + "minorFaults": 116116, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 375595008, + "peakRssBytes": 383168512, + "pssBytes": 377257984, + "virtualBytes": 62196948992, + "minorFaults": 125818, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375066624, + "peakRssBytes": 383168512, + "pssBytes": 377257984, + "virtualBytes": 5459238912, + "minorFaults": 125818, + "majorFaults": 0 + } + }, + "drainMs": 25.503278999996837 + }, + { + "level": 100, + "mode": "repeated", + "durationMs": 65.54685799998697, + "throughputPerSecond": 305.1252281231234, + "fulfilled": 100, + "successful": 20, + "failedExitCodes": 80, + "failureExamples": [ + "sidecar rejected request 1200: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1201: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1202: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 375066624, + "peakRssBytes": 383168512, + "pssBytes": 377257984, + "virtualBytes": 5459238912, + "minorFaults": 125818, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 375214080, + "peakRssBytes": 383168512, + "pssBytes": 378347520, + "virtualBytes": 9826271232, + "minorFaults": 126267, + "majorFaults": 0 + }, + "end": { + "rssBytes": 375201792, + "peakRssBytes": 383168512, + "pssBytes": 377393152, + "virtualBytes": 5459238912, + "minorFaults": 126267, + "majorFaults": 0 + } + }, + "drainMs": 25.440158999990672 + }, + { + "level": 100, + "mode": "diverse", + "durationMs": 518.441301999992, + "throughputPerSecond": 38.57717339040305, + "fulfilled": 100, + "successful": 20, + "failedExitCodes": 80, + "failureExamples": [ + "sidecar rejected request 1343: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1344: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms", + "sidecar rejected request 1345: execution_error: ERR_AGENTOS_VM_EXECUTOR_LIMIT: ERR_AGENTOS_VM_EXECUTOR_LIMIT: active guest executors reached limit of 20 (active=20); raise runtime.executor.maxActiveVms" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 375201792, + "peakRssBytes": 383168512, + "pssBytes": 377393152, + "virtualBytes": 5459238912, + "minorFaults": 126267, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 384253952, + "peakRssBytes": 386203648, + "pssBytes": 385384448, + "virtualBytes": 66542477312, + "minorFaults": 129081, + "majorFaults": 0 + }, + "end": { + "rssBytes": 383193088, + "peakRssBytes": 386203648, + "pssBytes": 385384448, + "virtualBytes": 5459238912, + "minorFaults": 129081, + "majorFaults": 0 + } + }, + "drainMs": 25.41857800001162 + }, + { + "level": 200, + "mode": "repeated", + "durationMs": 63.71382299999823, + "throughputPerSecond": 15.695181248188918, + "fulfilled": 200, + "successful": 1, + "failedExitCodes": 199, + "failureExamples": [ + "sidecar rejected request 1690: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1691: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1699: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 383193088, + "peakRssBytes": 386203648, + "pssBytes": 385384448, + "virtualBytes": 5459238912, + "minorFaults": 129081, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 383209472, + "peakRssBytes": 386203648, + "pssBytes": 386478080, + "virtualBytes": 5461585920, + "minorFaults": 129528, + "majorFaults": 0 + }, + "end": { + "rssBytes": 383193088, + "peakRssBytes": 386203648, + "pssBytes": 385388544, + "virtualBytes": 5459238912, + "minorFaults": 129528, + "majorFaults": 0 + } + }, + "drainMs": 25.42750699998578 + }, + { + "level": 200, + "mode": "diverse", + "durationMs": 167.54472800000804, + "throughputPerSecond": 11.93711090688514, + "fulfilled": 200, + "successful": 2, + "failedExitCodes": 198, + "failureExamples": [ + "sidecar rejected request 1930: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1932: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains", + "sidecar rejected request 1934: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: ERR_AGENTOS_PROTOCOL_FRAME_LIMIT: stdio ordinary ingress used=128 requested=1 limit=128 frames; raise runtime.protocol.maxIngressFrames; retry after the current request backlog drains" + ], + "rejectedCount": 0, + "rejectionExamples": [], + "memory": { + "start": { + "rssBytes": 383193088, + "peakRssBytes": 386203648, + "pssBytes": 385388544, + "virtualBytes": 5459238912, + "minorFaults": 129528, + "majorFaults": 0 + }, + "peak": { + "rssBytes": 387649536, + "peakRssBytes": 389029888, + "pssBytes": 389259264, + "virtualBytes": 70923468800, + "minorFaults": 131322, + "majorFaults": 0 + }, + "end": { + "rssBytes": 387063808, + "peakRssBytes": 389029888, + "pssBytes": 389259264, + "virtualBytes": 5459238912, + "minorFaults": 131322, + "majorFaults": 0 + } + }, + "drainMs": 25.508613999991212 + } + ] + } + ], + "paths": [ + { + "backend": "v8", + "denial": { + "exitCode": 7, + "stderr": "curl: (7) getsockname() failed with errno 28: Invalid argument", + "passed": true + }, + "cancellation": { + "rejected": true, + "name": "AbortError", + "message": "AbortError: This operation was aborted", + "durationMs": 116.84724900001311, + "passed": true + }, + "resourceLimit": { + "exitCode": 137, + "durationMs": 5012.056822000013, + "stderr": "", + "passed": true + } + }, + { + "backend": "wasmtime", + "denial": { + "exitCode": 7, + "stderr": "curl: (7) getsockname() failed with errno 28: Invalid argument", + "passed": true + }, + "cancellation": { + "rejected": true, + "name": "AbortError", + "message": "AbortError: This operation was aborted", + "durationMs": 37.76239200000418, + "passed": true + }, + "resourceLimit": { + "exitCode": 137, + "durationMs": 5021.572392000002, + "stderr": "ECANCELED: Wasmtime execution was canceled", + "passed": true + } + } + ], + "status": "complete", + "summary": { + "workloads": [ + { + "name": "trivial", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 63.10325200000079, + "p50": 74.31707399999141, + "p95": 96.1820848000001, + "max": 312.37073899999814 + }, + "wasmtime": { + "count": 25, + "min": 19.18946200000937, + "p50": 24.14887599999929, + "p95": 77.8040359999955, + "max": 81.32793099999981 + }, + "cold": { + "v8": { + "count": 5, + "min": 63.93654700000479, + "p50": 74.84915600001113, + "p95": 87.51541400000023, + "max": 88.16289200000028 + }, + "wasmtime": { + "count": 5, + "min": 67.10170700002345, + "p50": 68.44208799999615, + "p95": 81.09124939999893, + "max": 81.32793099999981 + }, + "p50Ratio": 0.91440026391194 + }, + "warm": { + "v8": { + "count": 20, + "min": 63.10325200000079, + "p50": 74.02421350000077, + "p95": 108.89607580000013, + "max": 312.37073899999814 + }, + "wasmtime": { + "count": 20, + "min": 19.18946200000937, + "p50": 22.932051499992667, + "p95": 29.9801303499994, + "max": 31.287318000000596 + }, + "p50Ratio": 0.30979122121969493 + }, + "p50Ratio": 0.3249438480315046, + "p95Ratio": 0.8089244079266976 + }, + { + "name": "coreutils", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 200.46537000000535, + "p50": 235.94026999999187, + "p95": 321.0504691999999, + "max": 382.93910600000004 + }, + "wasmtime": { + "count": 25, + "min": 77.91669600000023, + "p50": 86.73017600000458, + "p95": 1212.4291321999972, + "max": 1301.9156930000008 + }, + "cold": { + "v8": { + "count": 5, + "min": 208.54451100000006, + "p50": 228.58471599999757, + "p95": 314.18747680000195, + "max": 330.21409300000005 + }, + "wasmtime": { + "count": 5, + "min": 1132.8213069999983, + "p50": 1142.0087930000154, + "p95": 1287.5393977999993, + "max": 1301.9156930000008 + }, + "p50Ratio": 4.995998039519089 + }, + "warm": { + "v8": { + "count": 20, + "min": 200.46537000000535, + "p50": 237.18443899999693, + "p95": 289.32313060000007, + "max": 382.93910600000004 + }, + "wasmtime": { + "count": 20, + "min": 77.91669600000023, + "p50": 82.77384050001092, + "p95": 112.33699550000095, + "max": 151.87632799999847 + }, + "p50Ratio": 0.3489851225021216 + }, + "p50Ratio": 0.36759378125661873, + "p95Ratio": 3.776444043894883 + }, + { + "name": "shell", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 335.79073900000367, + "p50": 384.20413800000097, + "p95": 480.83270699999986, + "max": 486.5630520000004 + }, + "wasmtime": { + "count": 25, + "min": 80.97056700001121, + "p50": 97.67350400000578, + "p95": 4028.3821319999984, + "max": 4073.150912000012 + }, + "cold": { + "v8": { + "count": 5, + "min": 335.79073900000367, + "p50": 379.7415260000125, + "p95": 405.6379036000002, + "max": 410.996345 + }, + "wasmtime": { + "count": 5, + "min": 3900.4119800000044, + "p50": 3928.6977999999945, + "p95": 4069.1813726000096, + "max": 4073.150912000012 + }, + "p50Ratio": 10.34571552229941 + }, + "warm": { + "v8": { + "count": 20, + "min": 340.479164999997, + "p50": 397.2013894999982, + "p95": 485.18221179999995, + "max": 486.5630520000004 + }, + "wasmtime": { + "count": 20, + "min": 80.97056700001121, + "p50": 95.78599949999625, + "p95": 109.83036705000087, + "max": 130.83952299998782 + }, + "p50Ratio": 0.2411522266338816 + }, + "p50Ratio": 0.25422293603721036, + "p95Ratio": 8.377928691943161 + }, + { + "name": "curl", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 181.7958409999992, + "p50": 211.1024860000034, + "p95": 279.9201719999995, + "max": 301.4461789999914 + }, + "wasmtime": { + "count": 25, + "min": 44.091539999993984, + "p50": 52.72330800000054, + "p95": 1623.3995460000006, + "max": 1729.2114819999988 + }, + "cold": { + "v8": { + "count": 5, + "min": 181.7958409999992, + "p50": 235.7850829999952, + "p95": 278.9357899999988, + "max": 280.14638899999954 + }, + "wasmtime": { + "count": 5, + "min": 1583.730942999995, + "p50": 1609.063089999996, + "p95": 1708.7659175999993, + "max": 1729.2114819999988 + }, + "p50Ratio": 6.82427857406072 + }, + "warm": { + "v8": { + "count": 20, + "min": 184.0208330000023, + "p50": 208.87608150000233, + "p95": 280.13684774999916, + "max": 301.4461789999914 + }, + "wasmtime": { + "count": 20, + "min": 44.091539999993984, + "p50": 49.68852999999399, + "p95": 68.31309005000331, + "max": 70.89306299999589 + }, + "p50Ratio": 0.23788520755064738 + }, + "p50Ratio": 0.24975218908601432, + "p95Ratio": 5.799508961433488 + }, + { + "name": "sqlite", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 132.1042890000026, + "p50": 182.2041700000118, + "p95": 240.09304319999865, + "max": 249.06565800000044 + }, + "wasmtime": { + "count": 25, + "min": 34.031088000003365, + "p50": 43.085986000005505, + "p95": 1320.1533643999965, + "max": 1340.506143999999 + }, + "cold": { + "v8": { + "count": 5, + "min": 132.1042890000026, + "p50": 150.87885399999504, + "p95": 194.08777700000755, + "max": 202.70167000000947 + }, + "wasmtime": { + "count": 5, + "min": 1272.6397069999948, + "p50": 1295.2616340000022, + "p95": 1337.6801745999983, + "max": 1340.506143999999 + }, + "p50Ratio": 8.584779110265808 + }, + "warm": { + "v8": { + "count": 20, + "min": 143.4593789999999, + "p50": 183.81929549999404, + "p95": 240.71655164999967, + "max": 249.06565800000044 + }, + "wasmtime": { + "count": 20, + "min": 34.031088000003365, + "p50": 41.11062749999837, + "p95": 72.46452834999934, + "max": 167.80803600000218 + }, + "p50Ratio": 0.22364696474424092 + }, + "p50Ratio": 0.23647091062736222, + "p95Ratio": 5.498507357001188 + }, + { + "name": "vim", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 262.61806599999545, + "p50": 299.1098409999977, + "p95": 450.46657840000034, + "max": 507.95485899999767 + }, + "wasmtime": { + "count": 25, + "min": 67.71884800000043, + "p50": 81.08445900000515, + "p95": 3871.1063403999833, + "max": 3991.8115499999985 + }, + "cold": { + "v8": { + "count": 5, + "min": 270.2340569999942, + "p50": 295.7348579999816, + "p95": 426.3430161999997, + "max": 458.1513100000002 + }, + "wasmtime": { + "count": 5, + "min": 3667.212870999996, + "p50": 3711.566989999992, + "p95": 3975.6474755999952, + "max": 3991.8115499999985 + }, + "p50Ratio": 12.550319617717243 + }, + "warm": { + "v8": { + "count": 20, + "min": 262.61806599999545, + "p50": 323.1422760000014, + "p95": 424.1390123500013, + "max": 507.95485899999767 + }, + "wasmtime": { + "count": 20, + "min": 67.71884800000043, + "p50": 77.0393105000112, + "p95": 349.5373114500035, + "max": 572.5858059999991 + }, + "p50Ratio": 0.23840678308526508 + }, + "p50Ratio": 0.2710858951645318, + "p95Ratio": 8.593548391868843 + }, + { + "name": "large-module", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 266.6714829999837, + "p50": 344.80199699999866, + "p95": 422.7817050000056, + "max": 430.0022839999874 + }, + "wasmtime": { + "count": 25, + "min": 56.04536000000371, + "p50": 70.34671600000001, + "p95": 3971.4375866000014, + "max": 4264.67938799999 + }, + "cold": { + "v8": { + "count": 5, + "min": 273.39911199999915, + "p50": 325.3107860000018, + "p95": 401.4012325999996, + "max": 403.6235549999983 + }, + "wasmtime": { + "count": 5, + "min": 3926.064602000002, + "p50": 3953.44120500001, + "p95": 4206.930846799992, + "max": 4264.67938799999 + }, + "p50Ratio": 12.152813171709553 + }, + "warm": { + "v8": { + "count": 20, + "min": 266.6714829999837, + "p50": 346.73662149999836, + "p95": 423.90663940000525, + "max": 430.0022839999874 + }, + "wasmtime": { + "count": 20, + "min": 56.04536000000371, + "p50": 67.30011750000267, + "p95": 336.97251045001417, + "max": 658.2945529999997 + }, + "p50Ratio": 0.19409578719680462 + }, + "p50Ratio": 0.20402061650472486, + "p95Ratio": 9.393589031010574 + }, + { + "name": "compute-heavy", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 202.72369899999467, + "p50": 246.03889399999753, + "p95": 362.64392879999974, + "max": 377.56164700000045 + }, + "wasmtime": { + "count": 25, + "min": 72.01720000000205, + "p50": 85.02203700000246, + "p95": 719.5811986000015, + "max": 767.2871039999882 + }, + "cold": { + "v8": { + "count": 5, + "min": 246.036334000004, + "p50": 268.67901600000914, + "p95": 356.8988276000018, + "max": 364.35553699999946 + }, + "wasmtime": { + "count": 5, + "min": 705.496779000001, + "p50": 710.8036649999995, + "p95": 758.1847995999909, + "max": 767.2871039999882 + }, + "p50Ratio": 2.6455496063003867 + }, + "warm": { + "v8": { + "count": 20, + "min": 202.72369899999467, + "p50": 233.54828800000178, + "p95": 356.88570355000104, + "max": 377.56164700000045 + }, + "wasmtime": { + "count": 20, + "min": 72.01720000000205, + "p50": 83.92303000001266, + "p95": 100.66608509999497, + "max": 109.70068199999514 + }, + "p50Ratio": 0.35933909307873846 + }, + "p50Ratio": 0.3455634010450531, + "p95Ratio": 1.9842637404164423 + }, + { + "name": "host-call-heavy", + "correctness": { + "v8Failures": 0, + "wasmtimeFailures": 0 + }, + "v8": { + "count": 25, + "min": 167.53960400000506, + "p50": 197.95611800000188, + "p95": 329.5290498000001, + "max": 415.5584929999968 + }, + "wasmtime": { + "count": 25, + "min": 42.20804399999906, + "p50": 50.38200099999085, + "p95": 1449.6886112000066, + "max": 1616.9195569999865 + }, + "cold": { + "v8": { + "count": 5, + "min": 180.50901599999634, + "p50": 249.80005299999902, + "p95": 384.9203271999984, + "max": 415.5584929999968 + }, + "wasmtime": { + "count": 5, + "min": 1430.9679920000199, + "p50": 1443.8645760000072, + "p95": 1583.7645695999904, + "max": 1616.9195569999865 + }, + "p50Ratio": 5.7800811435376795 + }, + "warm": { + "v8": { + "count": 20, + "min": 167.53960400000506, + "p50": 192.63154800000484, + "p95": 300.4632790500021, + "max": 337.2799219999997 + }, + "wasmtime": { + "count": 20, + "min": 42.20804399999906, + "p50": 49.18875850000768, + "p95": 73.79086494998795, + "max": 366.3740299999772 + }, + "p50Ratio": 0.2553515195756328 + }, + "p50Ratio": 0.2545109568171587, + "p95Ratio": 4.399274091555391 + } + ], + "geometricMeanP50Ratio": 0.274065788303547, + "throughput": [ + { + "level": 1, + "mode": "repeated", + "v8": 13.711705657067133, + "wasmtime": 30.026416941335665, + "ratio": 2.1898382077549767 + }, + { + "level": 1, + "mode": "diverse", + "v8": 13.29462823898511, + "wasmtime": 39.543057822763686, + "ratio": 2.9743635633832763 + }, + { + "level": 10, + "mode": "repeated", + "v8": 20.24243334096838, + "wasmtime": 272.16129105475596, + "ratio": 13.445087676486626 + }, + { + "level": 10, + "mode": "diverse", + "v8": 8.587430263851205, + "wasmtime": 159.38299400347663, + "ratio": 18.560033573070104 + }, + { + "level": 50, + "mode": "repeated", + "v8": 7.697572729212354, + "wasmtime": 436.0825137889235, + "ratio": 56.65195109284082 + }, + { + "level": 50, + "mode": "diverse", + "v8": 3.971898866526879, + "wasmtime": 160.27587838171442, + "ratio": 40.3524570407462 + }, + { + "level": 100, + "mode": "repeated", + "v8": 5.757170266117122, + "wasmtime": 305.1252281231234, + "ratio": 52.999166955142485 + }, + { + "level": 100, + "mode": "diverse", + "v8": 3.0725041596446725, + "wasmtime": 38.57717339040305, + "ratio": 12.55561307193296 + }, + { + "level": 200, + "mode": "repeated", + "v8": 0, + "wasmtime": 15.695181248188918, + "ratio": null + }, + { + "level": 200, + "mode": "diverse", + "v8": 0, + "wasmtime": 11.93711090688514, + "ratio": null + } + ], + "retained": { + "v8RssBytes": 161894400, + "wasmtimeRssBytes": 256995328, + "v8PssBytes": 162531328, + "wasmtimePssBytes": 257593344 + }, + "gates": { + "correctness": true, + "geometricMeanP50": true, + "individualP95": false, + "throughput": true, + "retainedRss": false, + "retainedPss": false + }, + "preferredBackend": "v8", + "omissionBehavior": "v8", + "rollbackBackend": "v8" + }, + "completedAt": "2026-07-21T09:10:53.443Z" +} diff --git a/packages/runtime-benchmarks/results/wasm-mixed-soak.json b/packages/runtime-benchmarks/results/wasm-mixed-soak.json new file mode 100644 index 0000000000..3a9fd6fb1d --- /dev/null +++ b/packages/runtime-benchmarks/results/wasm-mixed-soak.json @@ -0,0 +1,6938 @@ +{ + "benchmark": "wasm-mixed-soak", + "status": "complete", + "metadata": { + "startedAt": "2026-07-21T14:19:44.334Z", + "hostname": "nathan-dev", + "platform": "linux", + "arch": "x64", + "cpuModel": "12th Gen Intel(R) Core(TM) i7-12700KF", + "logicalCpus": 20, + "totalMemoryBytes": 67170398208, + "node": "v24.17.0", + "sidecar": { + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/target/release/agentos-native-sidecar", + "profile": "release", + "mtimeMs": 1784643409180.2722, + "mtimeIso": "2026-07-21T07:16:49.180-07:00", + "sizeBytes": 147835856 + }, + "wasmBackend": "wasmtime", + "javascriptBackend": "v8", + "vmCount": 1, + "warmupCycles": 10, + "measuredCycles": 200, + "settleMs": 100, + "operationTimeoutMs": 30000, + "maxRssGrowthBytes": 50331648, + "maxPssGrowthBytes": 50331648 + }, + "baseline": { + "cycle": -1, + "elapsedMs": 13151.242542, + "memory": { + "rssBytes": 401362944, + "peakRssBytes": 414846976, + "pssBytes": 405713920, + "virtualBytes": 2601406464, + "minorFaults": 94996, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + "samples": [ + { + "cycle": 0, + "elapsedMs": 13604.361766, + "memory": { + "rssBytes": 401145856, + "peakRssBytes": 414846976, + "pssBytes": 405718016, + "virtualBytes": 2601406464, + "minorFaults": 98113, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 1, + "elapsedMs": 14032.591631000001, + "memory": { + "rssBytes": 401215488, + "peakRssBytes": 414846976, + "pssBytes": 405746688, + "virtualBytes": 2601406464, + "minorFaults": 101256, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 2, + "elapsedMs": 14470.193888, + "memory": { + "rssBytes": 401350656, + "peakRssBytes": 414916608, + "pssBytes": 405787648, + "virtualBytes": 2601406464, + "minorFaults": 104384, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 3, + "elapsedMs": 14903.076777, + "memory": { + "rssBytes": 401408000, + "peakRssBytes": 414916608, + "pssBytes": 405795840, + "virtualBytes": 2601406464, + "minorFaults": 107521, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 4, + "elapsedMs": 15353.275351, + "memory": { + "rssBytes": 401522688, + "peakRssBytes": 415105024, + "pssBytes": 405808128, + "virtualBytes": 2601406464, + "minorFaults": 110641, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 5, + "elapsedMs": 15794.73596, + "memory": { + "rssBytes": 401559552, + "peakRssBytes": 415105024, + "pssBytes": 405812224, + "virtualBytes": 2601406464, + "minorFaults": 113777, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 6, + "elapsedMs": 16243.891102, + "memory": { + "rssBytes": 401620992, + "peakRssBytes": 415105024, + "pssBytes": 405812224, + "virtualBytes": 2601406464, + "minorFaults": 116894, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 7, + "elapsedMs": 16683.671051999998, + "memory": { + "rssBytes": 401661952, + "peakRssBytes": 415105024, + "pssBytes": 405812224, + "virtualBytes": 2601406464, + "minorFaults": 120029, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 8, + "elapsedMs": 17109.183377999998, + "memory": { + "rssBytes": 401494016, + "peakRssBytes": 415105024, + "pssBytes": 405811200, + "virtualBytes": 2601406464, + "minorFaults": 123146, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 9, + "elapsedMs": 17540.422369, + "memory": { + "rssBytes": 401543168, + "peakRssBytes": 415105024, + "pssBytes": 405835776, + "virtualBytes": 2601406464, + "minorFaults": 126287, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 10, + "elapsedMs": 17967.462414999998, + "memory": { + "rssBytes": 401604608, + "peakRssBytes": 415105024, + "pssBytes": 405843968, + "virtualBytes": 2601406464, + "minorFaults": 129406, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 11, + "elapsedMs": 18395.370516, + "memory": { + "rssBytes": 401629184, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 132542, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 12, + "elapsedMs": 18825.626665, + "memory": { + "rssBytes": 401489920, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 135660, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 13, + "elapsedMs": 19252.647699999998, + "memory": { + "rssBytes": 401526784, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 138796, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 14, + "elapsedMs": 19698.627409, + "memory": { + "rssBytes": 401383424, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 141915, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 15, + "elapsedMs": 20125.925122, + "memory": { + "rssBytes": 401416192, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 145050, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 16, + "elapsedMs": 20566.047456, + "memory": { + "rssBytes": 401494016, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 148167, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 17, + "elapsedMs": 20995.595405, + "memory": { + "rssBytes": 401571840, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 151302, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 18, + "elapsedMs": 21427.234924, + "memory": { + "rssBytes": 401440768, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 154419, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 19, + "elapsedMs": 21855.879237999998, + "memory": { + "rssBytes": 401477632, + "peakRssBytes": 415105024, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 157554, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 20, + "elapsedMs": 22293.793271, + "memory": { + "rssBytes": 401563648, + "peakRssBytes": 415178752, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 160673, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 21, + "elapsedMs": 22734.705475, + "memory": { + "rssBytes": 401346560, + "peakRssBytes": 415178752, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 163808, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 22, + "elapsedMs": 23175.677357, + "memory": { + "rssBytes": 401420288, + "peakRssBytes": 415178752, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 166925, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 23, + "elapsedMs": 23614.936416999997, + "memory": { + "rssBytes": 401494016, + "peakRssBytes": 415178752, + "pssBytes": 405848064, + "virtualBytes": 2601406464, + "minorFaults": 170060, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 24, + "elapsedMs": 24051.745472, + "memory": { + "rssBytes": 401571840, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 173179, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 25, + "elapsedMs": 24482.630944, + "memory": { + "rssBytes": 401350656, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 176314, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 26, + "elapsedMs": 24912.787139, + "memory": { + "rssBytes": 401420288, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 179433, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 27, + "elapsedMs": 25341.173718, + "memory": { + "rssBytes": 401494016, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 182570, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 28, + "elapsedMs": 25775.318811999998, + "memory": { + "rssBytes": 401313792, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 185688, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 29, + "elapsedMs": 26220.219687999997, + "memory": { + "rssBytes": 401379328, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 188821, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 30, + "elapsedMs": 26650.365394999997, + "memory": { + "rssBytes": 401510400, + "peakRssBytes": 415195136, + "pssBytes": 405852160, + "virtualBytes": 2601406464, + "minorFaults": 191939, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 31, + "elapsedMs": 27076.664945, + "memory": { + "rssBytes": 401330176, + "peakRssBytes": 415195136, + "pssBytes": 405853184, + "virtualBytes": 2601406464, + "minorFaults": 195075, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 32, + "elapsedMs": 27517.903631999998, + "memory": { + "rssBytes": 401461248, + "peakRssBytes": 415195136, + "pssBytes": 405857280, + "virtualBytes": 2601406464, + "minorFaults": 198194, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 33, + "elapsedMs": 27957.712456999998, + "memory": { + "rssBytes": 401485824, + "peakRssBytes": 415195136, + "pssBytes": 405857280, + "virtualBytes": 2601406464, + "minorFaults": 201329, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 34, + "elapsedMs": 28389.511513999998, + "memory": { + "rssBytes": 401293312, + "peakRssBytes": 415195136, + "pssBytes": 405857280, + "virtualBytes": 2601406464, + "minorFaults": 204447, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 35, + "elapsedMs": 28820.97212, + "memory": { + "rssBytes": 401330176, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 207583, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 36, + "elapsedMs": 29257.549608999998, + "memory": { + "rssBytes": 401481728, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 210701, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 37, + "elapsedMs": 29694.979685, + "memory": { + "rssBytes": 401289216, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 213835, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 38, + "elapsedMs": 30210.330712, + "memory": { + "rssBytes": 401367040, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 216953, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 39, + "elapsedMs": 30730.754235999997, + "memory": { + "rssBytes": 401387520, + "peakRssBytes": 415195136, + "pssBytes": 405860352, + "virtualBytes": 2601406464, + "minorFaults": 220088, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 40, + "elapsedMs": 31162.143925, + "memory": { + "rssBytes": 401219584, + "peakRssBytes": 415195136, + "pssBytes": 405860352, + "virtualBytes": 2601406464, + "minorFaults": 223205, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 41, + "elapsedMs": 31596.196077, + "memory": { + "rssBytes": 401301504, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 226339, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 42, + "elapsedMs": 32038.574119999997, + "memory": { + "rssBytes": 401362944, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 229457, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 43, + "elapsedMs": 32491.422982, + "memory": { + "rssBytes": 401432576, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 232594, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 44, + "elapsedMs": 32918.783744, + "memory": { + "rssBytes": 401223680, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 235711, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 45, + "elapsedMs": 33355.79667, + "memory": { + "rssBytes": 401289216, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 238844, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 46, + "elapsedMs": 33788.814731000006, + "memory": { + "rssBytes": 401395712, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 241962, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 47, + "elapsedMs": 34228.691949, + "memory": { + "rssBytes": 401412096, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 245097, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 48, + "elapsedMs": 34667.334862, + "memory": { + "rssBytes": 401272832, + "peakRssBytes": 415195136, + "pssBytes": 405861376, + "virtualBytes": 2601406464, + "minorFaults": 248215, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 49, + "elapsedMs": 35107.232836, + "memory": { + "rssBytes": 401379328, + "peakRssBytes": 415195136, + "pssBytes": 405890048, + "virtualBytes": 2601406464, + "minorFaults": 251357, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 50, + "elapsedMs": 35542.733352, + "memory": { + "rssBytes": 401240064, + "peakRssBytes": 415195136, + "pssBytes": 405894144, + "virtualBytes": 2601406464, + "minorFaults": 254476, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 51, + "elapsedMs": 35990.848521, + "memory": { + "rssBytes": 401338368, + "peakRssBytes": 415195136, + "pssBytes": 405894144, + "virtualBytes": 2601406464, + "minorFaults": 257608, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 52, + "elapsedMs": 36443.851813, + "memory": { + "rssBytes": 401416192, + "peakRssBytes": 415195136, + "pssBytes": 405893120, + "virtualBytes": 2601406464, + "minorFaults": 260726, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 53, + "elapsedMs": 36870.665521, + "memory": { + "rssBytes": 401227776, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 263862, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 54, + "elapsedMs": 37304.530920000005, + "memory": { + "rssBytes": 401317888, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 266980, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 55, + "elapsedMs": 37824.873098000004, + "memory": { + "rssBytes": 401367040, + "peakRssBytes": 415195136, + "pssBytes": 405898240, + "virtualBytes": 2601406464, + "minorFaults": 270114, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 56, + "elapsedMs": 38260.129960000006, + "memory": { + "rssBytes": 401186816, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 273232, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 57, + "elapsedMs": 38684.214578, + "memory": { + "rssBytes": 401227776, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 276367, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 58, + "elapsedMs": 39111.454388000006, + "memory": { + "rssBytes": 401350656, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 279486, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 59, + "elapsedMs": 39537.942981, + "memory": { + "rssBytes": 401178624, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 282619, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 60, + "elapsedMs": 39969.567265000005, + "memory": { + "rssBytes": 401330176, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 285735, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 61, + "elapsedMs": 40407.672695, + "memory": { + "rssBytes": 401100800, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 288872, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 62, + "elapsedMs": 40842.024853, + "memory": { + "rssBytes": 401190912, + "peakRssBytes": 415195136, + "pssBytes": 405897216, + "virtualBytes": 2601406464, + "minorFaults": 291990, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 63, + "elapsedMs": 41271.898030000004, + "memory": { + "rssBytes": 401211392, + "peakRssBytes": 415195136, + "pssBytes": 405898240, + "virtualBytes": 2601406464, + "minorFaults": 295125, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 64, + "elapsedMs": 41706.868014, + "memory": { + "rssBytes": 401342464, + "peakRssBytes": 415195136, + "pssBytes": 405898240, + "virtualBytes": 2601406464, + "minorFaults": 298243, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 65, + "elapsedMs": 42132.708554000004, + "memory": { + "rssBytes": 401129472, + "peakRssBytes": 415195136, + "pssBytes": 405902336, + "virtualBytes": 2601406464, + "minorFaults": 301378, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 66, + "elapsedMs": 42562.904, + "memory": { + "rssBytes": 401199104, + "peakRssBytes": 415195136, + "pssBytes": 405902336, + "virtualBytes": 2601406464, + "minorFaults": 304496, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 67, + "elapsedMs": 42995.140767000004, + "memory": { + "rssBytes": 401207296, + "peakRssBytes": 415195136, + "pssBytes": 405902336, + "virtualBytes": 2601406464, + "minorFaults": 307630, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 68, + "elapsedMs": 43425.901365000005, + "memory": { + "rssBytes": 401330176, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 310792, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 69, + "elapsedMs": 43862.245601, + "memory": { + "rssBytes": 401379328, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 313926, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 70, + "elapsedMs": 44299.878721, + "memory": { + "rssBytes": 401453056, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 317044, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 71, + "elapsedMs": 44727.185944000004, + "memory": { + "rssBytes": 401518592, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 320177, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 72, + "elapsedMs": 45154.038270000005, + "memory": { + "rssBytes": 401321984, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 323295, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 73, + "elapsedMs": 45588.402228, + "memory": { + "rssBytes": 401317888, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 326429, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 74, + "elapsedMs": 46014.111302000005, + "memory": { + "rssBytes": 401424384, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 329547, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 75, + "elapsedMs": 46444.513351, + "memory": { + "rssBytes": 401498112, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 332682, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 76, + "elapsedMs": 46869.940278, + "memory": { + "rssBytes": 401317888, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 335800, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 77, + "elapsedMs": 47306.405262, + "memory": { + "rssBytes": 401387520, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 338937, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 78, + "elapsedMs": 47731.859350000006, + "memory": { + "rssBytes": 401510400, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 342055, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 79, + "elapsedMs": 48165.318702000004, + "memory": { + "rssBytes": 401260544, + "peakRssBytes": 415195136, + "pssBytes": 406082560, + "virtualBytes": 2601406464, + "minorFaults": 345190, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 80, + "elapsedMs": 48599.557347, + "memory": { + "rssBytes": 401371136, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 348308, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 81, + "elapsedMs": 49029.072073, + "memory": { + "rssBytes": 401371136, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 351443, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 82, + "elapsedMs": 49463.320157, + "memory": { + "rssBytes": 401461248, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 354561, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 83, + "elapsedMs": 49892.607244, + "memory": { + "rssBytes": 401469440, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 357695, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 84, + "elapsedMs": 50325.658747, + "memory": { + "rssBytes": 401260544, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 360812, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 85, + "elapsedMs": 50759.872627000004, + "memory": { + "rssBytes": 401281024, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 363947, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 86, + "elapsedMs": 51197.667895000006, + "memory": { + "rssBytes": 401371136, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 367065, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 87, + "elapsedMs": 51625.013031, + "memory": { + "rssBytes": 401440768, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 370201, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 88, + "elapsedMs": 52051.980237, + "memory": { + "rssBytes": 401260544, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 373318, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 89, + "elapsedMs": 52480.115938, + "memory": { + "rssBytes": 401297408, + "peakRssBytes": 415195136, + "pssBytes": 406085632, + "virtualBytes": 2601406464, + "minorFaults": 376453, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 90, + "elapsedMs": 52911.638731, + "memory": { + "rssBytes": 401371136, + "peakRssBytes": 415195136, + "pssBytes": 406085632, + "virtualBytes": 2601406464, + "minorFaults": 379571, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 91, + "elapsedMs": 53355.334621, + "memory": { + "rssBytes": 401199104, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 382704, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 92, + "elapsedMs": 53785.75706, + "memory": { + "rssBytes": 401272832, + "peakRssBytes": 415195136, + "pssBytes": 406085632, + "virtualBytes": 2601406464, + "minorFaults": 385822, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 93, + "elapsedMs": 54221.567456000004, + "memory": { + "rssBytes": 401354752, + "peakRssBytes": 415195136, + "pssBytes": 406085632, + "virtualBytes": 2601406464, + "minorFaults": 388957, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 94, + "elapsedMs": 54653.335941000005, + "memory": { + "rssBytes": 401215488, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 392075, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 95, + "elapsedMs": 55081.518474000004, + "memory": { + "rssBytes": 401543168, + "peakRssBytes": 415195136, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 395209, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 96, + "elapsedMs": 55511.778845, + "memory": { + "rssBytes": 401629184, + "peakRssBytes": 415244288, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 398327, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 97, + "elapsedMs": 55945.23823, + "memory": { + "rssBytes": 402075648, + "peakRssBytes": 415244288, + "pssBytes": 406086656, + "virtualBytes": 2601406464, + "minorFaults": 401461, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 98, + "elapsedMs": 56385.102723, + "memory": { + "rssBytes": 402997248, + "peakRssBytes": 416587776, + "pssBytes": 407155712, + "virtualBytes": 2601406464, + "minorFaults": 404839, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 99, + "elapsedMs": 56816.528932, + "memory": { + "rssBytes": 403066880, + "peakRssBytes": 416587776, + "pssBytes": 407286784, + "virtualBytes": 2601406464, + "minorFaults": 408005, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 100, + "elapsedMs": 57256.81874, + "memory": { + "rssBytes": 403136512, + "peakRssBytes": 416587776, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 411127, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 101, + "elapsedMs": 57681.534827, + "memory": { + "rssBytes": 403152896, + "peakRssBytes": 416587776, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 414262, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 102, + "elapsedMs": 58113.434273, + "memory": { + "rssBytes": 402984960, + "peakRssBytes": 416587776, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 417379, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 103, + "elapsedMs": 58538.648165000006, + "memory": { + "rssBytes": 403054592, + "peakRssBytes": 416587776, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 420515, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 104, + "elapsedMs": 58966.481882, + "memory": { + "rssBytes": 403177472, + "peakRssBytes": 416755712, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 423634, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 105, + "elapsedMs": 59535.419362, + "memory": { + "rssBytes": 403730432, + "peakRssBytes": 416755712, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 426772, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 106, + "elapsedMs": 59974.468246000004, + "memory": { + "rssBytes": 403578880, + "peakRssBytes": 417161216, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 429889, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 107, + "elapsedMs": 60419.237452, + "memory": { + "rssBytes": 403677184, + "peakRssBytes": 417161216, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 433021, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 108, + "elapsedMs": 60855.242149000005, + "memory": { + "rssBytes": 403730432, + "peakRssBytes": 417161216, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 436139, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 109, + "elapsedMs": 61285.897962, + "memory": { + "rssBytes": 403496960, + "peakRssBytes": 417161216, + "pssBytes": 407299072, + "virtualBytes": 2601406464, + "minorFaults": 439274, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 110, + "elapsedMs": 61710.886912, + "memory": { + "rssBytes": 403615744, + "peakRssBytes": 417198080, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 442392, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 111, + "elapsedMs": 62140.291824, + "memory": { + "rssBytes": 403664896, + "peakRssBytes": 417198080, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 445526, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 112, + "elapsedMs": 62686.546694000004, + "memory": { + "rssBytes": 403525632, + "peakRssBytes": 417198080, + "pssBytes": 407303168, + "virtualBytes": 2601406464, + "minorFaults": 448643, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 113, + "elapsedMs": 63115.653267, + "memory": { + "rssBytes": 403574784, + "peakRssBytes": 417198080, + "pssBytes": 407303168, + "virtualBytes": 2601406464, + "minorFaults": 451777, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 114, + "elapsedMs": 63550.110256, + "memory": { + "rssBytes": 403701760, + "peakRssBytes": 417275904, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 454895, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 115, + "elapsedMs": 63974.868721, + "memory": { + "rssBytes": 403468288, + "peakRssBytes": 417275904, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 458030, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 116, + "elapsedMs": 64406.056238000005, + "memory": { + "rssBytes": 403529728, + "peakRssBytes": 417275904, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 461147, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 117, + "elapsedMs": 64834.505742, + "memory": { + "rssBytes": 403562496, + "peakRssBytes": 417275904, + "pssBytes": 407302144, + "virtualBytes": 2601406464, + "minorFaults": 464284, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 118, + "elapsedMs": 65265.389200000005, + "memory": { + "rssBytes": 403636224, + "peakRssBytes": 417275904, + "pssBytes": 407303168, + "virtualBytes": 2601406464, + "minorFaults": 467402, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 119, + "elapsedMs": 65699.522076, + "memory": { + "rssBytes": 403435520, + "peakRssBytes": 417275904, + "pssBytes": 407303168, + "virtualBytes": 2601406464, + "minorFaults": 470538, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 120, + "elapsedMs": 66131.706112, + "memory": { + "rssBytes": 403300352, + "peakRssBytes": 417275904, + "pssBytes": 407344128, + "virtualBytes": 2601406464, + "minorFaults": 473666, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 121, + "elapsedMs": 66569.116509, + "memory": { + "rssBytes": 403398656, + "peakRssBytes": 417275904, + "pssBytes": 407360512, + "virtualBytes": 2601406464, + "minorFaults": 476804, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 122, + "elapsedMs": 67003.394005, + "memory": { + "rssBytes": 403501056, + "peakRssBytes": 417275904, + "pssBytes": 407360512, + "virtualBytes": 2601406464, + "minorFaults": 479922, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 123, + "elapsedMs": 67439.674132, + "memory": { + "rssBytes": 403279872, + "peakRssBytes": 417275904, + "pssBytes": 407360512, + "virtualBytes": 2601406464, + "minorFaults": 483056, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 124, + "elapsedMs": 67878.851257, + "memory": { + "rssBytes": 403353600, + "peakRssBytes": 417275904, + "pssBytes": 407360512, + "virtualBytes": 2601406464, + "minorFaults": 486174, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 125, + "elapsedMs": 68313.81547799999, + "memory": { + "rssBytes": 403390464, + "peakRssBytes": 417275904, + "pssBytes": 407364608, + "virtualBytes": 2601406464, + "minorFaults": 489310, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 126, + "elapsedMs": 68754.862996, + "memory": { + "rssBytes": 403251200, + "peakRssBytes": 417275904, + "pssBytes": 407364608, + "virtualBytes": 2601406464, + "minorFaults": 492427, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 127, + "elapsedMs": 69188.76113299999, + "memory": { + "rssBytes": 403329024, + "peakRssBytes": 417275904, + "pssBytes": 407389184, + "virtualBytes": 2601406464, + "minorFaults": 495567, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 128, + "elapsedMs": 69625.852025, + "memory": { + "rssBytes": 403386368, + "peakRssBytes": 417275904, + "pssBytes": 407389184, + "virtualBytes": 2601406464, + "minorFaults": 498686, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 129, + "elapsedMs": 70057.704189, + "memory": { + "rssBytes": 403664896, + "peakRssBytes": 417275904, + "pssBytes": 407389184, + "virtualBytes": 2601406464, + "minorFaults": 501820, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 130, + "elapsedMs": 70493.38569899999, + "memory": { + "rssBytes": 403513344, + "peakRssBytes": 417275904, + "pssBytes": 407389184, + "virtualBytes": 2601406464, + "minorFaults": 504937, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 131, + "elapsedMs": 70925.71278999999, + "memory": { + "rssBytes": 403550208, + "peakRssBytes": 417275904, + "pssBytes": 407388160, + "virtualBytes": 2601406464, + "minorFaults": 508072, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 132, + "elapsedMs": 71354.013135, + "memory": { + "rssBytes": 404451328, + "peakRssBytes": 418062336, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 511454, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 133, + "elapsedMs": 71784.21586499999, + "memory": { + "rssBytes": 404467712, + "peakRssBytes": 418062336, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 514589, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 134, + "elapsedMs": 72216.682435, + "memory": { + "rssBytes": 404332544, + "peakRssBytes": 418062336, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 517707, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 135, + "elapsedMs": 72643.861184, + "memory": { + "rssBytes": 404414464, + "peakRssBytes": 418062336, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 520842, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 136, + "elapsedMs": 73073.840451, + "memory": { + "rssBytes": 404516864, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 523959, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 137, + "elapsedMs": 73504.101575, + "memory": { + "rssBytes": 404344832, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 527092, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 138, + "elapsedMs": 73935.20135999999, + "memory": { + "rssBytes": 404422656, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 530210, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 139, + "elapsedMs": 74363.550061, + "memory": { + "rssBytes": 404451328, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 533348, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 140, + "elapsedMs": 74796.70958899999, + "memory": { + "rssBytes": 404525056, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 536466, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 141, + "elapsedMs": 75224.23258299999, + "memory": { + "rssBytes": 404262912, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 539600, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 142, + "elapsedMs": 75652.711184, + "memory": { + "rssBytes": 404324352, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 542717, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 143, + "elapsedMs": 76081.24595899999, + "memory": { + "rssBytes": 404361216, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 545852, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 144, + "elapsedMs": 76510.85444299999, + "memory": { + "rssBytes": 404500480, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 548970, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 145, + "elapsedMs": 76940.76076199999, + "memory": { + "rssBytes": 404312064, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 552103, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 146, + "elapsedMs": 77380.913236, + "memory": { + "rssBytes": 404451328, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 555221, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 147, + "elapsedMs": 77814.470299, + "memory": { + "rssBytes": 404451328, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 558356, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 148, + "elapsedMs": 78243.83518699999, + "memory": { + "rssBytes": 404316160, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 561474, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 149, + "elapsedMs": 78671.653439, + "memory": { + "rssBytes": 404414464, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 564608, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 150, + "elapsedMs": 79098.794088, + "memory": { + "rssBytes": 404275200, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 567726, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 151, + "elapsedMs": 79528.60048, + "memory": { + "rssBytes": 404623360, + "peakRssBytes": 418115584, + "pssBytes": 408469504, + "virtualBytes": 2601406464, + "minorFaults": 570861, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 152, + "elapsedMs": 79959.574199, + "memory": { + "rssBytes": 404455424, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 573978, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 153, + "elapsedMs": 80387.854265, + "memory": { + "rssBytes": 404504576, + "peakRssBytes": 418115584, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 577112, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 154, + "elapsedMs": 80825.31032599999, + "memory": { + "rssBytes": 404594688, + "peakRssBytes": 418205696, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 580231, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 155, + "elapsedMs": 81264.47484699999, + "memory": { + "rssBytes": 404643840, + "peakRssBytes": 418205696, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 583366, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 156, + "elapsedMs": 81696.30911999999, + "memory": { + "rssBytes": 404443136, + "peakRssBytes": 418205696, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 586485, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 157, + "elapsedMs": 82136.454386, + "memory": { + "rssBytes": 404451328, + "peakRssBytes": 418205696, + "pssBytes": 408470528, + "virtualBytes": 2601406464, + "minorFaults": 589619, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 158, + "elapsedMs": 82574.549159, + "memory": { + "rssBytes": 404512768, + "peakRssBytes": 418205696, + "pssBytes": 408474624, + "virtualBytes": 2601406464, + "minorFaults": 592737, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 159, + "elapsedMs": 83009.56287, + "memory": { + "rssBytes": 404533248, + "peakRssBytes": 418205696, + "pssBytes": 408474624, + "virtualBytes": 2601406464, + "minorFaults": 595872, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 160, + "elapsedMs": 83440.164936, + "memory": { + "rssBytes": 404635648, + "peakRssBytes": 418234368, + "pssBytes": 408474624, + "virtualBytes": 2601406464, + "minorFaults": 598989, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 161, + "elapsedMs": 83874.36669499999, + "memory": { + "rssBytes": 404467712, + "peakRssBytes": 418234368, + "pssBytes": 408474624, + "virtualBytes": 2601406464, + "minorFaults": 602124, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 162, + "elapsedMs": 84302.67554899999, + "memory": { + "rssBytes": 404434944, + "peakRssBytes": 418234368, + "pssBytes": 408474624, + "virtualBytes": 2601406464, + "minorFaults": 605243, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 163, + "elapsedMs": 84725.445778, + "memory": { + "rssBytes": 404512768, + "peakRssBytes": 418234368, + "pssBytes": 408609792, + "virtualBytes": 2601406464, + "minorFaults": 608409, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 164, + "elapsedMs": 85157.287751, + "memory": { + "rssBytes": 404574208, + "peakRssBytes": 418234368, + "pssBytes": 408609792, + "virtualBytes": 2601406464, + "minorFaults": 611527, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 165, + "elapsedMs": 85581.57696199999, + "memory": { + "rssBytes": 404369408, + "peakRssBytes": 418234368, + "pssBytes": 408609792, + "virtualBytes": 2601406464, + "minorFaults": 614661, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 166, + "elapsedMs": 86016.726274, + "memory": { + "rssBytes": 404504576, + "peakRssBytes": 418234368, + "pssBytes": 408609792, + "virtualBytes": 2601406464, + "minorFaults": 617779, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 167, + "elapsedMs": 86446.864695, + "memory": { + "rssBytes": 404598784, + "peakRssBytes": 418234368, + "pssBytes": 408626176, + "virtualBytes": 2601406464, + "minorFaults": 620917, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 168, + "elapsedMs": 86888.033998, + "memory": { + "rssBytes": 404455424, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 624037, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 169, + "elapsedMs": 87325.174701, + "memory": { + "rssBytes": 404525056, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 627172, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 170, + "elapsedMs": 87755.88849099999, + "memory": { + "rssBytes": 404381696, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 630290, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 171, + "elapsedMs": 88186.10448299999, + "memory": { + "rssBytes": 404398080, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 633426, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 172, + "elapsedMs": 88618.858643, + "memory": { + "rssBytes": 404512768, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 636542, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 173, + "elapsedMs": 89050.862732, + "memory": { + "rssBytes": 404549632, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 639677, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 174, + "elapsedMs": 89488.01374299999, + "memory": { + "rssBytes": 404357120, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 642795, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 175, + "elapsedMs": 89927.22968599999, + "memory": { + "rssBytes": 404434944, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 645930, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 176, + "elapsedMs": 90363.637779, + "memory": { + "rssBytes": 404283392, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 649047, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 177, + "elapsedMs": 90796.05602399999, + "memory": { + "rssBytes": 404340736, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 652183, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 178, + "elapsedMs": 91222.921492, + "memory": { + "rssBytes": 404471808, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 655301, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 179, + "elapsedMs": 91652.216502, + "memory": { + "rssBytes": 404283392, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 658436, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 180, + "elapsedMs": 92080.00573399999, + "memory": { + "rssBytes": 404357120, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 661554, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 181, + "elapsedMs": 92510.07999, + "memory": { + "rssBytes": 404373504, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 664690, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 182, + "elapsedMs": 92943.899654, + "memory": { + "rssBytes": 404439040, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 667807, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 183, + "elapsedMs": 93375.92663399999, + "memory": { + "rssBytes": 404504576, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 670941, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 184, + "elapsedMs": 93810.499694, + "memory": { + "rssBytes": 404365312, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 674059, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 185, + "elapsedMs": 94237.613709, + "memory": { + "rssBytes": 404430848, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 677193, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 186, + "elapsedMs": 94677.475987, + "memory": { + "rssBytes": 404508672, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 680311, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 187, + "elapsedMs": 95114.532872, + "memory": { + "rssBytes": 404340736, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 683446, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 188, + "elapsedMs": 95545.842165, + "memory": { + "rssBytes": 404471808, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 686563, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 189, + "elapsedMs": 95983.95303399999, + "memory": { + "rssBytes": 404537344, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 689697, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 190, + "elapsedMs": 96426.746888, + "memory": { + "rssBytes": 404369408, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 692814, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 191, + "elapsedMs": 96861.085278, + "memory": { + "rssBytes": 404434944, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 695948, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 192, + "elapsedMs": 97300.515023, + "memory": { + "rssBytes": 404504576, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 699065, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 193, + "elapsedMs": 97759.566077, + "memory": { + "rssBytes": 404271104, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 702200, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 194, + "elapsedMs": 98197.253761, + "memory": { + "rssBytes": 404361216, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 705318, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 195, + "elapsedMs": 98633.56032799999, + "memory": { + "rssBytes": 404426752, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 708452, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 196, + "elapsedMs": 99069.61441899999, + "memory": { + "rssBytes": 404246528, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 711570, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 197, + "elapsedMs": 99501.418163, + "memory": { + "rssBytes": 404299776, + "peakRssBytes": 418234368, + "pssBytes": 408630272, + "virtualBytes": 2601406464, + "minorFaults": 714705, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 198, + "elapsedMs": 99932.148213, + "memory": { + "rssBytes": 404377600, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 717823, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + }, + { + "cycle": 199, + "elapsedMs": 100357.99377999999, + "memory": { + "rssBytes": 404426752, + "peakRssBytes": 418234368, + "pssBytes": 408629248, + "virtualBytes": 2601406464, + "minorFaults": 720957, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 8, + "wasmtimeChargedModuleBytes": 69673704, + "queueDepth": 0 + } + } + ], + "completedAt": "2026-07-21T14:21:25.019Z", + "signalProbe": [ + { + "wasmExitCode": 143, + "javascriptExitCode": 143 + } + ], + "postSignal": { + "cycle": 200, + "elapsedMs": 100450.699964, + "memory": { + "rssBytes": 405786624, + "peakRssBytes": 418234368, + "pssBytes": 410059776, + "virtualBytes": 2601713664, + "minorFaults": 723661, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21, + "pids": [ + 3882452 + ] + }, + "resources": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 1, + "wasmtimeModuleEntries": 9, + "wasmtimeChargedModuleBytes": 70722280, + "queueDepth": 0 + } + }, + "summary": { + "measuredCycles": 200, + "windowSize": 50, + "rssGrowthBytes": 3022848, + "maxRssGrowthBytes": 50331648, + "pssSupported": true, + "pssGrowthBytes": 2778112, + "maxPssGrowthBytes": 50331648, + "processGrowth": 0, + "threadGrowth": 0, + "resourceGrowth": { + "runningProcesses": 0, + "stoppedProcesses": 0, + "exitedProcesses": 0, + "fdTables": 0, + "openFds": 0, + "pipes": 0, + "ptys": 0, + "sockets": 0, + "socketListeners": 0, + "socketConnections": 0, + "wasmReservedMemoryBytes": 0, + "wasmtimeEngineProfiles": 0, + "wasmtimeModuleEntries": 0, + "wasmtimeChargedModuleBytes": 0, + "queueDepth": 0 + }, + "resourcesPassed": true, + "passed": true, + "signalsPassed": true + } +} diff --git a/packages/runtime-benchmarks/results/wasmtime-threads.json b/packages/runtime-benchmarks/results/wasmtime-threads.json new file mode 100644 index 0000000000..801cb05507 --- /dev/null +++ b/packages/runtime-benchmarks/results/wasmtime-threads.json @@ -0,0 +1,1024 @@ +{ + "metadata": { + "startedAt": "2026-07-21T13:13:51.448Z", + "hostname": "nathan-dev", + "platform": "linux", + "arch": "x64", + "cpuModel": "12th Gen Intel(R) Core(TM) i7-12700KF", + "logicalCpus": 20, + "totalMemoryBytes": 67170398208, + "node": "v24.17.0", + "sidecar": { + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/target/release/agentos-native-sidecar", + "profile": "release", + "mtimeMs": 1784637258003.8499, + "mtimeIso": "2026-07-21T05:34:18.004-07:00", + "sizeBytes": 147918600 + }, + "fixture": { + "path": "/home/nathan/.herdr/workspaces/agent-os/wasmtime-executor/toolchain/c/build/pthread_benchmark.wasm", + "sizeBytes": 17082, + "sha256": "f9dc5046b1e9d0316e0ea82796b391adf332fde05a50cd041772656a53406a6b" + }, + "startupSamples": 5, + "throughputSamples": 20, + "memorySamples": 3, + "threadCounts": [ + 1, + 2, + 4, + 8 + ], + "concurrencyLevels": [ + 1, + 2, + 4, + 8 + ], + "concurrentWorkerThreadsPerGroup": 2, + "maxThreadsPerGroup": 9, + "maxConcurrentThreads": 72, + "backend": "wasmtime-threads", + "aot": false, + "pooling": false, + "wizer": false, + "liveSnapshots": false + }, + "startup": [ + { + "index": 0, + "vmSetupMs": 724.705965, + "readyMs": 51.03019900000004, + "totalMs": 63.9161949999999, + "exitCode": 0, + "stdout": "ready:4\ndone:4\n", + "stderr": "", + "baseline": { + "rssBytes": 28487680, + "peakRssBytes": 28487680, + "pssBytes": 28674048, + "virtualBytes": 1255784448, + "minorFaults": 2450, + "majorFaults": 0, + "processCount": 1, + "threadCount": 16, + "pids": [ + 3110442 + ] + }, + "retained": { + "rssBytes": 32387072, + "peakRssBytes": 32387072, + "pssBytes": 32545792, + "virtualBytes": 1325174784, + "minorFaults": 2574, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3110442 + ] + }, + "drainMs": 0.7464350000000195, + "passed": true + }, + { + "index": 1, + "vmSetupMs": 661.7602650000001, + "readyMs": 46.731183999999985, + "totalMs": 58.768970999999965, + "exitCode": 0, + "stdout": "ready:4\ndone:4\n", + "stderr": "", + "baseline": { + "rssBytes": 28884992, + "peakRssBytes": 28884992, + "pssBytes": 29148160, + "virtualBytes": 1255784448, + "minorFaults": 2465, + "majorFaults": 0, + "processCount": 1, + "threadCount": 16, + "pids": [ + 3110649 + ] + }, + "retained": { + "rssBytes": 32411648, + "peakRssBytes": 32411648, + "pssBytes": 32566272, + "virtualBytes": 1325285376, + "minorFaults": 2585, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3110649 + ] + }, + "drainMs": 0.46948300000008203, + "passed": true + }, + { + "index": 2, + "vmSetupMs": 680.2218339999997, + "readyMs": 45.73889800000006, + "totalMs": 57.57805899999994, + "exitCode": 0, + "stdout": "ready:4\ndone:4\n", + "stderr": "", + "baseline": { + "rssBytes": 28610560, + "peakRssBytes": 28610560, + "pssBytes": 28747776, + "virtualBytes": 1255788544, + "minorFaults": 2453, + "majorFaults": 0, + "processCount": 1, + "threadCount": 16, + "pids": [ + 3110764 + ] + }, + "retained": { + "rssBytes": 32620544, + "peakRssBytes": 32620544, + "pssBytes": 32754688, + "virtualBytes": 1325178880, + "minorFaults": 2580, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3110764 + ] + }, + "drainMs": 0.3418789999996079, + "passed": true + }, + { + "index": 3, + "vmSetupMs": 675.066425, + "readyMs": 47.38774999999987, + "totalMs": 59.32174699999996, + "exitCode": 0, + "stdout": "ready:4\ndone:4\n", + "stderr": "", + "baseline": { + "rssBytes": 28643328, + "peakRssBytes": 28643328, + "pssBytes": 28732416, + "virtualBytes": 1255784448, + "minorFaults": 2448, + "majorFaults": 0, + "processCount": 1, + "threadCount": 16, + "pids": [ + 3110961 + ] + }, + "retained": { + "rssBytes": 32288768, + "peakRssBytes": 32288768, + "pssBytes": 32411648, + "virtualBytes": 1325170688, + "minorFaults": 2569, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3110961 + ] + }, + "drainMs": 0.4464250000000902, + "passed": true + }, + { + "index": 4, + "vmSetupMs": 655.8274780000002, + "readyMs": 45.818835000000036, + "totalMs": 57.97236699999985, + "exitCode": 0, + "stdout": "ready:4\ndone:4\n", + "stderr": "", + "baseline": { + "rssBytes": 28884992, + "peakRssBytes": 28884992, + "pssBytes": 29015040, + "virtualBytes": 1255784448, + "minorFaults": 2459, + "majorFaults": 0, + "processCount": 1, + "threadCount": 16, + "pids": [ + 3111185 + ] + }, + "retained": { + "rssBytes": 32526336, + "peakRssBytes": 32526336, + "pssBytes": 32686080, + "virtualBytes": 1325174784, + "minorFaults": 2578, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111185 + ] + }, + "drainMs": 0.4711050000000796, + "passed": true + } + ], + "throughput": { + "workerThreadsPerExecution": 4, + "totalGuestThreadsPerExecution": 5, + "durationsMs": [ + 61.76770699999997, + 57.73457600000074, + 57.472304999999324, + 54.0336099999995, + 56.477232999999615, + 56.86034500000005, + 58.63959299999988, + 59.24622999999974, + 53.42953900000066, + 58.153479999999945, + 54.196523999999954, + 58.631655999999566, + 54.40440800000033, + 54.58955900000001, + 60.276732000000266, + 57.60704799999985, + 56.37571099999968, + 57.83496800000012, + 55.76858799999991, + 56.09504500000003 + ], + "p50Ms": 56.86034500000005, + "p95Ms": 60.276732000000266, + "executionsPerSecond": 17.550096753376287, + "passed": true + }, + "memory": [ + { + "threadCount": 1, + "sample": 0, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33056768, + "virtualBytes": 1404039168, + "minorFaults": 2928, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57495552, + "peakRssBytes": 57544704, + "pssBytes": 49098752, + "virtualBytes": 6727262208, + "minorFaults": 5424, + "majorFaults": 1, + "processCount": 2, + "threadCount": 31, + "pids": [ + 3111339, + 3112035 + ] + }, + "delta": { + "rssBytes": 24788992, + "pssBytes": 16041984, + "virtualBytes": 5323223040, + "minorFaults": 2496, + "majorFaults": 0, + "processCount": 1, + "threadCount": 14 + }, + "exitCode": 143, + "terminationMs": 12.062133999999787, + "drainMs": 0.3599240000003192, + "passed": true + }, + { + "threadCount": 1, + "sample": 1, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33186816, + "virtualBytes": 1404039168, + "minorFaults": 2944, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57495552, + "peakRssBytes": 57544704, + "pssBytes": 49060864, + "virtualBytes": 6727262208, + "minorFaults": 5436, + "majorFaults": 1, + "processCount": 2, + "threadCount": 31, + "pids": [ + 3111339, + 3112113 + ] + }, + "delta": { + "rssBytes": 24788992, + "pssBytes": 15874048, + "virtualBytes": 5323223040, + "minorFaults": 2492, + "majorFaults": 0, + "processCount": 1, + "threadCount": 14 + }, + "exitCode": 143, + "terminationMs": 30.928291000000172, + "drainMs": 0.4798529999998209, + "passed": true + }, + { + "threadCount": 1, + "sample": 2, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33187840, + "virtualBytes": 1404039168, + "minorFaults": 2958, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57315328, + "peakRssBytes": 57364480, + "pssBytes": 49004544, + "virtualBytes": 6727262208, + "minorFaults": 5453, + "majorFaults": 1, + "processCount": 2, + "threadCount": 31, + "pids": [ + 3111339, + 3112193 + ] + }, + "delta": { + "rssBytes": 24608768, + "pssBytes": 15816704, + "virtualBytes": 5323223040, + "minorFaults": 2495, + "majorFaults": 0, + "processCount": 1, + "threadCount": 14 + }, + "exitCode": 143, + "terminationMs": 11.530754000000343, + "drainMs": 0.32052999999996246, + "passed": true + }, + { + "threadCount": 2, + "sample": 0, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33196032, + "virtualBytes": 1404039168, + "minorFaults": 2974, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57520128, + "peakRssBytes": 57569280, + "pssBytes": 49277952, + "virtualBytes": 6798852096, + "minorFaults": 5505, + "majorFaults": 1, + "processCount": 2, + "threadCount": 32, + "pids": [ + 3111339, + 3112212 + ] + }, + "delta": { + "rssBytes": 24813568, + "pssBytes": 16081920, + "virtualBytes": 5394812928, + "minorFaults": 2531, + "majorFaults": 0, + "processCount": 1, + "threadCount": 15 + }, + "exitCode": 143, + "terminationMs": 11.89134599999943, + "drainMs": 0.2693010000002687, + "passed": true + }, + { + "threadCount": 2, + "sample": 1, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33196032, + "virtualBytes": 1404039168, + "minorFaults": 2988, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57450496, + "peakRssBytes": 57499648, + "pssBytes": 48880640, + "virtualBytes": 6798860288, + "minorFaults": 5525, + "majorFaults": 1, + "processCount": 2, + "threadCount": 32, + "pids": [ + 3111339, + 3112247 + ] + }, + "delta": { + "rssBytes": 24743936, + "pssBytes": 15684608, + "virtualBytes": 5394821120, + "minorFaults": 2537, + "majorFaults": 0, + "processCount": 1, + "threadCount": 15 + }, + "exitCode": 143, + "terminationMs": 11.622113000000354, + "drainMs": 0.2879259999999704, + "passed": true + }, + { + "threadCount": 2, + "sample": 2, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33199104, + "virtualBytes": 1404039168, + "minorFaults": 3003, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57364480, + "peakRssBytes": 57413632, + "pssBytes": 49340416, + "virtualBytes": 6798852096, + "minorFaults": 5539, + "majorFaults": 1, + "processCount": 2, + "threadCount": 32, + "pids": [ + 3111339, + 3112273 + ] + }, + "delta": { + "rssBytes": 24657920, + "pssBytes": 16141312, + "virtualBytes": 5394812928, + "minorFaults": 2536, + "majorFaults": 0, + "processCount": 1, + "threadCount": 15 + }, + "exitCode": 143, + "terminationMs": 11.70908800000052, + "drainMs": 0.33450099999936356, + "passed": true + }, + { + "threadCount": 4, + "sample": 0, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33203200, + "virtualBytes": 1404039168, + "minorFaults": 3018, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 58908672, + "peakRssBytes": 58912768, + "pssBytes": 49357824, + "virtualBytes": 6942035968, + "minorFaults": 5641, + "majorFaults": 1, + "processCount": 2, + "threadCount": 34, + "pids": [ + 3111339, + 3112333 + ] + }, + "delta": { + "rssBytes": 26202112, + "pssBytes": 16154624, + "virtualBytes": 5537996800, + "minorFaults": 2623, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17 + }, + "exitCode": 143, + "terminationMs": 30.994345000000067, + "drainMs": 0.38987899999938236, + "passed": true + }, + { + "threadCount": 4, + "sample": 1, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33204224, + "virtualBytes": 1404039168, + "minorFaults": 3032, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57561088, + "peakRssBytes": 57610240, + "pssBytes": 49673216, + "virtualBytes": 6942040064, + "minorFaults": 5651, + "majorFaults": 1, + "processCount": 2, + "threadCount": 34, + "pids": [ + 3111339, + 3112362 + ] + }, + "delta": { + "rssBytes": 24854528, + "pssBytes": 16468992, + "virtualBytes": 5538000896, + "minorFaults": 2619, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17 + }, + "exitCode": 143, + "terminationMs": 11.590193, + "drainMs": 0.26421399999981077, + "passed": true + }, + { + "threadCount": 4, + "sample": 2, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33204224, + "virtualBytes": 1404039168, + "minorFaults": 3046, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 58949632, + "peakRssBytes": 58953728, + "pssBytes": 49693696, + "virtualBytes": 6942035968, + "minorFaults": 5670, + "majorFaults": 1, + "processCount": 2, + "threadCount": 34, + "pids": [ + 3111339, + 3112379 + ] + }, + "delta": { + "rssBytes": 26243072, + "pssBytes": 16489472, + "virtualBytes": 5537996800, + "minorFaults": 2624, + "majorFaults": 0, + "processCount": 1, + "threadCount": 17 + }, + "exitCode": 143, + "terminationMs": 21.802170000000842, + "drainMs": 0.33129399999961606, + "passed": true + }, + { + "threadCount": 8, + "sample": 0, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33203200, + "virtualBytes": 1404039168, + "minorFaults": 3060, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 58945536, + "peakRssBytes": 58949632, + "pssBytes": 50406400, + "virtualBytes": 7228395520, + "minorFaults": 5857, + "majorFaults": 1, + "processCount": 2, + "threadCount": 38, + "pids": [ + 3111339, + 3112407 + ] + }, + "delta": { + "rssBytes": 26238976, + "pssBytes": 17203200, + "virtualBytes": 5824356352, + "minorFaults": 2797, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21 + }, + "exitCode": 143, + "terminationMs": 11.559551000000283, + "drainMs": 0.40012300000034884, + "passed": true + }, + { + "threadCount": 8, + "sample": 1, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33203200, + "virtualBytes": 1404039168, + "minorFaults": 3074, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 58773504, + "peakRssBytes": 58777600, + "pssBytes": 50061312, + "virtualBytes": 7228391424, + "minorFaults": 5868, + "majorFaults": 1, + "processCount": 2, + "threadCount": 38, + "pids": [ + 3111339, + 3112484 + ] + }, + "delta": { + "rssBytes": 26066944, + "pssBytes": 16858112, + "virtualBytes": 5824352256, + "minorFaults": 2794, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21 + }, + "exitCode": 143, + "terminationMs": 11.723117000000457, + "drainMs": 0.3348790000000008, + "passed": true + }, + { + "threadCount": 8, + "sample": 2, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33204224, + "virtualBytes": 1404039168, + "minorFaults": 3088, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 58929152, + "peakRssBytes": 58933248, + "pssBytes": 50406400, + "virtualBytes": 7228395520, + "minorFaults": 5886, + "majorFaults": 1, + "processCount": 2, + "threadCount": 38, + "pids": [ + 3111339, + 3112506 + ] + }, + "delta": { + "rssBytes": 26222592, + "pssBytes": 17202176, + "virtualBytes": 5824356352, + "minorFaults": 2798, + "majorFaults": 0, + "processCount": 1, + "threadCount": 21 + }, + "exitCode": 143, + "terminationMs": 11.47647599999982, + "drainMs": 0.31678499999998166, + "passed": true + } + ], + "concurrency": [ + { + "groupCount": 1, + "workerThreadsPerGroup": 2, + "totalGuestThreads": 3, + "readyMs": 37.096559999999954, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33204224, + "virtualBytes": 1404039168, + "minorFaults": 3102, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 57651200, + "peakRssBytes": 57700352, + "pssBytes": 49432576, + "virtualBytes": 6798860288, + "minorFaults": 5645, + "majorFaults": 1, + "processCount": 2, + "threadCount": 32, + "pids": [ + 3111339, + 3112527 + ] + }, + "delta": { + "rssBytes": 24944640, + "pssBytes": 16228352, + "virtualBytes": 5394821120, + "minorFaults": 2543, + "majorFaults": 0, + "processCount": 1, + "threadCount": 15 + }, + "exitCodes": [ + 143 + ], + "terminationMs": 11.819405999999617, + "drainMs": 0.281872999999905, + "passed": true + }, + { + "groupCount": 2, + "workerThreadsPerGroup": 2, + "totalGuestThreads": 6, + "readyMs": 38.159560999999485, + "baseline": { + "rssBytes": 32706560, + "peakRssBytes": 32763904, + "pssBytes": 33207296, + "virtualBytes": 1404039168, + "minorFaults": 3117, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 82890752, + "peakRssBytes": 82980864, + "pssBytes": 57617408, + "virtualBytes": 12267692032, + "minorFaults": 8217, + "majorFaults": 1, + "processCount": 3, + "threadCount": 47, + "pids": [ + 3111339, + 3112554, + 3112556 + ] + }, + "delta": { + "rssBytes": 50184192, + "pssBytes": 24410112, + "virtualBytes": 10863652864, + "minorFaults": 5100, + "majorFaults": 0, + "processCount": 2, + "threadCount": 30 + }, + "exitCodes": [ + 143, + 143 + ], + "terminationMs": 32.18335500000012, + "drainMs": 0.2953590000006443, + "passed": true + }, + { + "groupCount": 4, + "workerThreadsPerGroup": 2, + "totalGuestThreads": 12, + "readyMs": 45.80602200000067, + "baseline": { + "rssBytes": 33361920, + "peakRssBytes": 33456128, + "pssBytes": 33343488, + "virtualBytes": 1468452864, + "minorFaults": 3182, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 133718016, + "peakRssBytes": 133808128, + "pssBytes": 74580992, + "virtualBytes": 23205330944, + "minorFaults": 13147, + "majorFaults": 461, + "processCount": 5, + "threadCount": 77, + "pids": [ + 3111339, + 3112642, + 3112643, + 3112644, + 3112645 + ] + }, + "delta": { + "rssBytes": 100356096, + "pssBytes": 41237504, + "virtualBytes": 21736878080, + "minorFaults": 9965, + "majorFaults": 460, + "processCount": 4, + "threadCount": 60 + }, + "exitCodes": [ + 143, + 143, + 143, + 143 + ], + "terminationMs": 31.421473999999762, + "drainMs": 0.30508199999985663, + "passed": true + }, + { + "groupCount": 8, + "workerThreadsPerGroup": 2, + "totalGuestThreads": 24, + "readyMs": 83.45482999999967, + "baseline": { + "rssBytes": 33624064, + "peakRssBytes": 33705984, + "pssBytes": 33605632, + "virtualBytes": 1606873088, + "minorFaults": 3311, + "majorFaults": 1, + "processCount": 1, + "threadCount": 17, + "pids": [ + 3111339 + ] + }, + "live": { + "rssBytes": 234811392, + "peakRssBytes": 234991616, + "pssBytes": 107987968, + "virtualBytes": 45080641536, + "minorFaults": 23030, + "majorFaults": 1174, + "processCount": 9, + "threadCount": 137, + "pids": [ + 3111339, + 3112706, + 3112707, + 3112708, + 3112709, + 3112710, + 3112711, + 3112712, + 3112713 + ] + }, + "delta": { + "rssBytes": 201187328, + "pssBytes": 74382336, + "virtualBytes": 43473768448, + "minorFaults": 19719, + "majorFaults": 1173, + "processCount": 8, + "threadCount": 120 + }, + "exitCodes": [ + 143, + 143, + 143, + 143, + 143, + 143, + 143, + 143 + ], + "terminationMs": 31.947091, + "drainMs": 0.29537600000003295, + "passed": true + } + ], + "status": "complete", + "summary": { + "coldReadyP50Ms": 46.731183999999985, + "coldReadyP95Ms": 51.03019900000004, + "warmThroughputPerSecond": 17.550096753376287, + "oneThreadGroupPssDeltaBytes": 15874048, + "maxThreadGroupPssDeltaBytes": 17202176, + "perAdditionalThreadPssBytes": 189732.57142857142, + "maxTerminationMs": 32.18335500000012, + "maxConcurrentGroupsMeasured": 8, + "maxConcurrentGuestThreadsMeasured": 24, + "passed": true + }, + "completedAt": "2026-07-21T13:13:59.039Z" +} diff --git a/packages/runtime-benchmarks/src/focused/wasm-mixed-soak.ts b/packages/runtime-benchmarks/src/focused/wasm-mixed-soak.ts new file mode 100644 index 0000000000..bcde44a1e2 --- /dev/null +++ b/packages/runtime-benchmarks/src/focused/wasm-mixed-soak.ts @@ -0,0 +1,566 @@ +import { writeFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { cpus, hostname, totalmem } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; +import type { NodeRuntimeResourceSnapshot } from "@rivet-dev/agentos-runtime-core"; +import { + type ProcessTreeMemorySnapshot, + readProcessTreeMemorySnapshot, +} from "../lib/memory.js"; +import { + type BenchVm, + createBenchSidecar, + createBenchVm, + formatSidecarProvenance, + resolveBenchSidecarProvenance, +} from "../lib/vm.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const outputPath = resolve( + process.env.AGENTOS_WASM_MIXED_SOAK_OUTPUT ?? + join(here, "../../results/wasm-mixed-soak.json"), +); +const vmCount = integerEnv("AGENTOS_WASM_MIXED_SOAK_VMS", 1); +const warmupCycles = integerEnv("AGENTOS_WASM_MIXED_SOAK_WARMUP", 5); +const measuredCycles = integerEnv("AGENTOS_WASM_MIXED_SOAK_CYCLES", 20); +const settleMs = integerEnv("AGENTOS_WASM_MIXED_SOAK_SETTLE_MS", 100); +const operationTimeoutMs = integerEnv( + "AGENTOS_WASM_MIXED_SOAK_OPERATION_TIMEOUT_MS", + 30_000, +); +const maxRssGrowthBytes = integerEnv( + "AGENTOS_WASM_MIXED_SOAK_MAX_RSS_GROWTH_BYTES", + 48 * 1024 * 1024, +); +const maxPssGrowthBytes = integerEnv( + "AGENTOS_WASM_MIXED_SOAK_MAX_PSS_GROWTH_BYTES", + 48 * 1024 * 1024, +); +const sidecarProvenance = resolveBenchSidecarProvenance(); +const guestProgramPath = "/tmp/mixed-soak.mjs"; + +const guestProgram = `import { spawn } from "node:child_process"; + +const response = await fetch(process.env.SOAK_URL, { signal: AbortSignal.timeout(5000) }); +const body = await response.text(); +if (!response.ok || !body.startsWith("mixed-http:")) { + throw new Error(\`unexpected fetch response \${response.status}: \${body}\`); +} + +const tag = process.env.SOAK_TAG; +const childResult = await new Promise((resolve, reject) => { + const child = spawn("printf", ["%s\\n", tag], { + env: { ...process.env, SOAK_TAG: tag }, + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { stdout += chunk; }); + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", reject); + child.on("close", (code) => resolve({ code, stdout, stderr })); +}); +if (childResult.code !== 0 || childResult.stdout !== \`\${tag}\\n\`) { + throw new Error(\`child shell failed: \${JSON.stringify(childResult)}\`); +} +process.stdout.write(JSON.stringify({ tag, body, child: childResult.stdout.trim() })); +`; + +interface SoakSample { + cycle: number; + elapsedMs: number; + memory: ProcessTreeMemorySnapshot; + resources: AggregateResources; +} + +interface AggregateResources { + runningProcesses: number; + stoppedProcesses: number; + exitedProcesses: number; + fdTables: number; + openFds: number; + pipes: number; + ptys: number; + sockets: number; + socketListeners: number; + socketConnections: number; + wasmReservedMemoryBytes: number; + wasmtimeEngineProfiles: number; + wasmtimeModuleEntries: number; + wasmtimeChargedModuleBytes: number; + queueDepth: number; +} + +async function main(): Promise { + if (process.platform !== "linux") { + throw new Error( + "mixed WASM/V8 soak memory validation requires Linux /proc", + ); + } + if (sidecarProvenance.profile !== "release") { + throw new Error( + `mixed soak requires a release sidecar, got ${formatSidecarProvenance(sidecarProvenance)}`, + ); + } + + const hostServer = createServer((request, response) => { + response.writeHead(200, { + "content-type": "text/plain", + connection: "close", + }); + response.end(`mixed-http:${request.url ?? "/"}`); + }); + await new Promise((resolveListen, rejectListen) => { + hostServer.once("error", rejectListen); + hostServer.listen(0, "127.0.0.1", () => resolveListen()); + }); + const address = hostServer.address(); + if (!address || typeof address === "string") { + throw new Error("mixed soak could not resolve loopback server port"); + } + + const sidecar = createBenchSidecar(); + const vms: BenchVm[] = []; + const started = performance.now(); + try { + for (let index = 0; index < vmCount; index++) { + const vm = await createBenchVm({ + sidecar, + wasmBackend: "wasmtime", + loopbackExemptPorts: [address.port], + }); + await vm.writeFile(guestProgramPath, guestProgram); + vms.push(vm); + } + + for (let cycle = 0; cycle < warmupCycles; cycle++) { + await runCycle(vms, address.port, `warm-${cycle}`); + } + await waitForDrain(vms); + await delay(Math.max(settleMs, 500)); + + const sidecarPid = vms[0]?.sidecarPid(); + if (sidecarPid === null || sidecarPid === undefined) { + throw new Error("mixed soak could not resolve the shared sidecar PID"); + } + const baseline = await takeSample(vms, sidecarPid, -1, started); + const samples: SoakSample[] = []; + const checkpoint = baseReport(baseline, samples); + writeCheckpoint(checkpoint); + + for (let cycle = 0; cycle < measuredCycles; cycle++) { + await runCycle(vms, address.port, `cycle-${cycle}`); + await waitForDrain(vms); + await delay(settleMs); + samples.push(await takeSample(vms, sidecarPid, cycle, started)); + writeCheckpoint(baseReport(baseline, samples)); + } + + const signalProbe = await Promise.all(vms.map((vm) => runSignalProbe(vm))); + await waitForDrain(vms); + const postSignal = await takeSample( + vms, + sidecarPid, + measuredCycles, + started, + ); + const plateau = summarize(baseline, samples); + const signalsPassed = signalProbe.every( + (result) => + result.wasmExitCode >= 128 && result.javascriptExitCode >= 128, + ); + const summary = { + ...plateau, + signalsPassed, + passed: plateau.passed && signalsPassed, + }; + const report = { + ...baseReport(baseline, samples), + completedAt: new Date().toISOString(), + status: summary.passed ? "complete" : "failed", + signalProbe, + postSignal, + summary, + }; + writeCheckpoint(report); + console.log(JSON.stringify(summary, null, 2)); + console.error(`raw results: ${outputPath}`); + if (!summary.passed) { + throw new Error("mixed V8-JavaScript/Wasmtime soak did not plateau"); + } + } finally { + await Promise.allSettled(vms.map((vm) => vm.dispose())); + await sidecar.dispose(); + await new Promise((resolveClose, rejectClose) => { + hostServer.close((error) => + error ? rejectClose(error) : resolveClose(), + ); + }); + } +} + +async function runCycle( + vms: BenchVm[], + port: number, + cycle: string, +): Promise { + await withTimeout( + Promise.all( + vms.map((vm, index) => runVmCycle(vm, port, `${cycle}-vm${index}`)), + ), + operationTimeoutMs * 4, + `mixed workload ${cycle}`, + ); +} + +async function runVmCycle( + vm: BenchVm, + port: number, + tag: string, +): Promise { + const url = `http://127.0.0.1:${port}/${tag}`; + const guestResult = await withTimeout( + runGuest(vm, tag, url), + operationTimeoutMs, + `V8 guest ${tag}`, + ); + let guestPayload: { tag?: string; body?: string; child?: string }; + try { + guestPayload = JSON.parse(guestResult); + } catch { + throw new Error( + `V8 mixed guest returned invalid JSON for ${tag}: ${guestResult}`, + ); + } + if ( + guestPayload.tag !== tag || + guestPayload.child !== tag || + guestPayload.body !== `mixed-http:/${tag}` + ) { + throw new Error( + `V8 guest/Wasmtime child affinity failed for ${tag}: ${JSON.stringify(guestPayload)}`, + ); + } + await delay(settleMs); + + const directory = `/tmp/${tag}`; + await runWasmCommand(vm, "mkdir", ["-p", directory], tag); + const pipelineResult = await runWasmCommand( + vm, + "sh", + [ + "-c", + 'printf "%s\\n" "$1" > "$2/value"; cat "$2/value" | tr "[:lower:]" "[:upper:]"', + "sh", + tag, + directory, + ], + tag, + ); + if (pipelineResult.stdout !== `${tag.toUpperCase()}\n`) { + throw new Error( + `Wasmtime child pipeline returned unexpected output for ${tag}: ${JSON.stringify(pipelineResult)}`, + ); + } + const filesystemResult = await runWasmCommand(vm, "ls", [directory], tag); + if (!filesystemResult.stdout.includes("value")) { + throw new Error(`Wasmtime filesystem workload lost ${directory}/value`); + } + await runWasmCommand(vm, "rm", ["-rf", directory], tag); + const networkResult = await withTimeout( + vm.execArgv("curl", ["--max-time", "5", "-fsS", url], { + timeout: operationTimeoutMs, + }), + operationTimeoutMs, + `Wasmtime network ${tag}`, + ); + if ( + networkResult.exitCode !== 0 || + !networkResult.stdout.includes(`mixed-http:/${tag}`) + ) { + throw new Error( + `Wasmtime network workload failed for ${tag}: exit=${networkResult.exitCode} stdout=${networkResult.stdout} stderr=${networkResult.stderr}`, + ); + } +} + +async function runWasmCommand( + vm: BenchVm, + command: string, + args: string[], + tag: string, +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + const result = await withTimeout( + vm.execArgv(command, args, { timeout: operationTimeoutMs }), + operationTimeoutMs, + `Wasmtime ${command} ${tag}`, + ); + if (result.exitCode !== 0) { + throw new Error( + `Wasmtime ${command} failed for ${tag}: exit=${result.exitCode} stdout=${result.stdout} stderr=${result.stderr}`, + ); + } + return result; +} + +async function runSignalProbe(vm: BenchVm): Promise<{ + wasmExitCode: number; + javascriptExitCode: number; +}> { + const wasm = vm.spawn("sleep", ["30"]); + const javascript = vm.spawn("node", ["-e", "setInterval(() => {}, 1000)"]); + await delay(50); + wasm.kill("SIGTERM"); + javascript.kill("SIGTERM"); + const [wasmExitCode, javascriptExitCode] = await Promise.all([ + withTimeout(wasm.wait(), operationTimeoutMs, "Wasmtime signal probe"), + withTimeout(javascript.wait(), operationTimeoutMs, "V8 signal probe"), + ]); + return { wasmExitCode, javascriptExitCode }; +} + +async function runGuest( + vm: BenchVm, + tag: string, + url: string, +): Promise { + let stdout = ""; + let stderr = ""; + const process = vm.spawn("node", [guestProgramPath], { + env: { SOAK_TAG: tag, SOAK_URL: url }, + onStdout(data) { + stdout += Buffer.from(data).toString("utf8"); + }, + onStderr(data) { + stderr += Buffer.from(data).toString("utf8"); + }, + }); + const exitCode = await process.wait(); + if (exitCode !== 0) { + throw new Error(`mixed V8 guest exited ${exitCode}: ${stderr}`); + } + return stdout; +} + +async function waitForDrain(vms: BenchVm[]): Promise { + const started = performance.now(); + for (;;) { + const snapshots = await Promise.all( + vms.map((vm) => vm.getResourceSnapshot()), + ); + if ( + snapshots.every( + (snapshot) => + snapshot.runningProcesses === 0 && + snapshot.stoppedProcesses === 0 && + snapshot.wasmReservedMemoryBytes === 0, + ) + ) { + return; + } + if (performance.now() - started > operationTimeoutMs) { + throw new Error( + `mixed workload resources did not drain: ${JSON.stringify(snapshots)}`, + ); + } + await delay(10); + } +} + +async function takeSample( + vms: BenchVm[], + sidecarPid: number, + cycle: number, + started: number, +): Promise { + return { + cycle, + elapsedMs: performance.now() - started, + memory: readProcessTreeMemorySnapshot(sidecarPid), + resources: aggregateResources( + await Promise.all(vms.map((vm) => vm.getResourceSnapshot())), + ), + }; +} + +function aggregateResources( + snapshots: NodeRuntimeResourceSnapshot[], +): AggregateResources { + const sum = (key: keyof NodeRuntimeResourceSnapshot) => + snapshots.reduce((total, snapshot) => total + Number(snapshot[key]), 0); + const max = (key: keyof NodeRuntimeResourceSnapshot) => + Math.max(0, ...snapshots.map((snapshot) => Number(snapshot[key]))); + return { + runningProcesses: sum("runningProcesses"), + stoppedProcesses: sum("stoppedProcesses"), + exitedProcesses: sum("exitedProcesses"), + fdTables: sum("fdTables"), + openFds: sum("openFds"), + pipes: sum("pipes"), + ptys: sum("ptys"), + sockets: sum("sockets"), + socketListeners: sum("socketListeners"), + socketConnections: sum("socketConnections"), + wasmReservedMemoryBytes: sum("wasmReservedMemoryBytes"), + wasmtimeEngineProfiles: max("wasmtimeEngineProfiles"), + wasmtimeModuleEntries: max("wasmtimeModuleEntries"), + wasmtimeChargedModuleBytes: max("wasmtimeChargedModuleBytes"), + queueDepth: snapshots.reduce( + (total, snapshot) => + total + + snapshot.queueSnapshots + .filter( + (queue) => + queue.category === "queue" && + queue.name !== "sidecar_stdin_frames", + ) + .reduce((sum, queue) => sum + queue.depth, 0), + 0, + ), + }; +} + +function summarize(baseline: SoakSample, samples: SoakSample[]) { + const windowSize = Math.max(1, Math.floor(samples.length / 4)); + const first = samples.slice(0, windowSize); + const last = samples.slice(-windowSize); + const drift = (key: "rssBytes" | "pssBytes") => + median(last.map((sample) => sample.memory[key])) - + median(first.map((sample) => sample.memory[key])); + const rssGrowthBytes = drift("rssBytes"); + const pssSupported = samples.some((sample) => sample.memory.pssBytes > 0); + const pssGrowthBytes = pssSupported ? drift("pssBytes") : 0; + const retainedResourceKeys: Array = [ + "runningProcesses", + "stoppedProcesses", + "exitedProcesses", + "fdTables", + "openFds", + "pipes", + "ptys", + "sockets", + "socketListeners", + "socketConnections", + "wasmReservedMemoryBytes", + "wasmtimeEngineProfiles", + "wasmtimeModuleEntries", + "wasmtimeChargedModuleBytes", + "queueDepth", + ]; + const resourceGrowth = Object.fromEntries( + retainedResourceKeys.map((key) => [ + key, + Math.max(...last.map((sample) => sample.resources[key])) - + baseline.resources[key], + ]), + ) as Record; + const processGrowth = + Math.max(...last.map((sample) => sample.memory.processCount)) - + baseline.memory.processCount; + const threadGrowth = + Math.max(...last.map((sample) => sample.memory.threadCount)) - + baseline.memory.threadCount; + const resourcesPassed = retainedResourceKeys.every( + (key) => resourceGrowth[key] <= 0, + ); + return { + measuredCycles: samples.length, + windowSize, + rssGrowthBytes, + maxRssGrowthBytes, + pssSupported, + pssGrowthBytes, + maxPssGrowthBytes, + processGrowth, + threadGrowth, + resourceGrowth, + resourcesPassed, + passed: + samples.length === measuredCycles && + rssGrowthBytes <= maxRssGrowthBytes && + (!pssSupported || pssGrowthBytes <= maxPssGrowthBytes) && + processGrowth <= 0 && + threadGrowth <= 0 && + resourcesPassed, + }; +} + +function baseReport(baseline: SoakSample, samples: SoakSample[]) { + return { + benchmark: "wasm-mixed-soak", + status: "running", + metadata: { + startedAt: new Date(Date.now() - performance.now()).toISOString(), + hostname: hostname(), + platform: process.platform, + arch: process.arch, + cpuModel: cpus()[0]?.model ?? "unknown", + logicalCpus: cpus().length, + totalMemoryBytes: totalmem(), + node: process.version, + sidecar: sidecarProvenance, + wasmBackend: "wasmtime", + javascriptBackend: "v8", + vmCount, + warmupCycles, + measuredCycles, + settleMs, + operationTimeoutMs, + maxRssGrowthBytes, + maxPssGrowthBytes, + }, + baseline, + samples, + }; +} + +function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((left, right) => left - right); + return sorted[Math.floor((sorted.length - 1) / 2)] ?? 0; +} + +function writeCheckpoint(report: unknown): void { + writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); +} + +function integerEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw === "") return fallback; + const value = Number(raw); + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${name} must be a positive integer`); + } + return value; +} + +function delay(ms: number): Promise { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +async function withTimeout( + value: Promise, + timeoutMs: number, + description: string, +): Promise { + let timer: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + value, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`${description} exceeded ${timeoutMs}ms`)), + timeoutMs, + ); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/runtime-benchmarks/src/focused/wasmtime-threads.bench.ts b/packages/runtime-benchmarks/src/focused/wasmtime-threads.bench.ts new file mode 100644 index 0000000000..fa03d3ac96 --- /dev/null +++ b/packages/runtime-benchmarks/src/focused/wasmtime-threads.bench.ts @@ -0,0 +1,535 @@ +import { createHash } from "node:crypto"; +import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { cpus, hostname, totalmem } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; +import { + type ProcessTreeMemorySnapshot, + readProcessTreeMemorySnapshot, +} from "../lib/memory.js"; +import { + type BenchVm, + type BenchVmProcess, + createBenchSidecar, + createBenchVm, + formatSidecarProvenance, + resolveBenchSidecarProvenance, +} from "../lib/vm.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const fixturePath = resolve( + process.env.AGENTOS_WASM_THREADS_BENCH_FIXTURE ?? + join(here, "../../../../toolchain/c/build/pthread_benchmark.wasm"), +); +const fixtureDirectory = dirname(fixturePath); +const fixtureCommand = fixturePath.slice(fixtureDirectory.length + 1); +const outputPath = resolve( + process.env.AGENTOS_WASM_THREADS_BENCH_OUTPUT ?? + join(here, "../../results/wasmtime-threads.json"), +); +const startupSamples = integerEnv("AGENTOS_WASM_THREADS_STARTUP_SAMPLES", 5); +const throughputSamples = integerEnv( + "AGENTOS_WASM_THREADS_THROUGHPUT_SAMPLES", + 20, +); +const memorySamples = integerEnv("AGENTOS_WASM_THREADS_MEMORY_SAMPLES", 3); +const steadyStateWorkerThreads = 4; +const threadCounts = listEnv("AGENTOS_WASM_THREADS_COUNTS", [1, 2, 4, 8]); +const concurrencyLevels = listEnv( + "AGENTOS_WASM_THREADS_CONCURRENCY", + [1, 2, 4, 8], +); +const concurrentWorkerThreadsPerGroup = integerEnv( + "AGENTOS_WASM_THREADS_CONCURRENT_WORKERS_PER_GROUP", + 2, +); +const operationTimeoutMs = integerEnv( + "AGENTOS_WASM_THREADS_OPERATION_TIMEOUT_MS", + 15_000, +); +const sidecarProvenance = resolveBenchSidecarProvenance(); + +interface LiveWorkload { + process: BenchVmProcess; + exit: Promise; + readyMs: number; + stdout(): string; + stderr(): string; +} + +async function main(): Promise { + if (process.platform !== "linux") { + throw new Error("Wasmtime thread memory benchmarks require Linux /proc"); + } + if (sidecarProvenance.profile !== "release") { + throw new Error( + `Wasmtime thread benchmarks require a release sidecar, got ${formatSidecarProvenance(sidecarProvenance)}`, + ); + } + if (!existsSync(fixturePath)) { + throw new Error( + `missing ${fixturePath}; run make -C toolchain/c pthread-benchmark-wasm`, + ); + } + for (const count of threadCounts) { + if (!Number.isInteger(count) || count < 1 || count > 15) { + throw new Error(`invalid thread count ${count}; expected 1..15`); + } + } + if (concurrentWorkerThreadsPerGroup > 15) { + throw new Error( + `invalid concurrent worker count ${concurrentWorkerThreadsPerGroup}; expected 1..15`, + ); + } + const maxThreadsPerGroup = + Math.max( + ...threadCounts, + concurrentWorkerThreadsPerGroup, + steadyStateWorkerThreads, + ) + 1; + const maxConcurrentThreads = + maxThreadsPerGroup * Math.max(...concurrencyLevels); + const vmLimits = { + wasm: { + maxThreads: maxThreadsPerGroup, + maxConcurrentThreads, + }, + }; + + const fixture = readFileSync(fixturePath); + const result: Record = { + metadata: { + startedAt: new Date().toISOString(), + hostname: hostname(), + platform: process.platform, + arch: process.arch, + cpuModel: cpus()[0]?.model ?? "unknown", + logicalCpus: cpus().length, + totalMemoryBytes: totalmem(), + node: process.version, + sidecar: sidecarProvenance, + fixture: { + path: fixturePath, + sizeBytes: statSync(fixturePath).size, + sha256: createHash("sha256").update(fixture).digest("hex"), + }, + startupSamples, + throughputSamples, + memorySamples, + threadCounts, + concurrencyLevels, + concurrentWorkerThreadsPerGroup, + maxThreadsPerGroup, + maxConcurrentThreads, + backend: "wasmtime-threads", + aot: false, + pooling: false, + wizer: false, + liveSnapshots: false, + }, + startup: [], + throughput: null, + memory: [], + concurrency: [], + status: "running", + }; + writeCheckpoint(result); + + for (let index = 0; index < startupSamples; index++) { + console.error(`thread cold start ${index + 1}/${startupSamples}`); + const sidecar = createBenchSidecar(); + let vm: BenchVm | undefined; + try { + const vmStarted = performance.now(); + vm = await createBenchVm({ + sidecar, + wasmCommandDirs: [fixtureDirectory], + limits: vmLimits, + }); + const vmSetupMs = performance.now() - vmStarted; + const pid = requiredSidecarPid(vm); + const baseline = readProcessTreeMemorySnapshot(pid); + const workloadStarted = performance.now(); + const workload = await startWorkload(vm, steadyStateWorkerThreads, false); + const exitCode = await withTimeout( + workload.exit, + operationTimeoutMs, + "cold startup completion", + ); + const drainMs = await waitForDrain(vm); + const totalMs = performance.now() - workloadStarted; + (result.startup as any[]).push({ + index, + vmSetupMs, + readyMs: workload.readyMs, + totalMs, + exitCode, + stdout: workload.stdout(), + stderr: workload.stderr(), + baseline, + retained: readProcessTreeMemorySnapshot(pid), + drainMs, + passed: + exitCode === 0 && + workload.stdout().includes(`ready:${steadyStateWorkerThreads}`) && + workload.stdout().includes(`done:${steadyStateWorkerThreads}`), + }); + } finally { + if (vm) await vm.dispose().catch(() => undefined); + await sidecar.dispose(); + } + writeCheckpoint(result); + } + + const sidecar = createBenchSidecar(); + let vm: BenchVm | undefined; + try { + vm = await createBenchVm({ + sidecar, + wasmCommandDirs: [fixtureDirectory], + limits: vmLimits, + }); + const pid = requiredSidecarPid(vm); + await runCompleted(vm, 1); + await waitForDrain(vm); + + const throughputDurations: number[] = []; + for (let index = 0; index < throughputSamples; index++) { + const started = performance.now(); + await runCompleted(vm, steadyStateWorkerThreads); + throughputDurations.push(performance.now() - started); + } + await waitForDrain(vm); + result.throughput = { + workerThreadsPerExecution: steadyStateWorkerThreads, + totalGuestThreadsPerExecution: steadyStateWorkerThreads + 1, + durationsMs: throughputDurations, + p50Ms: percentile(throughputDurations, 0.5), + p95Ms: percentile(throughputDurations, 0.95), + executionsPerSecond: + (throughputSamples * 1_000) / + throughputDurations.reduce((total, value) => total + value, 0), + passed: throughputDurations.length === throughputSamples, + }; + + for (const threadCount of threadCounts) { + for (let sample = 0; sample < memorySamples; sample++) { + console.error( + `thread memory count=${threadCount} sample=${sample + 1}`, + ); + const baseline = readProcessTreeMemorySnapshot(pid); + const workload = await startWorkload(vm, threadCount, true); + await delay(50); + const live = readProcessTreeMemorySnapshot(pid); + const terminationStarted = performance.now(); + workload.process.kill("SIGTERM"); + const exitCode = await withTimeout( + workload.exit, + operationTimeoutMs, + `termination with ${threadCount} threads`, + ); + const terminationMs = performance.now() - terminationStarted; + const drainMs = await waitForDrain(vm); + (result.memory as any[]).push({ + threadCount, + sample, + baseline, + live, + delta: memoryDelta(live, baseline), + exitCode, + terminationMs, + drainMs, + passed: exitCode >= 128 && terminationMs <= operationTimeoutMs, + }); + } + } + + for (const groupCount of concurrencyLevels) { + console.error( + `thread concurrency groups=${groupCount} workersPerGroup=${concurrentWorkerThreadsPerGroup}`, + ); + const baseline = readProcessTreeMemorySnapshot(pid); + const workloads = await Promise.all( + Array.from({ length: groupCount }, () => + startWorkload(vm!, concurrentWorkerThreadsPerGroup, true), + ), + ); + await delay(50); + const live = readProcessTreeMemorySnapshot(pid); + const terminationStarted = performance.now(); + for (const workload of workloads) workload.process.kill("SIGTERM"); + const exitCodes = await withTimeout( + Promise.all(workloads.map((workload) => workload.exit)), + operationTimeoutMs, + `terminating ${groupCount} concurrent thread groups`, + ); + const terminationMs = performance.now() - terminationStarted; + const drainMs = await waitForDrain(vm); + (result.concurrency as any[]).push({ + groupCount, + workerThreadsPerGroup: concurrentWorkerThreadsPerGroup, + totalGuestThreads: groupCount * (concurrentWorkerThreadsPerGroup + 1), + readyMs: Math.max(...workloads.map((workload) => workload.readyMs)), + baseline, + live, + delta: memoryDelta(live, baseline), + exitCodes, + terminationMs, + drainMs, + passed: + exitCodes.every((exitCode) => exitCode >= 128) && + terminationMs <= operationTimeoutMs, + }); + } + } finally { + if (vm) await vm.dispose().catch(() => undefined); + await sidecar.dispose(); + } + + result.summary = summarize(result); + result.completedAt = new Date().toISOString(); + result.status = result.summary.passed ? "complete" : "failed"; + writeCheckpoint(result); + console.log(JSON.stringify(result.summary, null, 2)); + console.error(`raw results: ${outputPath}`); + if (!result.summary.passed) { + throw new Error("Wasmtime thread benchmark validation failed"); + } +} + +async function startWorkload( + vm: BenchVm, + threadCount: number, + park: boolean, +): Promise { + let stdout = ""; + let stderr = ""; + let ready = false; + let resolveReady!: () => void; + let rejectReady!: (error: Error) => void; + const readyPromise = new Promise( + (resolveReadyValue, rejectReadyValue) => { + resolveReady = resolveReadyValue; + rejectReady = rejectReadyValue; + }, + ); + const started = performance.now(); + const child = vm.spawn( + fixtureCommand, + [String(threadCount), park ? "1" : "0"], + { + wasmBackend: "wasmtime-threads", + onStdout(data) { + stdout += new TextDecoder().decode(data); + if (!ready && stdout.includes(`ready:${threadCount}`)) { + ready = true; + resolveReady(); + } + }, + onStderr(data) { + stderr += new TextDecoder().decode(data); + }, + }, + ); + const exit = child.wait(); + exit.then( + (code) => { + if (!ready) { + rejectReady( + new Error( + `pthread benchmark exited ${code} before ready; stderr=${stderr}`, + ), + ); + } + }, + (error) => { + if (!ready) + rejectReady(error instanceof Error ? error : new Error(String(error))); + }, + ); + await withTimeout(readyPromise, operationTimeoutMs, "pthread readiness"); + return { + process: child, + exit, + readyMs: performance.now() - started, + stdout: () => stdout, + stderr: () => stderr, + }; +} + +async function runCompleted(vm: BenchVm, threadCount: number): Promise { + const workload = await startWorkload(vm, threadCount, false); + const exitCode = await withTimeout( + workload.exit, + operationTimeoutMs, + `completed ${threadCount}-thread workload`, + ); + if ( + exitCode !== 0 || + !workload.stdout().includes(`ready:${threadCount}`) || + !workload.stdout().includes(`done:${threadCount}`) + ) { + throw new Error( + `${threadCount}-thread workload failed: exit=${exitCode} stdout=${workload.stdout()} stderr=${workload.stderr()}`, + ); + } +} + +async function waitForDrain(vm: BenchVm): Promise { + const started = performance.now(); + for (;;) { + const snapshot = await vm.getResourceSnapshot(); + if ( + snapshot.runningProcesses === 0 && + snapshot.wasmReservedMemoryBytes === 0 + ) { + return performance.now() - started; + } + if (performance.now() - started >= operationTimeoutMs) { + throw new Error( + `thread resources did not drain: running=${snapshot.runningProcesses} wasmBytes=${snapshot.wasmReservedMemoryBytes}`, + ); + } + await delay(10); + } +} + +function summarize(result: Record) { + const startup = result.startup as any[]; + const memory = result.memory as any[]; + const concurrency = result.concurrency as any[]; + const oneThreadPss = median( + memory + .filter((row) => row.threadCount === Math.min(...threadCounts)) + .map((row) => row.delta.pssBytes), + ); + const maxThreadPss = median( + memory + .filter((row) => row.threadCount === Math.max(...threadCounts)) + .map((row) => row.delta.pssBytes), + ); + const threadSpan = Math.max(...threadCounts) - Math.min(...threadCounts); + return { + coldReadyP50Ms: percentile( + startup.map((row) => row.readyMs), + 0.5, + ), + coldReadyP95Ms: percentile( + startup.map((row) => row.readyMs), + 0.95, + ), + warmThroughputPerSecond: result.throughput.executionsPerSecond, + oneThreadGroupPssDeltaBytes: oneThreadPss, + maxThreadGroupPssDeltaBytes: maxThreadPss, + perAdditionalThreadPssBytes: + threadSpan > 0 + ? Math.max(0, maxThreadPss - oneThreadPss) / threadSpan + : 0, + maxTerminationMs: Math.max( + 0, + ...memory.map((row) => row.terminationMs), + ...concurrency.map((row) => row.terminationMs), + ), + maxConcurrentGroupsMeasured: Math.max(...concurrencyLevels), + maxConcurrentGuestThreadsMeasured: + Math.max(...concurrencyLevels) * (concurrentWorkerThreadsPerGroup + 1), + passed: + startup.every((row) => row.passed) && + result.throughput.passed === true && + memory.every((row) => row.passed) && + concurrency.every((row) => row.passed), + }; +} + +function memoryDelta( + end: ProcessTreeMemorySnapshot, + start: ProcessTreeMemorySnapshot, +) { + return { + rssBytes: end.rssBytes - start.rssBytes, + pssBytes: end.pssBytes - start.pssBytes, + virtualBytes: end.virtualBytes - start.virtualBytes, + minorFaults: end.minorFaults - start.minorFaults, + majorFaults: end.majorFaults - start.majorFaults, + processCount: end.processCount - start.processCount, + threadCount: end.threadCount - start.threadCount, + }; +} + +function percentile(values: number[], fraction: number): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((left, right) => left - right); + return sorted[ + Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1) + ]!; +} + +function median(values: number[]): number { + return percentile(values, 0.5); +} + +function integerEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw === "") return fallback; + const parsed = Number(raw); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${name} must be a positive integer`); + } + return parsed; +} + +function listEnv(name: string, fallback: number[]): number[] { + const raw = process.env[name]; + if (raw === undefined || raw === "") return fallback; + const parsed = raw.split(",").map((value) => Number(value.trim())); + if ( + parsed.length === 0 || + parsed.some((value) => !Number.isInteger(value) || value <= 0) + ) { + throw new Error( + `${name} must be a comma-separated list of positive integers`, + ); + } + return parsed; +} + +function requiredSidecarPid(vm: BenchVm): number { + const pid = vm.sidecarPid(); + if (pid === null) throw new Error("benchmark could not resolve sidecar PID"); + return pid; +} + +function writeCheckpoint(result: Record): void { + writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`); +} + +function delay(ms: number): Promise { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +async function withTimeout( + value: Promise, + timeoutMs: number, + description: string, +): Promise { + let timer: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + value, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`${description} exceeded ${timeoutMs}ms`)), + timeoutMs, + ); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/runtime-benchmarks/src/lib/memory.ts b/packages/runtime-benchmarks/src/lib/memory.ts index 74a31fabc8..59a78d154e 100644 --- a/packages/runtime-benchmarks/src/lib/memory.ts +++ b/packages/runtime-benchmarks/src/lib/memory.ts @@ -55,6 +55,12 @@ export interface ProcessMemorySnapshot { majorFaults: number; } +export interface ProcessTreeMemorySnapshot extends ProcessMemorySnapshot { + processCount: number; + threadCount: number; + pids: number[]; +} + /** Read orthogonal Linux process-memory counters without conflating VIRT/RSS/PSS. */ export function readProcessMemorySnapshot(pid: number): ProcessMemorySnapshot { const status = readFileSync(`/proc/${pid}/status`, "utf8"); @@ -85,6 +91,69 @@ export function readProcessMemorySnapshot(pid: number): ProcessMemorySnapshot { }; } +/** + * Sum resident counters across a process and every live descendant. Linux + * records children against the creating thread, so enumerate every task's + * `children` file instead of looking only at the thread-group leader. + */ +export function readProcessTreeMemorySnapshot( + rootPid: number, +): ProcessTreeMemorySnapshot { + const pending = [rootPid]; + const visited = new Set(); + const snapshots: Array<[number, ProcessMemorySnapshot, number]> = []; + while (pending.length > 0) { + const pid = pending.pop(); + if (pid === undefined || visited.has(pid)) continue; + visited.add(pid); + try { + const tasks = readdirSync(`/proc/${pid}/task`).filter((entry) => + /^\d+$/.test(entry), + ); + const children = new Set(); + for (const task of tasks) { + try { + for (const child of readFileSync( + `/proc/${pid}/task/${task}/children`, + "utf8", + ) + .trim() + .split(/\s+/) + .filter(Boolean)) { + const parsed = Number(child); + if (Number.isInteger(parsed) && parsed > 0) children.add(parsed); + } + } catch { + // A task may exit while its process remains live. + } + } + snapshots.push([pid, readProcessMemorySnapshot(pid), tasks.length]); + pending.push(...children); + } catch { + // A descendant may exit between discovery and sampling. + } + } + if (snapshots.length === 0) { + throw new Error(`process tree rooted at ${rootPid} is unavailable`); + } + const sum = (select: (snapshot: ProcessMemorySnapshot) => number) => + snapshots.reduce((total, [, snapshot]) => total + select(snapshot), 0); + return { + rssBytes: sum((snapshot) => snapshot.rssBytes), + peakRssBytes: sum((snapshot) => snapshot.peakRssBytes), + pssBytes: sum((snapshot) => snapshot.pssBytes), + virtualBytes: sum((snapshot) => snapshot.virtualBytes), + minorFaults: sum((snapshot) => snapshot.minorFaults), + majorFaults: sum((snapshot) => snapshot.majorFaults), + processCount: snapshots.length, + threadCount: snapshots.reduce( + (total, [, , threadCount]) => total + threadCount, + 0, + ), + pids: snapshots.map(([pid]) => pid).sort((left, right) => left - right), + }; +} + function readKibibytes(contents: string, field: string): number { const match = contents.match(new RegExp(`^${field}:\\s+(\\d+)\\s+kB`, "m")); return match ? Number(match[1]) * 1024 : 0; diff --git a/packages/runtime-benchmarks/src/lib/vm.ts b/packages/runtime-benchmarks/src/lib/vm.ts index ca302e6ab7..ff7467bc8c 100644 --- a/packages/runtime-benchmarks/src/lib/vm.ts +++ b/packages/runtime-benchmarks/src/lib/vm.ts @@ -23,6 +23,8 @@ export interface BenchVmOptions { loopbackExemptPorts?: number[]; mounts?: HostDirectoryMount[]; permissions?: NodeRuntimeCreateOptions["permissions"]; + limits?: NodeRuntimeCreateOptions["limits"]; + wasmBackend?: NodeRuntimeCreateOptions["wasmBackend"]; wasmCommandDirs?: string[]; sidecar?: SidecarProcess; } @@ -34,7 +36,7 @@ export interface BenchVmProcess { } export interface BenchVmExecOptions { - wasmBackend?: "v8" | "wasmtime"; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; env?: Record; cwd?: string; stdin?: string | Uint8Array; @@ -107,6 +109,8 @@ export async function createBenchVm( env: "allow", ...options.permissions, }, + limits: options.limits, + wasmBackend: options.wasmBackend, mounts: options.mounts, commandsDir: options.commandsDir, loopbackExemptPorts: options.loopbackExemptPorts, diff --git a/packages/runtime-core/scripts/copy-wasm-commands.mjs b/packages/runtime-core/scripts/copy-wasm-commands.mjs index 79a6de495b..26d2006d19 100644 --- a/packages/runtime-core/scripts/copy-wasm-commands.mjs +++ b/packages/runtime-core/scripts/copy-wasm-commands.mjs @@ -29,11 +29,10 @@ const SOURCE_DIR = path.join( const DEST_DIR = path.join(PACKAGE_ROOT, "commands"); const SOFTWARE_ROOT = path.join(REPO_ROOT, "software"); -// These packages are intentionally outside `make -C toolchain commands`: -// codex is built from its separately pinned upstream checkout, while duckdb -// and vim are explicit heavy builds. If any are present they are still copied; -// they are simply not prerequisites for `--require`. -const OPTIONAL_COMMAND_PACKAGES = new Set(["codex-cli", "duckdb", "vim"]); +// Codex is built from its separately pinned upstream checkout and remains an +// explicit opt-in artifact. DuckDB and Vim are heavy explicit builds too, but +// CI and publish build them and `--require` must reject their absence. +const OPTIONAL_COMMAND_PACKAGES = new Set(["codex-cli"]); function commandNames(manifest, manifestPath) { const names = [ diff --git a/packages/runtime-core/src/generated-protocol.ts b/packages/runtime-core/src/generated-protocol.ts index abeae46ce5..ea95a5aec4 100644 --- a/packages/runtime-core/src/generated-protocol.ts +++ b/packages/runtime-core/src/generated-protocol.ts @@ -1160,6 +1160,7 @@ export function writeWasmPermissionTier(bc: bare.ByteCursor, x: WasmPermissionTi export enum StandaloneWasmBackend { V8 = "V8", Wasmtime = "Wasmtime", + WasmtimeThreads = "WasmtimeThreads", } export function readStandaloneWasmBackend(bc: bare.ByteCursor): StandaloneWasmBackend { @@ -1170,6 +1171,8 @@ export function readStandaloneWasmBackend(bc: bare.ByteCursor): StandaloneWasmBa return StandaloneWasmBackend.V8 case 1: return StandaloneWasmBackend.Wasmtime + case 2: + return StandaloneWasmBackend.WasmtimeThreads default: { bc.offset = offset throw new bare.BareError(offset, "invalid tag") @@ -1187,6 +1190,10 @@ export function writeStandaloneWasmBackend(bc: bare.ByteCursor, x: StandaloneWas bare.writeU8(bc, 1) break } + case StandaloneWasmBackend.WasmtimeThreads: { + bare.writeU8(bc, 2) + break + } } } diff --git a/packages/runtime-core/src/generated/CreateVmConfig.ts b/packages/runtime-core/src/generated/CreateVmConfig.ts index 66c7da792b..b0910a03d4 100644 --- a/packages/runtime-core/src/generated/CreateVmConfig.ts +++ b/packages/runtime-core/src/generated/CreateVmConfig.ts @@ -3,6 +3,7 @@ import type { JsRuntimeConfig } from "./JsRuntimeConfig.js"; import type { NativeRootFilesystemConfig } from "./NativeRootFilesystemConfig.js"; import type { PermissionsPolicy } from "./PermissionsPolicy.js"; import type { RootFilesystemConfig } from "./RootFilesystemConfig.js"; +import type { StandaloneWasmBackend } from "./StandaloneWasmBackend.js"; import type { VmDnsConfig } from "./VmDnsConfig.js"; import type { VmLimitsConfig } from "./VmLimitsConfig.js"; import type { VmListenPolicyConfig } from "./VmListenPolicyConfig.js"; @@ -15,4 +16,4 @@ import type { VmUserConfig } from "./VmUserConfig.js"; * `packages/core/src/node-runtime-options-schema.ts`; update both when a * public `NodeRuntime.create(...)` option changes the generated VM config. */ -export type CreateVmConfig = { cwd?: string, env: Record, database?: VmSqliteDescriptor, user?: VmUserConfig, rootFilesystem: RootFilesystemConfig, permissions?: PermissionsPolicy, limits?: VmLimitsConfig, dns?: VmDnsConfig, nativeRoot?: NativeRootFilesystemConfig, listen?: VmListenPolicyConfig, loopbackExemptPorts: Array, jsRuntime?: JsRuntimeConfig, bootstrapCommands?: Array, }; +export type CreateVmConfig = { cwd?: string, env: Record, wasmBackend?: StandaloneWasmBackend, database?: VmSqliteDescriptor, user?: VmUserConfig, rootFilesystem: RootFilesystemConfig, permissions?: PermissionsPolicy, limits?: VmLimitsConfig, dns?: VmDnsConfig, nativeRoot?: NativeRootFilesystemConfig, listen?: VmListenPolicyConfig, loopbackExemptPorts: Array, jsRuntime?: JsRuntimeConfig, bootstrapCommands?: Array, }; diff --git a/packages/runtime-core/src/generated/StandaloneWasmBackend.ts b/packages/runtime-core/src/generated/StandaloneWasmBackend.ts new file mode 100644 index 0000000000..9999ff16b8 --- /dev/null +++ b/packages/runtime-core/src/generated/StandaloneWasmBackend.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * VM-wide default engine for standalone WebAssembly process images. + * + * This does not affect JavaScript's `WebAssembly.*` APIs, which always run in + * the owning V8 isolate. Individual process launches may override this value. + */ +export type StandaloneWasmBackend = "v8" | "wasmtime" | "wasmtime-threads"; diff --git a/packages/runtime-core/src/generated/WasmLimitsConfig.ts b/packages/runtime-core/src/generated/WasmLimitsConfig.ts index 7743ce23bd..52c610898d 100644 --- a/packages/runtime-core/src/generated/WasmLimitsConfig.ts +++ b/packages/runtime-core/src/generated/WasmLimitsConfig.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, activeCpuTimeLimitMs?: number, wallClockLimitMs?: number, deterministicFuel?: number, }; +export type WasmLimitsConfig = { maxModuleFileBytes?: number, capturedOutputLimitBytes?: number, syncReadLimitBytes?: number, prewarmTimeoutMs?: number, runnerHeapLimitMb?: number, activeCpuTimeLimitMs?: number, wallClockLimitMs?: number, deterministicFuel?: number, maxThreads?: number, maxConcurrentThreads?: number, }; diff --git a/packages/runtime-core/src/kernel-proxy.ts b/packages/runtime-core/src/kernel-proxy.ts index dd34fb975e..1890adfa4b 100644 --- a/packages/runtime-core/src/kernel-proxy.ts +++ b/packages/runtime-core/src/kernel-proxy.ts @@ -3,6 +3,17 @@ import { rmSync } from "node:fs"; import { constants as osConstants } from "node:os"; import { posix as posixPath } from "node:path"; import type { NativeMountPluginDescriptor } from "./descriptors.js"; +import type { + AuthenticatedSession, + CreatedVm, + GuestFilesystemStat, + SidecarMountDescriptor, + SidecarProcess, + SidecarProcessSnapshotEntry, + SidecarResourceSnapshot, + SidecarSignalHandlerRegistration, + SidecarSocketStateEntry, +} from "./sidecar-process.js"; import type { ConnectTerminalOptions, Kernel, @@ -16,17 +27,6 @@ import type { VirtualFileSystem, VirtualStat, } from "./test-runtime.js"; -import type { - AuthenticatedSession, - CreatedVm, - GuestFilesystemStat, - SidecarProcess, - SidecarMountDescriptor, - SidecarProcessSnapshotEntry, - SidecarResourceSnapshot, - SidecarSignalHandlerRegistration, - SidecarSocketStateEntry, -} from "./sidecar-process.js"; export interface PlainMountConfig { path: string; @@ -79,9 +79,7 @@ const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10; const TRAILING_OUTPUT_DRAIN_MAX_MS = 250; const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2; -async function drainTrailingProcessOutputTurn( - delayMs = 0, -): Promise { +async function drainTrailingProcessOutputTurn(delayMs = 0): Promise { // Native-sidecar `process_output` events can lag one macrotask behind the // terminal `process_exited` notification for very short-lived processes, and // under suite load the sidecar event pump can need a little extra time to @@ -242,7 +240,7 @@ function shellSingleQuote(value: string): string { if (value.length === 0) { return "''"; } - return `'${value.replace(/'/g, `'\"'\"'`)}'`; + return `'${value.replace(/'/g, `'"'"'`)}'`; } function buildSignalNameByNumber(): Map { @@ -307,7 +305,7 @@ interface TrackedProcessEntry { driver: string; cwd: string; env: Record; - wasmBackend: "v8" | "wasmtime" | undefined; + wasmBackend: "v8" | "wasmtime" | "wasmtime-threads" | undefined; startTime: number; exitTime: number | null; hostPid: number | null; @@ -620,8 +618,9 @@ export class NativeSidecarKernelProxy { ): ManagedProcess { let spawnCommand = command; let spawnArgs = [...args]; - const shellOption = (options as ({ shell?: unknown } & KernelSpawnOptions) | undefined) - ?.shell; + const shellOption = ( + options as ({ shell?: unknown } & KernelSpawnOptions) | undefined + )?.shell; if (shellOption === true || typeof shellOption === "string") { // Node's shell mode hands the raw command line to the shell. Shell // grammar belongs to the guest shell, so the bridge never parses it. @@ -712,10 +711,7 @@ export class NativeSidecarKernelProxy { .catch((error) => { this.handleBackgroundProcessError(entry, error); }); - if ( - (signal === 9 || signal === 15) && - entry.exitCode === null - ) { + if ((signal === 9 || signal === 15) && entry.exitCode === null) { this.finishProcess(entry, 128 + signal); } }, @@ -751,8 +747,7 @@ export class NativeSidecarKernelProxy { (command === "sh" || command === "/bin/sh" ? ["-i"] : []); const synthesizePrompt = !options?.command && !options?.args; const autoCloseExplicitCommandStdin = - Boolean(options?.command) && - !["sh", "/bin/sh", "bash"].includes(command); + Boolean(options?.command) && !["sh", "/bin/sh", "bash"].includes(command); const promptText = "sh-0.4$ "; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); @@ -963,7 +958,9 @@ export class NativeSidecarKernelProxy { if (newlineIndex < 0) { break; } - const line = bufferedInput.slice(0, newlineIndex).replace(/\r$/, ""); + const line = bufferedInput + .slice(0, newlineIndex) + .replace(/\r$/, ""); bufferedInput = bufferedInput.slice(newlineIndex + 1); emitSyntheticStdout(`${line}\n`); const nextCommand = bufferedCommand @@ -983,7 +980,9 @@ export class NativeSidecarKernelProxy { } const exitMatch = trimmed.match(/^exit(?:\s+(-?\d+))?$/); if (exitMatch) { - finishSyntheticShell(Number.parseInt(exitMatch[1] ?? "0", 10)); + finishSyntheticShell( + Number.parseInt(exitMatch[1] ?? "0", 10), + ); return; } const exportMatch = trimmed.match( @@ -1014,6 +1013,7 @@ export class NativeSidecarKernelProxy { { env: shellEnv, cwd: shellCwd, + wasmBackend: options?.wasmBackend, streamStdin: true, onStdout: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)), @@ -1035,6 +1035,7 @@ export class NativeSidecarKernelProxy { const result = await execCommand(nextCommand, { env: shellEnv, cwd: shellCwd, + wasmBackend: options?.wasmBackend, }); const sanitizedStdout = sanitizeSyntheticShellText( result.stdout, @@ -1082,6 +1083,7 @@ export class NativeSidecarKernelProxy { const proc = this.spawn(command, args, { env: options?.env, cwd: options?.cwd, + wasmBackend: options?.wasmBackend, streamStdin: true, onStdout: (chunk) => { for (const handler of terminalHandlers) { @@ -1459,7 +1461,7 @@ export class NativeSidecarKernelProxy { } private async refreshProcessSnapshot(): Promise { - if (this.processSnapshotRefresh) { + if (this.processSnapshotRefresh !== null) { await this.processSnapshotRefresh; return; } @@ -1510,9 +1512,7 @@ export class NativeSidecarKernelProxy { return; } - await drainTrailingProcessOutputTurn( - Math.min(delayMs, remainingMs), - ); + await drainTrailingProcessOutputTurn(Math.min(delayMs, remainingMs)); if (entry.outputGeneration === observedGeneration) { quietTurns += 1; } else { @@ -1558,13 +1558,9 @@ export class NativeSidecarKernelProxy { private async runEventPump(): Promise { while (!this.disposed) { try { - const event = await this.client.waitForEvent( - { any: true }, - undefined, - { - signal: this.eventPumpAbortController.signal, - }, - ); + const event = await this.client.waitForEvent({ any: true }, undefined, { + signal: this.eventPumpAbortController.signal, + }); if (event.payload.type === "process_output") { const entry = this.trackedProcessesById.get(event.payload.process_id); if (!entry) { @@ -1679,13 +1675,10 @@ export class NativeSidecarKernelProxy { entry.hostExitObservedAt = now; continue; } - if ( - now - entry.hostExitObservedAt >= MISSING_EXIT_EVENT_GRACE_MS - ) { + if (now - entry.hostExitObservedAt >= MISSING_EXIT_EVENT_GRACE_MS) { this.finishProcess(entry, 0); break; } - continue; } } catch { // Fall back to the next wait interval if the sidecar snapshot query fails. @@ -1749,39 +1742,39 @@ export class NativeSidecarKernelProxy { } entry.stdinFlushPromise = entry.startPromise - .then(async () => { - if (entry.exitCode !== null) { - return; - } - while (entry.pendingStdin.length > 0) { + .then(async () => { + if (entry.exitCode !== null) { + return; + } + while (entry.pendingStdin.length > 0) { const chunk = entry.pendingStdin.shift(); if (chunk === undefined) { break; } - await this.client.writeStdin( - this.session, - this.vm, - entry.processId, - chunk, - ); - } - }) - .catch((error) => { - if (isNoSuchProcessError(error) || isUnknownVmError(error)) { - return; - } - throw error; - }) - .finally(() => { + await this.client.writeStdin( + this.session, + this.vm, + entry.processId, + chunk, + ); + } + }) + .catch((error) => { + if (isNoSuchProcessError(error) || isUnknownVmError(error)) { + return; + } + throw error; + }) + .finally(() => { entry.stdinFlushPromise = null; - if (entry.pendingStdin.length > 0 && entry.exitCode === null) { - void this.flushPendingStdin(entry).catch((error) => { - this.handleBackgroundProcessError(entry, error); - }); - } - }); - return entry.stdinFlushPromise; - } + if (entry.pendingStdin.length > 0 && entry.exitCode === null) { + void this.flushPendingStdin(entry).catch((error) => { + this.handleBackgroundProcessError(entry, error); + }); + } + }); + return entry.stdinFlushPromise; + } private async closeTrackedStdin(entry: TrackedProcessEntry): Promise { await entry.startPromise; @@ -1789,14 +1782,14 @@ export class NativeSidecarKernelProxy { if (entry.exitCode !== null || !entry.pendingCloseStdin) { return; } - entry.pendingCloseStdin = false; - try { - await this.client.closeStdin(this.session, this.vm, entry.processId); - } catch (error) { - if (isNoSuchProcessError(error) || isUnknownVmError(error)) { - return; - } - throw error; + entry.pendingCloseStdin = false; + try { + await this.client.closeStdin(this.session, this.vm, entry.processId); + } catch (error) { + if (isNoSuchProcessError(error) || isUnknownVmError(error)) { + return; + } + throw error; } } @@ -1804,7 +1797,11 @@ export class NativeSidecarKernelProxy { entry: TrackedProcessEntry, error: unknown, ): void { - if (this.disposed || isNoSuchProcessError(error) || isUnknownVmError(error)) { + if ( + this.disposed || + isNoSuchProcessError(error) || + isUnknownVmError(error) + ) { return; } if (entry.exitCode !== null) { @@ -1819,7 +1816,11 @@ export class NativeSidecarKernelProxy { entry: TrackedProcessEntry, error: unknown, ): number { - if (this.disposed || isNoSuchProcessError(error) || isUnknownVmError(error)) { + if ( + this.disposed || + isNoSuchProcessError(error) || + isUnknownVmError(error) + ) { return entry.exitCode ?? 1; } this.emitBackgroundProcessError(entry, error); @@ -2199,10 +2200,7 @@ export class NativeSidecarKernelProxy { private assertGuestPathWritable(path: string): void { const normalizedPath = posixPath.normalize(path); for (const root of PROTECTED_READ_ONLY_GUEST_ROOTS) { - if ( - normalizedPath === root || - normalizedPath.startsWith(`${root}/`) - ) { + if (normalizedPath === root || normalizedPath.startsWith(`${root}/`)) { throw errnoError("EROFS", "read-only file system"); } } @@ -2381,7 +2379,6 @@ export type { AuthenticatedSession, CreatedVm, GuestFilesystemStat, - SidecarSpawnOptions, RootFilesystemEntry, SidecarEventSelector, SidecarPermissionsPolicy, @@ -2392,10 +2389,11 @@ export type { SidecarSessionState, SidecarSignalHandlerRegistration, SidecarSocketStateEntry, + SidecarSpawnOptions, } from "./sidecar-process.js"; export { - SidecarProcess, SidecarEventBufferOverflow, + SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "./sidecar-process.js"; diff --git a/packages/runtime-core/src/node-runtime-options-schema.ts b/packages/runtime-core/src/node-runtime-options-schema.ts index 989b9ea961..87b26a01f5 100644 --- a/packages/runtime-core/src/node-runtime-options-schema.ts +++ b/packages/runtime-core/src/node-runtime-options-schema.ts @@ -319,6 +319,13 @@ export const nodeRuntimeCreateOptionsSchema = z env: z.record(z.string(), z.string()).optional(), cwd: z.string().optional(), user: vmUserConfigSchema.optional(), + wasmBackend: z.enum(["v8", "wasmtime", "wasmtime-threads"]).optional(), + limits: z + .custom( + (value: unknown) => typeof value === "object" && value !== null, + { message: "Expected VM limits object" }, + ) + .optional(), permissions: nodeRuntimePermissionsSchema.optional(), commandsDir: z.string().optional(), wasmCommandDirs: stringArray.optional(), diff --git a/packages/runtime-core/src/node-runtime.ts b/packages/runtime-core/src/node-runtime.ts index a7144e7a3f..6704f788ff 100644 --- a/packages/runtime-core/src/node-runtime.ts +++ b/packages/runtime-core/src/node-runtime.ts @@ -22,6 +22,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; +import type { VmLimitsConfig } from "./generated/VmLimitsConfig.js"; import type { VmUserConfig } from "./generated/VmUserConfig.js"; import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js"; import type { SidecarProcess } from "./sidecar-process.js"; @@ -150,6 +151,10 @@ export interface NodeRuntimeCreateOptions { cwd?: string; /** Initial virtual Linux credentials and account record. Defaults to `1000:1000` (`agentos`). */ user?: VmUserConfig; + /** VM-wide default for standalone WASM commands. JavaScript remains on V8. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; + /** Sidecar-enforced VM resource and runtime limits. */ + limits?: VmLimitsConfig; /** * Permission policy for the VM. Merged over a secure default that **denies * network access** (guest code cannot reach the network until you opt in); @@ -352,7 +357,7 @@ export interface NodeRuntimeExecOptions { * Select the engine for a standalone WebAssembly command. JavaScript and * Python commands ignore this option. Omission preserves the runtime default. */ - wasmBackend?: "v8" | "wasmtime"; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; /** Extra environment variables for this run, merged over the VM env. */ env?: Record; /** Working directory for this run. */ @@ -669,6 +674,8 @@ export class NodeRuntime { env: options.env, cwd: options.cwd, user: options.user, + wasmBackend: options.wasmBackend, + limits: options.limits, sidecar: options.sidecar, onBootTiming: (timing) => options.onBootTiming?.(timing), loopbackExemptPorts: options.loopbackExemptPorts, diff --git a/packages/runtime-core/src/protocol-maps.ts b/packages/runtime-core/src/protocol-maps.ts index 896672eb1e..d8a9b4e35e 100644 --- a/packages/runtime-core/src/protocol-maps.ts +++ b/packages/runtime-core/src/protocol-maps.ts @@ -13,7 +13,7 @@ export type LiveWasmPermissionTier = | "read-write" | "read-only" | "isolated"; -export type LiveStandaloneWasmBackend = "v8" | "wasmtime"; +export type LiveStandaloneWasmBackend = "v8" | "wasmtime" | "wasmtime-threads"; export type LivePermissionMode = "allow" | "ask" | "deny"; export type LiveGuestFilesystemOperation = | "read_file" @@ -156,6 +156,8 @@ export function toGeneratedStandaloneWasmBackend( return protocol.StandaloneWasmBackend.V8; case "wasmtime": return protocol.StandaloneWasmBackend.Wasmtime; + case "wasmtime-threads": + return protocol.StandaloneWasmBackend.WasmtimeThreads; } } diff --git a/packages/runtime-core/src/sidecar-process.ts b/packages/runtime-core/src/sidecar-process.ts index 52074b2bb8..afb95919b7 100644 --- a/packages/runtime-core/src/sidecar-process.ts +++ b/packages/runtime-core/src/sidecar-process.ts @@ -69,7 +69,7 @@ type GuestRuntimeKind = Extract< "java_script" | "python" | "web_assembly" >; type WasmPermissionTier = LiveWasmPermissionTier; -type StandaloneWasmBackend = "v8" | "wasmtime"; +type StandaloneWasmBackend = "v8" | "wasmtime" | "wasmtime-threads"; type RootFilesystemEntryEncoding = LiveRootFilesystemEntryEncoding; type RootFilesystemDescriptor = { diff --git a/packages/runtime-core/src/test-runtime.ts b/packages/runtime-core/src/test-runtime.ts index a14762e88b..dca32c56ed 100644 --- a/packages/runtime-core/src/test-runtime.ts +++ b/packages/runtime-core/src/test-runtime.ts @@ -6,24 +6,24 @@ import * as path from "node:path"; import * as posixPath from "node:path/posix"; import { fileURLToPath } from "node:url"; import "./native-client.js"; -import { resolvePublishedSidecarBinary } from "./binary.js"; -import { findCargoBinary, resolveCargoBinary } from "./cargo.js"; -import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; -import type { PermissionsPolicy } from "./generated/PermissionsPolicy.js"; -import type { VmLimitsConfig } from "./generated/VmLimitsConfig.js"; -import type { VmUserConfig } from "./generated/VmUserConfig.js"; import { type AuthenticatedSession, type CreatedVm, type LocalCompatMount, NativeSidecarKernelProxy, - type RootFilesystemEntry, SidecarProcess, + type RootFilesystemEntry, type SidecarRegisteredHostCallbackDefinition, type SidecarRequestFrame, type SidecarResponsePayload, serializeMountConfigForSidecar, } from "./kernel-proxy.js"; +import { resolvePublishedSidecarBinary } from "./binary.js"; +import { findCargoBinary, resolveCargoBinary } from "./cargo.js"; +import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; +import type { PermissionsPolicy } from "./generated/PermissionsPolicy.js"; +import type { VmLimitsConfig } from "./generated/VmLimitsConfig.js"; +import type { VmUserConfig } from "./generated/VmUserConfig.js"; export const AF_INET = 2; export const AF_UNIX = 1; @@ -265,6 +265,8 @@ export interface OpenShellOptions { cwd?: string; cols?: number; rows?: number; + /** Engine affinity inherited by standalone WASM commands launched by the shell. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; /** Optional stderr-only diagnostic tap; do not render it alongside `onData`. */ onStderr?: (data: Uint8Array) => void; } @@ -284,7 +286,7 @@ export interface ExecOptions { filePath?: string; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; - wasmBackend?: "v8" | "wasmtime"; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; } export interface ExecResult { @@ -2801,12 +2803,13 @@ class NativeKernel implements Kernel { env?: Record; cwd?: string; user?: VmUserConfig; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; + limits?: VmLimitsConfig; sidecar?: SidecarProcess; onBootTiming?: (timing: KernelBootTiming) => void; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; jsRuntime?: Partial; - limits?: VmLimitsConfig; mounts?: Array<{ path: string; fs: VirtualFileSystem; @@ -3340,6 +3343,8 @@ class NativeKernel implements Kernel { runtime: "java_script", config: { env: createVmEnv, + wasmBackend: this.options.wasmBackend, + limits: this.options.limits, ...(this.options.user ? { user: this.options.user } : {}), rootFilesystem, ...(bootstrapPermissions @@ -3467,13 +3472,14 @@ export function createKernel(options: { env?: Record; cwd?: string; user?: VmUserConfig; + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; + limits?: VmLimitsConfig; sidecar?: SidecarProcess; onBootTiming?: (timing: KernelBootTiming) => void; maxProcesses?: number; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; jsRuntime?: Partial; - limits?: VmLimitsConfig; logger?: unknown; mounts?: Array<{ path: string; fs: VirtualFileSystem; readOnly?: boolean }>; syncFilesystemOnDispose?: boolean; diff --git a/packages/runtime-core/src/vm-config.ts b/packages/runtime-core/src/vm-config.ts index f3c6cbf628..4413ac2103 100644 --- a/packages/runtime-core/src/vm-config.ts +++ b/packages/runtime-core/src/vm-config.ts @@ -1,8 +1,6 @@ export type { AcpLimitsConfig } from "./generated/AcpLimitsConfig.js"; +export type { BindingLimitsConfig } from "./generated/BindingLimitsConfig.js"; export type { CreateVmConfig } from "./generated/CreateVmConfig.js"; -export type { VmUserConfig } from "./generated/VmUserConfig.js"; -export type { VmUserAccountConfig } from "./generated/VmUserAccountConfig.js"; -export type { VmGroupConfig } from "./generated/VmGroupConfig.js"; export type { FsPermissionRule } from "./generated/FsPermissionRule.js"; export type { FsPermissionRuleSet } from "./generated/FsPermissionRuleSet.js"; export type { FsPermissionScope } from "./generated/FsPermissionScope.js"; @@ -28,8 +26,11 @@ export type { RootFilesystemEntryEncoding } from "./generated/RootFilesystemEntr export type { RootFilesystemEntryKind } from "./generated/RootFilesystemEntryKind.js"; export type { RootFilesystemLowerDescriptor } from "./generated/RootFilesystemLowerDescriptor.js"; export type { RootFilesystemMode } from "./generated/RootFilesystemMode.js"; -export type { BindingLimitsConfig } from "./generated/BindingLimitsConfig.js"; +export type { StandaloneWasmBackend } from "./generated/StandaloneWasmBackend.js"; export type { VmDnsConfig } from "./generated/VmDnsConfig.js"; +export type { VmGroupConfig } from "./generated/VmGroupConfig.js"; export type { VmLimitsConfig } from "./generated/VmLimitsConfig.js"; export type { VmListenPolicyConfig } from "./generated/VmListenPolicyConfig.js"; +export type { VmUserAccountConfig } from "./generated/VmUserAccountConfig.js"; +export type { VmUserConfig } from "./generated/VmUserConfig.js"; export type { WasmLimitsConfig } from "./generated/WasmLimitsConfig.js"; diff --git a/packages/runtime-core/tests/copy-wasm-commands.test.ts b/packages/runtime-core/tests/copy-wasm-commands.test.ts index 1dd3e718be..ee6f3df561 100644 --- a/packages/runtime-core/tests/copy-wasm-commands.test.ts +++ b/packages/runtime-core/tests/copy-wasm-commands.test.ts @@ -69,12 +69,14 @@ afterEach(() => { }); describe("copy WASM commands", () => { - it("derives commands, aliases, and stubs while excluding optional builds", () => { + it("derives commands, aliases, stubs, and required heavy builds", () => { const { softwareRoot } = fixture(); expect(requiredSoftwareCommandNames(softwareRoot)).toEqual([ "alpha", "alpha-alias", + "duckdb", "legacy", + "vim", ]); }); @@ -122,7 +124,9 @@ describe("copy WASM commands", () => { requireCommands: true, log: () => {}, }), - ).toThrow(/missing required default WASM commands.*alpha-alias, legacy/); + ).toThrow( + /missing required default WASM commands.*alpha-alias, duckdb, legacy, vim/, + ); expect(readFileSync(join(destDir, "known-good"), "utf8")).toBe( "preserve me", ); @@ -130,7 +134,14 @@ describe("copy WASM commands", () => { it("copies optional extras with exact basenames and dereferences aliases", () => { const { sourceDir, destDir, softwareRoot } = fixture(); - for (const name of ["alpha", "alpha-alias", "legacy", "codex"]) { + for (const name of [ + "alpha", + "alpha-alias", + "duckdb", + "legacy", + "vim", + "codex", + ]) { writeFileSync(join(sourceDir, name), name); } writeFileSync(join(sourceDir, "extra-real"), "extra"); @@ -165,7 +176,9 @@ describe("copy WASM commands", () => { requireCommands: true, log: () => {}, }), - ).toThrow(/missing required default WASM commands.*alpha-alias, legacy/); + ).toThrow( + /missing required default WASM commands.*alpha-alias, duckdb, legacy, vim/, + ); expect(existsSync(join(destDir, "alpha"))).toBe(true); }); diff --git a/packages/runtime-core/tests/integration/bridge-child-process.nightly.test.ts b/packages/runtime-core/tests/integration/bridge-child-process.nightly.test.ts index 97bc565c4b..1e34f80830 100644 --- a/packages/runtime-core/tests/integration/bridge-child-process.nightly.test.ts +++ b/packages/runtime-core/tests/integration/bridge-child-process.nightly.test.ts @@ -17,6 +17,7 @@ import { symlinkSync, writeFileSync, } from "node:fs"; +import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -28,6 +29,7 @@ import { createNodeRuntime, createWasmVmRuntime, describeIf, + itIf, NodeFileSystem, } from "@rivet-dev/agentos-vm-test-harness"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -54,6 +56,9 @@ const skipReason = BRIDGE_COMMAND_DIRS.length === 0 ? `WASM shell command not found at ${COMMANDS_DIR} or ${PACKAGED_COREUTILS_COMMANDS_DIR}` : false; +const runReleaseWasmtimeReuseGate = + process.env.AGENTOS_TEST_WASM_BACKEND === "wasmtime" && + /[/\\]release[/\\]/.test(process.env.AGENTOS_SIDECAR_BIN ?? ""); function createBridgeIntegrationKernel(): Promise { return createIntegrationKernel({ @@ -195,6 +200,105 @@ describeIf( expect(output).toContain("async-ok"); }); + itIf( + runReleaseWasmtimeReuseGate, + "reuses Wasmtime after V8 children wait for piped WASM commands [release Wasmtime matrix]", + async () => { + const server = createServer((_request, response) => { + response.writeHead(200, { + "content-type": "text/plain", + connection: "close", + }); + response.end("bridge-reuse-ok"); + }); + await new Promise((resolveListen, rejectListen) => { + server.once("error", rejectListen); + server.listen(0, "127.0.0.1", resolveListen); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("test server did not expose a TCP port"); + } + + try { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: BRIDGE_COMMAND_DIRS, + loopbackExemptPorts: [address.port], + wasmBackend: "wasmtime", + }); + + for (let cycle = 0; cycle < 10; cycle += 1) { + await ctx.vfs.writeFile( + "/tmp/bridge-reuse-input.txt", + `direct-${cycle}\n`, + ); + const nodeOutput: Uint8Array[] = []; + const nodeProcess = ctx.kernel.spawn( + "node", + [ + "-e", + ` + const { spawn } = require('child_process'); + const child = spawn('sh', ['-lc', "printf 'from-node-${cycle}\\n' | tr '[:lower:]' '[:upper:]'"], { + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; + child.stdout.on('data', (chunk) => { stdout += chunk; }); + child.on('close', (code) => { + if (code !== 0 || stdout !== 'FROM-NODE-${cycle}\\n') process.exit(1); + console.log(stdout.trim()); + }); + `, + ], + { + onStdout: (data) => nodeOutput.push(data), + }, + ); + expect(await nodeProcess.wait()).toBe(0); + expect( + nodeOutput + .map((chunk) => new TextDecoder().decode(chunk)) + .join(""), + ).toContain(`FROM-NODE-${cycle}`); + + const stdout: Uint8Array[] = []; + const stderr: Uint8Array[] = []; + const wasmProcess = ctx.kernel.spawn( + "sh", + [ + "-lc", + `cat /tmp/bridge-reuse-input.txt | tr '[:lower:]' '[:upper:]'; curl --max-time 5 -fsS http://127.0.0.1:${address.port}/`, + ], + { + onStdout: (data) => stdout.push(data), + onStderr: (data) => stderr.push(data), + }, + ); + const timeout = setTimeout(() => wasmProcess.kill(9), 60_000); + const exitCode = await wasmProcess + .wait() + .finally(() => clearTimeout(timeout)); + const output = stdout + .map((chunk) => new TextDecoder().decode(chunk)) + .join(""); + const errorOutput = stderr + .map((chunk) => new TextDecoder().decode(chunk)) + .join(""); + expect(exitCode, `cycle ${cycle}: ${errorOutput}`).toBe(0); + expect(output).toContain(`DIRECT-${cycle}\n`); + expect(output).toContain("bridge-reuse-ok"); + } + } finally { + await new Promise((resolveClose, rejectClose) => { + server.close((error) => + error ? rejectClose(error) : resolveClose(), + ); + }); + } + }, + ); + it("child_process.spawn with shell:true preserves shell builtin exit codes", async () => { ctx = await createBridgeIntegrationKernel(); diff --git a/packages/runtime-core/tests/integration/cross-runtime-network.nightly.test.ts b/packages/runtime-core/tests/integration/cross-runtime-network.nightly.test.ts index 9336b2320c..37d5404ebb 100644 --- a/packages/runtime-core/tests/integration/cross-runtime-network.nightly.test.ts +++ b/packages/runtime-core/tests/integration/cross-runtime-network.nightly.test.ts @@ -6,152 +6,161 @@ * client and listener runtimes. */ -import { describe, it, expect, afterEach } from 'vitest'; -import { existsSync } from 'node:fs'; -import { createServer as createHttpServer } from 'node:http'; -import { resolve } from 'node:path'; +import { describe, it, expect, afterEach } from "vitest"; +import { existsSync } from "node:fs"; +import { createServer as createHttpServer } from "node:http"; +import { resolve } from "node:path"; import { - COMMANDS_DIR, - C_BUILD_DIR, - createIntegrationKernel, - itIf, - skipUnlessWasmBuilt, -} from '@rivet-dev/agentos-vm-test-harness'; -import type { IntegrationKernelResult, Kernel } from '@rivet-dev/agentos-vm-test-harness'; - -const WASM_CURL = resolve(C_BUILD_DIR, 'curl'); -const WASM_HTTP_SERVER = resolve(C_BUILD_DIR, 'http_server'); -const WASM_TCP_ECHO = resolve(C_BUILD_DIR, 'tcp_echo'); -const WASM_TCP_SERVER = resolve(C_BUILD_DIR, 'tcp_server'); + COMMANDS_DIR, + C_BUILD_DIR, + createIntegrationKernel, + itIf, + skipUnlessWasmBuilt, +} from "@rivet-dev/agentos-vm-test-harness"; +import type { + IntegrationKernelResult, + Kernel, +} from "@rivet-dev/agentos-vm-test-harness"; + +const WASM_CURL = resolve(C_BUILD_DIR, "curl"); +const WASM_HTTP_SERVER = resolve(C_BUILD_DIR, "http_server"); +const WASM_TCP_ECHO = resolve(C_BUILD_DIR, "tcp_echo"); +const WASM_TCP_SERVER = resolve(C_BUILD_DIR, "tcp_server"); function skipReasonWasmNetwork(): string | false { - const wasmSkipReason = skipUnlessWasmBuilt(); - if (wasmSkipReason) return wasmSkipReason; - for (const [name, path] of [ - ['curl', WASM_CURL], - ['http_server', WASM_HTTP_SERVER], - ['tcp_echo', WASM_TCP_ECHO], - ['tcp_server', WASM_TCP_SERVER], - ] as const) { - if (!existsSync(path)) { - return `${name} WASM binary not found at ${path} - rebuild registry C command artifacts`; - } - } - return false; + const wasmSkipReason = skipUnlessWasmBuilt(); + if (wasmSkipReason) return wasmSkipReason; + for (const [name, path] of [ + ["curl", WASM_CURL], + ["http_server", WASM_HTTP_SERVER], + ["tcp_echo", WASM_TCP_ECHO], + ["tcp_server", WASM_TCP_SERVER], + ] as const) { + if (!existsSync(path)) { + return `${name} WASM binary not found at ${path} - rebuild registry C command artifacts`; + } + } + return false; } const wasmNetworkSkipReason = skipReasonWasmNetwork(); interface RunningGuestProgram { - process: ReturnType; - stdoutChunks: Uint8Array[]; - stderrChunks: Uint8Array[]; - getExitCode: () => number | null; + process: ReturnType; + stdoutChunks: Uint8Array[]; + stderrChunks: Uint8Array[]; + getExitCode: () => number | null; } function decodeChunks(chunks: Uint8Array[]): string { - return chunks.map((chunk) => new TextDecoder().decode(chunk)).join(''); + return chunks.map((chunk) => new TextDecoder().decode(chunk)).join(""); } function spawnGuestProgram( - kernel: Kernel, - command: string, - args: string[], + kernel: Kernel, + command: string, + args: string[], ): RunningGuestProgram { - const stdoutChunks: Uint8Array[] = []; - const stderrChunks: Uint8Array[] = []; - let exitCode: number | null = null; - const process = kernel.spawn(command, args, { - onStdout: (chunk) => stdoutChunks.push(chunk), - onStderr: (chunk) => stderrChunks.push(chunk), - }); - void process.wait().then((code) => { - exitCode = code; - }); - return { - process, - stdoutChunks, - stderrChunks, - getExitCode: () => exitCode, - }; + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + let exitCode: number | null = null; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => stdoutChunks.push(chunk), + onStderr: (chunk) => stderrChunks.push(chunk), + }); + void process.wait().then((code) => { + exitCode = code; + }); + return { + process, + stdoutChunks, + stderrChunks, + getExitCode: () => exitCode, + }; } function spawnGuestNodeProgram( - kernel: Kernel, - code: string, + kernel: Kernel, + code: string, ): RunningGuestProgram { - return spawnGuestProgram(kernel, 'node', ['-e', code]); + return spawnGuestProgram(kernel, "node", ["-e", code]); } async function runGuestNodeProgram( - kernel: Kernel, - code: string, + kernel: Kernel, + code: string, ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - const program = spawnGuestNodeProgram(kernel, code); - const exitCode = await program.process.wait(); - return { - exitCode, - stdout: decodeChunks(program.stdoutChunks), - stderr: decodeChunks(program.stderrChunks), - }; + const program = spawnGuestNodeProgram(kernel, code); + const exitCode = await program.process.wait(); + return { + exitCode, + stdout: decodeChunks(program.stdoutChunks), + stderr: decodeChunks(program.stderrChunks), + }; } async function waitForOutput( - program: RunningGuestProgram, - needle: string, - label: string, + program: RunningGuestProgram, + needle: string, + label: string, ): Promise { - const deadline = Date.now() + 20_000; - while (Date.now() < deadline) { - const stdout = decodeChunks(program.stdoutChunks); - if (stdout.includes(needle)) { - return; - } - if (program.getExitCode() !== null) { - throw new Error( - `${label} exited before ${JSON.stringify(needle)}\nstdout:\n${stdout}\nstderr:\n${decodeChunks(program.stderrChunks)}`, - ); - } - await new Promise((resolveWait) => setTimeout(resolveWait, 20)); - } - throw new Error( - `Timed out waiting for ${label} to print ${JSON.stringify(needle)}\nstdout:\n${decodeChunks(program.stdoutChunks)}\nstderr:\n${decodeChunks(program.stderrChunks)}`, - ); + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + const stdout = decodeChunks(program.stdoutChunks); + if (stdout.includes(needle)) { + return; + } + if (program.getExitCode() !== null) { + throw new Error( + `${label} exited before ${JSON.stringify(needle)}\nstdout:\n${stdout}\nstderr:\n${decodeChunks(program.stderrChunks)}`, + ); + } + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error( + `Timed out waiting for ${label} to print ${JSON.stringify(needle)}\nstdout:\n${decodeChunks(program.stdoutChunks)}\nstderr:\n${decodeChunks(program.stderrChunks)}`, + ); } async function waitForListener( - kernel: Kernel, - port: number, - label: string, + kernel: Kernel, + port: number, + label: string, + program?: RunningGuestProgram, ): Promise { - const deadline = Date.now() + 20_000; - while (Date.now() < deadline) { - if (kernel.socketTable.findListener({ host: '0.0.0.0', port })) { - return; - } - await new Promise((resolveWait) => setTimeout(resolveWait, 20)); - } - throw new Error(`Timed out waiting for ${label} listener on port ${port}`); + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + if (kernel.socketTable.findListener({ host: "0.0.0.0", port })) { + return; + } + if (program && program.getExitCode() !== null) { + throw new Error( + `${label} exited before opening port ${port}\nstdout:\n${decodeChunks(program.stdoutChunks)}\nstderr:\n${decodeChunks(program.stderrChunks)}`, + ); + } + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error(`Timed out waiting for ${label} listener on port ${port}`); } function parseVmFetchResponse(responseJson: string): { - status: number; - body: string; + status: number; + body: string; } { - const parsed = JSON.parse(responseJson) as { - status?: number; - body?: string; - bodyEncoding?: string; - }; - let body = parsed.body ?? ''; - if (parsed.bodyEncoding === 'base64' && body.length > 0) { - body = Buffer.from(body, 'base64').toString('utf8'); - } - return { status: parsed.status ?? 0, body }; + const parsed = JSON.parse(responseJson) as { + status?: number; + body?: string; + bodyEncoding?: string; + }; + let body = parsed.body ?? ""; + if (parsed.bodyEncoding === "base64" && body.length > 0) { + body = Buffer.from(body, "base64").toString("utf8"); + } + return { status: parsed.status ?? 0, body }; } function guestJsHttpServer(port: number): string { - return ` + return ` const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': 'text/plain' }); @@ -164,7 +173,7 @@ server.listen(${port}, '127.0.0.1', () => { } function guestJsTcpServer(port: number): string { - return ` + return ` const net = require('net'); const server = net.createServer((socket) => { socket.on('data', (chunk) => { @@ -177,290 +186,341 @@ server.listen(${port}, '127.0.0.1', () => { `; } -describe('cross-runtime network integration', { timeout: 90_000 }, () => { - let ctx: IntegrationKernelResult; - - afterEach(async () => { - await ctx?.dispose(); - }); - - it('J1 JS fetch -> JS node:http server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['node'], - }); - const server = spawnGuestNodeProgram(ctx.kernel, guestJsHttpServer(3101)); - await waitForOutput(server, 'js http listening 3101', 'JS HTTP server'); - - const client = await runGuestNodeProgram( - ctx.kernel, - [ - "fetch('http://127.0.0.1:3101/from-js')", - " .then(async (res) => console.log(res.status + ':' + await res.text()))", - " .catch((error) => { console.error(error); process.exit(1); });", - ].join('\n'), - ); - - server.process.kill(15); - await server.process.wait().catch(() => {}); - expect(client.exitCode).toBe(0); - expect(client.stderr).toBe(''); - expect(client.stdout.trim()).toBe('200:js:GET:/from-js'); - }); - - it('J2 JS net.connect -> JS net.Server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['node'], - }); - const server = spawnGuestNodeProgram(ctx.kernel, guestJsTcpServer(3105)); - await waitForListener(ctx.kernel, 3105, 'JS TCP server'); - - const client = await runGuestNodeProgram( - ctx.kernel, - [ - "const net = require('net');", - "const client = net.connect({ host: '127.0.0.1', port: 3105 }, () => client.write('ping'));", - "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", - "client.on('error', (error) => { console.error(error); process.exit(1); });", - ].join('\n'), - ); - - server.process.kill(15); - await server.process.wait().catch(() => {}); - expect(client.exitCode).toBe(0); - expect(client.stderr).toBe(''); - expect(client.stdout.trim()).toBe('js-pong:ping'); - }); - - itIf(!wasmNetworkSkipReason, 'W1 WASM curl -> JS node:http server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestNodeProgram(ctx.kernel, guestJsHttpServer(3102)); - await waitForOutput(server, 'js http listening 3102', 'JS HTTP server'); - - const wasm = await ctx.kernel.exec('curl -fsS http://127.0.0.1:3102/from-wasm'); - - server.process.kill(15); - await server.process.wait().catch(() => {}); - expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).toBe(''); - expect(wasm.stdout.trim()).toBe('js:GET:/from-wasm'); - }); - - itIf(!wasmNetworkSkipReason, 'J3 JS fetch -> WASM HTTP server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestProgram(ctx.kernel, 'http_server', ['3103']); - await waitForListener(ctx.kernel, 3103, 'WASM HTTP server'); - - const client = await runGuestNodeProgram( - ctx.kernel, - [ - "fetch('http://127.0.0.1:3103/from-js')", - " .then(async (res) => console.log(res.status + ':' + await res.text()))", - " .catch((error) => { console.error(error); process.exit(1); });", - ].join('\n'), - ); - const serverExit = await server.process.wait(); - - expect(client.exitCode).toBe(0); - expect(client.stderr).toBe(''); - expect(client.stdout.trim()).toBe('200:wasm:GET:/from-js'); - expect(serverExit).toBe(0); - expect(decodeChunks(server.stdoutChunks)).toContain('received request: GET /from-js'); - }); - - itIf(!wasmNetworkSkipReason, 'J4 JS net.connect -> WASM TCP server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestProgram(ctx.kernel, 'tcp_server', ['3106']); - await waitForListener(ctx.kernel, 3106, 'WASM TCP server'); - - const client = await runGuestNodeProgram( - ctx.kernel, - [ - "const net = require('net');", - "const client = net.connect({ host: '127.0.0.1', port: 3106 }, () => client.write('ping'));", - "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", - "client.on('error', (error) => { console.error(error); process.exit(1); });", - ].join('\n'), - ); - const serverExit = await server.process.wait(); - - expect(client.exitCode).toBe(0); - expect(client.stderr).toBe(''); - expect(client.stdout.trim()).toBe('pong'); - expect(serverExit).toBe(0); - expect(decodeChunks(server.stdoutChunks)).toContain('received: ping'); - }); - - itIf(!wasmNetworkSkipReason, 'H2 host vmFetch -> WASM HTTP server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestProgram(ctx.kernel, 'http_server', ['3104']); - await waitForListener(ctx.kernel, 3104, 'WASM HTTP server'); - - const response = parseVmFetchResponse( - await ctx.kernel.vmFetch({ - port: 3104, - method: 'GET', - path: '/from-host', - headersJson: JSON.stringify({}), - }), - ); - const serverExit = await server.process.wait(); - - expect(response.status).toBe(200); - expect(response.body).toBe('wasm:GET:/from-host'); - expect(serverExit).toBe(0); - expect(decodeChunks(server.stdoutChunks)).toContain('received request: GET /from-host'); - }); - - itIf(!wasmNetworkSkipReason, 'W2 WASM tcp_echo -> JS net.Server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestNodeProgram(ctx.kernel, guestJsTcpServer(3107)); - await waitForListener(ctx.kernel, 3107, 'JS TCP server'); - - const wasm = await ctx.kernel.exec('tcp_echo 3107'); - - server.process.kill(15); - await server.process.wait().catch(() => {}); - expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).not.toContain('socket error'); - expect(wasm.stdout).toContain('sent: 5'); - expect(wasm.stdout).toContain('received: js-pong:hello'); - }); - - itIf(!wasmNetworkSkipReason, 'W3 WASM curl -> WASM HTTP server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestProgram(ctx.kernel, 'http_server', ['3108']); - await waitForListener(ctx.kernel, 3108, 'WASM HTTP server'); - - const wasm = await ctx.kernel.exec('curl -fsS http://127.0.0.1:3108/from-wasm'); - const serverExit = await server.process.wait(); - - expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).toBe(''); - expect(wasm.stdout.trim()).toBe('wasm:GET:/from-wasm'); - expect(serverExit).toBe(0); - }); - - itIf(!wasmNetworkSkipReason, 'W4 WASM tcp_echo -> WASM TCP server over VM loopback', async () => { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const server = spawnGuestProgram(ctx.kernel, 'tcp_server', ['3109']); - await waitForListener(ctx.kernel, 3109, 'WASM TCP server'); - - const wasm = await ctx.kernel.exec('tcp_echo 3109'); - const serverExit = await server.process.wait(); - - expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).not.toContain('socket error'); - expect(wasm.stdout).toContain('sent: 5'); - expect(wasm.stdout).toContain('received: pong'); - expect(serverExit).toBe(0); - }); - - it('O1 JS fetch -> host loopback requires loopback exemption', async () => { - const seenRequests: string[] = []; - const hostServer = createHttpServer((req, res) => { - seenRequests.push(req.url ?? ''); - res.writeHead(200, { 'content-type': 'text/plain' }); - res.end('host:' + req.url); - }); - await new Promise((resolveListen) => { - hostServer.listen(0, '127.0.0.1', () => resolveListen()); - }); - const port = (hostServer.address() as import('node:net').AddressInfo).port; - - try { - ctx = await createIntegrationKernel({ - runtimes: ['node'], - }); - const noExemption = await runGuestNodeProgram( - ctx.kernel, - [ - `fetch('http://127.0.0.1:${port}/blocked')`, - " .then(async (res) => console.log('unexpected:' + res.status + ':' + await res.text()))", - " .catch((error) => { console.log(error.cause?.code || error.code || error.name); });", - ].join('\n'), - ); - expect(noExemption.exitCode).toBe(0); - expect(noExemption.stdout.trim()).toBe('EACCES'); - expect(seenRequests).toEqual([]); - await ctx.dispose(); - - ctx = await createIntegrationKernel({ - runtimes: ['node'], - loopbackExemptPorts: [port], - }); - const allowed = await runGuestNodeProgram( - ctx.kernel, - [ - `fetch('http://127.0.0.1:${port}/allowed')`, - " .then(async (res) => console.log(res.status + ':' + await res.text()))", - " .catch((error) => { console.error(error); process.exit(1); });", - ].join('\n'), - ); - expect(allowed.exitCode).toBe(0); - expect(allowed.stderr).toBe(''); - expect(allowed.stdout.trim()).toBe('200:host:/allowed'); - expect(seenRequests).toEqual(['/allowed']); - } finally { - await new Promise((resolveClose) => hostServer.close(() => resolveClose())); - } - }); - - itIf(!wasmNetworkSkipReason, 'O2 WASM curl -> host loopback requires loopback exemption', async () => { - const seenRequests: string[] = []; - const hostServer = createHttpServer((req, res) => { - seenRequests.push(req.url ?? ''); - res.writeHead(200, { 'content-type': 'text/plain' }); - res.end('host:' + req.url); - }); - await new Promise((resolveListen) => { - hostServer.listen(0, '127.0.0.1', () => resolveListen()); - }); - const port = (hostServer.address() as import('node:net').AddressInfo).port; - - try { - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - }); - const noExemption = await ctx.kernel.exec(`curl -fsS http://127.0.0.1:${port}/blocked`); - expect(noExemption.exitCode).not.toBe(0); - expect(noExemption.stderr).toMatch(/EACCES|Bad address|Connection refused|connect|Failed to connect|Invalid argument/); - expect(seenRequests).toEqual([]); - await ctx.dispose(); - - ctx = await createIntegrationKernel({ - runtimes: ['wasmvm', 'node'], - commandDirs: [C_BUILD_DIR, COMMANDS_DIR], - loopbackExemptPorts: [port], - }); - const allowed = await ctx.kernel.exec(`curl -fsS http://127.0.0.1:${port}/allowed`); - expect(allowed.exitCode).toBe(0); - expect(allowed.stderr).toBe(''); - expect(allowed.stdout.trim()).toBe('host:/allowed'); - expect(seenRequests).toEqual(['/allowed']); - } finally { - await new Promise((resolveClose) => hostServer.close(() => resolveClose())); - } - }); +describe("cross-runtime network integration", { timeout: 90_000 }, () => { + let ctx: IntegrationKernelResult; + + afterEach(async () => { + await ctx?.dispose(); + }); + + it("J1 JS fetch -> JS node:http server over VM loopback", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["node"], + }); + const server = spawnGuestNodeProgram(ctx.kernel, guestJsHttpServer(3101)); + await waitForOutput(server, "js http listening 3101", "JS HTTP server"); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "fetch('http://127.0.0.1:3101/from-js')", + " .then(async (res) => console.log(res.status + ':' + await res.text()))", + " .catch((error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + + server.process.kill(15); + await server.process.wait().catch(() => {}); + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("200:js:GET:/from-js"); + }); + + it("J2 JS net.connect -> JS net.Server over VM loopback", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["node"], + }); + const server = spawnGuestNodeProgram(ctx.kernel, guestJsTcpServer(3105)); + await waitForListener(ctx.kernel, 3105, "JS TCP server", server); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "const net = require('net');", + "const client = net.connect({ host: '127.0.0.1', port: 3105 }, () => client.write('ping'));", + "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", + "client.on('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + + server.process.kill(15); + await server.process.wait().catch(() => {}); + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("js-pong:ping"); + }); + + itIf( + !wasmNetworkSkipReason, + "W1 WASM curl -> JS node:http server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestNodeProgram(ctx.kernel, guestJsHttpServer(3102)); + await waitForOutput(server, "js http listening 3102", "JS HTTP server"); + + const wasm = await ctx.kernel.exec( + "curl -fsS http://127.0.0.1:3102/from-wasm", + ); + + server.process.kill(15); + await server.process.wait().catch(() => {}); + expect(wasm.exitCode).toBe(0); + expect(wasm.stderr).toBe(""); + expect(wasm.stdout.trim()).toBe("js:GET:/from-wasm"); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "J3 JS fetch -> WASM HTTP server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "http_server", ["3103"]); + await waitForListener(ctx.kernel, 3103, "WASM HTTP server", server); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "fetch('http://127.0.0.1:3103/from-js')", + " .then(async (res) => console.log(res.status + ':' + await res.text()))", + " .catch((error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("200:wasm:GET:/from-js"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + "received request: GET /from-js", + ); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "J4 JS net.connect -> WASM TCP server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "tcp_server", ["3106"]); + await waitForListener(ctx.kernel, 3106, "WASM TCP server", server); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "const net = require('net');", + "const client = net.connect({ host: '127.0.0.1', port: 3106 }, () => client.write('ping'));", + "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", + "client.on('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("pong"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain("received: ping"); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "H2 host vmFetch -> WASM HTTP server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "http_server", ["3104"]); + await waitForListener(ctx.kernel, 3104, "WASM HTTP server", server); + + const response = parseVmFetchResponse( + await ctx.kernel.vmFetch({ + port: 3104, + method: "GET", + path: "/from-host", + headersJson: JSON.stringify({}), + }), + ); + const serverExit = await server.process.wait(); + + expect(response.status).toBe(200); + expect(response.body).toBe("wasm:GET:/from-host"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + "received request: GET /from-host", + ); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "W2 WASM tcp_echo -> JS net.Server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestNodeProgram(ctx.kernel, guestJsTcpServer(3107)); + await waitForListener(ctx.kernel, 3107, "JS TCP server", server); + + const wasm = await ctx.kernel.exec("tcp_echo 3107"); + + server.process.kill(15); + await server.process.wait().catch(() => {}); + expect(wasm.exitCode).toBe(0); + expect(wasm.stderr).not.toContain("socket error"); + expect(wasm.stdout).toContain("sent: 5"); + expect(wasm.stdout).toContain("received: js-pong:hello"); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "W3 WASM curl -> WASM HTTP server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "http_server", ["3108"]); + await waitForListener(ctx.kernel, 3108, "WASM HTTP server", server); + + const wasm = await ctx.kernel.exec( + "curl -fsS http://127.0.0.1:3108/from-wasm", + ); + const serverExit = await server.process.wait(); + + expect(wasm.exitCode).toBe(0); + expect(wasm.stderr).toBe(""); + expect(wasm.stdout.trim()).toBe("wasm:GET:/from-wasm"); + expect(serverExit).toBe(0); + }, + ); + + itIf( + !wasmNetworkSkipReason, + "W4 WASM tcp_echo -> WASM TCP server over VM loopback", + async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "tcp_server", ["3109"]); + await waitForListener(ctx.kernel, 3109, "WASM TCP server", server); + + const wasm = await ctx.kernel.exec("tcp_echo 3109"); + const serverExit = await server.process.wait(); + + expect(wasm.exitCode).toBe(0); + expect(wasm.stderr).not.toContain("socket error"); + expect(wasm.stdout).toContain("sent: 5"); + expect(wasm.stdout).toContain("received: pong"); + expect(serverExit).toBe(0); + }, + ); + + it("O1 JS fetch -> host loopback requires loopback exemption", async () => { + const seenRequests: string[] = []; + const hostServer = createHttpServer((req, res) => { + seenRequests.push(req.url ?? ""); + res.writeHead(200, { "content-type": "text/plain" }); + res.end("host:" + req.url); + }); + await new Promise((resolveListen) => { + hostServer.listen(0, "127.0.0.1", () => resolveListen()); + }); + const port = (hostServer.address() as import("node:net").AddressInfo).port; + + try { + ctx = await createIntegrationKernel({ + runtimes: ["node"], + }); + const noExemption = await runGuestNodeProgram( + ctx.kernel, + [ + `fetch('http://127.0.0.1:${port}/blocked')`, + " .then(async (res) => console.log('unexpected:' + res.status + ':' + await res.text()))", + " .catch((error) => { console.log(error.cause?.code || error.code || error.name); });", + ].join("\n"), + ); + expect(noExemption.exitCode).toBe(0); + expect(noExemption.stdout.trim()).toBe("EACCES"); + expect(seenRequests).toEqual([]); + await ctx.dispose(); + + ctx = await createIntegrationKernel({ + runtimes: ["node"], + loopbackExemptPorts: [port], + }); + const allowed = await runGuestNodeProgram( + ctx.kernel, + [ + `fetch('http://127.0.0.1:${port}/allowed')`, + " .then(async (res) => console.log(res.status + ':' + await res.text()))", + " .catch((error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + expect(allowed.exitCode).toBe(0); + expect(allowed.stderr).toBe(""); + expect(allowed.stdout.trim()).toBe("200:host:/allowed"); + expect(seenRequests).toEqual(["/allowed"]); + } finally { + await new Promise((resolveClose) => + hostServer.close(() => resolveClose()), + ); + } + }); + + itIf( + !wasmNetworkSkipReason, + "O2 WASM curl -> host loopback requires loopback exemption", + async () => { + const seenRequests: string[] = []; + const hostServer = createHttpServer((req, res) => { + seenRequests.push(req.url ?? ""); + res.writeHead(200, { "content-type": "text/plain" }); + res.end("host:" + req.url); + }); + await new Promise((resolveListen) => { + hostServer.listen(0, "127.0.0.1", () => resolveListen()); + }); + const port = (hostServer.address() as import("node:net").AddressInfo) + .port; + + try { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const noExemption = await ctx.kernel.exec( + `curl -fsS http://127.0.0.1:${port}/blocked`, + ); + expect(noExemption.exitCode).not.toBe(0); + expect(noExemption.stderr).toMatch( + /EACCES|Bad address|Connection refused|connect|Failed to connect|Invalid argument/, + ); + expect(seenRequests).toEqual([]); + await ctx.dispose(); + + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + loopbackExemptPorts: [port], + }); + const allowed = await ctx.kernel.exec( + `curl -fsS http://127.0.0.1:${port}/allowed`, + ); + expect(allowed.exitCode).toBe(0); + expect(allowed.stderr).toBe(""); + expect(allowed.stdout.trim()).toBe("host:/allowed"); + expect(seenRequests).toEqual(["/allowed"]); + } finally { + await new Promise((resolveClose) => + hostServer.close(() => resolveClose()), + ); + } + }, + ); }); diff --git a/packages/runtime-core/tests/integration/wasi-http.nightly.test.ts b/packages/runtime-core/tests/integration/wasi-http.nightly.test.ts index 7c47c7b5c6..bc73d09d27 100644 --- a/packages/runtime-core/tests/integration/wasi-http.nightly.test.ts +++ b/packages/runtime-core/tests/integration/wasi-http.nightly.test.ts @@ -11,346 +11,419 @@ * Tests start local HTTP/HTTPS servers and run http-test via kernel.exec(). */ -import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; -import { createWasmVmRuntime } from '@rivet-dev/agentos-vm-test-harness'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@rivet-dev/agentos-vm-test-harness'; -import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; -import { createServer as createHttpServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'; -import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; -import { execSync } from 'node:child_process'; -import { unlinkSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { describe, it, expect, afterEach, beforeAll, afterAll } from "vitest"; +import { createWasmVmRuntime } from "@rivet-dev/agentos-vm-test-harness"; +import { + COMMANDS_DIR, + createKernel, + describeIf, + hasWasmBinaries, +} from "@rivet-dev/agentos-vm-test-harness"; +import type { Kernel } from "@rivet-dev/agentos-vm-test-harness"; +import { + createServer as createHttpServer, + type Server, + type IncomingMessage, + type ServerResponse, +} from "node:http"; +import { + createServer as createHttpsServer, + type Server as HttpsServer, +} from "node:https"; +import { execSync } from "node:child_process"; +import { unlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; // Check if openssl CLI is available for generating test certs let hasOpenssl = false; try { - execSync('openssl version', { stdio: 'pipe' }); - hasOpenssl = true; -} catch { /* openssl not available */ } + execSync("openssl version", { stdio: "pipe" }); + hasOpenssl = true; +} catch { + /* openssl not available */ +} function generateSelfSignedCert(): { key: string; cert: string } { - const keyPath = join(tmpdir(), `wasi-http-test-key-${process.pid}-${Date.now()}.pem`); - try { - const key = execSync( - 'openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null', - { encoding: 'utf8' }, - ); - writeFileSync(keyPath, key); - const cert = execSync( - `openssl req -new -x509 -key "${keyPath}" -days 1 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null`, - { encoding: 'utf8' }, - ); - return { key, cert }; - } finally { - try { - unlinkSync(keyPath); - } catch { - // Best effort cleanup for test temp files. - } - } + const keyPath = join( + tmpdir(), + `wasi-http-test-key-${process.pid}-${Date.now()}.pem`, + ); + try { + const key = execSync( + "openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null", + { encoding: "utf8" }, + ); + writeFileSync(keyPath, key); + const cert = execSync( + `openssl req -new -x509 -key "${keyPath}" -days 1 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null`, + { encoding: "utf8" }, + ); + return { key, cert }; + } finally { + try { + unlinkSync(keyPath); + } catch { + // Best effort cleanup for test temp files. + } + } } // Minimal in-memory VFS for kernel tests class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map(name => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { - this.dirs.add(path); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const data = this.files.get(path); - if (!isDir && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isDir ? 0o40755 : 0o100644, - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: false, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod(_path: string, _mode: number) {} - async lstat(path: string) { return this.stat(path); } - async removeFile(path: string) { this.files.delete(path); } - async removeDir(path: string) { this.dirs.delete(path); } - async rename(oldPath: string, newPath: string) { - const data = this.files.get(oldPath); - if (data) { - this.files.set(newPath, data); - this.files.delete(oldPath); - } - } - async pread(path: string, buffer: Uint8Array, offset: number, length: number, position: number): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - const available = Math.min(length, data.length - position); - if (available <= 0) return 0; - buffer.set(data.subarray(position, position + available), offset); - return available; - } + private files = new Map(); + private dirs = new Set(["/"]); + + async readFile(path: string): Promise { + const data = this.files.get(path); + if (!data) throw new Error(`ENOENT: ${path}`); + return data; + } + async readTextFile(path: string): Promise { + return new TextDecoder().decode(await this.readFile(path)); + } + async readDir(path: string): Promise { + const prefix = path === "/" ? "/" : path + "/"; + const entries: string[] = []; + for (const p of [...this.files.keys(), ...this.dirs]) { + if (p !== path && p.startsWith(prefix)) { + const rest = p.slice(prefix.length); + if (!rest.includes("/")) entries.push(rest); + } + } + return entries; + } + async readDirWithTypes(path: string) { + return (await this.readDir(path)).map((name) => ({ + name, + isDirectory: this.dirs.has(path === "/" ? `/${name}` : `${path}/${name}`), + })); + } + async writeFile(path: string, content: string | Uint8Array): Promise { + const data = + typeof content === "string" ? new TextEncoder().encode(content) : content; + this.files.set(path, new Uint8Array(data)); + const parts = path.split("/").filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add("/" + parts.slice(0, i).join("/")); + } + } + async createDir(path: string) { + this.dirs.add(path); + } + async mkdir(path: string, _options?: { recursive?: boolean }) { + this.dirs.add(path); + const parts = path.split("/").filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add("/" + parts.slice(0, i).join("/")); + } + } + async exists(path: string): Promise { + return this.files.has(path) || this.dirs.has(path); + } + async stat(path: string) { + const isDir = this.dirs.has(path); + const data = this.files.get(path); + if (!isDir && !data) throw new Error(`ENOENT: ${path}`); + return { + mode: isDir ? 0o40755 : 0o100644, + size: data?.length ?? 0, + isDirectory: isDir, + isSymbolicLink: false, + atimeMs: Date.now(), + mtimeMs: Date.now(), + ctimeMs: Date.now(), + birthtimeMs: Date.now(), + ino: 0, + nlink: 1, + uid: 1000, + gid: 1000, + }; + } + async chmod(_path: string, _mode: number) {} + async lstat(path: string) { + return this.stat(path); + } + async removeFile(path: string) { + this.files.delete(path); + } + async removeDir(path: string) { + this.dirs.delete(path); + } + async rename(oldPath: string, newPath: string) { + const data = this.files.get(oldPath); + if (data) { + this.files.set(newPath, data); + this.files.delete(oldPath); + } + } + async pread( + path: string, + buffer: Uint8Array, + offset: number, + length: number, + position: number, + ): Promise { + const data = this.files.get(path); + if (!data) throw new Error(`ENOENT: ${path}`); + const available = Math.min(length, data.length - position); + if (available <= 0) return 0; + buffer.set(data.subarray(position, position + available), offset); + return available; + } } // HTTP request handler function requestHandler(port: number) { - return (req: IncomingMessage, res: ServerResponse) => { - const url = req.url ?? '/'; - - // GET / — basic response - if (url === '/' && req.method === 'GET') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('hello from wasi-http test'); - return; - } - - // GET /json — JSON response - if (url === '/json' && req.method === 'GET') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', message: 'json response' })); - return; - } - - // POST /echo-body — echo JSON body back - if (url === '/echo-body' && req.method === 'POST') { - let body = ''; - req.on('data', (chunk: Buffer) => { body += chunk.toString(); }); - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ received: body, contentType: req.headers['content-type'] })); - }); - return; - } - - // GET /echo-headers — echo back request headers - if (url === '/echo-headers') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - const xCustom = req.headers['x-custom-header'] ?? 'none'; - const xAnother = req.headers['x-another'] ?? 'none'; - res.end(`x-custom-header: ${xCustom}\nx-another: ${xAnother}`); - return; - } - - // GET /sse — SSE stream with 3 events - if (url === '/sse') { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'close', - }); - res.write('event: message\ndata: hello\n\n'); - res.write('event: update\ndata: world\nid: 1\n\n'); - res.write('data: done\n\n'); - res.end(); - return; - } - - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('not found'); - }; + return (req: IncomingMessage, res: ServerResponse) => { + const url = req.url ?? "/"; + + // GET / — basic response + if (url === "/" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello from wasi-http test"); + return; + } + + // GET /json — JSON response + if (url === "/json" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", message: "json response" })); + return; + } + + // POST /echo-body — echo JSON body back + if (url === "/echo-body" && req.method === "POST") { + let body = ""; + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + req.on("end", () => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + received: body, + contentType: req.headers["content-type"], + }), + ); + }); + return; + } + + // GET /echo-headers — echo back request headers + if (url === "/echo-headers") { + res.writeHead(200, { "Content-Type": "text/plain" }); + const xCustom = req.headers["x-custom-header"] ?? "none"; + const xAnother = req.headers["x-another"] ?? "none"; + res.end(`x-custom-header: ${xCustom}\nx-another: ${xAnother}`); + return; + } + + // GET /sse — SSE stream with 3 events + if (url === "/sse") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "close", + }); + res.write("event: message\ndata: hello\n\n"); + res.write("event: update\ndata: world\nid: 1\n\n"); + res.write("data: done\n\n"); + res.end(); + return; + } + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("not found"); + }; } -describeIf(hasWasmBinaries, 'wasi-http client (http-test binary)', () => { - let kernel: Kernel; - let server: Server; - let port: number; - - function createHttpKernel(loopbackPort: number): Kernel { - const vfs = new SimpleVFS(); - return createKernel({ - filesystem: vfs as any, - loopbackExemptPorts: [loopbackPort], - }); - } - - beforeAll(async () => { - server = createHttpServer(requestHandler(0)); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - port = (server.address() as import('node:net').AddressInfo).port; - // Patch handler to use actual port - server.removeAllListeners('request'); - server.on('request', requestHandler(port)); - }); - - afterAll(async () => { - await new Promise((resolve) => server.close(() => resolve())); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('GET returns status and body', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec(`http-test get http://127.0.0.1:${port}/`); - expect(result.stdout).toContain('status: 200'); - expect(result.stdout).toContain('body: hello from wasi-http test'); - }); - - it('GET returns JSON response', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec(`http-test get http://127.0.0.1:${port}/json`); - expect(result.stdout).toContain('status: 200'); - expect(result.stdout).toContain('"status":"ok"'); - }); - - it('POST sends JSON body correctly', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const jsonBody = '{"key":"value","num":42}'; - const result = await kernel.exec(`http-test post http://127.0.0.1:${port}/echo-body '${jsonBody}'`); - expect(result.stdout).toContain('status: 200'); - // Verify server received the JSON body and content-type - expect(result.stdout).toContain('"received":"{\\"key\\":\\"value\\",\\"num\\":42}"'); - expect(result.stdout).toContain('application/json'); - }); - - it('GET with custom headers sends headers correctly', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec( - `http-test headers http://127.0.0.1:${port}/echo-headers 'X-Custom-Header:test-value' 'X-Another:second'` - ); - expect(result.stdout).toContain('status: 200'); - expect(result.stdout).toContain('x-custom-header: test-value'); - expect(result.stdout).toContain('x-another: second'); - }); - - it('SSE streaming receives events', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec(`http-test sse http://127.0.0.1:${port}/sse`); - expect(result.stdout).toContain('status: 200'); - expect(result.stdout).toContain('event: message'); - expect(result.stdout).toContain('data: hello'); - expect(result.stdout).toContain('event: update'); - expect(result.stdout).toContain('data: world'); - expect(result.stdout).toContain('data: done'); - }); - - it('GET to non-existent path returns 404', async () => { - kernel = createHttpKernel(port); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec(`http-test get http://127.0.0.1:${port}/nonexistent`); - expect(result.stdout).toContain('status: 404'); - }); +describeIf(hasWasmBinaries, "wasi-http client (http-test binary)", () => { + let kernel: Kernel; + let server: Server; + let port: number; + + function createHttpKernel(loopbackPort: number): Kernel { + const vfs = new SimpleVFS(); + return createKernel({ + filesystem: vfs as any, + loopbackExemptPorts: [loopbackPort], + }); + } + + beforeAll(async () => { + server = createHttpServer(requestHandler(0)); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + port = (server.address() as import("node:net").AddressInfo).port; + // Patch handler to use actual port + server.removeAllListeners("request"); + server.on("request", requestHandler(port)); + }); + + afterAll(async () => { + await new Promise((resolve) => server.close(() => resolve())); + }); + + afterEach(async () => { + await kernel?.dispose(); + }); + + it("GET returns status and body", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec(`http-test get http://127.0.0.1:${port}/`); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + expect(result.stdout).toContain("body: hello from wasi-http test"); + }); + + it("GET returns JSON response", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + `http-test get http://127.0.0.1:${port}/json`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + expect(result.stdout).toContain('"status":"ok"'); + }); + + it("POST sends JSON body correctly", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const jsonBody = '{"key":"value","num":42}'; + const result = await kernel.exec( + `http-test post http://127.0.0.1:${port}/echo-body '${jsonBody}'`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + // Verify server received the JSON body and content-type + expect(result.stdout).toContain( + '"received":"{\\"key\\":\\"value\\",\\"num\\":42}"', + ); + expect(result.stdout).toContain("application/json"); + }); + + it("GET with custom headers sends headers correctly", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + `http-test headers http://127.0.0.1:${port}/echo-headers 'X-Custom-Header:test-value' 'X-Another:second'`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + expect(result.stdout).toContain("x-custom-header: test-value"); + expect(result.stdout).toContain("x-another: second"); + }); + + it("SSE streaming receives events", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + `http-test sse http://127.0.0.1:${port}/sse`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + expect(result.stdout).toContain("event: message"); + expect(result.stdout).toContain("data: hello"); + expect(result.stdout).toContain("event: update"); + expect(result.stdout).toContain("data: world"); + expect(result.stdout).toContain("data: done"); + }); + + it("GET to non-existent path returns 404", async () => { + kernel = createHttpKernel(port); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + `http-test get http://127.0.0.1:${port}/nonexistent`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 404"); + }); }); -describeIf(hasWasmBinaries && hasOpenssl, 'wasi-http HTTPS (http-test binary)', () => { - let kernel: Kernel; - let httpsServer: HttpsServer; - let httpsPort: number; - - function createHttpsKernel(loopbackPort: number): Kernel { - const vfs = new SimpleVFS(); - return createKernel({ - filesystem: vfs as any, - loopbackExemptPorts: [loopbackPort], - }); - } - - beforeAll(async () => { - const tlsCert = generateSelfSignedCert(); - - httpsServer = createHttpsServer({ key: tlsCert.key, cert: tlsCert.cert }, (req, res) => { - if (req.url === '/' && req.method === 'GET') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('hello from https'); - return; - } - res.writeHead(404); - res.end('not found'); - }); - await new Promise((resolve) => httpsServer.listen(0, '127.0.0.1', resolve)); - httpsPort = (httpsServer.address() as import('node:net').AddressInfo).port; - }); - - afterAll(async () => { - if (httpsServer) { - await new Promise((resolve) => httpsServer.close(() => resolve())); - } - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('HTTPS GET via TLS upgrade returns response', async () => { - kernel = createHttpsKernel(httpsPort); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - // Disable TLS verification for self-signed cert in tests - const origReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - try { - const result = await kernel.exec(`http-test get https://127.0.0.1:${httpsPort}/`, { - env: { NODE_TLS_REJECT_UNAUTHORIZED: '0' }, - }); - expect(result.exitCode, result.stderr).toBe(0); - expect(result.stdout).toContain('status: 200'); - expect(result.stdout).toContain('body: hello from https'); - } finally { - if (origReject === undefined) { - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; - } else { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = origReject; - } - } - }); -}); +describeIf( + hasWasmBinaries && hasOpenssl, + "wasi-http HTTPS (http-test binary)", + () => { + let kernel: Kernel; + let httpsServer: HttpsServer; + let httpsPort: number; + + function createHttpsKernel(loopbackPort: number): Kernel { + const vfs = new SimpleVFS(); + return createKernel({ + filesystem: vfs as any, + loopbackExemptPorts: [loopbackPort], + }); + } + + beforeAll(async () => { + const tlsCert = generateSelfSignedCert(); + + httpsServer = createHttpsServer( + { key: tlsCert.key, cert: tlsCert.cert }, + (req, res) => { + if (req.url === "/" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello from https"); + return; + } + res.writeHead(404); + res.end("not found"); + }, + ); + await new Promise((resolve) => + httpsServer.listen(0, "127.0.0.1", resolve), + ); + httpsPort = (httpsServer.address() as import("node:net").AddressInfo) + .port; + }); + + afterAll(async () => { + if (httpsServer) { + await new Promise((resolve) => + httpsServer.close(() => resolve()), + ); + } + }); + + afterEach(async () => { + await kernel?.dispose(); + }); + + it("HTTPS GET via TLS upgrade returns response", async () => { + kernel = createHttpsKernel(httpsPort); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + // Disable TLS verification for self-signed cert in tests + const origReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + try { + const result = await kernel.exec( + `http-test get https://127.0.0.1:${httpsPort}/`, + { + env: { NODE_TLS_REJECT_UNAUTHORIZED: "0" }, + }, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("status: 200"); + expect(result.stdout).toContain("body: hello from https"); + } finally { + if (origReject === undefined) { + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } else { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = origReject; + } + } + }); + }, +); diff --git a/packages/runtime-core/tests/request-payloads.test.ts b/packages/runtime-core/tests/request-payloads.test.ts index 77b2e0fdf7..8e55ef5353 100644 --- a/packages/runtime-core/tests/request-payloads.test.ts +++ b/packages/runtime-core/tests/request-payloads.test.ts @@ -46,6 +46,7 @@ describe("request payload conversion", () => { runtime: "java_script", config: { env: {}, + wasmBackend: "wasmtime", rootFilesystem: { mode: "read-only", disableDefaultBaseLayer: true, @@ -63,6 +64,7 @@ describe("request payload conversion", () => { }, }); expect(JSON.parse(createVmPayload.val.config)).toMatchObject({ + wasmBackend: "wasmtime", rootFilesystem: { mode: "read-only", disableDefaultBaseLayer: true, @@ -210,6 +212,27 @@ describe("request payload conversion", () => { }); }); + it("maps every standalone WASM backend selector", () => { + for (const [wasm_backend, wasmBackend] of [ + ["v8", protocol.StandaloneWasmBackend.V8], + ["wasmtime", protocol.StandaloneWasmBackend.Wasmtime], + ["wasmtime-threads", protocol.StandaloneWasmBackend.WasmtimeThreads], + ] as const) { + expect( + toGeneratedRequestPayload({ + type: "execute", + process_id: `proc-${wasm_backend}`, + args: [], + env: {}, + wasm_backend, + }), + ).toMatchObject({ + tag: "ExecuteRequest", + val: { wasmBackend }, + }); + } + }); + it("maps guest kernel call requests", () => { const payload = new TextEncoder().encode( JSON.stringify({ host: "127.0.0.1", port: 39221 }), diff --git a/packages/runtime-core/tests/response-payloads.test.ts b/packages/runtime-core/tests/response-payloads.test.ts index 23ade0c93a..42208bbf4c 100644 --- a/packages/runtime-core/tests/response-payloads.test.ts +++ b/packages/runtime-core/tests/response-payloads.test.ts @@ -224,6 +224,16 @@ describe("response payload conversion", () => { socketConnections: 2n, socketBufferedBytes: 256n, socketDatagramQueueLen: 4n, + wasmReservedMemoryBytes: 1024n, + wasmtimeEngineProfiles: 2n, + wasmtimeModuleEntries: 3n, + wasmtimeModuleCacheHits: 4n, + wasmtimeModuleCacheMisses: 5n, + wasmtimeModuleCacheEvictions: 6n, + wasmtimeCompiledSourceBytes: 2048n, + wasmtimeChargedModuleBytes: 4096n, + wasmtimeCompileTimeMicros: 7000n, + wasmtimeProcessRetainedRssBytes: 8192n, queueSnapshots: [ { name: "pending_process_events", @@ -253,6 +263,16 @@ describe("response payload conversion", () => { socket_connections: 2, socket_buffered_bytes: 256, socket_datagram_queue_len: 4, + wasm_reserved_memory_bytes: 1024, + wasmtime_engine_profiles: 2, + wasmtime_module_entries: 3, + wasmtime_module_cache_hits: 4, + wasmtime_module_cache_misses: 5, + wasmtime_module_cache_evictions: 6, + wasmtime_compiled_source_bytes: 2048, + wasmtime_charged_module_bytes: 4096, + wasmtime_compile_time_micros: 7000, + wasmtime_process_retained_rss_bytes: 8192, queue_snapshots: [ { name: "pending_process_events", diff --git a/packages/runtime-sidecar/npm/darwin-arm64/package.json b/packages/runtime-sidecar/npm/darwin-arm64/package.json index 9a30b703e0..3b496f87df 100644 --- a/packages/runtime-sidecar/npm/darwin-arm64/package.json +++ b/packages/runtime-sidecar/npm/darwin-arm64/package.json @@ -17,6 +17,9 @@ "files": [ "agentos-native-sidecar" ], + "bin": { + "agentos-native-sidecar": "./agentos-native-sidecar" + }, "scripts": { "check-types": "node -e \"process.exit(0)\"" }, diff --git a/packages/runtime-sidecar/npm/darwin-x64/package.json b/packages/runtime-sidecar/npm/darwin-x64/package.json index 0dd77457dd..4380bb9a48 100644 --- a/packages/runtime-sidecar/npm/darwin-x64/package.json +++ b/packages/runtime-sidecar/npm/darwin-x64/package.json @@ -17,6 +17,9 @@ "files": [ "agentos-native-sidecar" ], + "bin": { + "agentos-native-sidecar": "./agentos-native-sidecar" + }, "scripts": { "check-types": "node -e \"process.exit(0)\"" }, diff --git a/packages/runtime-sidecar/npm/linux-arm64-gnu/package.json b/packages/runtime-sidecar/npm/linux-arm64-gnu/package.json index 430e10f5d9..d25d098486 100644 --- a/packages/runtime-sidecar/npm/linux-arm64-gnu/package.json +++ b/packages/runtime-sidecar/npm/linux-arm64-gnu/package.json @@ -20,6 +20,9 @@ "files": [ "agentos-native-sidecar" ], + "bin": { + "agentos-native-sidecar": "./agentos-native-sidecar" + }, "scripts": { "check-types": "node -e \"process.exit(0)\"" }, diff --git a/packages/runtime-sidecar/npm/linux-x64-gnu/package.json b/packages/runtime-sidecar/npm/linux-x64-gnu/package.json index 38ae2b9c0f..e04a5c8a0f 100644 --- a/packages/runtime-sidecar/npm/linux-x64-gnu/package.json +++ b/packages/runtime-sidecar/npm/linux-x64-gnu/package.json @@ -20,6 +20,9 @@ "files": [ "agentos-native-sidecar" ], + "bin": { + "agentos-native-sidecar": "./agentos-native-sidecar" + }, "scripts": { "check-types": "node -e \"process.exit(0)\"" }, diff --git a/packages/sidecar-binary/npm/darwin-arm64/package.json b/packages/sidecar-binary/npm/darwin-arm64/package.json index dab834593c..dab7f042a0 100644 --- a/packages/sidecar-binary/npm/darwin-arm64/package.json +++ b/packages/sidecar-binary/npm/darwin-arm64/package.json @@ -17,6 +17,9 @@ "files": [ "agentos-sidecar" ], + "bin": { + "agentos-sidecar": "./agentos-sidecar" + }, "engines": { "node": ">=20" }, diff --git a/packages/sidecar-binary/npm/darwin-x64/package.json b/packages/sidecar-binary/npm/darwin-x64/package.json index 485dcf5557..aab906f742 100644 --- a/packages/sidecar-binary/npm/darwin-x64/package.json +++ b/packages/sidecar-binary/npm/darwin-x64/package.json @@ -17,6 +17,9 @@ "files": [ "agentos-sidecar" ], + "bin": { + "agentos-sidecar": "./agentos-sidecar" + }, "engines": { "node": ">=20" }, diff --git a/packages/sidecar-binary/npm/linux-arm64-gnu/package.json b/packages/sidecar-binary/npm/linux-arm64-gnu/package.json index bafc0c89bb..72e2de94ee 100644 --- a/packages/sidecar-binary/npm/linux-arm64-gnu/package.json +++ b/packages/sidecar-binary/npm/linux-arm64-gnu/package.json @@ -20,6 +20,9 @@ "files": [ "agentos-sidecar" ], + "bin": { + "agentos-sidecar": "./agentos-sidecar" + }, "engines": { "node": ">=20" }, diff --git a/packages/sidecar-binary/npm/linux-x64-gnu/package.json b/packages/sidecar-binary/npm/linux-x64-gnu/package.json index 05ea56072e..39dd81153f 100644 --- a/packages/sidecar-binary/npm/linux-x64-gnu/package.json +++ b/packages/sidecar-binary/npm/linux-x64-gnu/package.json @@ -20,6 +20,9 @@ "files": [ "agentos-sidecar" ], + "bin": { + "agentos-sidecar": "./agentos-sidecar" + }, "engines": { "node": ">=20" }, diff --git a/packages/vm-test-harness/src/index.ts b/packages/vm-test-harness/src/index.ts index 4e6335b9c5..7adb28ad61 100644 --- a/packages/vm-test-harness/src/index.ts +++ b/packages/vm-test-harness/src/index.ts @@ -69,6 +69,7 @@ export { SOCK_DGRAM, SOCK_STREAM, } from "../../runtime-core/src/test-runtime.js"; + import { allowAll, createInMemoryFileSystem, @@ -76,6 +77,7 @@ import { createNodeRuntime, createWasmVmRuntime, } from "../../runtime-core/src/test-runtime.js"; + export type { DriverProcess, Kernel, @@ -86,19 +88,29 @@ export type { VirtualFileSystem, } from "../../runtime-core/src/test-runtime.js"; export { + createNodeHostNetworkAdapter, + createNodeRuntime, createWasmVmRuntime, DEFAULT_FIRST_PARTY_TIERS, - WASMVM_COMMANDS, + NodeFileSystem, type PermissionTier, + WASMVM_COMMANDS, type WasmVmRuntimeOptions, } from "../../runtime-core/src/test-runtime.js"; -export { - createNodeHostNetworkAdapter, - createNodeRuntime, - NodeFileSystem, -} from "../../runtime-core/src/test-runtime.js"; export { TerminalHarness } from "./terminal-harness.js"; +type TestWasmBackend = "v8" | "wasmtime"; + +function configuredTestWasmBackend(): TestWasmBackend | undefined { + const backend = process.env.AGENTOS_TEST_WASM_BACKEND; + if (backend === undefined || backend === "v8" || backend === "wasmtime") { + return backend; + } + throw new Error( + `AGENTOS_TEST_WASM_BACKEND must be "v8" or "wasmtime", got ${JSON.stringify(backend)}`, + ); +} + /** * Registry integration tests assume they can bootstrap runtimes and /bin stubs * unless they explicitly opt into a stricter permission policy. @@ -109,6 +121,7 @@ export function createKernel( return createKernelBase({ ...options, permissions: options.permissions ?? allowAll, + wasmBackend: options.wasmBackend ?? configuredTestWasmBackend(), }); } @@ -123,6 +136,8 @@ export interface IntegrationKernelOptions { loopbackExemptPorts?: number[]; commandDirs?: string[]; permissions?: Parameters[0]["permissions"]; + /** VM-wide engine used by standalone WASM commands in this test kernel. */ + wasmBackend?: "v8" | "wasmtime" | "wasmtime-threads"; } /** @@ -141,11 +156,14 @@ export async function createIntegrationKernel( filesystem: vfs, loopbackExemptPorts: options?.loopbackExemptPorts, permissions: options?.permissions, + wasmBackend: options?.wasmBackend, }); if (runtimes.includes("wasmvm")) { await kernel.mount( - createWasmVmRuntime({ commandDirs: options?.commandDirs ?? [COMMANDS_DIR] }), + createWasmVmRuntime({ + commandDirs: options?.commandDirs ?? [COMMANDS_DIR], + }), ); } if (runtimes.includes("node")) { diff --git a/scripts/ci/smoke-packed-wasm-backends.mjs b/scripts/ci/smoke-packed-wasm-backends.mjs new file mode 100644 index 0000000000..9b91e3acb7 --- /dev/null +++ b/scripts/ci/smoke-packed-wasm-backends.mjs @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const runtimeCoreDir = join(repositoryRoot, "packages/runtime-core"); +const runtimeSidecarDir = join(repositoryRoot, "packages/runtime-sidecar"); + +let platformPackageDir; +let sidecarBin; +let threadFixture; +for (let index = 2; index < process.argv.length; index += 2) { + const flag = process.argv[index]; + const value = process.argv[index + 1]; + if (!value) usage(`missing value for ${flag}`); + if (flag === "--platform-package") platformPackageDir = resolve(value); + else if (flag === "--sidecar-bin") sidecarBin = resolve(value); + else if (flag === "--thread-fixture") threadFixture = resolve(value); + else usage(`unknown option ${flag}`); +} + +if (Boolean(platformPackageDir) === Boolean(sidecarBin)) { + usage("provide exactly one of --platform-package or --sidecar-bin"); +} +if (sidecarBin && !existsSync(sidecarBin)) { + throw new Error(`sidecar binary does not exist: ${sidecarBin}`); +} +if (platformPackageDir && !existsSync(join(platformPackageDir, "agentos-native-sidecar"))) { + throw new Error( + `platform package is missing agentos-native-sidecar: ${platformPackageDir}`, + ); +} +if (!threadFixture || !existsSync(threadFixture)) { + usage("--thread-fixture must name the generated pthread conformance module"); +} + +const scratch = mkdtempSync(join(tmpdir(), "agentos-packed-wasm-")); +try { + const packedPackages = [pack(runtimeSidecarDir)]; + if (platformPackageDir) packedPackages.push(pack(platformPackageDir)); + packedPackages.push(pack(runtimeCoreDir)); + + const installDir = join(scratch, "install"); + mkdirSync(installDir, { recursive: true }); + const localPackages = Object.fromEntries( + packedPackages.map(({ name, tarball }) => [name, `file:${tarball}`]), + ); + writeFileSync( + join(installDir, "package.json"), + `${JSON.stringify( + { + private: true, + type: "module", + dependencies: localPackages, + pnpm: { overrides: localPackages }, + }, + null, + 2, + )}\n`, + ); + run( + "pnpm", + [ + "install", + "--ignore-scripts", + "--config.optional=false", + "--no-frozen-lockfile", + ], + installDir, + ); + + const runnerPath = join(installDir, "smoke.mjs"); + copyFileSync(threadFixture, join(installDir, "pthread-conformance.wasm")); + writeFileSync( + runnerPath, + `import { fileURLToPath } from "node:url"; +import { NodeRuntime } from "@rivet-dev/agentos-runtime-core"; +import { createInMemoryFileSystem } from "@rivet-dev/agentos-runtime-core/test-runtime"; + +const backends = ["v8", "wasmtime", "wasmtime-threads"]; +for (const backend of backends) { + const runtime = await NodeRuntime.create({ + filesystem: createInMemoryFileSystem(), + wasmBackend: backend, + wasmCommandDirs: [fileURLToPath(new URL(".", import.meta.url))], + permissions: { + fs: "allow", + network: "allow", + childProcess: "allow", + process: "allow", + env: "allow", + }, + }); + try { + const result = await runtime.execCommand("sh", [ + "-c", + \`printf 'packed-\${backend}\\n' | tr '[:lower:]' '[:upper:]'\`, + ]); + const expected = \`PACKED-\${backend.toUpperCase()}\\n\`; + if (result.exitCode !== 0 || result.stdout !== expected) { + throw new Error( + \`\${backend} packaged smoke failed: exit=\${result.exitCode} stdout=\${JSON.stringify(result.stdout)} stderr=\${JSON.stringify(result.stderr)}\`, + ); + } + if (backend === "wasmtime-threads") { + const threaded = await runtime.execCommand("pthread-conformance.wasm", []); + if (threaded.exitCode !== 0 || !threaded.stdout.includes("pthread-ok")) { + throw new Error( + \`packaged pthread smoke failed: exit=\${threaded.exitCode} stdout=\${JSON.stringify(threaded.stdout)} stderr=\${JSON.stringify(threaded.stderr)}\`, + ); + } + } + process.stdout.write(\`packaged backend smoke passed: \${backend}\\n\`); + } finally { + await runtime.dispose(); + } +} +`, + ); + + const runnerEnv = { ...process.env }; + delete runnerEnv.AGENTOS_SIDECAR_BIN; + delete runnerEnv.AGENTOS_WASMTIME_WORKER_PATH; + if (sidecarBin) { + runnerEnv.AGENTOS_SIDECAR_BIN = sidecarBin; + runnerEnv.AGENTOS_WASMTIME_WORKER_PATH = sidecarBin; + } + run(process.execPath, [runnerPath], installDir, { env: runnerEnv }); +} finally { + rmSync(scratch, { recursive: true, force: true }); +} + +function pack(packageDir) { + const manifest = JSON.parse( + readFileSync(join(packageDir, "package.json"), "utf8"), + ); + if (typeof manifest.name !== "string" || manifest.name.length === 0) { + throw new Error(`package has no valid name: ${packageDir}`); + } + const before = new Set(readdirSync(scratch)); + run("pnpm", ["pack", "--pack-destination", scratch], packageDir); + const created = readdirSync(scratch).filter( + (entry) => entry.endsWith(".tgz") && !before.has(entry), + ); + if (created.length !== 1) { + throw new Error( + `expected one tarball from ${packageDir}, found ${created.join(", ") || "none"}`, + ); + } + return { name: manifest.name, tarball: join(scratch, created[0]) }; +} + +function run(command, args, cwd, options = {}) { + if (options.createCwd) mkdirSync(cwd, { recursive: true }); + const result = spawnSync(command, args, { + cwd, + env: options.env ?? process.env, + stdio: "inherit", + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${command} exited with status ${result.status}`); + } +} + +function usage(message) { + throw new Error( + `${message}\nusage: smoke-packed-wasm-backends.mjs (--platform-package | --sidecar-bin ) --thread-fixture `, + ); +} diff --git a/tests/xfstests/Makefile b/tests/xfstests/Makefile index f2778549d8..da39e85cbe 100644 --- a/tests/xfstests/Makefile +++ b/tests/xfstests/Makefile @@ -30,6 +30,7 @@ XFSTESTS_CONCURRENCY ?= 2 # write-back, durability, and request-amplification contract is incomplete. # XFSTESTS_BACKENDS ?= chunked_local,memory,chunked_s3,object_s3 XFSTESTS_BACKENDS ?= chunked_local,memory,chunked_s3 +XFSTESTS_WASM_BACKENDS ?= v8 wasmtime XFSTESTS_MAX_TEMP_BYTES ?= 8589934592 XFSTESTS_TEST_TIMEOUT_SECONDS ?= 3600 @@ -45,7 +46,6 @@ run: helpers (pnpm install --frozen-lockfile --offline || pnpm install --frozen-lockfile) && \ node packages/build-tools/scripts/build-v8-bridge.mjs --out-dir "$(V8_BRIDGE_STAGE)" && \ export XFSTESTS_ROOT="$$run_tmp/source" && \ - export XFSTESTS_REPORT_DIR="$(REPORT)" && \ export XFSTESTS_EXCEPTIONS="$(CURDIR)/exceptions.toml" && \ export XFSTESTS_CONCURRENCY="$(XFSTESTS_CONCURRENCY)" && \ export XFSTESTS_BACKENDS="$(XFSTESTS_BACKENDS)" && \ @@ -55,12 +55,18 @@ run: helpers export CARGO_TARGET_DIR="$(CARGO_TARGET_DIR)" && \ export CARGO_BUILD_JOBS="$(CARGO_BUILD_JOBS)" && \ export AGENTOS_V8_BRIDGE_PREBUILT_DIR="$(V8_BRIDGE_STAGE)" && \ - cargo test --manifest-path "$(CURDIR)/../../Cargo.toml" \ - -p agentos-native-sidecar --test xfstests_correctness \ - xfstests_wasi_helper_ports -- --ignored --exact --nocapture && \ - cargo test --manifest-path "$(CURDIR)/../../Cargo.toml" \ - -p agentos-native-sidecar --test xfstests_correctness \ - xfstests_generic_quick_matrix -- --ignored --exact --nocapture + for wasm_backend in $(XFSTESTS_WASM_BACKENDS); do \ + case "$$wasm_backend" in v8|wasmtime) ;; *) echo "unsupported XFSTESTS_WASM_BACKENDS entry: $$wasm_backend" >&2; exit 2;; esac; \ + export AGENTOS_TEST_WASM_BACKEND="$$wasm_backend"; \ + export XFSTESTS_REPORT_DIR="$(REPORT)/$$wasm_backend"; \ + mkdir -p "$$XFSTESTS_REPORT_DIR"; \ + cargo test --release --manifest-path "$(CURDIR)/../../Cargo.toml" \ + -p agentos-native-sidecar --test xfstests_correctness \ + xfstests_wasi_helper_ports -- --ignored --exact --nocapture && \ + cargo test --release --manifest-path "$(CURDIR)/../../Cargo.toml" \ + -p agentos-native-sidecar --test xfstests_correctness \ + xfstests_generic_quick_matrix -- --ignored --exact --nocapture || exit $$?; \ + done native-commands: $(MAKE) -C "$(TOOLCHAIN)" wasm diff --git a/tests/xfstests/README.md b/tests/xfstests/README.md index f5cc7c0b44..112eaf704c 100644 --- a/tests/xfstests/README.md +++ b/tests/xfstests/README.md @@ -27,9 +27,11 @@ reduced counts plus focused semantic coverage; the reduced test still executes and any non-pass outcome fails. Wildcards, auto-blessing, unused records, duplicate records, and stale records are errors. -Reports are written to `report/results.md`, `report/agentos-gaps.md`, and -`report/surface-audit.md`, with driver-specific summaries under -`report/backends//results.md`. `XFSTESTS_BACKENDS` defaults to the +Each WASM engine writes reports under `report//`: the main files +are `results.md`, `agentos-gaps.md`, and `surface-audit.md`, with +driver-specific summaries under `backends//results.md`. +`XFSTESTS_WASM_BACKENDS` defaults to the required `v8 wasmtime` executor +matrix. `XFSTESTS_BACKENDS` defaults to the `chunked_local,memory,chunked_s3` supported writable-engine matrix and rejects unknown or duplicate entries. The dormant `object_s3` harness paths are retained for focused return-to-service validation, but the plugin is intentionally not @@ -44,6 +46,10 @@ external cancellation. `XFSTESTS_CONCURRENCY` bounds simultaneous VMs; per-test watchdog is 3,600 seconds, calibrated from the pinned helper workloads with byte-for-byte verification enabled; a timeout remains a strict harness failure. +Nightly CI also runs the ignored 1,000-file `dirstress` process matrix once per +WASM engine on `chunked_local`; it covers the one-process, five-process shared, +and five-process/five-directory layouts without multiplying the same workload +across every storage-plugin leg. ## Correctness constraints diff --git a/toolchain/Makefile b/toolchain/Makefile index 084ac9938d..e656156c2c 100644 --- a/toolchain/Makefile +++ b/toolchain/Makefile @@ -1,9 +1,14 @@ WASM_TARGET := wasm32-wasip1 RELEASE_DIR := target/$(WASM_TARGET)/release WASI_C_SYSROOT := $(CURDIR)/c/sysroot +WASI_C_THREADED_SYSROOT := $(CURDIR)/c/sysroot-threads WASI_C_CC := $(CURDIR)/c/vendor/wasi-sdk/bin/clang WASI_C_AR := $(CURDIR)/c/vendor/wasi-sdk/bin/llvm-ar WASI_C_LIBDIR := $(WASI_C_SYSROOT)/lib/wasm32-wasi +BINARYEN_VERSION := 128 +WASM_OPT := $(CURDIR)/.cache/binaryen-version_$(BINARYEN_VERSION)/bin/wasm-opt +export WASM_OPT +export PATH := $(dir $(WASM_OPT)):$(PATH) # Standalone binary output directory (configurable) COMMANDS_DIR ?= $(RELEASE_DIR)/commands @@ -20,7 +25,8 @@ PATCH_SCRIPT := scripts/patch-std.sh # whenever the external sysroot is rebuilt; Cargo does not otherwise track # archives passed as linker arguments. WASI_C_LIBC_DIGEST = $(shell if command -v sha256sum >/dev/null 2>&1; then sha256sum "$(WASI_C_LIBDIR)/libc.a"; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$(WASI_C_LIBDIR)/libc.a"; else cksum "$(WASI_C_LIBDIR)/libc.a"; fi 2>/dev/null | cut -d ' ' -f1 | cut -c1-16) -WASI_RUSTFLAGS = -C link-self-contained=no -C link-arg=$(WASI_C_LIBDIR)/crt1-command.o -C link-arg=$(WASI_C_LIBDIR)/libc.a -C link-arg=$(WASI_C_LIBDIR)/libwasi-emulated-pthread.a -C link-arg=-L$(WASI_C_LIBDIR) -C metadata=agentos-libc-$(WASI_C_LIBC_DIGEST) --cfg tokio_unstable +WASI_RUST_STD_PATCH_DIGEST = $(shell cat $(PATCHES) 2>/dev/null | if command -v sha256sum >/dev/null 2>&1; then sha256sum; elif command -v shasum >/dev/null 2>&1; then shasum -a 256; else cksum; fi | cut -d ' ' -f1 | cut -c1-16) +WASI_RUSTFLAGS = -C link-self-contained=no -C link-arg=$(WASI_C_LIBDIR)/crt1-command.o -C link-arg=$(WASI_C_LIBDIR)/libc.a -C link-arg=$(WASI_C_LIBDIR)/libwasi-emulated-pthread.a -C link-arg=-L$(WASI_C_LIBDIR) -C metadata=agentos-libc-$(WASI_C_LIBC_DIGEST)-std-$(WASI_RUST_STD_PATCH_DIGEST) --cfg tokio_unstable # Discover command binary names from colocated package crates plus toolchain # helper crates. Keep known slow/heavy commands out of the default software @@ -60,7 +66,7 @@ STUB_COMMANDS := \ pinky who users uptime \ stty sync tty -.PHONY: all commands pr-commands wasm wasm-opt-check host test clean patch-std patch-check vendor patch-vendor vendor-patch-check size-report install c-wasm codex fd-mapping-contract-wasm fd-mapping-contract-native +.PHONY: all commands pr-commands wasm wasm-opt-check host test clean patch-std patch-check vendor patch-vendor vendor-patch-check size-report install c-wasm codex fd-mapping-contract-wasm fd-mapping-contract-native sysroot-threads all: wasm @@ -121,6 +127,11 @@ c/vendor/wasi-sdk/bin/clang: c/sysroot/lib/wasm32-wasi/libc.a: $(MAKE) -C c sysroot +c/sysroot-threads/lib/wasm32-wasi-threads/libc.a: + $(MAKE) -C c sysroot-threads + +sysroot-threads: c/sysroot-threads/lib/wasm32-wasi-threads/libc.a + # Strict variant for environments that require the codex artifact. With CODEX_REPO # unset it builds reproducibly from the pin; with CODEX_REPO set it fails hard if # that checkout lacks the fork build script. @@ -190,14 +201,12 @@ vendor-patch-check: vendor echo "Warning: scripts/patch-vendor.sh not found or not executable"; \ fi -# Ensure wasm-opt is installed (needed for post-build optimization) +# Install the pinned Binaryen build used for optimization and exception +# finalization. The previous `cargo install wasm-opt` fallback embedded +# Binaryen 116, which cannot translate LLVM's legacy exception encoding to the +# finalized exnref proposal accepted by Wasmtime. wasm-opt-check: - @if ! command -v wasm-opt >/dev/null 2>&1; then \ - echo "wasm-opt not found — installing via cargo..."; \ - cargo install wasm-opt; \ - else \ - echo "wasm-opt found: $$(wasm-opt --version)"; \ - fi + @./scripts/ensure-wasm-opt.sh "$(WASM_OPT)" # Build all standalone command binaries, optimize, strip .wasm extension, create symlinks wasm: c/sysroot/lib/wasm32-wasi/libc.a vendor patch-vendor patch-std wasm-opt-check diff --git a/toolchain/c/Makefile b/toolchain/c/Makefile index ed2125b51f..ee907ec032 100644 --- a/toolchain/c/Makefile +++ b/toolchain/c/Makefile @@ -51,9 +51,14 @@ c_source = $(firstword $(foreach dir,$(C_SOURCE_DIRS),$(wildcard $(dir)/$(1).c)) # Toolchain CC := $(WASI_SDK_DIR)/bin/clang NATIVE_CC := $(shell command -v cc 2>/dev/null || command -v gcc 2>/dev/null || command -v clang 2>/dev/null) +BINARYEN_VERSION := 128 +WASM_OPT ?= $(abspath ../.cache/binaryen-version_$(BINARYEN_VERSION)/bin/wasm-opt) +export WASM_OPT +export PATH := $(dir $(WASM_OPT)):$(PATH) # Sysroot: use patched sysroot if built, otherwise use wasi-sdk's vanilla sysroot PATCHED_SYSROOT := sysroot +THREADED_SYSROOT := sysroot-threads ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) SYSROOT := $(WASI_SDK_DIR)/share/wasi-sysroot else @@ -82,7 +87,10 @@ PATCHED_PROGRAMS := http_get_test isatty_test getpid_test getppid_test getppid_v # Discover all package command and test-program C source files. ALL_SOURCES := $(foreach dir,$(C_SOURCE_DIRS),$(wildcard $(dir)/*.c)) -SKIPPED_BULK_PROGRAMS := +# Threaded fixtures require the dedicated wasm32-wasip1-threads sysroot and +# linker contract below. Never let the generic single-thread discovery rule +# compile them against the ordinary wasm32-wasip1 sysroot. +SKIPPED_BULK_PROGRAMS := pthread_benchmark pthread_conformance # Exclude patched-sysroot programs when only vanilla sysroot is available ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) @@ -112,9 +120,9 @@ NATIVE_OUTPUTS := $(addprefix $(NATIVE_DIR)/,$(NATIVE_PROG_NAMES)) $(NATIVE_DIR) .SECONDEXPANSION: -.PHONY: all wasi-sdk programs sysroot install native clean wasm-opt-check os-test os-test-native +.PHONY: all wasi-sdk programs sysroot sysroot-threads pthread-benchmark-wasm pthread-conformance-wasm install native conformance-artifacts clean wasm-opt-check os-test os-test-native -HAS_WASM_OPT := $(shell command -v wasm-opt >/dev/null 2>&1 && echo 1 || echo 0) +HAS_WASM_OPT = $(shell test -x "$(WASM_OPT)" && echo 1 || echo 0) all: programs @@ -485,6 +493,27 @@ $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a: $(WASI_SDK_DIR)/bin/clang ../scripts/ sysroot: $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a +$(THREADED_SYSROOT)/lib/wasm32-wasi-threads/libc.a: $(WASI_SDK_DIR)/bin/clang ../scripts/patch-wasi-libc.sh scripts/build-llvm-runtimes.sh $(WASI_LIBC_PATCHES) $(WASI_LIBC_OVERRIDES) $(LLVM_RUNTIME_PATCHES) + ../scripts/patch-wasi-libc.sh --threads + +sysroot-threads: $(THREADED_SYSROOT)/lib/wasm32-wasi-threads/libc.a + +build/pthread_conformance.wasm: ../test-programs/pthread_conformance.c $(THREADED_SYSROOT)/lib/wasm32-wasi-threads/libc.a + @mkdir -p $(dir $@) + $(CC) --target=wasm32-wasip1-threads --sysroot=$(THREADED_SYSROOT) -O2 -pthread \ + -Wl,--import-memory -Wl,--export-memory -Wl,--max-memory=134217728 \ + -Wl,--export=wasi_thread_start -o $@ $< + +pthread-conformance-wasm: build/pthread_conformance.wasm + +build/pthread_benchmark.wasm: ../test-programs/pthread_benchmark.c $(THREADED_SYSROOT)/lib/wasm32-wasi-threads/libc.a + @mkdir -p $(dir $@) + $(CC) --target=wasm32-wasip1-threads --sysroot=$(THREADED_SYSROOT) -O2 -pthread \ + -Wl,--import-memory -Wl,--export-memory -Wl,--max-memory=134217728 \ + -Wl,--export=wasi_thread_start -o $@ $< + +pthread-benchmark-wasm: build/pthread_benchmark.wasm + # Patched fixtures must relink whenever the owned libc changes. Without this # edge, make can leave an older host-import ABI embedded in an otherwise # up-to-date test binary after a sysroot rebuild. @@ -496,11 +525,7 @@ $(BUILD_DIR)/select_edge: WASM_CFLAGS += -DFD_SETSIZE=8192 # --- wasm-opt check --- wasm-opt-check: - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - echo "wasm-opt found: $$(wasm-opt --version | head -1)"; \ - else \ - echo "Warning: wasm-opt (binaryen) is not installed; skipping WASM optimization."; \ - fi + @../scripts/ensure-wasm-opt.sh "$(WASM_OPT)" # --- Compile all C programs to WASM --- @@ -894,7 +919,7 @@ $(BUILD_DIR)/grep: scripts/build-grep-upstream.sh $(PATCHED_SYSROOT)/lib/wasm32- --output "$(abspath $@)" # duckdb: upstream DuckDB CLI built from source with our patched WASI/POSIX sysroot -$(BUILD_DIR)/duckdb: libs/duckdb/CMakeLists.txt $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang $(WASI_SDK_DIR)/bin/clang++ cmake/FindThreads.cmake scripts/build-duckdb.sh include/fcntl.h include/ifaddrs.h include/net/if.h include/sys/ioctl.h +$(BUILD_DIR)/duckdb: libs/duckdb/CMakeLists.txt $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang $(WASI_SDK_DIR)/bin/clang++ cmake/FindThreads.cmake scripts/build-duckdb.sh include/fcntl.h include/ifaddrs.h include/net/if.h include/sys/ioctl.h wasm-opt-check @mkdir -p $(BUILD_DIR) DUCKDB_SRC_DIR="$(abspath $(LIBS_DIR)/duckdb)" \ DUCKDB_BUILD_DIR="$(abspath $(BUILD_DIR)/duckdb-cmake)" \ @@ -916,6 +941,23 @@ native: $(NATIVE_OUTPUTS) @echo "Output: $(NATIVE_DIR)/" @echo "=== Build complete ===" +# Stage only the runnable C parity corpus. The build directory also contains +# downloaded libraries and intermediate objects that must not enter CI +# artifacts or shipped command packages. +CONFORMANCE_STAGE := $(BUILD_DIR)/conformance +conformance-artifacts: programs native + rm -rf "$(CONFORMANCE_STAGE)" + mkdir -p "$(CONFORMANCE_STAGE)/native" + @set -e; \ + for name in $(PROGRAM_REPORT_NAMES); do \ + test -f "$(BUILD_DIR)/$$name" || { echo "missing WASM conformance fixture: $$name" >&2; exit 1; }; \ + cp -p "$(BUILD_DIR)/$$name" "$(CONFORMANCE_STAGE)/$$name"; \ + done; \ + for name in $(NATIVE_REPORT_NAMES); do \ + test -f "$(NATIVE_DIR)/$$name" || { echo "missing native conformance fixture: $$name" >&2; exit 1; }; \ + cp -p "$(NATIVE_DIR)/$$name" "$(CONFORMANCE_STAGE)/native/$$name"; \ + done + $(NATIVE_DIR)/%: $$(call c_source,$$*) @mkdir -p $(NATIVE_DIR) $(NATIVE_CC) $(NATIVE_CFLAGS) -o $@ $< diff --git a/toolchain/c/scripts/build-duckdb.sh b/toolchain/c/scripts/build-duckdb.sh index 0b1289dd77..82896067b6 100644 --- a/toolchain/c/scripts/build-duckdb.sh +++ b/toolchain/c/scripts/build-duckdb.sh @@ -106,4 +106,22 @@ cmake \ -DOVERRIDE_GIT_DESCRIBE="$DUCKDB_GIT_DESCRIBE" cmake --build "$DUCKDB_BUILD_DIR" --target shell -j"$(nproc 2>/dev/null || echo 4)" -cp "$DUCKDB_BUILD_DIR/duckdb" "$DUCKDB_OUTPUT" + +# LLVM 19 emits the Phase-3 exception encoding for -fwasm-exceptions, while +# Wasmtime supports the finalized exnref form. Binaryen is already a required +# AgentOS toolchain dependency; translate at the sysroot/toolchain boundary so +# both V8 and Wasmtime execute one canonical artifact. +WASM_OPT="${WASM_OPT:-wasm-opt}" +if ! command -v "$WASM_OPT" >/dev/null 2>&1; then + echo "pinned wasm-opt is required to finalize DuckDB exception instructions" >&2 + exit 1 +fi +if ! "$WASM_OPT" --version | grep -Fq "version 128"; then + echo "Binaryen 128 is required to finalize DuckDB exception instructions" >&2 + exit 1 +fi +"$WASM_OPT" \ + --enable-exception-handling \ + --translate-to-exnref \ + "$DUCKDB_BUILD_DIR/duckdb" \ + -o "$DUCKDB_OUTPUT" diff --git a/toolchain/conformance/c-parity.test.ts b/toolchain/conformance/c-parity.test.ts index 25c657407f..83fd14ce8c 100644 --- a/toolchain/conformance/c-parity.test.ts +++ b/toolchain/conformance/c-parity.test.ts @@ -318,7 +318,7 @@ class SimpleVFS { } } -describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => { +describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 120_000 }, () => { let kernel: Kernel; let vfs: SimpleVFS; @@ -341,6 +341,13 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => beforeEach(async () => { vfs = new SimpleVFS(); + await vfs.createDir('/tmp'); + await vfs.createDir('/workspace'); + await vfs.writeFile( + '/workspace/exec_variants', + await fsReadFile(join(C_BUILD_DIR, 'exec_variants')), + ); + await vfs.chmod('/workspace/exec_variants', 0o755); kernel = await mountParityKernel(); }); @@ -354,10 +361,7 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('hello'); const wasm = await kernel.exec('hello'); - expect( - wasm.exitCode, - `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, - ).toBe(native.exitCode); + expect(wasm.exitCode).toBe(native.exitCode); expect(wasm.stdout).toBe(native.stdout); }); @@ -375,7 +379,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('env', [], { env }); const wasm = await kernel.exec('env', { env }); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); // Shell may inject extra env vars; compare only the TEST_PARITY_ vars expect(extractEnvPrefix(wasm.stdout, 'TEST_PARITY_')).toBe( extractEnvPrefix(native.stdout, 'TEST_PARITY_'), @@ -393,9 +400,12 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => it('cat: stdin passthrough matches', async () => { const input = 'hello world\nfoo bar\n'; const native = await runNative('cat', [], { input }); - const wasm = await kernel.exec('cat', { stdin: input }); + const wasm = await kernel.exec('c-cat', { stdin: input }); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.stdout).toBe(native.stdout); }); @@ -404,7 +414,7 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => it('wc: word/line/byte counts match', async () => { const input = 'hello world\nfoo bar baz\n'; const native = await runNative('wc', [], { input }); - const wasm = await kernel.exec('wc', { stdin: input }); + const wasm = await kernel.exec('c-wc', { stdin: input }); expect(wasm.exitCode).toBe(native.exitCode); const counts = (output: string) => output.trim().split(/\s+/).map(Number); @@ -469,7 +479,7 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => it('sort: sorted output matches', async () => { const input = 'banana\napple\ncherry\ndate\n'; const native = await runNative('sort', [], { input }); - const wasm = await kernel.exec('sort', { stdin: input }); + const wasm = await kernel.exec('c-sort', { stdin: input }); expect(wasm.exitCode).toBe(native.exitCode); expect(wasm.stdout).toBe(native.stdout); @@ -1245,7 +1255,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('waitpid_edge'); const wasm = await kernel.exec('waitpid_edge'); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); // Test 1: 3 concurrent children with correct exit codes @@ -1302,7 +1315,10 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => const native = await runNative('itimer_contract'); const wasm = await kernel.exec('itimer_contract'); - expect(wasm.exitCode).toBe(native.exitCode); + expect( + wasm.exitCode, + `native stdout:\n${native.stdout}\nnative stderr:\n${native.stderr}\nWASM stdout:\n${wasm.stdout}\nWASM stderr:\n${wasm.stderr}`, + ).toBe(native.exitCode); expect(wasm.exitCode).toBe(0); expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); expect(wasm.stdout).toBe(native.stdout); @@ -1551,7 +1567,7 @@ describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => ] as const) { itIf(!tier3Skip, `exec_variants ${mode}: matches Linux replacement behavior`, async () => { const native = await runNative('exec_variants', [mode]); - const wasm = await kernel.exec(`exec_variants ${mode}`); + const wasm = await kernel.exec(`/workspace/exec_variants ${mode}`); expect( wasm.exitCode, diff --git a/toolchain/crates/libs/wasi-http/src/lib.rs b/toolchain/crates/libs/wasi-http/src/lib.rs index 4e67ba826b..9dbc908971 100644 --- a/toolchain/crates/libs/wasi-http/src/lib.rs +++ b/toolchain/crates/libs/wasi-http/src/lib.rs @@ -15,7 +15,9 @@ use std::fmt; use std::io; -// AgentOS's owned wasi-libc p1 ABI values for AF_INET and SOCK_STREAM. +// AgentOS private host_net ABI values. These are wasi-libc Preview 1 values, +// not Linux's AF_INET=2 / SOCK_STREAM=1 values; libc performs that translation +// for POSIX callers, while this crate calls host_net directly through wasi-ext. const AF_INET: u32 = 1; const SOCK_STREAM: u32 = 6; const MAX_URL_BYTES: usize = 8 * 1024; diff --git a/toolchain/crates/wasi-ext/src/lib.rs b/toolchain/crates/wasi-ext/src/lib.rs index 32193931d9..ce2ff2bf74 100644 --- a/toolchain/crates/wasi-ext/src/lib.rs +++ b/toolchain/crates/wasi-ext/src/lib.rs @@ -868,8 +868,8 @@ pub fn sigaction_set( extern "C" { /// Create a socket. /// - /// `domain` is the address family (e.g. AF_INET=2). - /// `sock_type` is the socket type (e.g. SOCK_STREAM=1). + /// `domain` is the private Preview 1 address family (e.g. AF_INET=1). + /// `sock_type` is the private Preview 1 socket type (e.g. SOCK_STREAM=6). /// `protocol` is the protocol (0 for default). /// On success, the socket FD is written to `ret_fd`. /// Returns errno. diff --git a/toolchain/scripts/ensure-wasm-opt.sh b/toolchain/scripts/ensure-wasm-opt.sh new file mode 100755 index 0000000000..7369c9fc6c --- /dev/null +++ b/toolchain/scripts/ensure-wasm-opt.sh @@ -0,0 +1,63 @@ +#!/bin/sh +set -eu + +BINARYEN_VERSION=128 +DESTINATION=${1:?usage: ensure-wasm-opt.sh } + +if [ -x "$DESTINATION" ] \ + && "$DESTINATION" --version 2>/dev/null | grep -Fq "version $BINARYEN_VERSION" \ + && "$DESTINATION" --help 2>/dev/null | grep -Fq -- "--translate-to-exnref"; then + echo "wasm-opt found: $($DESTINATION --version)" + exit 0 +fi + +case "$(uname -s):$(uname -m)" in + Linux:x86_64) + PLATFORM=x86_64-linux + SHA256=4ce79586d1c4762502eebe9a1db071fa5e446ef8897f2f766eb1cce5ec6dee9e + ;; + Linux:aarch64 | Linux:arm64) + PLATFORM=aarch64-linux + SHA256=bafe0468976d923f09052f8ec6a6a0a9d942ee7f02ac113c85a80afea7ba3679 + ;; + Darwin:x86_64) + PLATFORM=x86_64-macos + SHA256=0b4bbd58c46b73a3de1fd485579a56cd413dd395414306d9f33df407fde58b9b + ;; + Darwin:arm64 | Darwin:aarch64) + PLATFORM=arm64-macos + SHA256=0ef730ecedf2dac894812185fc78f5940ab980cdde79427e49fa87331d24422f + ;; + *) + echo "unsupported Binaryen host platform: $(uname -s) $(uname -m)" >&2 + exit 1 + ;; +esac + +ARCHIVE="binaryen-version_${BINARYEN_VERSION}-${PLATFORM}.tar.gz" +URL="https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/${ARCHIVE}" +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT HUP INT TERM + +echo "downloading pinned Binaryen $BINARYEN_VERSION for $PLATFORM" >&2 +curl -fL "$URL" -o "$TEMP_DIR/$ARCHIVE" +if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "$SHA256" "$TEMP_DIR/$ARCHIVE" | sha256sum -c - +elif command -v shasum >/dev/null 2>&1; then + printf '%s %s\n' "$SHA256" "$TEMP_DIR/$ARCHIVE" | shasum -a 256 -c - +else + echo "sha256sum or shasum is required to verify Binaryen" >&2 + exit 1 +fi + +tar -xzf "$TEMP_DIR/$ARCHIVE" -C "$TEMP_DIR" +SOURCE="$TEMP_DIR/binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt" +test -x "$SOURCE" +mkdir -p "$(dirname "$DESTINATION")" +STAGED_DESTINATION="$DESTINATION.tmp.$$" +cp "$SOURCE" "$STAGED_DESTINATION" +chmod 0755 "$STAGED_DESTINATION" +mv "$STAGED_DESTINATION" "$DESTINATION" + +"$DESTINATION" --version | grep -F "version $BINARYEN_VERSION" +"$DESTINATION" --help | grep -Fq -- "--translate-to-exnref" diff --git a/toolchain/scripts/patch-wasi-libc.sh b/toolchain/scripts/patch-wasi-libc.sh index dc04d9dc59..aa0c298648 100755 --- a/toolchain/scripts/patch-wasi-libc.sh +++ b/toolchain/scripts/patch-wasi-libc.sh @@ -6,11 +6,12 @@ # host_user WASM imports, and builds the patched sysroot. # # Usage: -# ./scripts/patch-wasi-libc.sh [--check] [--reverse] +# ./scripts/patch-wasi-libc.sh [--check] [--reverse] [--threads] # # Options: # --check Dry-run: verify patches apply cleanly without building # --reverse Reverse (unapply) previously applied patches +# --threads Build the real POSIX pthread sysroot in c/sysroot-threads set -euo pipefail @@ -30,6 +31,9 @@ WASI_LIBC_DIR="$VENDOR_DIR/wasi-libc" LLVM_PROJECT_DIR="$VENDOR_DIR/llvm-project" WASI_SDK_DIR="$VENDOR_DIR/wasi-sdk" SYSROOT_DIR="$WASMCORE_DIR/c/sysroot" +THREAD_MODEL="single" +TARGET_TRIPLE="wasm32-wasi" +WASIP1_TRIPLE="wasm32-wasip1" WASI_LIBC_SRC_DIR="$WASI_LIBC_DIR" WORKTREE_DIR="" @@ -43,9 +47,15 @@ for arg in "$@"; do --reverse) MODE="reverse" ;; + --threads) + THREAD_MODEL="posix" + TARGET_TRIPLE="wasm32-wasi-threads" + WASIP1_TRIPLE="wasm32-wasip1-threads" + SYSROOT_DIR="$WASMCORE_DIR/c/sysroot-threads" + ;; *) echo "Unknown argument: $arg" - echo "Usage: $0 [--check] [--reverse]" + echo "Usage: $0 [--check] [--reverse] [--threads]" exit 1 ;; esac @@ -244,13 +254,14 @@ make -C "$WASI_LIBC_SRC_DIR" \ AR="$WASI_AR" \ NM="$WASI_NM" \ SYSROOT="$SYSROOT_DIR" \ + THREAD_MODEL="$THREAD_MODEL" \ libc \ -j"$(nproc 2>/dev/null || echo 4)" # Install CRT startup files (crt1.o etc.) from the vanilla wasi-sdk sysroot. # CRT objects are standard startup routines that don't need our patches. -SYSROOT_LIB="$SYSROOT_DIR/lib/wasm32-wasi" -VANILLA_LIB="$WASI_SDK_DIR/share/wasi-sysroot/lib/wasm32-wasi" +SYSROOT_LIB="$SYSROOT_DIR/lib/$TARGET_TRIPLE" +VANILLA_LIB="$WASI_SDK_DIR/share/wasi-sysroot/lib/$TARGET_TRIPLE" for crt in "$VANILLA_LIB"/crt*.o; do [ -f "$crt" ] && cp "$crt" "$SYSROOT_LIB/" done @@ -260,9 +271,9 @@ done # thread-capable headers/libs from wasm32-wasi-threads because libc++'s mutex # support expects those definitions even when we satisfy pthread calls through # wasi-emulated-pthread. -VANILLA_INCLUDE="$WASI_SDK_DIR/share/wasi-sysroot/include/wasm32-wasi" +VANILLA_INCLUDE="$WASI_SDK_DIR/share/wasi-sysroot/include/$TARGET_TRIPLE" THREADS_INCLUDE="$WASI_SDK_DIR/share/wasi-sysroot/include/wasm32-wasi-threads" -SYSROOT_INCLUDE="$SYSROOT_DIR/include/wasm32-wasi" +SYSROOT_INCLUDE="$SYSROOT_DIR/include/$TARGET_TRIPLE" mkdir -p "$SYSROOT_INCLUDE/c++/v1" if [ -d "$VANILLA_INCLUDE/c++/v1" ]; then cp -R "$VANILLA_INCLUDE/c++/v1/." "$SYSROOT_INCLUDE/c++/v1/" @@ -286,13 +297,15 @@ done LLVM_RUNTIME_BUILD_SCRIPT="$WASMCORE_DIR/c/scripts/build-llvm-runtimes.sh" LLVM_RUNTIME_BUILD_DIR="$WASMCORE_DIR/c/build/llvm-runtimes" LLVM_RUNTIME_INSTALL_DIR="$WASMCORE_DIR/c/build/llvm-runtimes-install" -echo "Rebuilding libc++/libc++abi/libunwind with -fwasm-exceptions..." -LLVM_PROJECT_SRC_DIR="$LLVM_PROJECT_DIR" \ -LLVM_RUNTIME_BUILD_DIR="$LLVM_RUNTIME_BUILD_DIR" \ -LLVM_RUNTIME_INSTALL_DIR="$LLVM_RUNTIME_INSTALL_DIR" \ -WASI_SDK_DIR="$WASI_SDK_DIR" \ -SYSROOT_DIR="$SYSROOT_DIR" \ -bash "$LLVM_RUNTIME_BUILD_SCRIPT" +if [ "$THREAD_MODEL" = "single" ]; then + echo "Rebuilding libc++/libc++abi/libunwind with -fwasm-exceptions..." + LLVM_PROJECT_SRC_DIR="$LLVM_PROJECT_DIR" \ + LLVM_RUNTIME_BUILD_DIR="$LLVM_RUNTIME_BUILD_DIR" \ + LLVM_RUNTIME_INSTALL_DIR="$LLVM_RUNTIME_INSTALL_DIR" \ + WASI_SDK_DIR="$WASI_SDK_DIR" \ + SYSROOT_DIR="$SYSROOT_DIR" \ + bash "$LLVM_RUNTIME_BUILD_SCRIPT" +fi # Create empty dummy libraries (libm, librt, libpthread, etc.) for lib in m rt pthread crypt util xnet resolv; do @@ -303,8 +316,8 @@ echo "" echo "=== Sysroot build complete ===" # Verify the build output -if [ -f "$SYSROOT_DIR/lib/wasm32-wasi/libc.a" ]; then - echo "OK: $SYSROOT_DIR/lib/wasm32-wasi/libc.a exists" +if [ -f "$SYSROOT_LIB/libc.a" ]; then + echo "OK: $SYSROOT_LIB/libc.a exists" else echo "ERROR: libc.a not found in sysroot — build may have failed" exit 1 @@ -323,9 +336,9 @@ echo "Removed conflicting sigaction.o/signal.o from libc.a" # wasi-libc builds under wasm32-wasi, but clang --target=wasm32-wasip1 expects # wasm32-wasip1 subdirectories. Create symlinks so both targets work. for subdir in include lib; do - if [ -d "$SYSROOT_DIR/$subdir/wasm32-wasi" ] && [ ! -e "$SYSROOT_DIR/$subdir/wasm32-wasip1" ]; then - ln -s wasm32-wasi "$SYSROOT_DIR/$subdir/wasm32-wasip1" - echo "Symlink: $subdir/wasm32-wasip1 -> wasm32-wasi" + if [ -d "$SYSROOT_DIR/$subdir/$TARGET_TRIPLE" ] && [ ! -e "$SYSROOT_DIR/$subdir/$WASIP1_TRIPLE" ]; then + ln -s "$TARGET_TRIPLE" "$SYSROOT_DIR/$subdir/$WASIP1_TRIPLE" + echo "Symlink: $subdir/$WASIP1_TRIPLE -> $TARGET_TRIPLE" fi done @@ -340,7 +353,15 @@ done # Overrides are compiled and added to libc.a so ALL WASM programs get the fixes. OVERRIDES_DIR="$WASMCORE_DIR/std-patches/wasi-libc-overrides" OVERRIDE_INCLUDE_DIR="$WASMCORE_DIR/c/include" -OVERRIDE_CFLAGS="--target=wasm32-wasip1 --sysroot=$SYSROOT_DIR -O2 -D_GNU_SOURCE -I$OVERRIDE_INCLUDE_DIR" +OVERRIDE_CFLAGS="--target=$WASIP1_TRIPLE --sysroot=$SYSROOT_DIR -O2 -D_GNU_SOURCE -I$OVERRIDE_INCLUDE_DIR" +if [ "$THREAD_MODEL" = "posix" ]; then + # Clang's wasm32-wasip1-threads triple selects shared memory at link time, + # but it does not enable the atomics/bulk-memory code-generation features + # for standalone override objects. Mixing such an object into threaded + # libc produces an archive with a `-shared-mem` member and fails as soon as + # a threaded program pulls that override (for example through fcntl/stdio). + OVERRIDE_CFLAGS="$OVERRIDE_CFLAGS -pthread -matomics -mbulk-memory" +fi # Extra flags for overrides that need musl internal headers (struct __pthread, etc.) MUSL_INTERNAL_DIR="$WASI_LIBC_SRC_DIR/libc-top-half/musl/src/internal" @@ -364,7 +385,11 @@ if [ -d "$OVERRIDES_DIR" ] && ls "$OVERRIDES_DIR"/*.c >/dev/null 2>&1; then # are in a single mutex.o — remove it so our override replaces them all. # pthread_key: create, delete, and tsd_run_dtors are in a single .o — remove # via __pthread_key_create to replace the whole TSD compilation unit. - for sym in fcntl close strfmon open_wmemstream swprintf inet_ntop __pthread_mutex_lock pthread_attr_setguardsize pthread_mutexattr_setrobust __pthread_key_create fmtmsg; do + REPLACED_SYMBOLS="fcntl close strfmon open_wmemstream swprintf inet_ntop fmtmsg" + if [ "$THREAD_MODEL" = "single" ]; then + REPLACED_SYMBOLS="$REPLACED_SYMBOLS __pthread_mutex_lock pthread_attr_setguardsize pthread_mutexattr_setrobust __pthread_key_create" + fi + for sym in $REPLACED_SYMBOLS; do OBJ_LINE=$("$WASI_NM" --print-file-name "$SYSROOT_LIB/libc.a" 2>/dev/null | { grep " [TW] ${sym}\$" || true; } | head -1) if [ -n "$OBJ_LINE" ]; then OBJ=$(echo "$OBJ_LINE" | extract_obj) @@ -378,6 +403,10 @@ if [ -d "$OVERRIDES_DIR" ] && ls "$OVERRIDES_DIR"/*.c >/dev/null 2>&1; then # Compile each override and add to libc.a for src in "$OVERRIDES_DIR"/*.c; do name="$(basename "${src%.c}")" + if [ "$THREAD_MODEL" = "posix" ] && [[ "$name" == pthread_* ]]; then + echo " Keeping threaded libc implementation: $name" + continue + fi EXTRA_FLAGS="" # pthread_key needs musl internal headers for struct __pthread case "$name" in diff --git a/toolchain/std-patches/0012-wasi-hidden-preopen-path-alias.patch b/toolchain/std-patches/0012-wasi-hidden-preopen-path-alias.patch index c13b268e79..f6c2c07b8d 100644 --- a/toolchain/std-patches/0012-wasi-hidden-preopen-path-alias.patch +++ b/toolchain/std-patches/0012-wasi-hidden-preopen-path-alias.patch @@ -1,10 +1,9 @@ -Keep Rust std's pathname metadata lookup outside the guest fd namespace. +Resolve Rust std's supplemental pathname metadata from the process cwd. -Rust std supplements WASI filestat with AgentOS' host_fs.path_mode import. -That lookup is part of ordinary pathname resolution, but it passed raw fd 3, -allowing close(3)/dup2(..., 3) to redirect subsequent stat metadata queries. -Use the same private preopen tag as the patched libc; explicit descriptor APIs -continue to pass their exact guest descriptor. +Rust std supplements WASI filestat with AgentOS' host_fs.path_mode import. That +extension receives the application's original absolute or cwd-relative path, +not wasi-libc's capability-relative path. Use AgentOS' cwd sentinel so fd 3 +cannot redirect the lookup and relative paths keep normal POSIX semantics. --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -13,7 +12,7 @@ continue to pass their exact guest descriptor. } +#[cfg(target_os = "wasi")] -+const AGENTOS_HIDDEN_PREOPEN_FD: u32 = 0x40000003; ++const AGENTOS_CWD_FD: u32 = u32::MAX; #[cfg(target_os = "wasi")] use crate::os::wasi::prelude::*; use crate::path::{Path, PathBuf}; @@ -23,7 +22,7 @@ continue to pass their exact guest descriptor. let bytes = p.to_bytes(); - let mode = unsafe { host_fs::path_mode(3, bytes.as_ptr(), bytes.len() as u32, 1) }; + let mode = unsafe { -+ host_fs::path_mode(AGENTOS_HIDDEN_PREOPEN_FD, bytes.as_ptr(), bytes.len() as u32, 1) ++ host_fs::path_mode(AGENTOS_CWD_FD, bytes.as_ptr(), bytes.len() as u32, 1) + }; if mode != 0 { stat.st_mode = mode as libc::mode_t; } } @@ -34,7 +33,7 @@ continue to pass their exact guest descriptor. let bytes = p.to_bytes(); - let mode = unsafe { host_fs::path_mode(3, bytes.as_ptr(), bytes.len() as u32, 0) }; + let mode = unsafe { -+ host_fs::path_mode(AGENTOS_HIDDEN_PREOPEN_FD, bytes.as_ptr(), bytes.len() as u32, 0) ++ host_fs::path_mode(AGENTOS_CWD_FD, bytes.as_ptr(), bytes.len() as u32, 0) + }; if mode != 0 { stat.st_mode = mode as libc::mode_t; } } diff --git a/toolchain/std-patches/crates/platform-info/0001-agentos-system-identity.patch b/toolchain/std-patches/crates/platform-info/0001-agentos-system-identity.patch new file mode 100644 index 0000000000..5944b85855 --- /dev/null +++ b/toolchain/std-patches/crates/platform-info/0001-agentos-system-identity.patch @@ -0,0 +1,21 @@ +Use AgentOS' kernel-owned system identity on WASI. + +The guest is a Linux-in-WASM environment, so target_os=wasi is an execution +detail rather than the identity applications should observe. Route uname-style +queries through the canonical host_system import used by both executors. + +--- a/src/lib_impl.rs ++++ b/src/lib_impl.rs +@@ -62,9 +62,12 @@ const HOST_OS_NAME: &str = if cfg!(all( + #[cfg(windows)] + #[path = "platform/windows.rs"] + mod target; +-#[cfg(not(any(unix, windows)))] ++#[cfg(all(not(any(unix, windows)), not(target_os = "wasi")))] + #[path = "platform/unknown.rs"] + mod target; ++#[cfg(target_os = "wasi")] ++#[path = "platform/wasi.rs"] ++mod target; + + pub use target::*; diff --git a/toolchain/std-patches/crates/platform-info/copy.manifest b/toolchain/std-patches/crates/platform-info/copy.manifest new file mode 100644 index 0000000000..9b8e4de6b9 --- /dev/null +++ b/toolchain/std-patches/crates/platform-info/copy.manifest @@ -0,0 +1 @@ +wasi.rs src/platform/wasi.rs diff --git a/toolchain/std-patches/crates/platform-info/wasi.rs b/toolchain/std-patches/crates/platform-info/wasi.rs new file mode 100644 index 0000000000..24c5710d0c --- /dev/null +++ b/toolchain/std-patches/crates/platform-info/wasi.rs @@ -0,0 +1,79 @@ +// AgentOS exposes a Linux system identity through a runtime-neutral host ABI. + +#![warn(unused_results)] + +use std::ffi::{OsStr, OsString}; +use std::io; + +use crate::{PlatformInfoAPI, PlatformInfoError, UNameAPI}; + +const IDENTITY_BUFFER_BYTES: usize = 256; + +#[link(wasm_import_module = "host_system")] +unsafe extern "C" { + fn get_identity(field: u32, buffer: *mut u8, capacity: u32) -> u32; +} + +fn identity(field: u32) -> Result { + let mut buffer = [0u8; IDENTITY_BUFFER_BYTES]; + let errno = unsafe { get_identity(field, buffer.as_mut_ptr(), buffer.len() as u32) }; + if errno != 0 { + return Err(io::Error::from_raw_os_error(errno as i32).into()); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "unterminated system identity"))?; + let value = std::str::from_utf8(&buffer[..length]) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + Ok(OsString::from(value)) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlatformInfo { + sysname: OsString, + nodename: OsString, + release: OsString, + version: OsString, + machine: OsString, + osname: OsString, +} + +impl PlatformInfoAPI for PlatformInfo { + fn new() -> Result { + Ok(Self { + nodename: identity(0)?, + sysname: identity(1)?, + release: identity(2)?, + version: identity(3)?, + machine: identity(4)?, + osname: OsString::from("GNU/Linux"), + }) + } +} + +impl UNameAPI for PlatformInfo { + fn sysname(&self) -> &OsStr { + &self.sysname + } + + fn nodename(&self) -> &OsStr { + &self.nodename + } + + fn release(&self) -> &OsStr { + &self.release + } + + fn version(&self) -> &OsStr { + &self.version + } + + fn machine(&self) -> &OsStr { + &self.machine + } + + fn osname(&self) -> &OsStr { + &self.osname + } +} diff --git a/toolchain/std-patches/crates/uu_chmod/0001-wasi-compat.patch b/toolchain/std-patches/crates/uu_chmod/0001-wasi-compat.patch index 8e41964b65..4b682ae402 100644 --- a/toolchain/std-patches/crates/uu_chmod/0001-wasi-compat.patch +++ b/toolchain/std-patches/crates/uu_chmod/0001-wasi-compat.patch @@ -1,6 +1,6 @@ --- a/src/chmod.rs +++ b/src/chmod.rs -@@ -8,16 +8,87 @@ +@@ -8,16 +8,93 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs; @@ -18,12 +18,14 @@ use uucore::mode; use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion}; -+// wasmVM: WASI compatibility — MetadataExt lacks mode(), Permissions lacks from_mode() ++// AgentOS WASM compatibility: MetadataExt lacks mode(), Permissions lacks from_mode(). +#[cfg(target_os = "wasi")] +mod wasi_compat { + use std::fs; + use std::path::Path; + ++ const AGENTOS_CWD_FD: u32 = u32::MAX; ++ + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { @@ -53,7 +55,7 @@ + }; + let mode = unsafe { + host_fs::path_mode( -+ 3, ++ AGENTOS_CWD_FD, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -66,7 +68,7 @@ + } + } + -+ /// Set POSIX-style permissions on a file (best-effort on WASI). ++ /// Set POSIX-style permissions through AgentOS' kernel-owned pathname API. + pub fn set_permissions_from_mode(path: &Path, mode: u32) -> std::io::Result<()> { + use std::io::{Error, ErrorKind}; + @@ -74,21 +76,25 @@ + .to_str() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "path is not valid UTF-8"))? + .as_bytes(); -+ let status = unsafe { host_fs::chmod(3, bytes.as_ptr(), bytes.len() as u32, mode) }; ++ let status = unsafe { ++ host_fs::chmod( ++ AGENTOS_CWD_FD, ++ bytes.as_ptr(), ++ bytes.len() as u32, ++ mode, ++ ) ++ }; + if status == 0 { + return Ok(()); + } -+ -+ let mut perms = fs::metadata(path)?.permissions(); -+ perms.set_readonly(mode & 0o222 == 0); -+ fs::set_permissions(path, perms) ++ Err(Error::from_raw_os_error(status as i32)) + } +} + #[cfg(all(unix, not(target_os = "redox")))] use uucore::safe_traversal::{DirFd, SymlinkBehavior}; use uucore::{format_usage, show, show_error}; -@@ -121,7 +192,16 @@ +@@ -121,7 +198,16 @@ let preserve_root = matches.get_flag(options::PRESERVE_ROOT); let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { @@ -106,7 +112,7 @@ Err(_) => { return Err(ChmodError::CannotStat(fref.into()).into()); } -@@ -623,7 +703,16 @@ +@@ -623,7 +709,16 @@ let metadata = get_metadata(file, dereference); let fperm = match metadata { @@ -124,7 +130,7 @@ Err(err) => { // Handle dangling symlinks or other errors return if file.is_symlink() && !dereference { -@@ -683,7 +772,12 @@ +@@ -683,7 +778,12 @@ // Use the helper method for consistent reporting self.report_permission_change(file, fperm, mode); Ok(()) diff --git a/toolchain/std-patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch b/toolchain/std-patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch index ff68f349e2..b9f6668e19 100644 --- a/toolchain/std-patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch +++ b/toolchain/std-patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch @@ -9,7 +9,7 @@ fsext::{MetadataTimeField, metadata_get_time}, line_ending::LineEnding, os_str_as_bytes_lossy, -@@ -77,6 +77,60 @@ +@@ -77,6 +77,61 @@ translate, version_cmp::version_cmp, }; @@ -21,6 +21,7 @@ + use std::env; + + use super::{Metadata, Path}; ++ const AGENTOS_CWD_FD: u32 = u32::MAX; + + mod host_fs { + #[link(wasm_import_module = "host_fs")] @@ -44,7 +45,7 @@ + }; + unsafe { + host_fs::path_mode( -+ 3, ++ AGENTOS_CWD_FD, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -70,7 +71,7 @@ mod dired; use dired::{DiredOutput, is_dired_arg_present}; -@@ -2982,7 +3036,15 @@ +@@ -2982,7 +3037,15 @@ let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] let is_acl_set = has_acl(item.path()); diff --git a/toolchain/std-patches/crates/uu_stat/0001-wasi-metadata-compat.patch b/toolchain/std-patches/crates/uu_stat/0001-wasi-metadata-compat.patch index 250b747bab..e2029ecc25 100644 --- a/toolchain/std-patches/crates/uu_stat/0001-wasi-metadata-compat.patch +++ b/toolchain/std-patches/crates/uu_stat/0001-wasi-metadata-compat.patch @@ -25,7 +25,7 @@ use uucore::{entries, format_usage, show_error, show_warning}; use clap::{Arg, ArgAction, ArgMatches, Command}; -@@ -23,10 +29,91 @@ +@@ -23,10 +29,92 @@ use std::ffi::{OsStr, OsString}; use std::fs::{FileType, Metadata}; use std::io::Write; @@ -61,6 +61,7 @@ +#[cfg(target_os = "wasi")] +mod wasi_host_fs { + use super::{Metadata, MetadataExt, Path}; ++ const AGENTOS_CWD_FD: u32 = u32::MAX; + + mod host_fs { + #[link(wasm_import_module = "host_fs")] @@ -86,7 +87,7 @@ + }; + let mode = unsafe { + host_fs::path_mode( -+ 3, ++ AGENTOS_CWD_FD, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -105,7 +106,7 @@ + }; + unsafe { + host_fs::path_rdev( -+ 3, ++ AGENTOS_CWD_FD, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -117,7 +118,7 @@ use thiserror::Error; use uucore::time::{FormatSystemTimeFallback, format_system_time, system_time_to_sec}; -@@ -1032,6 +1119,8 @@ +@@ -1032,6 +1120,8 @@ display_name: &str, file: &OsString, file_type: FileType, @@ -126,7 +127,7 @@ from_user: bool, #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] follow_symbolic_links: bool, -@@ -1050,9 +1139,9 @@ +@@ -1050,9 +1140,9 @@ } => { let output = match format { // access rights in octal @@ -138,7 +139,7 @@ // number of blocks allocated (see %B) 'b' => OutputType::Unsigned(meta.blocks()), -@@ -1096,9 +1185,9 @@ +@@ -1096,9 +1186,9 @@ // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), // raw mode in hex @@ -150,7 +151,7 @@ // group ID of owner 'g' => OutputType::Unsigned(meta.gid() as u64), // group name of owner -@@ -1130,10 +1219,10 @@ +@@ -1130,10 +1220,10 @@ 's' => OutputType::Integer(meta.len() as i64), // major device type in hex, for character/block device special // files @@ -163,7 +164,7 @@ // user ID of owner 'u' => OutputType::Unsigned(meta.uid() as u64), // user name of owner -@@ -1176,10 +1265,10 @@ +@@ -1176,10 +1266,10 @@ .map_or((0, 0), system_time_to_sec); OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0) } @@ -178,7 +179,7 @@ _ => OutputType::Unknown, }; print_it(&output, flag, width, precision); -@@ -1234,6 +1323,16 @@ +@@ -1234,6 +1324,16 @@ match result { Ok(meta) => { let file_type = meta.file_type(); @@ -195,7 +196,7 @@ let tokens = if self.from_user || !(file_type.is_char_device() || file_type.is_block_device()) { -@@ -1249,6 +1348,8 @@ +@@ -1249,6 +1349,8 @@ &display_name, &file, file_type, diff --git a/toolchain/std-patches/wasi-libc/0048-cooperative-pthread-cancel.patch b/toolchain/std-patches/wasi-libc/0048-cooperative-pthread-cancel.patch new file mode 100644 index 0000000000..7ee75cc794 --- /dev/null +++ b/toolchain/std-patches/wasi-libc/0048-cooperative-pthread-cancel.patch @@ -0,0 +1,121 @@ +diff --git a/libc-top-half/musl/include/pthread.h b/libc-top-half/musl/include/pthread.h +index a0fc84f..f14f137 100644 +--- a/libc-top-half/musl/include/pthread.h ++++ b/libc-top-half/musl/include/pthread.h +@@ -82,11 +82,12 @@ int pthread_create(pthread_t *__restrict, const pthread_attr_t *__restrict, void + int pthread_detach(pthread_t); + _Noreturn void pthread_exit(void *); + int pthread_join(pthread_t, void **); + #else + #if defined(_WASI_EMULATED_PTHREAD) || defined(_REENTRANT) + int pthread_create(pthread_t *__restrict, const pthread_attr_t *__restrict, void *(*)(void *), void *__restrict); + int pthread_detach(pthread_t); ++_Noreturn void pthread_exit(void *); + int pthread_join(pthread_t, void **); + #else + #include + #define pthread_create(...) ({ _Static_assert(0, "This mode of WASI does not have threads enabled; \ +@@ -114,9 +114,7 @@ int pthread_equal(pthread_t, pthread_t); + int pthread_setcancelstate(int, int *); + int pthread_setcanceltype(int, int *); + void pthread_testcancel(void); +-#ifdef __wasilibc_unmodified_upstream /* WASI has no cancellation support. */ + int pthread_cancel(pthread_t); +-#endif + + #ifdef __wasilibc_unmodified_upstream /* WASI has no CPU scheduling support. */ + int pthread_getschedparam(pthread_t, int *__restrict, struct sched_param *__restrict); +diff --git a/libc-top-half/musl/src/thread/pthread_cancel.c b/libc-top-half/musl/src/thread/pthread_cancel.c +index 8e1f6c6..1df3abf 100644 +--- a/libc-top-half/musl/src/thread/pthread_cancel.c ++++ b/libc-top-half/musl/src/thread/pthread_cancel.c +@@ -87,8 +87,25 @@ int pthread_cancel(pthread_t t) + return pthread_kill(t, SIGCANCEL); + } + #else ++_Noreturn void pthread_exit(void *); ++ ++void __testcancel() ++{ ++ pthread_t self = __pthread_self(); ++ if (self->cancel && !self->canceldisable) ++ pthread_exit(PTHREAD_CANCELED); ++} ++ + int pthread_cancel(pthread_t t) + { +- return ENOTSUP; ++ a_store(&t->cancel, 1); ++ if (t == pthread_self()) { ++ if (t->canceldisable == PTHREAD_CANCEL_ENABLE && t->cancelasync) ++ pthread_exit(PTHREAD_CANCELED); ++ return 0; ++ } ++ /* AgentOS cancellation is cooperative: the target observes this shared ++ * flag at pthread_testcancel or another libc cancellation point. */ ++ return 0; + } + #endif +diff --git a/libc-top-half/musl/src/thread/pthread_create.c b/libc-top-half/musl/src/thread/pthread_create.c +index 7e21bb1..415c4c5 100644 +--- a/libc-top-half/musl/src/thread/pthread_create.c ++++ b/libc-top-half/musl/src/thread/pthread_create.c +@@ -233,6 +233,18 @@ static void __pthread_exit(void *result) + #endif + } + ++#ifndef __wasilibc_unmodified_upstream ++/* Complete the assembly-only unlock used by wasi_thread_start before ++ * terminating this Store. Keeping the final unlock out of C prevents a ++ * concurrent joiner from freeing the current stack while C still uses it. */ ++_Noreturn void __wasi_pthread_exit(void); ++_Noreturn void pthread_exit(void *result) ++{ ++ __pthread_exit(result); ++ __wasi_pthread_exit(); ++} ++#endif ++ + void __do_cleanup_push(struct __ptcb *cb) + { + struct pthread *self = __pthread_self(); +diff --git a/libc-top-half/musl/src/thread/wasm32/wasi_thread_start.s b/libc-top-half/musl/src/thread/wasm32/wasi_thread_start.s +index 9af7b6a..9961101 100644 +--- a/libc-top-half/musl/src/thread/wasm32/wasi_thread_start.s ++++ b/libc-top-half/musl/src/thread/wasm32/wasi_thread_start.s +@@ -5,6 +5,7 @@ + .globaltype __stack_pointer, i32 + .globaltype __tls_base, i32 + .functype __wasi_thread_start_C (i32, i32) -> () ++ .functype __wasi_proc_exit (i32) -> () + + .hidden wasi_thread_start + .globl wasi_thread_start +@@ -46,3 +47,27 @@ wasi_thread_start: + drop + + end_function ++ ++ .hidden __wasi_pthread_exit ++ .globl __wasi_pthread_exit ++ .type __wasi_pthread_exit,@function ++ ++__wasi_pthread_exit: ++ .functype __wasi_pthread_exit () -> () ++ ++ # Mirror the final assembly-only thread-list unlock above. The current ++ # pthread stack may be reclaimed immediately after the notify, so no C ++ # code or stack access is permitted beyond this point. ++ i32.const __thread_list_lock ++ i32.const 0 ++ i32.atomic.store 0 ++ i32.const __thread_list_lock ++ i32.const 1 ++ memory.atomic.notify 0 ++ drop ++ ++ i32.const 0 ++ call __wasi_proc_exit ++ unreachable ++ ++ end_function diff --git a/toolchain/test-programs/pthread_benchmark.c b/toolchain/test-programs/pthread_benchmark.c new file mode 100644 index 0000000000..ddf09454ed --- /dev/null +++ b/toolchain/test-programs/pthread_benchmark.c @@ -0,0 +1,112 @@ +#include +#include +#include + +static pthread_mutex_t gate_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t gate_condition = PTHREAD_COND_INITIALIZER; +static unsigned ready_threads; +static int release_threads; + +static void *parked_worker(void *argument) { + (void)argument; + if (pthread_mutex_lock(&gate_mutex) != 0) { + return (void *)(uintptr_t)1; + } + ready_threads++; + pthread_cond_broadcast(&gate_condition); + while (!release_threads) { + if (pthread_cond_wait(&gate_condition, &gate_mutex) != 0) { + pthread_mutex_unlock(&gate_mutex); + return (void *)(uintptr_t)2; + } + } + pthread_mutex_unlock(&gate_mutex); + return NULL; +} + +static int parse_unsigned(const char *text, unsigned minimum, unsigned maximum, + unsigned *output) { + unsigned value = 0; + if (*text == '\0') { + return -1; + } + for (const char *cursor = text; *cursor != '\0'; cursor++) { + if (*cursor < '0' || *cursor > '9') { + return -1; + } + value = value * 10u + (unsigned)(*cursor - '0'); + if (value > maximum) { + return -1; + } + } + if (value < minimum) { + return -1; + } + *output = (unsigned)value; + return 0; +} + +static void write_state(const char *state, unsigned thread_count) { + char output[16]; + unsigned length = 0; + while (state[length] != '\0') { + output[length] = state[length]; + length++; + } + if (thread_count >= 10) { + output[length++] = (char)('0' + thread_count / 10); + } + output[length++] = (char)('0' + thread_count % 10); + output[length++] = '\n'; + (void)write(STDOUT_FILENO, output, length); +} + +int main(int argc, char **argv) { + unsigned thread_count = 1; + unsigned park = 0; + if ((argc > 1 && parse_unsigned(argv[1], 1, 15, &thread_count) != 0) || + (argc > 2 && parse_unsigned(argv[2], 0, 1, &park) != 0)) { + static const char usage[] = + "usage: pthread_benchmark [threads:1..15] [park:0|1]\n"; + (void)write(STDERR_FILENO, usage, sizeof(usage) - 1); + return 2; + } + + pthread_t threads[15]; + for (unsigned index = 0; index < thread_count; index++) { + if (pthread_create(&threads[index], NULL, parked_worker, NULL) != 0) { + return 10; + } + } + + pthread_mutex_lock(&gate_mutex); + while (ready_threads != thread_count) { + if (pthread_cond_wait(&gate_condition, &gate_mutex) != 0) { + pthread_mutex_unlock(&gate_mutex); + return 11; + } + } + pthread_mutex_unlock(&gate_mutex); + + write_state("ready:", thread_count); + if (park != 0) { + pthread_mutex_lock(&gate_mutex); + for (;;) { + pthread_cond_wait(&gate_condition, &gate_mutex); + } + } + + pthread_mutex_lock(&gate_mutex); + release_threads = 1; + pthread_cond_broadcast(&gate_condition); + pthread_mutex_unlock(&gate_mutex); + + for (unsigned index = 0; index < thread_count; index++) { + void *result = NULL; + if (pthread_join(threads[index], &result) != 0 || result != NULL) { + return 13; + } + } + write_state("done:", thread_count); + return 0; +} diff --git a/toolchain/test-programs/pthread_conformance.c b/toolchain/test-programs/pthread_conformance.c new file mode 100644 index 0000000000..3581006b3e --- /dev/null +++ b/toolchain/test-programs/pthread_conformance.c @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t condition = PTHREAD_COND_INITIALIZER; +static pthread_key_t tls_key; +static int joined_ready; +static int detached_ready; +static int tls_destructors; + +static void tls_destructor(void *value) { + if (value != NULL) { + pthread_mutex_lock(&mutex); + tls_destructors++; + pthread_cond_broadcast(&condition); + pthread_mutex_unlock(&mutex); + } +} + +static void *joined_worker(void *argument) { + pthread_setspecific(tls_key, argument); + pthread_mutex_lock(&mutex); + joined_ready = 1; + pthread_cond_broadcast(&condition); + pthread_mutex_unlock(&mutex); + return (void *)(uintptr_t)42; +} + +static void *detached_worker(void *argument) { + pthread_setspecific(tls_key, argument); + pthread_mutex_lock(&mutex); + detached_ready = 1; + pthread_cond_broadcast(&condition); + pthread_mutex_unlock(&mutex); + return NULL; +} + +static void *cancelled_worker(void *argument) { + (void)argument; + for (;;) { + pthread_testcancel(); + sched_yield(); + } +} + +int main(void) { + pthread_t joined; + pthread_t detached; + pthread_t cancelled; + pthread_attr_t detached_attributes; + void *joined_result = NULL; + void *cancelled_result = NULL; + + /* + * Force the owned fcntl override into this threaded link. This catches a + * threaded sysroot whose upstream libc objects use atomics/shared memory + * while AgentOS override objects were accidentally compiled for the + * ordinary single-thread target. + */ + if (fcntl(STDOUT_FILENO, F_GETFD) < 0) { + return 9; + } + + if (pthread_key_create(&tls_key, tls_destructor) != 0 || + pthread_create(&joined, NULL, joined_worker, (void *)(uintptr_t)1) != 0) { + return 10; + } + pthread_mutex_lock(&mutex); + while (!joined_ready) { + pthread_cond_wait(&condition, &mutex); + } + pthread_mutex_unlock(&mutex); + if (pthread_join(joined, &joined_result) != 0 || + (uintptr_t)joined_result != 42) { + return 11; + } + + if (pthread_attr_init(&detached_attributes) != 0 || + pthread_attr_setdetachstate(&detached_attributes, PTHREAD_CREATE_DETACHED) != 0 || + pthread_create(&detached, &detached_attributes, detached_worker, + (void *)(uintptr_t)1) != 0) { + return 12; + } + pthread_attr_destroy(&detached_attributes); + pthread_mutex_lock(&mutex); + while (!detached_ready || tls_destructors < 2) { + pthread_cond_wait(&condition, &mutex); + } + pthread_mutex_unlock(&mutex); + + if (pthread_create(&cancelled, NULL, cancelled_worker, NULL) != 0 || + pthread_cancel(cancelled) != 0 || + pthread_join(cancelled, &cancelled_result) != 0 || + cancelled_result != PTHREAD_CANCELED) { + return 13; + } + + pthread_key_delete(tls_key); + if (write(STDOUT_FILENO, "pthread-ok\n", 11) != 11) { + return 14; + } + return 0; +} diff --git a/turbo.json b/turbo.json index 699a3942c7..fcf565fd91 100644 --- a/turbo.json +++ b/turbo.json @@ -4,6 +4,7 @@ "AGENTOS_PLUGIN_BIN", "AGENTOS_SIDECAR_BIN", "AGENTOS_NATIVE_SIDECAR_BIN", + "AGENTOS_TEST_WASM_BACKEND", "AGENTOS_SKIP_NATIVE_META_BUILD" ], "globalPassThroughEnv": ["CARGO_TARGET_DIR"], diff --git a/website/public/docs/docs/custom-software/building-wasm.md b/website/public/docs/docs/custom-software/building-wasm.md index 563485e0b7..d17d86edbe 100644 --- a/website/public/docs/docs/custom-software/building-wasm.md +++ b/website/public/docs/docs/custom-software/building-wasm.md @@ -25,7 +25,7 @@ just software-build # stage + assemble every software package just software-build ripgrep # ... or just one ``` -The native build compiles each command for `wasm32-wasip1` with the pinned **nightly** toolchain from `rust-toolchain.toml` (the build vendors and patches `std` for WASI), optimizes with `wasm-opt`, and drops the binaries in `toolchain/target/wasm32-wasip1/release/commands/`. C-based commands (e.g. `sqlite3`, `unzip`, `wget`, `zip`) compile with a **wasi-sdk** clang toolchain via `make -C toolchain/c`. +The native build compiles each command for `wasm32-wasip1` with the pinned **nightly** toolchain from `rust-toolchain.toml` (the build vendors and patches `std` for WASI), optimizes with the checksum-verified Binaryen 128 `wasm-opt`, and drops the binaries in `toolchain/target/wasm32-wasip1/release/commands/`. C-based commands (e.g. `sqlite3`, `unzip`, `wget`, `zip`) compile with a **wasi-sdk** clang toolchain via `make -C toolchain/c`. Each package's build then runs the **agentos-toolchain** lifecycle: `agentos-toolchain stage` copies the binaries listed in the package's `agentos-package.json` into its `bin/`, and `agentos-toolchain build` assembles the clean `dist/package/` dir with a `bin` map in its `package.json` and packs it into `dist/package.aospkg` (the `{ packagePath }` target). @@ -56,4 +56,4 @@ and `pnpm --filter './software/*' test`, and fix any failures. Install a published package and pass it to `software`. Registry WASM packages are `{ packagePath }` descriptors — import and pass them directly: -Meta-packages bundle a full set, e.g. `@agentos-software/common` (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip). Run the commands from the client; see [Processes & Shell](/docs/processes). Browse the full catalog on the [Registry](/registry), and see the package descriptor in [Software Definition](/docs/custom-software/definition). To ship your package to npm or use a local build, see [Publishing Packages](/docs/custom-software/publishing). \ No newline at end of file +Meta-packages bundle a full set, e.g. `@agentos-software/common` (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip). Run the commands from the client; see [Processes & Shell](/docs/processes). Browse the full catalog on the [Registry](/registry), and see the package descriptor in [Software Definition](/docs/custom-software/definition). To ship your package to npm or use a local build, see [Publishing Packages](/docs/custom-software/publishing). From d219ad7103717afa943dd01dc862ec0c2cb3bcdc Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 21 Jul 2026 23:19:24 -0700 Subject: [PATCH 6/6] test: complete Wasmtime parity validation --- .github/workflows/ci-nightly.yml | 132 +- .github/workflows/ci.yml | 69 +- .github/workflows/publish.yaml | 1 + Cargo.lock | 389 +- benchmarks/agentos-apps/package.json | 2 +- .../agentos-apps/{src => tests}/load.test.ts | 2 +- crates/agentos-sidecar-core/src/engine.rs | 11 +- crates/agentos-sidecar/src/acp/mod.rs | 7 +- crates/client/src/agent_os.rs | 17 +- crates/client/src/fs.rs | 2 +- crates/client/src/process.rs | 409 +- crates/client/src/shell.rs | 6 +- crates/client/tests/fetch_e2e.rs | 7 +- crates/client/tests/opencode_session_e2e.rs | 36 +- crates/client/tests/os_instructions_e2e.rs | 6 +- crates/client/tests/process_e2e.rs | 24 +- .../execution/assets/runners/wasi-module.js | 18 +- .../execution/assets/runners/wasm-runner.mjs | 360 +- .../execution/assets/undici-shims/stream.js | 103 +- crates/execution/src/backend/reply.rs | 13 + crates/execution/src/host/filesystem.rs | 5 + crates/execution/src/javascript.rs | 352 +- crates/execution/src/wasm.rs | 90 +- crates/execution/src/wasm/wasmtime/engine.rs | 39 +- .../execution/src/wasm/wasmtime/lifecycle.rs | 34 +- .../src/wasm/wasmtime/linker/filesystem.rs | 114 +- .../src/wasm/wasmtime/linker/network.rs | 84 +- .../src/wasm/wasmtime/linker/preview1.rs | 68 +- .../src/wasm/wasmtime/linker/process.rs | 89 +- .../src/wasm/wasmtime/linker/terminal.rs | 81 +- crates/execution/src/wasm/wasmtime/store.rs | 54 +- crates/execution/src/wasm/wasmtime/threads.rs | 4 + crates/execution/src/wasm/wasmtime/worker.rs | 50 +- crates/execution/tests/javascript_v8.rs | 61 +- .../execution/tests/wasm_abi_link_contract.rs | 6 +- .../tests/wasm_host_fs_errno_contract.rs | 74 + .../tests/wasm_host_net_errno_contract.rs | 4 +- crates/kernel/Cargo.toml | 2 +- crates/kernel/src/device_layer.rs | 87 +- crates/kernel/src/fd_table.rs | 43 +- crates/kernel/src/kernel.rs | 910 +- crates/kernel/src/process_table.rs | 1 + crates/kernel/tests/api_surface.rs | 181 +- crates/kernel/tests/dac.rs | 87 + crates/kernel/tests/device_layer.rs | 18 + crates/kernel/tests/ownership.rs | 67 + crates/kernel/tests/permissions.rs | 26 +- crates/native-sidecar/Cargo.toml | 6 +- crates/native-sidecar/src/bridge.rs | 4 +- .../src/execution/child_process.rs | 561 +- .../src/execution/coordinator.rs | 23 +- .../src/execution/host_dispatch/clock.rs | 3 + .../src/execution/host_dispatch/filesystem.rs | 127 +- .../src/execution/host_dispatch/inventory.rs | 2 + .../src/execution/host_dispatch/mod.rs | 24 +- .../execution/host_dispatch/network_compat.rs | 376 +- .../src/execution/host_dispatch/process.rs | 18 + .../src/execution/javascript/http.rs | 94 +- .../src/execution/javascript/rpc.rs | 62 +- crates/native-sidecar/src/execution/launch.rs | 33 +- .../src/execution/network/dns.rs | 34 +- .../native-sidecar/src/execution/process.rs | 13 + .../src/execution/process_events.rs | 80 +- crates/native-sidecar/src/execution/stdio.rs | 10 + crates/native-sidecar/src/filesystem.rs | 26 +- crates/native-sidecar/src/service.rs | 194 +- crates/native-sidecar/src/state.rs | 47 +- crates/native-sidecar/src/stdio.rs | 31 +- crates/native-sidecar/src/vm.rs | 20 +- .../tests/architecture_guards.rs | 125 +- crates/native-sidecar/tests/extension.rs | 54 +- crates/native-sidecar/tests/filesystem.rs | 21 +- .../tests/fixtures/limits-inventory.json | 6 + .../native-sidecar/tests/posix_path_repro.rs | 22 +- crates/native-sidecar/tests/service.rs | 385 +- crates/native-sidecar/tests/support/mod.rs | 24 + crates/native-sidecar/tests/wasm_raw_abi.rs | 14 +- .../tests/wasm_software_parity.rs | 51 + .../tests/xfstests_correctness.rs | 344 +- crates/v8-runtime/src/execution.rs | 18 + crates/v8-runtime/src/host_call.rs | 206 +- crates/v8-runtime/src/session.rs | 35 +- .../tests/embedded_runtime_session.rs | 18 +- crates/vfs-store/Cargo.toml | 2 +- crates/vfs/src/engine/engines/chunked.rs | 65 +- crates/vfs/src/engine/engines/object.rs | 221 +- crates/vfs/src/engine/mem/metadata_store.rs | 10 +- crates/vfs/src/engine/types.rs | 12 +- crates/vfs/src/posix/mount_table.rs | 154 +- crates/vfs/src/posix/overlay_fs.rs | 29 + crates/vfs/src/posix/vfs.rs | 10 +- crates/vfs/tests/conformance.rs | 54 + crates/vfs/tests/posix_mount_table.rs | 108 +- crates/vfs/tests/posix_vfs.rs | 39 +- docs/design/wasmtime-executor.md | 291 +- docs/design/wasmtime-phase-0.md | 38 +- justfile | 4 + package.json | 12 +- .../tests/actor-lifecycle.nightly.test.ts | 35 +- .../tests/agent-os-conformance.e2e.test.ts | 6 +- .../bridge-src/builtins/active-handles.ts | 163 +- .../build-tools/bridge-src/builtins/http.ts | 9688 +++++----- .../bridge-src/builtins/process.ts | 2374 ++- .../build-tools/bridge-src/builtins/stdin.ts | 818 +- .../build-tools/bridge-src/global-exposure.ts | 14 +- packages/build-tools/bridge-src/index.ts | 107 +- .../build-tools/scripts/build-v8-bridge.mjs | 3 +- packages/core/package.json | 1 + packages/core/src/agent-os.ts | 123 +- packages/core/src/sidecar/rpc-client.ts | 4 + packages/core/src/test/mock-s3.ts | 41 +- .../brush-interactive.nightly.test.ts.snap | 77 + .../pty-line-discipline.nightly.test.ts.snap | 871 + .../agentos-base-filesystem.nightly.test.ts | 64 +- packages/core/tests/browserbase-e2e.test.ts | 31 +- packages/core/tests/browserbase-ws.test.ts | 94 +- .../tests/brush-interactive.nightly.test.ts | 60 +- .../child-process-detached.nightly.test.ts | 778 +- .../claude-code-investigate.nightly.test.ts | 37 +- .../core/tests/claude-session.nightly.test.ts | 35 +- .../core/tests/codex-fullturn.nightly.test.ts | 384 +- .../core/tests/codex-session.nightly.test.ts | 87 +- .../tests/cron-integration.nightly.test.ts | 5 +- .../core/tests/duckdb-package.nightly.test.ts | 12 + .../core/tests/filesystem.nightly.test.ts | 12 +- .../core/tests/fixtures/pty/pty_probe.mjs | 9 +- .../tests/fs-native-parity.nightly.test.ts | 17 +- .../tests/helpers/fixture-node-modules.ts | 17 +- .../core/tests/helpers/opencode-helper.ts | 4 + packages/core/tests/helpers/rich-media.ts | 2 +- packages/core/tests/mount-reconfigure.test.ts | 8 +- packages/core/tests/mount.test.ts | 11 +- ...idecar-process-permissions.nightly.test.ts | 10 +- .../native-sidecar-process.nightly.test.ts | 24 +- .../core/tests/network-http-request.test.ts | 75 +- packages/core/tests/opencode-headless.test.ts | 8 +- .../core/tests/opencode-real-session.test.ts | 10 +- .../tests/opencode-session.nightly.test.ts | 75 +- .../core/tests/pi-acp-adapter.nightly.test.ts | 11 +- packages/core/tests/pi-headless.test.ts | 79 +- .../core/tests/pi-package-projection.test.ts | 51 +- .../tests/pi-vanilla-bash.nightly.test.ts | 129 +- .../core/tests/process-management.test.ts | 110 +- .../tests/pty-line-discipline.nightly.test.ts | 157 +- .../core/tests/pty-protocol.nightly.test.ts | 163 +- .../core/tests/public-api-exports.test.ts | 1 + .../core/tests/s3-backend.nightly.test.ts | 94 +- .../tests/session-cleanup.nightly.test.ts | 32 +- .../core/tests/session-update-live.test.ts | 4 +- .../core/tests/shell-flat-api.nightly.test.ts | 8 +- .../sidecar-binding-dispatch.nightly.test.ts | 20 +- .../tests/software-projection.nightly.test.ts | 35 +- packages/core/tests/spawn-flat-api.test.ts | 17 +- .../tests/vim-interactive.nightly.test.ts | 14 +- .../tests/vim-native-parity.nightly.test.ts | 6 +- .../core/tests/vim-provides.nightly.test.ts | 451 +- .../core/tests/vim-render.nightly.test.ts | 271 +- .../core/tests/wasm-commands.nightly.test.ts | 32 +- packages/core/tests/websocket-wss.test.ts | 13 + packages/runtime-benchmarks/README.md | 21 +- packages/runtime-benchmarks/package.json | 1 + .../results/wasm-mixed-soak.json | 3654 ++-- .../results/wasmtime-threads.json | 1027 +- .../src/compare-baseline.ts | 24 +- .../src/focused/wasmtime-threads.bench.ts | 80 +- packages/runtime-benchmarks/src/quick-gate.ts | 89 +- .../tests/compare-baseline.test.ts | 54 + packages/runtime-benchmarks/tsconfig.json | 2 +- packages/runtime-core/src/test-runtime.ts | 14 +- packages/runtime-sidecar/tests/index.test.cjs | 28 +- packages/vm-test-harness/src/index.ts | 33 +- pnpm-lock.yaml | 13 +- scripts/check-layout.mjs | 2 + scripts/check-layout.test.mjs | 30 + scripts/verify-check-types.mjs | 2 +- scripts/verify-fixed-versions.mjs | 7 + scripts/verify-fixed-versions.test.mjs | 37 +- software/attr/native/c/xattr_tools.c | 3 + software/attr/test/attr.test.ts | 10 + software/claude/agentos-package.json | 1 + software/codex/agentos-package.json | 1 + software/coreutils/agentos-package.json | 1 + software/coreutils/native/c/getconf.c | 50 + software/coreutils/native/c/mknod.c | 4 +- .../native/crates/cmd-mv/src/main.rs | 16 + .../native/crates/cmd-mv/tests/simple_mv.rs | 22 + software/coreutils/test/getconf.test.ts | 53 + software/coreutils/test/runas.nightly.test.ts | 55 + .../test/shell-redirect.nightly.test.ts | 9 +- .../test/shell-terminal.nightly.test.ts | 6 +- software/curl/test/curl.nightly.test.ts | 96 +- .../diffutils/native/crates/diff/src/lib.rs | 75 +- software/diffutils/test/diff.nightly.test.ts | 4 +- .../envsubst/test/envsubst.nightly.test.ts | 4 +- software/fd/test/fd.nightly.test.ts | 4 +- software/file/test/file.nightly.test.ts | 6 +- .../findutils/test/findutils.nightly.test.ts | 71 +- software/gawk/test/gawk.nightly.test.ts | 4 +- software/git/test/git.nightly.test.ts | 13 +- software/grep/test/grep.nightly.test.ts | 4 +- software/gzip/test/gzip.nightly.test.ts | 4 +- software/jq/test/jq.nightly.test.ts | 4 +- software/opencode/agentos-package.json | 1 + software/pi/agentos-package.json | 1 + software/pi/package.json | 2 +- software/pi/tests/package.test.mjs | 2 +- software/ripgrep/test/ripgrep.nightly.test.ts | 4 +- software/sed/test/sed.nightly.test.ts | 11 +- software/ssh/test/ssh.nightly.test.ts | 15 +- software/tar/test/tar.nightly.test.ts | 4 +- software/unzip/test/unzip.nightly.test.ts | 4 +- software/wget/test/wget.nightly.test.ts | 74 +- software/xfsprogs/native/c/xfs_io.c | 295 +- software/xfsprogs/test/xfs-io.test.ts | 91 + software/yq/test/yq.nightly.test.ts | 4 +- software/zip/test/zip.nightly.test.ts | 4 +- tests/xfstests/Makefile | 42 +- tests/xfstests/README.md | 35 +- tests/xfstests/exceptions.toml | 15882 ++++++++++++++-- .../0005-cache-absent-agentos-tools.patch | 6 +- .../patches/0009-wasi-helper-processes.patch | 44 +- .../patches/0010-wasi-fs-perms-process.patch | 57 +- ...018-port-iopat-preallocation-to-wasi.patch | 46 +- .../0020-port-common-line-filter-to-awk.patch | 37 +- ...28-bound-generic-011-dirstress-files.patch | 35 + .../0029-trace-agentos-test-script.patch | 30 + ...0030-bound-generic-069-append-stream.patch | 50 + ...lassify-agentos-logical-vfs-features.patch | 42 + .../0032-port-min-dio-linux-header.patch | 16 + ...3-port-generic-193-permission-errors.patch | 114 + ...0034-port-pwrite-mmap-format-to-wasi.patch | 28 + .../0035-port-t-dir-offset2-getdents.patch | 39 + ...alize-generic-294-uutils-diagnostics.patch | 46 + ...-port-generic-306-without-bind-mount.patch | 59 + .../0038-classify-generic-346-pthread.patch | 29 + ...39-port-generic-360-364-capabilities.patch | 50 + .../0040-bound-generic-371-enospc-race.patch | 40 + .../0041-classify-generic-391-pthread.patch | 22 + .../0042-bound-generic-404-insert-range.patch | 83 + ...0043-declare-agentos-timestamp-range.patch | 24 + ...assify-generic-409-mount-propagation.patch | 25 + ...fy-generic-410-411-mount-propagation.patch | 39 + .../0046-classify-generic-428-dax.patch | 24 + .../0047-classify-generic-437-dax.patch | 14 + .../0048-classify-generic-449-enospc.patch | 14 + ...9-classify-host-kernel-storage-tests.patch | 28 + ...50-portable-generic-469-replay-label.patch | 19 + ...051-classify-swap-and-sysv-ipc-tests.patch | 28 + .../0052-bound-generic-471-rewinddir.patch | 87 + ...-classify-freeze-label-swap-shutdown.patch | 154 + .../0054-bound-generic-488-open-unlink.patch | 38 + ...55-classify-page-size-and-proc-locks.patch | 14 + .../0056-classify-fibmap-filefrag.patch | 14 + .../patches/0057-classify-sync-range.patch | 14 + .../0058-classify-tmpfile-fanout-stress.patch | 15 + .../patches/0059-classify-casefold.patch | 22 + .../0060-classify-swap-and-file-leases.patch | 42 + .../0061-port-splice-test-basename.patch | 21 + .../0062-port-punch-alternating-statfs.patch | 12 + .../0063-gate-detached-mount-namespaces.patch | 14 + .../0064-classify-late-swap-tests.patch | 28 + ...rt-late-directory-and-splice-helpers.patch | 21 + .../0066-classify-idmapped-mounts.patch | 26 + ...0067-classify-dirty-pipe-kernel-test.patch | 14 + ...classify-negative-timestamp-extremes.patch | 14 + ...classify-late-linux-storage-features.patch | 256 + .../0070-bound-generic-676-seekdir.patch | 12 + ...lassify-final-linux-storage-features.patch | 300 + ...72-bound-generic-736-readdir-renames.patch | 84 + ...assify-latest-linux-storage-features.patch | 396 + .../0074-bound-generic-129-looptest.patch | 52 + .../xfstests/runner/agentos_helper_support.c | 140 +- .../runner/include/agentos_helper_compat.h | 9 + tests/xfstests/runner/include/config.h | 1 + tests/xfstests/runner/include/linux/fiemap.h | 37 + tests/xfstests/runner/include/linux/fs.h | 40 + tests/xfstests/runner/include/linux/limits.h | 2 + tests/xfstests/runner/include/linux/mman.h | 7 + tests/xfstests/runner/include/linux/types.h | 15 + tests/xfstests/runner/include/xfs/xfs.h | 76 + tests/xfstests/runner/prepare.py | 24 +- tests/xfstests/runner/test_prepare.py | 185 +- toolchain/Makefile | 8 +- toolchain/c/Makefile | 4 +- toolchain/conformance/c-parity.test.ts | 4 + toolchain/crates/libs/builtins/src/lib.rs | 64 +- toolchain/crates/libs/shims/src/which.rs | 10 +- .../scripts/clone-and-build-codex-wasi.sh | 19 +- toolchain/scripts/ensure-wasm-opt.sh | 26 +- toolchain/scripts/patch-wasi-libc.sh | 36 +- .../std-patches/0001-wasi-process-spawn.patch | 30 +- ...-wasi-directory-not-empty-error-kind.patch | 12 + .../brush-builtins/0004-wasi-ulimit.patch | 58 + .../0005-wasi-trap-signal-state.patch | 103 + .../0006-bash-ulimit-block-units.patch | 24 + .../0001-wasi-command-substitution.patch | 7 +- .../brush-core/0006-wasi-linux-signals.patch | 58 +- .../0008-assignment-status-expansion.patch | 2 +- ...2-wasi-background-invocation-options.patch | 29 + ...13-wasi-expanded-path-background-pid.patch | 38 + .../0014-exit-trap-status-override.patch | 30 + .../0015-foreground-signal-diagnostics.patch | 146 + .../0001-wasi-linux-dirent-layout.patch | 29 + .../libc/0001-wasi-linux-dirent-layout.patch | 29 + .../rlimit/0001-wasi-resource-limits.patch | 145 + .../uu_chmod/0002-linux-error-context.patch | 20 + .../wasi-libc-overrides/fallocate.c | 112 + .../std-patches/wasi-libc-overrides/fcntl.c | 14 +- .../std-patches/wasi-libc-overrides/mman.c | 517 +- .../std-patches/wasi-libc-overrides/statfs.c | 30 + .../0013-posix-socket-header-surface.patch | 4 +- .../0017-resource-limits-and-groups.patch | 54 +- ...018-posix-spawn-and-terminal-headers.patch | 106 +- .../0034-posix-ownership-and-access.patch | 15 +- .../0049-linux-vector-io-and-splice.patch | 283 + .../0050-agentos-stat-block-size.patch | 15 + .../wasi-libc/0051-linux-statfs-headers.patch | 18 + .../0052-linux-erange-diagnostic.patch | 16 + .../wasi-libc/0053-agentos-statvfs.patch | 78 + .../wasi-libc/0054-linux-dirent-layout.patch | 88 + .../test-programs/libc_compat_contract.c | 68 + toolchain/test-programs/mmap_test.c | 82 + toolchain/test-programs/pwritev_test.c | 132 +- toolchain/test-programs/readdir_contract.c | 56 +- .../docs/custom-software/building-wasm.md | 10 +- website/public/docs/docs/resource-limits.md | 9 +- website/scripts/gen-registry.mjs | 6 +- website/scripts/gen-registry.test.mjs | 66 + website/src/generated/registry.json | 121 +- website/src/generated/routes.json | 20 +- 330 files changed, 41485 insertions(+), 13046 deletions(-) rename benchmarks/agentos-apps/{src => tests}/load.test.ts (97%) create mode 100644 packages/core/tests/__snapshots__/brush-interactive.nightly.test.ts.snap create mode 100644 packages/core/tests/__snapshots__/pty-line-discipline.nightly.test.ts.snap create mode 100644 packages/runtime-benchmarks/tests/compare-baseline.test.ts create mode 100644 software/coreutils/native/c/getconf.c create mode 100644 software/coreutils/test/getconf.test.ts create mode 100644 software/coreutils/test/runas.nightly.test.ts create mode 100644 tests/xfstests/patches/0028-bound-generic-011-dirstress-files.patch create mode 100644 tests/xfstests/patches/0029-trace-agentos-test-script.patch create mode 100644 tests/xfstests/patches/0030-bound-generic-069-append-stream.patch create mode 100644 tests/xfstests/patches/0031-classify-agentos-logical-vfs-features.patch create mode 100644 tests/xfstests/patches/0032-port-min-dio-linux-header.patch create mode 100644 tests/xfstests/patches/0033-port-generic-193-permission-errors.patch create mode 100644 tests/xfstests/patches/0034-port-pwrite-mmap-format-to-wasi.patch create mode 100644 tests/xfstests/patches/0035-port-t-dir-offset2-getdents.patch create mode 100644 tests/xfstests/patches/0036-normalize-generic-294-uutils-diagnostics.patch create mode 100644 tests/xfstests/patches/0037-port-generic-306-without-bind-mount.patch create mode 100644 tests/xfstests/patches/0038-classify-generic-346-pthread.patch create mode 100644 tests/xfstests/patches/0039-port-generic-360-364-capabilities.patch create mode 100644 tests/xfstests/patches/0040-bound-generic-371-enospc-race.patch create mode 100644 tests/xfstests/patches/0041-classify-generic-391-pthread.patch create mode 100644 tests/xfstests/patches/0042-bound-generic-404-insert-range.patch create mode 100644 tests/xfstests/patches/0043-declare-agentos-timestamp-range.patch create mode 100644 tests/xfstests/patches/0044-classify-generic-409-mount-propagation.patch create mode 100644 tests/xfstests/patches/0045-classify-generic-410-411-mount-propagation.patch create mode 100644 tests/xfstests/patches/0046-classify-generic-428-dax.patch create mode 100644 tests/xfstests/patches/0047-classify-generic-437-dax.patch create mode 100644 tests/xfstests/patches/0048-classify-generic-449-enospc.patch create mode 100644 tests/xfstests/patches/0049-classify-host-kernel-storage-tests.patch create mode 100644 tests/xfstests/patches/0050-portable-generic-469-replay-label.patch create mode 100644 tests/xfstests/patches/0051-classify-swap-and-sysv-ipc-tests.patch create mode 100644 tests/xfstests/patches/0052-bound-generic-471-rewinddir.patch create mode 100644 tests/xfstests/patches/0053-classify-freeze-label-swap-shutdown.patch create mode 100644 tests/xfstests/patches/0054-bound-generic-488-open-unlink.patch create mode 100644 tests/xfstests/patches/0055-classify-page-size-and-proc-locks.patch create mode 100644 tests/xfstests/patches/0056-classify-fibmap-filefrag.patch create mode 100644 tests/xfstests/patches/0057-classify-sync-range.patch create mode 100644 tests/xfstests/patches/0058-classify-tmpfile-fanout-stress.patch create mode 100644 tests/xfstests/patches/0059-classify-casefold.patch create mode 100644 tests/xfstests/patches/0060-classify-swap-and-file-leases.patch create mode 100644 tests/xfstests/patches/0061-port-splice-test-basename.patch create mode 100644 tests/xfstests/patches/0062-port-punch-alternating-statfs.patch create mode 100644 tests/xfstests/patches/0063-gate-detached-mount-namespaces.patch create mode 100644 tests/xfstests/patches/0064-classify-late-swap-tests.patch create mode 100644 tests/xfstests/patches/0065-port-late-directory-and-splice-helpers.patch create mode 100644 tests/xfstests/patches/0066-classify-idmapped-mounts.patch create mode 100644 tests/xfstests/patches/0067-classify-dirty-pipe-kernel-test.patch create mode 100644 tests/xfstests/patches/0068-classify-negative-timestamp-extremes.patch create mode 100644 tests/xfstests/patches/0069-classify-late-linux-storage-features.patch create mode 100644 tests/xfstests/patches/0070-bound-generic-676-seekdir.patch create mode 100644 tests/xfstests/patches/0071-classify-final-linux-storage-features.patch create mode 100644 tests/xfstests/patches/0072-bound-generic-736-readdir-renames.patch create mode 100644 tests/xfstests/patches/0073-classify-latest-linux-storage-features.patch create mode 100644 tests/xfstests/patches/0074-bound-generic-129-looptest.patch create mode 100644 tests/xfstests/runner/include/linux/fiemap.h create mode 100644 tests/xfstests/runner/include/linux/fs.h create mode 100644 tests/xfstests/runner/include/linux/mman.h create mode 100644 tests/xfstests/runner/include/linux/types.h create mode 100644 tests/xfstests/runner/include/xfs/xfs.h create mode 100644 toolchain/std-patches/0015-wasi-directory-not-empty-error-kind.patch create mode 100644 toolchain/std-patches/crates/brush-builtins/0004-wasi-ulimit.patch create mode 100644 toolchain/std-patches/crates/brush-builtins/0005-wasi-trap-signal-state.patch create mode 100644 toolchain/std-patches/crates/brush-builtins/0006-bash-ulimit-block-units.patch create mode 100644 toolchain/std-patches/crates/brush-core/0012-wasi-background-invocation-options.patch create mode 100644 toolchain/std-patches/crates/brush-core/0013-wasi-expanded-path-background-pid.patch create mode 100644 toolchain/std-patches/crates/brush-core/0014-exit-trap-status-override.patch create mode 100644 toolchain/std-patches/crates/brush-core/0015-foreground-signal-diagnostics.patch create mode 100644 toolchain/std-patches/crates/libc-0.2.178/0001-wasi-linux-dirent-layout.patch create mode 100644 toolchain/std-patches/crates/libc/0001-wasi-linux-dirent-layout.patch create mode 100644 toolchain/std-patches/crates/rlimit/0001-wasi-resource-limits.patch create mode 100644 toolchain/std-patches/crates/uu_chmod/0002-linux-error-context.patch create mode 100644 toolchain/std-patches/wasi-libc-overrides/fallocate.c create mode 100644 toolchain/std-patches/wasi-libc-overrides/statfs.c create mode 100644 toolchain/std-patches/wasi-libc/0049-linux-vector-io-and-splice.patch create mode 100644 toolchain/std-patches/wasi-libc/0050-agentos-stat-block-size.patch create mode 100644 toolchain/std-patches/wasi-libc/0051-linux-statfs-headers.patch create mode 100644 toolchain/std-patches/wasi-libc/0052-linux-erange-diagnostic.patch create mode 100644 toolchain/std-patches/wasi-libc/0053-agentos-statvfs.patch create mode 100644 toolchain/std-patches/wasi-libc/0054-linux-dirent-layout.patch create mode 100644 website/scripts/gen-registry.test.mjs diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 638a4f8af7..8ef55ab63d 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -9,7 +9,7 @@ jobs: xfstests: name: "xfstests (${{ matrix.wasm_backend }}, ${{ matrix.storage_backend }})" runs-on: [self-hosted, agentos-builder] - timeout-minutes: 360 + timeout-minutes: 420 strategy: fail-fast: false matrix: @@ -20,10 +20,17 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-wasip1 + - name: Materialize the pinned docs theme + run: | + rm -rf /tmp/docs-theme + git clone https://github.com/rivet-dev/docs-theme.git /tmp/docs-theme + git -C /tmp/docs-theme checkout 450c498555135098c6a927adfdf13458be9be22a + rm -rf website/vendor/theme && mkdir -p website/vendor + cp -r /tmp/docs-theme/packages/theme website/vendor/theme - run: pnpm install --frozen-lockfile - name: Run the pinned filesystem conformance corpus run: make -C tests/xfstests run @@ -31,19 +38,6 @@ jobs: XFSTESTS_WASM_BACKENDS: ${{ matrix.wasm_backend }} XFSTESTS_BACKENDS: ${{ matrix.storage_backend }} XFSTESTS_CONCURRENCY: '2' - - name: Run the 1,000-file multi-process directory stress gate - if: matrix.storage_backend == 'chunked_local' - run: | - cargo test --release -p agentos-native-sidecar \ - --test xfstests_correctness \ - xfstests_wasi_dirstress_process_matrix -- \ - --ignored --exact --nocapture --test-threads=1 - env: - AGENTOS_TEST_WASM_BACKEND: ${{ matrix.wasm_backend }} - XFSTESTS_ROOT: ${{ github.workspace }}/tests/xfstests/.work/xfstests - AGENTOS_V8_BRIDGE_PREBUILT_DIR: ${{ github.workspace }}/tests/xfstests/.cache/v8-bridge - CARGO_TARGET_DIR: ${{ github.workspace }}/tests/xfstests/.cache/cargo-target - CARGO_BUILD_JOBS: '1' - uses: actions/upload-artifact@v4 if: always() with: @@ -51,6 +45,91 @@ jobs: path: tests/xfstests/report if-no-files-found: warn + xfstests-endurance: + name: "xfstests endurance (${{ matrix.wasm_backend }})" + runs-on: [self-hosted, agentos-builder] + timeout-minutes: 480 + strategy: + fail-fast: false + matrix: + wasm_backend: [v8, wasmtime] + env: + AGENTOS_TEST_WASM_BACKEND: ${{ matrix.wasm_backend }} + XFSTESTS_ROOT: ${{ github.workspace }}/tests/xfstests/.work/xfstests + XFSTESTS_TEST_TIMEOUT_SECONDS: '3600' + AGENTOS_V8_BRIDGE_PREBUILT_DIR: ${{ github.workspace }}/tests/xfstests/.cache/v8-bridge + CARGO_TARGET_DIR: ${{ github.workspace }}/tests/xfstests/.cache/cargo-target + CARGO_BUILD_JOBS: '1' + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - name: Materialize the pinned docs theme + run: | + rm -rf /tmp/docs-theme + git clone https://github.com/rivet-dev/docs-theme.git /tmp/docs-theme + git -C /tmp/docs-theme checkout 450c498555135098c6a927adfdf13458be9be22a + rm -rf website/vendor/theme && mkdir -p website/vendor + cp -r /tmp/docs-theme/packages/theme website/vendor/theme + - run: pnpm install --frozen-lockfile + - name: Stage the pinned corpus and helper binaries + run: env -u CARGO_TARGET_DIR make -C tests/xfstests helpers + - name: Build the V8 bridge used by the shared harness + run: node packages/build-tools/scripts/build-v8-bridge.mjs --out-dir tests/xfstests/.cache/v8-bridge + - name: Run the 1,000-file multi-process directory stress gate + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_dirstress_process_matrix -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full parallel ENOSPC race + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_parallel_enospc_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full insert-range reproduction workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_insert_range_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full 162,000-iteration looptest workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_looptest_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full 10,000-file rewinddir workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_rewinddir_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full 10,000-file open-unlink workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_open_unlink_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full 4,000-file seekdir/getdents workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_seekdir_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + - name: Run the full 5,000-file readdir/rename workload + run: | + cargo test --release -p agentos-native-sidecar \ + --test xfstests_correctness \ + xfstests_wasi_readdir_rename_endurance_probe -- \ + --ignored --exact --nocapture --test-threads=1 + nightly: name: Nightly runtime validation runs-on: [self-hosted, agentos-builder] @@ -58,6 +137,7 @@ jobs: # Cargo validation and the shared sidecar build are explicit below. # Do not let TypeScript dependency builds compile a second Rust copy. AGENTOS_SKIP_NATIVE_META_BUILD: '1' + XFSTESTS_ROOT: ${{ github.workspace }}/tests/xfstests/.work/xfstests steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -84,19 +164,39 @@ jobs: sudo apt-get update sudo apt-get install --yes cmake fi + - name: Materialize the pinned docs theme + run: | + rm -rf /tmp/docs-theme + git clone https://github.com/rivet-dev/docs-theme.git /tmp/docs-theme + git -C /tmp/docs-theme checkout 450c498555135098c6a927adfdf13458be9be22a + rm -rf website/vendor/theme && mkdir -p website/vendor + cp -r /tmp/docs-theme/packages/theme website/vendor/theme - run: pnpm install --frozen-lockfile - run: node --test scripts/generate-secure-exec-mirror.test.mjs - run: make -C toolchain commands - - run: make -C toolchain cmd/duckdb + - run: make -C toolchain cmd/duckdb cmd/vim - run: make -C toolchain codex - run: make -C toolchain/c pthread-conformance-wasm pthread-benchmark-wasm - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require + - name: Stage pinned xfstests and its WASM helper corpus + run: make -C tests/xfstests helpers XFSTESTS_BUILD_NATIVE_COMMANDS=0 - name: Use stable Rust for repository validation run: echo "RUSTUP_TOOLCHAIN=stable" >> "$GITHUB_ENV" # Browser support is retained in-tree but disabled during the unified # native sidecar reactor migration, so it must not gate native CI. - run: cargo check --workspace --exclude agentos-sidecar-browser --exclude agentos-native-sidecar-browser - run: cargo clippy --workspace --exclude agentos-sidecar-browser --exclude agentos-native-sidecar-browser --all-targets -- -D warnings + - name: Build JavaScript workspace dependencies + run: | + pnpm exec turbo build \ + --only \ + --concurrency=4 \ + --filter='!@rivet-dev/agentos-website' \ + --filter='!@agentos-software/codex' \ + --filter='!@rivet-dev/agentos-browser' \ + --filter='!@rivet-dev/agentos-runtime-browser' \ + --filter='!@rivet-dev/agentos-example-browser-terminal' \ + --filter='!@rivet-dev/agentos-playground' - run: pnpm check-types - name: Build sidecars once for all TypeScript runtime suites run: cargo build -p agentos-sidecar -p agentos-native-sidecar diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fdfddf8d3..15fabcbdd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,7 @@ jobs: --filter='!@agentos-software/codex' \ --filter='!@rivet-dev/agentos-browser' \ --filter='!@rivet-dev/agentos-runtime-browser' \ + --filter='!@rivet-dev/agentos-example-browser-terminal' \ --filter='!@rivet-dev/agentos-playground' else npx turbo build \ @@ -58,6 +59,7 @@ jobs: --filter='!@agentos-software/codex' \ --filter='!@rivet-dev/agentos-browser' \ --filter='!@rivet-dev/agentos-runtime-browser' \ + --filter='!@rivet-dev/agentos-example-browser-terminal' \ --filter='!@rivet-dev/agentos-playground' fi # The Codex adapter is ordinary TypeScript, while its executable is a @@ -74,6 +76,7 @@ jobs: - run: node scripts/verify-fixed-versions.mjs - run: node --test scripts/check-layout.test.mjs - run: pnpm check-layout + - run: node --test website/scripts/gen-registry.test.mjs - run: node --test scripts/generate-secure-exec-mirror.test.mjs - run: node --test scripts/check-rustfmt.test.mjs - run: node scripts/check-rustfmt.mjs @@ -82,6 +85,7 @@ jobs: run: | pnpm --parallel --aggregate-output \ --filter '@rivet-dev/agentos-build-tools' \ + --filter '@rivet-dev/agentos-runtime-benchmarks' \ --filter '@rivet-dev/agentos-toolchain' \ --filter '@rivet-dev/agentos-runtime-sidecar' \ --filter 'secure-exec' \ @@ -123,17 +127,33 @@ jobs: workspaces: toolchain -> target key: wasm-commands-${{ hashFiles('toolchain/Cargo.lock') }} - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' - - run: make -C toolchain commands + - name: Build the bounded PR command corpus + if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + run: make -C toolchain pr-commands + - name: Build the complete command corpus + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + run: make -C toolchain commands # DuckDB and Vim are intentionally outside the default command set because # they are heavy builds, but the runtime/parity suites require their real # command artifacts. - name: Build required heavy integration commands + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') run: make -C toolchain cmd/duckdb cmd/vim + - name: Build the pinned Codex WASI artifacts required by the software suite + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + run: make -C toolchain codex-required - name: Build mandatory threaded-WASM conformance fixtures + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') run: make -C toolchain/c pthread-conformance-wasm pthread-benchmark-wasm - name: Stage native/WASM C parity fixtures + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') run: make -C toolchain/c conformance-artifacts - - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require + - name: Stage the bounded PR command corpus + if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require-coreutils + - name: Stage the complete command corpus + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - uses: actions/upload-artifact@v4 with: name: wasm-commands @@ -141,6 +161,13 @@ jobs: retention-days: 1 if-no-files-found: error - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') + with: + name: codex-wasi + path: software/codex/wasm + if-no-files-found: error + - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') with: name: wasm-thread-fixtures path: | @@ -149,6 +176,7 @@ jobs: toolchain/c/build/exec_variants if-no-files-found: error - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') with: name: wasm-c-parity-fixtures path: toolchain/c/build/conformance @@ -331,7 +359,7 @@ jobs: required: name: CI / Required if: ${{ always() && (github.event_name != 'pull_request' || github.base_ref == 'main' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci')) }} - needs: [checks, wasm-commands, rust, core-pr, runtime-core-pr, actor-pr] + needs: [checks, wasm-commands, rust, core-pr, runtime-core-pr, actor-pr, wasm-backend-matrix] runs-on: ubuntu-latest steps: - name: Require every PR gate @@ -342,6 +370,8 @@ jobs: CORE_RESULT: ${{ needs.core-pr.result }} RUNTIME_CORE_RESULT: ${{ needs.runtime-core-pr.result }} ACTOR_RESULT: ${{ needs.actor-pr.result }} + WASM_BACKEND_RESULT: ${{ needs.wasm-backend-matrix.result }} + EXPECT_WASM_BACKEND_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'aggregate-ci') }} run: | for result in \ "$CHECKS_RESULT" \ @@ -355,6 +385,10 @@ jobs: exit 1 fi done + if [ "$EXPECT_WASM_BACKEND_MATRIX" = true ] && [ "$WASM_BACKEND_RESULT" != success ]; then + echo "required dual-backend CI job did not succeed: $WASM_BACKEND_RESULT" >&2 + exit 1 + fi wasm-backend-matrix: name: "WASM backend (${{ matrix.backend }})" @@ -370,25 +404,39 @@ jobs: AGENTOS_TEST_WASM_BACKEND: ${{ matrix.backend }} AGENTOS_E2E_NETWORK: '1' AGENT_OS_CLIENT_ALLOW_E2E_SKIPS: '0' - AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-native-sidecar + AGENTOS_SIDECAR_BIN: ${{ github.workspace }}/target/release/agentos-sidecar AGENTOS_WASMTIME_WORKER_PATH: ${{ github.workspace }}/target/release/agentos-native-sidecar AGENTOS_WASM_COMMANDS_DIR: ${{ github.workspace }}/packages/runtime-core/commands AGENTOS_C_WASM_COMMANDS_DIR: ${{ github.workspace }}/toolchain/c/build/conformance + XFSTESTS_ROOT: ${{ github.workspace }}/tests/xfstests/.work/xfstests steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: key: wasm-backend-${{ matrix.backend }} + - name: Materialize the pinned docs theme + run: | + rm -rf /tmp/docs-theme + git clone https://github.com/rivet-dev/docs-theme.git /tmp/docs-theme + git -C /tmp/docs-theme checkout 450c498555135098c6a927adfdf13458be9be22a + rm -rf website/vendor/theme && mkdir -p website/vendor + cp -r /tmp/docs-theme/packages/theme website/vendor/theme - run: pnpm install --frozen-lockfile - uses: actions/download-artifact@v4 with: name: wasm-commands path: packages/runtime-core/commands + - uses: actions/download-artifact@v4 + with: + name: codex-wasi + path: software/codex/wasm + - name: Restore Codex WASI executable modes + run: chmod 0755 software/codex/wasm/codex software/codex/wasm/codex-exec - uses: actions/download-artifact@v4 with: name: wasm-thread-fixtures @@ -402,17 +450,24 @@ jobs: mkdir -p toolchain/target/wasm32-wasip1/release/commands cp -a packages/runtime-core/commands/. toolchain/target/wasm32-wasip1/release/commands/ node packages/runtime-core/scripts/copy-wasm-commands.mjs --require + pnpm --filter @agentos-software/manifest build + pnpm --filter @rivet-dev/agentos-toolchain build pnpm --filter @agentos-software/coreutils build:runtime pnpm --filter '@agentos-software/common...' build + - name: Stage pinned xfstests and its WASM helper corpus + run: make -C tests/xfstests helpers XFSTESTS_BUILD_NATIVE_COMMANDS=0 - name: Build the release sidecar and public test clients run: | - cargo build --release -p agentos-native-sidecar + cargo build --release -p agentos-sidecar -p agentos-native-sidecar pnpm --filter '@rivet-dev/agentos-runtime-core...' build pnpm --filter @rivet-dev/agentos-vm-test-harness build pnpm --filter @rivet-dev/agentos-core build pnpm --filter @rivet-dev/agentos build - name: Run all native sidecar tests through the selected VM backend run: cargo test --release -p agentos-native-sidecar --tests -- --test-threads=1 + - name: Run artifact-backed V8-WASM and Wasmtime software parity + if: matrix.backend == 'wasmtime' + run: cargo test --release -p agentos-native-sidecar --test wasm_software_parity -- --ignored --nocapture --test-threads=1 - name: Run owned pthread libc conformance if: matrix.backend == 'wasmtime' run: cargo test --release -p agentos-native-sidecar --test wasmtime_safety owned_pthread_libc_mutex_cond_tls_join_detach_and_cancel_conform -- --ignored --exact --nocapture --test-threads=1 @@ -426,6 +481,8 @@ jobs: run: cargo test -p agentos-client -- --test-threads=1 - name: Run every registry software suite through the selected WASM backend run: pnpm exec turbo test --concurrency=1 --filter='@agentos-software/*' + - name: Run every registry software nightly suite through the selected WASM backend + run: pnpm exec turbo test:nightly --concurrency=1 --filter='@agentos-software/*' - name: Run deterministic actor, ACP, and agent-tool conformance run: pnpm --filter @rivet-dev/agentos test:e2e:run - name: Run mixed V8-JavaScript and Wasmtime resource-plateau smoke diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 10ea334bc3..34b8ca6535 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -405,6 +405,7 @@ jobs: --filter='!@rivet-dev/agentos-website' \ --filter='!@rivet-dev/agentos-browser' \ --filter='!@rivet-dev/agentos-runtime-browser' \ + --filter='!@rivet-dev/agentos-example-browser-terminal' \ --filter='!@rivet-dev/agentos-playground' \ --filter='!./examples/*' \ --filter='!./examples/quickstart/*' diff --git a/Cargo.lock b/Cargo.lock index 340ba049bb..c2d37e6567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ dependencies = [ "agentos-vm-config", "anyhow", "async-trait", - "base64 0.22.1", + "base64", "bytes", "chrono", "futures", @@ -138,7 +138,7 @@ dependencies = [ "agentos-runtime", "agentos-v8-runtime", "agentos-wasm-abi-generator", - "base64 0.22.1", + "base64", "ciborium", "flume", "getrandom 0.2.17", @@ -162,7 +162,7 @@ dependencies = [ "agentos-bridge", "agentos-resource", "agentos-vfs-core", - "base64 0.22.1", + "base64", "event-listener", "getrandom 0.2.17", "hickory-proto", @@ -196,13 +196,13 @@ dependencies = [ "aws-config", "aws-credential-types", "aws-sdk-s3", - "base64 0.22.1", + "base64", "bytes", "command-fds", "ctr", "filetime", "getrandom 0.2.17", - "h2 0.4.15", + "h2", "hickory-resolver", "hmac 0.12.1", "http 1.4.2", @@ -214,7 +214,7 @@ dependencies = [ "pbkdf2", "rusqlite", "rustix 1.1.4", - "rustls 0.23.41", + "rustls", "rustls-pemfile", "scrypt", "serde", @@ -223,12 +223,12 @@ dependencies = [ "sha1 0.10.6", "sha2 0.10.9", "shlex 1.3.0", - "socket2 0.6.4", + "socket2", "tar", "tempfile", "thiserror 2.0.18", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tracing", "tracing-subscriber", "ureq", @@ -248,7 +248,7 @@ dependencies = [ "agentos-native-sidecar-core", "agentos-sidecar-protocol", "agentos-vm-config", - "base64 0.22.1", + "base64", "getrandom 0.2.17", "js-sys", "serde_json", @@ -264,7 +264,7 @@ dependencies = [ "agentos-sidecar-protocol", "agentos-vfs-core", "agentos-vm-config", - "base64 0.22.1", + "base64", "serde_json", "webpki-root-certs", ] @@ -305,7 +305,7 @@ dependencies = [ "agentos-runtime", "agentos-vm-config", "async-trait", - "base64 0.22.1", + "base64", "chrono", "nix 0.29.0", "rusqlite", @@ -401,7 +401,7 @@ dependencies = [ "aws-config", "aws-credential-types", "aws-sdk-s3", - "base64 0.22.1", + "base64", "rusqlite", "serde", "serde_json", @@ -416,7 +416,7 @@ dependencies = [ "agentos-runtime", "anyhow", "async-trait", - "base64 0.22.1", + "base64", "blake3", "memmap2", "rivet-vbare-compiler", @@ -543,9 +543,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.8.18" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" +checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -574,9 +574,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -609,9 +609,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.5" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" +checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -637,9 +637,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.137.0" +version = "1.139.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd7213994e2ff9382ff100403b78c30d1b74cdfcd8fa9d0d1dc3a94a5c4874" +checksum = "a159b9721a6a41468f967d1029bece78f410b0beb0594498435deb6ff72bfe48" dependencies = [ "arc-swap", "aws-credential-types", @@ -653,6 +653,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -673,9 +674,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.102.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c82b3ac19f1431854f7ace3a7531674633e286bfdde21976893bfee36fd493b" +checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22" dependencies = [ "arc-swap", "aws-credential-types", @@ -686,6 +687,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -698,9 +700,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.104.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321000d2b4c5519ee573f73167f612efd7329322d9b26969ad1979f0427f1913" +checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188" dependencies = [ "arc-swap", "aws-credential-types", @@ -711,6 +713,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -723,9 +726,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.107.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0d328ba962af23ecfa3c9f23b98d3d35e325fa218d7f13d17a6bf522f8a560" +checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" dependencies = [ "arc-swap", "aws-credential-types", @@ -737,6 +740,7 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -749,9 +753,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.5" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -776,9 +780,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" dependencies = [ "futures-util", "pin-project-lite", @@ -787,9 +791,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.8" +version = "0.65.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -808,9 +812,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.21" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d8391e65fcea47c586a22e1a41f173b38615b112b2c6b7a44e80cec3e6b706" +checksum = "5a9381123ab62d20c13082b151f30f962a3b112b727345394536dfa39a482944" dependencies = [ "aws-smithy-types", "bytes", @@ -819,9 +823,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.6" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -841,39 +845,33 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.13" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3ef8931ad1c98aa6a55b4256f847f3116090819844e0dd41ea682cac5dd2d3" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.15", - "http 0.2.12", + "h2", "http 1.4.2", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.10.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.9", + "hyper", + "hyper-rustls", "hyper-util", "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.41", + "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.62.7" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -882,28 +880,31 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", + "aws-smithy-xml", "urlencoding", ] [[package]] name = "aws-smithy-runtime" -version = "1.11.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -927,9 +928,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" +checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -945,9 +946,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" dependencies = [ "proc-macro2", "quote", @@ -956,9 +957,9 @@ dependencies = [ [[package]] name = "aws-smithy-schema" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -967,9 +968,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" dependencies = [ "base64-simd", "bytes", @@ -993,18 +994,21 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.16" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1021,18 +1025,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1569,9 +1561,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1833,7 +1825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2137,25 +2129,6 @@ dependencies = [ "crc32fast", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.15" @@ -2246,9 +2219,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.0-beta.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbafe58dd6a1bfa058c9c3dd3372c54665a1935e504a25783cdcf9bf14b21d6" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", @@ -2270,9 +2243,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.0-beta.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ddac4552e5be0deead6df196824a5964b0797302569ef4686b75d32efad052" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" dependencies = [ "data-encoding", "idna", @@ -2281,7 +2254,7 @@ dependencies = [ "once_cell", "prefix-trie", "rand", - "ring 0.17.14", + "ring", "thiserror 2.0.18", "tinyvec", "tracing", @@ -2290,9 +2263,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.0-beta.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751e330e7cdf445892d6ce47cb4666a8b127834d2e42cee4db15713b9a27780" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", @@ -2402,12 +2375,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hybrid-array" version = "0.4.13" @@ -2417,30 +2384,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.10.1" @@ -2451,7 +2394,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.15", + "h2", "http 1.4.2", "http-body 1.0.1", "httparse", @@ -2462,21 +2405,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.9" @@ -2484,12 +2412,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.2", - "hyper 1.10.1", + "hyper", "hyper-util", - "rustls 0.23.41", + "rustls", "rustls-native-certs", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", ] @@ -2499,18 +2427,18 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", "http 1.4.2", "http-body 1.0.1", - "hyper 1.10.1", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2", "tokio", "tower-service", "tracing", @@ -2702,7 +2630,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.6.4", + "socket2", "widestring", "windows-registry", "windows-result", @@ -2814,13 +2742,14 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "8.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.21.7", + "base64", + "js-sys", "pem", - "ring 0.16.20", + "ring", "serde", "serde_json", "simple_asn1", @@ -3096,7 +3025,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3290,11 +3219,12 @@ dependencies = [ [[package]] name = "pem" -version = "1.1.1" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.13.1", + "base64", + "serde_core", ] [[package]] @@ -3625,21 +3555,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.14" @@ -3650,7 +3565,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -3745,19 +3660,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring 0.17.14", - "rustls-webpki 0.101.7", - "sct", + "windows-sys 0.59.0", ] [[package]] @@ -3769,9 +3672,9 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring 0.17.14", + "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki", "subtle", "zeroize", ] @@ -3806,16 +3709,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - [[package]] name = "rustls-webpki" version = "0.103.13" @@ -3823,9 +3716,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", - "ring 0.17.14", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -3931,16 +3824,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - [[package]] name = "sdd" version = "3.0.10" @@ -4074,7 +3957,7 @@ version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "base64 0.22.1", + "base64", "bs58", "chrono", "hex", @@ -4244,16 +4127,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.4" @@ -4264,12 +4137,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.9" @@ -4416,7 +4283,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4544,7 +4411,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.4", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -4560,23 +4427,13 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.41", + "rustls", "tokio", ] @@ -4777,12 +4634,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -4795,11 +4646,11 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.22.1", + "base64", "flate2", "log", "once_cell", - "rustls 0.23.41", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -5251,16 +5102,6 @@ dependencies = [ "wast 252.0.0", ] -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -5338,7 +5179,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/benchmarks/agentos-apps/package.json b/benchmarks/agentos-apps/package.json index 6287f47751..8591a06ec8 100644 --- a/benchmarks/agentos-apps/package.json +++ b/benchmarks/agentos-apps/package.json @@ -6,7 +6,7 @@ "scripts": { "load": "node --import tsx src/load.ts", "check-types": "tsc --noEmit", - "test": "node --import tsx --test src/load.test.ts" + "test": "node --import tsx --test tests/load.test.ts" }, "devDependencies": { "@types/node": "^22.19.15", diff --git a/benchmarks/agentos-apps/src/load.test.ts b/benchmarks/agentos-apps/tests/load.test.ts similarity index 97% rename from benchmarks/agentos-apps/src/load.test.ts rename to benchmarks/agentos-apps/tests/load.test.ts index 1a9e5d7dee..a798740442 100644 --- a/benchmarks/agentos-apps/src/load.test.ts +++ b/benchmarks/agentos-apps/tests/load.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from "node:assert"; import { createServer } from "node:http"; import { describe, it } from "node:test"; -import { readLoadConfig, runLoadTest } from "./load.js"; +import { readLoadConfig, runLoadTest } from "../src/load.js"; describe("AgentOS Apps load driver", () => { it("rejects an impossible success-rate gate", () => { diff --git a/crates/agentos-sidecar-core/src/engine.rs b/crates/agentos-sidecar-core/src/engine.rs index e1ae0e1fe8..053d433fca 100644 --- a/crates/agentos-sidecar-core/src/engine.rs +++ b/crates/agentos-sidecar-core/src/engine.rs @@ -27,9 +27,9 @@ use crate::AcpCoreError; /// Matches the native sidecar's `SESSION_CLOSE_TIMEOUT` (5s). const SESSION_CLOSE_TIMEOUT_MS: u64 = 5_000; -/// Matches the native `INITIALIZE_TIMEOUT` (10s) and `SESSION_NEW_TIMEOUT` (30s). -const INITIALIZE_TIMEOUT_MS: u64 = 10_000; -const SESSION_NEW_TIMEOUT_MS: u64 = 30_000; +/// Matches the native bootstrap/control-operation timeout policy. +const INITIALIZE_TIMEOUT_MS: u64 = 60_000; +const SESSION_NEW_TIMEOUT_MS: u64 = 120_000; const MAX_ACP_ADDITIONAL_DIRECTORIES: usize = 128; const MAX_ACP_GUEST_PATH_BYTES: usize = 4_096; @@ -63,6 +63,7 @@ struct AgentPackageManifest { struct AgentPackageAgentBlock { #[serde(rename = "acpEntrypoint", default)] acp_entrypoint: String, + #[serde(default)] env: BTreeMap, #[serde(rename = "launchArgs", default)] launch_args: Vec, @@ -1457,8 +1458,8 @@ mod tests { #[test] fn prompt_has_no_deadline_while_bootstrap_close_and_machine_rpcs_remain_bounded() { assert_eq!(request_timeout_ms("session/prompt"), None); - assert_eq!(request_timeout_ms("initialize"), Some(10_000)); - assert_eq!(request_timeout_ms("session/new"), Some(30_000)); + assert_eq!(request_timeout_ms("initialize"), Some(60_000)); + assert_eq!(request_timeout_ms("session/new"), Some(120_000)); assert_eq!(request_timeout_ms("session/set_mode"), Some(120_000)); assert_eq!(SESSION_CLOSE_TIMEOUT_MS, 5_000); } diff --git a/crates/agentos-sidecar/src/acp/mod.rs b/crates/agentos-sidecar/src/acp/mod.rs index 1907500e99..b144731e43 100644 --- a/crates/agentos-sidecar/src/acp/mod.rs +++ b/crates/agentos-sidecar/src/acp/mod.rs @@ -48,7 +48,10 @@ use turn::*; // opening their local database on a contended host. Keep both bootstrap phases // bounded by one attempt without imposing a shorter deadline on either phase. const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(60); -const SESSION_NEW_TIMEOUT: Duration = Duration::from_secs(60); +// Cold agent adapters can spend close to a minute loading projected packages +// before replying. Keep bootstrap bounded, but use the standard control-plane +// budget so healthy cold starts do not fail under CI or host contention. +const SESSION_NEW_TIMEOUT: Duration = Duration::from_secs(120); const SESSION_CLOSE_TIMEOUT: Duration = Duration::from_secs(5); const ACP_MACHINE_HOST_CALLBACK_TIMEOUT: Duration = Duration::from_secs(120); // Long-running turns and human-mediated permission waits are not failures. @@ -1614,7 +1617,7 @@ mod tests { assert_eq!(request_timeout("initialize"), Some(Duration::from_secs(60))); assert_eq!( request_timeout("session/new"), - Some(Duration::from_secs(60)) + Some(Duration::from_secs(120)) ); assert_eq!(request_timeout("session/prompt"), None); assert_eq!(SESSION_CLOSE_TIMEOUT, Duration::from_secs(5)); diff --git a/crates/client/src/agent_os.rs b/crates/client/src/agent_os.rs index 3b2ee68fa6..1caebdcc29 100644 --- a/crates/client/src/agent_os.rs +++ b/crates/client/src/agent_os.rs @@ -400,7 +400,7 @@ impl AgentOs { loopback_exempt_ports: config.loopback_exempt_ports.clone(), packages, packages_mount_at: config.packages_mount_at.clone().unwrap_or_default(), - bootstrap_commands: Vec::new(), + bootstrap_commands: runtime_bootstrap_commands(), binding_shim_commands: Vec::new(), }), ) @@ -1064,16 +1064,17 @@ fn serialize_create_vm_config_for_sidecar( high_resolution_time: None, } }), - bootstrap_commands: Some(vec![ - String::from("node"), - String::from("npm"), - String::from("npx"), - String::from("python"), - String::from("python3"), - ]), + bootstrap_commands: Some(runtime_bootstrap_commands()), }) } +pub(crate) fn runtime_bootstrap_commands() -> Vec { + ["node", "npm", "npx", "python", "python3"] + .into_iter() + .map(String::from) + .collect() +} + fn serialize_root_filesystem_config_for_sidecar( config: &RootFilesystemConfig, ) -> Result< diff --git a/crates/client/src/fs.rs b/crates/client/src/fs.rs index 069a498083..ed3dc083be 100644 --- a/crates/client/src/fs.rs +++ b/crates/client/src/fs.rs @@ -942,7 +942,7 @@ impl AgentOs { loopback_exempt_ports: config.loopback_exempt_ports.clone(), packages: crate::agent_os::build_package_descriptors(config), packages_mount_at: config.packages_mount_at.clone().unwrap_or_default(), - bootstrap_commands: Vec::new(), + bootstrap_commands: crate::agent_os::runtime_bootstrap_commands(), binding_shim_commands: Vec::new(), }), ) diff --git a/crates/client/src/process.rs b/crates/client/src/process.rs index 163eaec8ec..09d03529df 100644 --- a/crates/client/src/process.rs +++ b/crates/client/src/process.rs @@ -35,6 +35,14 @@ const OBSERVED_PROCESS_TIME_LIMIT: usize = 4096; /// Maximum bytes captured by `exec` across stdout and stderr. const EXEC_OUTPUT_CAPTURE_LIMIT_BYTES: usize = 16 * 1024 * 1024; +/// Snapshot polling interval used to recover a process exit when its terminal event is missed. +const PROCESS_EXIT_SNAPSHOT_POLL_INTERVAL: std::time::Duration = + std::time::Duration::from_millis(50); + +/// A started process must remain absent from the authoritative kernel snapshot for this long before +/// the client treats it as reaped. This matches the TypeScript client fallback. +const MISSING_PROCESS_EXIT_EVENT_GRACE: std::time::Duration = std::time::Duration::from_millis(500); + /// Default guest working directory for `exec`/`spawn`, matching the TS sidecar client. pub(crate) const DEFAULT_EXEC_CWD: &str = "/workspace"; @@ -143,7 +151,6 @@ pub enum SpawnStdio { } /// Callback-free options for portable `spawn`. -#[derive(Default)] pub struct SpawnOptions { pub env: BTreeMap, pub cwd: Option, @@ -155,6 +162,21 @@ pub struct SpawnOptions { pub wasm_backend: Option, } +impl Default for SpawnOptions { + fn default() -> Self { + Self { + env: BTreeMap::new(), + cwd: Some(DEFAULT_EXEC_CWD.to_string()), + stdio: None, + stdin_fd: None, + stdout_fd: None, + stderr_fd: None, + stream_stdin: None, + wasm_backend: None, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ProcessStream { @@ -287,7 +309,7 @@ impl AgentOs { if let Some(stdin) = options.stdin.take() { let chunk = stdin_to_bytes(stdin); let ownership = self.vm_scope(); - let _ = self + let response = self .transport() .request_wire( ownership, @@ -296,11 +318,15 @@ impl AgentOs { chunk, }), ) - .await; + .await + .context("exec: WriteStdin request failed")?; + if let wire::ResponsePayload::RejectedResponse(rejected) = response { + return Err(ClientError::from_rejection(rejected).into()); + } } { let ownership = self.vm_scope(); - let _ = self + let response = self .transport() .request_wire( ownership, @@ -308,7 +334,11 @@ impl AgentOs { process_id: process_id.clone(), }), ) - .await; + .await + .context("exec: CloseStdin request failed")?; + if let wire::ResponsePayload::RejectedResponse(rejected) = response { + return Err(ClientError::from_rejection(rejected).into()); + } } let mut on_stdout = options.on_stdout.take(); @@ -498,47 +528,92 @@ impl AgentOs { Ok(SpawnHandle { pid }) } - /// Write to a spawned process's stdin. SYNC. Errors with `ProcessNotFound`. - pub fn write_process_stdin( + /// Write to a spawned process's stdin and await the sidecar acknowledgement. Errors with + /// `ProcessNotFound` for an unknown SDK pid and preserves typed sidecar rejections. + pub async fn write_process_stdin( &self, pid: u32, data: StdinInput, ) -> std::result::Result<(), ClientError> { - let process_id = self.lookup_process_id(pid)?; + let process_id = self.await_spawn_ready(pid).await?; let chunk: Vec = stdin_to_bytes(data); - let this = self.clone(); - // Fire-and-forget: the TS API is synchronous and does not surface a write error. - tokio::spawn(async move { - let ownership = this.vm_scope(); - let _ = this - .transport() - .request_wire( - ownership, - wire::RequestPayload::WriteStdinRequest(wire::WriteStdinRequest { - process_id, - chunk, - }), - ) - .await; - }); - Ok(()) + let response = self + .transport() + .request_wire( + self.vm_scope(), + wire::RequestPayload::WriteStdinRequest(wire::WriteStdinRequest { + process_id, + chunk, + }), + ) + .await?; + match response { + wire::ResponsePayload::RejectedResponse(rejected) => { + Err(ClientError::from_rejection(rejected)) + } + _ => Ok(()), + } } - /// Close a spawned process's stdin. SYNC. Errors with `ProcessNotFound`. - pub fn close_process_stdin(&self, pid: u32) -> std::result::Result<(), ClientError> { - let process_id = self.lookup_process_id(pid)?; - let this = self.clone(); - tokio::spawn(async move { - let ownership = this.vm_scope(); - let _ = this - .transport() - .request_wire( - ownership, - wire::RequestPayload::CloseStdinRequest(wire::CloseStdinRequest { process_id }), + /// Close a spawned process's stdin and await the sidecar acknowledgement. Awaiting a preceding + /// [`Self::write_process_stdin`] call preserves write-before-EOF ordering across cold starts. + pub async fn close_process_stdin(&self, pid: u32) -> std::result::Result<(), ClientError> { + let process_id = self.await_spawn_ready(pid).await?; + let response = self + .transport() + .request_wire( + self.vm_scope(), + wire::RequestPayload::CloseStdinRequest(wire::CloseStdinRequest { process_id }), + ) + .await?; + match response { + wire::ResponsePayload::RejectedResponse(rejected) => { + Err(ClientError::from_rejection(rejected)) + } + _ => Ok(()), + } + } + + async fn await_spawn_ready(&self, pid: u32) -> std::result::Result { + let (process_id, mut kernel_pid, mut exit) = self + .inner() + .processes + .read(&pid, |_, entry| { + ( + entry.process_id.clone(), + entry.kernel_pid.subscribe(), + entry.exit_tx.subscribe(), ) - .await; - }); - Ok(()) + }) + .ok_or(ClientError::ProcessNotFound(pid))?; + + loop { + if kernel_pid.borrow().is_some() { + return Ok(process_id); + } + if let Some(exit_code) = *exit.borrow() { + return Err(ClientError::Sidecar(format!( + "process {pid} exited with code {exit_code} before stdin became writable" + ))); + } + + tokio::select! { + changed = kernel_pid.changed() => { + if changed.is_err() { + return Err(ClientError::Sidecar(format!( + "process {pid} readiness channel closed before stdin became writable" + ))); + } + } + changed = exit.changed() => { + if changed.is_err() { + return Err(ClientError::Sidecar(format!( + "process {pid} exit channel closed before stdin became writable" + ))); + } + } + } + } } /// Subscribe to the unified stdout/stderr event stream for a process. @@ -651,24 +726,10 @@ impl AgentOs { /// can correlate `spawn()` with `all_processes()`/`process_tree()`. Results are sorted ascending /// by display pid (TS `snapshotProcesses` `.sort((l,r) => l.pid - r.pid)`). pub async fn all_processes(&self) -> Result> { - let ownership = self.vm_scope(); - let response = self - .transport() - .request_wire(ownership, wire::RequestPayload::GetProcessSnapshotRequest) + let snapshot = self + .fetch_process_snapshot() .await .context("all_processes: GetProcessSnapshot request failed")?; - let snapshot = match response { - wire::ResponsePayload::ProcessSnapshotResponse(snapshot) => snapshot, - wire::ResponsePayload::RejectedResponse(rejected) => { - return Err(ClientError::from_rejection(rejected).into()); - } - other => { - return Err(ClientError::Sidecar(format!( - "all_processes: unexpected response {other:?}" - )) - .into()); - } - }; // Snapshot the SDK process registry, keyed by wire `process_id`, capturing exit code, // command, and args. This mirrors the TS `trackedProcessesById` lookup used to build @@ -905,20 +966,33 @@ impl AgentOs { }) } + /// Fetch the authoritative kernel process snapshot without overlaying SDK-tracked processes. + async fn fetch_process_snapshot(&self) -> Result { + let response = self + .transport() + .request_wire( + self.vm_scope(), + wire::RequestPayload::GetProcessSnapshotRequest, + ) + .await?; + match response { + wire::ResponsePayload::ProcessSnapshotResponse(snapshot) => Ok(snapshot), + wire::ResponsePayload::RejectedResponse(rejected) => { + Err(ClientError::from_rejection(rejected).into()) + } + other => Err(ClientError::Sidecar(format!( + "GetProcessSnapshot: unexpected response {other:?}" + )) + .into()), + } + } + /// Allocate a fresh wire `process_id` (used by `exec`, which does not register in the SDK map). fn next_process_id(&self) -> String { let n = self.inner().process_counter.fetch_add(1, Ordering::SeqCst); format!("proc-{n}-{}", uuid::Uuid::new_v4()) } - /// Resolve the wire `process_id` for an SDK pid, erroring with `ProcessNotFound` if unknown. - fn lookup_process_id(&self, pid: u32) -> std::result::Result { - self.inner() - .processes - .read(&pid, |_, entry| entry.process_id.clone()) - .ok_or(ClientError::ProcessNotFound(pid)) - } - /// Send the `Execute` wire request, mapping a rejection into [`ClientError::Kernel`]. async fn send_execute( &self, @@ -966,16 +1040,36 @@ impl AgentOs { let this = self.clone(); tokio::spawn(async move { let ownership = this.vm_scope(); - let _ = this + match this .transport() .request_wire( ownership, wire::RequestPayload::KillProcessRequest(wire::KillProcessRequest { - process_id, - signal, + process_id: process_id.clone(), + signal: signal.clone(), }), ) - .await; + .await + { + Ok(wire::ResponsePayload::RejectedResponse(rejected)) => { + let error = ClientError::from_rejection(rejected); + tracing::error!( + ?error, + %process_id, + %signal, + "exec: asynchronous process kill was rejected" + ); + } + Ok(_) => {} + Err(error) => { + tracing::error!( + ?error, + %process_id, + %signal, + "exec: asynchronous process kill request failed" + ); + } + } }); } @@ -996,16 +1090,38 @@ impl AgentOs { let this = self.clone(); tokio::spawn(async move { let ownership = this.vm_scope(); - let _ = this + match this .transport() .request_wire( ownership, wire::RequestPayload::KillProcessRequest(wire::KillProcessRequest { - process_id, - signal, + process_id: process_id.clone(), + signal: signal.clone(), }), ) - .await; + .await + { + Ok(wire::ResponsePayload::RejectedResponse(rejected)) => { + let error = ClientError::from_rejection(rejected); + tracing::error!( + ?error, + pid, + %process_id, + %signal, + "process signal was rejected" + ); + } + Ok(_) => {} + Err(error) => { + tracing::error!( + ?error, + pid, + %process_id, + %signal, + "process signal request failed" + ); + } + } }); Ok(()) } @@ -1071,12 +1187,16 @@ impl AgentOs { exit_tx: watch::Sender>, kernel_pid_tx: watch::Sender>, ) { + let mut env = options.env.clone(); + if options.stream_stdin == Some(true) { + env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); + } match self .send_execute( &process_id, Some(command), args, - options.env.clone(), + env, options.cwd.clone(), options.wasm_backend, ) @@ -1086,7 +1206,7 @@ impl AgentOs { // Seed the kernel pid so `all_processes`/`process_tree` can remap this process's // kernel-snapshot entry back to its display pid. if let Some(kernel_pid) = started.pid { - let _ = kernel_pid_tx.send(Some(kernel_pid)); + let _ = kernel_pid_tx.send_replace(Some(kernel_pid)); } } Err(error) => { @@ -1102,54 +1222,127 @@ impl AgentOs { data: bytes, }); tracing::error!(?error, pid, %process_id, "spawn: Execute request failed"); - let _ = exit_tx.send(Some(1)); + let _ = exit_tx.send_replace(Some(1)); let _guard = self.inner().process_registry_lock.lock(); self.prune_exited_processes_locked(0); return; } } + let mut snapshot_poll = tokio::time::interval(PROCESS_EXIT_SNAPSHOT_POLL_INTERVAL); + snapshot_poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut missing_since: Option = None; + let mut snapshot_error_reported = false; + loop { - let (_, payload) = match events.recv().await { - Ok(frame) => frame, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => { - // The event stream closed before an exit event landed. The TS fallback treats a - // process that has fully disappeared from the VM snapshot as reaped with exit - // code 0; mirror that terminal value so waiters resolve instead of hanging. - let _ = exit_tx.send(Some(0)); - break; - } - }; - match payload { - EventPayload::ProcessOutputEvent(output) if output.process_id == process_id => { - let bytes = output.chunk; - let _ = output_tx.send(ProcessOutput { - pid, - stream: match output.channel { - StreamChannel::Stdout => ProcessStream::Stdout, - StreamChannel::Stderr => ProcessStream::Stderr, - }, - data: bytes.clone(), - }); - match output.channel { - StreamChannel::Stdout => { - let _ = stdout_tx.send(bytes); + tokio::select! { + biased; + + event = events.recv() => { + let (_, payload) = match event { + Ok(frame) => frame, + Err(broadcast::error::RecvError::Lagged(skipped)) => { + tracing::warn!( + pid, + %process_id, + skipped, + "spawn event receiver lagged; recovering from the kernel process snapshot" + ); + continue; } - StreamChannel::Stderr => { - let _ = stderr_tx.send(bytes); + Err(broadcast::error::RecvError::Closed) => { + // The event stream closed before an exit event landed. The TS fallback + // treats a process that has fully disappeared from the VM snapshot as + // reaped with exit code 0; mirror that terminal value so waiters resolve. + let _ = exit_tx.send_replace(Some(0)); + break; + } + }; + match payload { + EventPayload::ProcessOutputEvent(output) if output.process_id == process_id => { + let bytes = output.chunk; + let _ = output_tx.send(ProcessOutput { + pid, + stream: match output.channel { + StreamChannel::Stdout => ProcessStream::Stdout, + StreamChannel::Stderr => ProcessStream::Stderr, + }, + data: bytes.clone(), + }); + match output.channel { + StreamChannel::Stdout => { + let _ = stdout_tx.send(bytes); + } + StreamChannel::Stderr => { + let _ = stderr_tx.send(bytes); + } + } + } + EventPayload::ProcessExitedEvent(exited) if exited.process_id == process_id => { + let _ = exit_tx.send_replace(Some(exited.exit_code)); + break; } + EventPayload::ProcessOutputEvent(_) + | EventPayload::ProcessExitedEvent(_) + | EventPayload::VmLifecycleEvent(_) + | EventPayload::StructuredEvent(_) + | EventPayload::ExtEnvelope(_) => {} } } - EventPayload::ProcessExitedEvent(exited) if exited.process_id == process_id => { - let _ = exit_tx.send(Some(exited.exit_code)); - break; + _ = snapshot_poll.tick() => { + match self.fetch_process_snapshot().await { + Ok(snapshot) => { + if snapshot_error_reported { + tracing::info!( + pid, + %process_id, + "spawn process snapshot polling recovered" + ); + snapshot_error_reported = false; + } + + match snapshot + .processes + .iter() + .find(|entry| entry.process_id == process_id) + { + Some(entry) if entry.status == ProcessSnapshotStatus::Exited => { + let _ = exit_tx.send_replace(Some(entry.exit_code.unwrap_or(0))); + break; + } + Some(_) => { + missing_since = None; + } + None => { + let now = tokio::time::Instant::now(); + let first_missing = *missing_since.get_or_insert(now); + if now.duration_since(first_missing) + >= MISSING_PROCESS_EXIT_EVENT_GRACE + { + tracing::warn!( + pid, + %process_id, + "started process disappeared from the kernel snapshot without an exit event; treating it as reaped" + ); + let _ = exit_tx.send_replace(Some(0)); + break; + } + } + } + } + Err(error) => { + if !snapshot_error_reported { + tracing::warn!( + ?error, + pid, + %process_id, + "spawn process snapshot query failed; terminal-event tracking remains active" + ); + snapshot_error_reported = true; + } + } + } } - EventPayload::ProcessOutputEvent(_) - | EventPayload::ProcessExitedEvent(_) - | EventPayload::VmLifecycleEvent(_) - | EventPayload::StructuredEvent(_) - | EventPayload::ExtEnvelope(_) => {} } } let _guard = self.inner().process_registry_lock.lock(); diff --git a/crates/client/src/shell.rs b/crates/client/src/shell.rs index 374c699ab4..01faa83290 100644 --- a/crates/client/src/shell.rs +++ b/crates/client/src/shell.rs @@ -374,7 +374,7 @@ impl AgentOs { retained.pop_front(); } } - let _ = exit_tx.send(Some(exited.exit_code)); + let _ = exit_tx.send_replace(Some(exited.exit_code)); break; } } @@ -468,7 +468,7 @@ impl AgentOs { tracing::warn!(?error, shell_id = %exit_shell_id, "acp_open_terminal spawn failed"); agent.inner().shells.remove(&exit_shell_id); agent.inner().pending_shell_exits.remove(&exit_key); - let _ = exit_tx.send(Some(1)); + let _ = exit_tx.send_replace(Some(1)); return; } }; @@ -525,7 +525,7 @@ impl AgentOs { agent.inner().shells.remove_if(&exit_shell_id, |existing| { existing.process_id == route_process_id }); - let _ = exit_tx.send(Some(exit_code)); + let _ = exit_tx.send_replace(Some(exit_code)); }); // The fan-out/exit task is tracked in `pending_shell_exits` (drained by `dispose`), exactly diff --git a/crates/client/tests/fetch_e2e.rs b/crates/client/tests/fetch_e2e.rs index 29aae9dc56..e4dd7d4617 100644 --- a/crates/client/tests/fetch_e2e.rs +++ b/crates/client/tests/fetch_e2e.rs @@ -1,9 +1,9 @@ //! Port-based virtual `fetch` e2e against a real `agentos-sidecar`. //! //! `fetch` dispatches to a guest HTTP server listening on a port INSIDE the kernel (never the host). -//! Standing up that guest listener requires the V8/JS guest runtime, which may be broken in this -//! environment. This suite fails fast by default when prerequisites are missing; set -//! `AGENT_OS_CLIENT_ALLOW_E2E_SKIPS=1` only for local skip-only runs: +//! Standing up that guest listener requires the V8/JS guest runtime. This suite fails fast by +//! default when prerequisites are missing; set `AGENT_OS_CLIENT_ALLOW_E2E_SKIPS=1` only for local +//! skip-only runs: //! //! 1. The sidecar binary must be present. //! 2. The guest command/runtime toolchain must be present. @@ -130,7 +130,6 @@ fn append_output(buffer: &mut String, chunk: Vec) { } #[tokio::test] -#[ignore = "TODO(P6): guest fetch network-permission E2E is artifact/runtime-dependent"] async fn fetch_surface_get_post_and_headers() { if !common::require_sidecar("fetch_surface_get_post_and_headers") { return; diff --git a/crates/client/tests/opencode_session_e2e.rs b/crates/client/tests/opencode_session_e2e.rs index ac12dea0d1..a4d37384e6 100644 --- a/crates/client/tests/opencode_session_e2e.rs +++ b/crates/client/tests/opencode_session_e2e.rs @@ -1,8 +1,8 @@ -//! Packed OpenCode ACP smoke test against the real AgentOS sidecar. +//! Packed OpenCode ACP smoke test against the real agentOS sidecar. //! //! It proves that the production `.aospkg` projects, its native upstream ACP //! adapter initializes, exposes its real model directory, creates a session, -//! and completes a prompt through a host llmock LLM inside an AgentOS VM. +//! and completes a prompt through a host llmock LLM inside an agentOS VM. mod common; @@ -20,6 +20,7 @@ use agentos_client::fs::MkdirOptions; use agentos_client::{ AgentOs, ContentBlock, ExecOptions, ListSessionsInput, OpenSessionInput, PromptInput, }; +use agentos_vm_config::VmSqliteDescriptor; const LLMOCK_SENTINEL: &str = "PONG_FROM_LLMOCK"; @@ -31,15 +32,15 @@ fn repo_root() -> PathBuf { } fn opencode_package_path() -> Option { - let package = repo_root().join("registry/agent/opencode/dist/package.aospkg"); + let package = repo_root().join("software/opencode/dist/package.aospkg"); package.is_file().then_some(package) } fn coreutils_package_path() -> PathBuf { - let package = repo_root().join("registry/software/coreutils/dist/package.aospkg"); + let package = repo_root().join("software/coreutils/dist/package.aospkg"); assert!( package.is_file(), - "Coreutils package is not built; run `pnpm --dir registry/software/coreutils build`" + "Coreutils package is not built; run `pnpm --dir software/coreutils build`" ); package } @@ -98,11 +99,20 @@ async fn packed_opencode_initializes_and_creates_session() { return; } let package_path = opencode_package_path() - .expect("OpenCode package is not built; run `pnpm --dir registry/agent/opencode build`"); + .expect("OpenCode package is not built; run `pnpm --dir software/opencode build`"); let coreutils_path = coreutils_package_path(); let llmock = LlmockServer::start(); let os = AgentOs::create(AgentOsConfig { + database: Some(VmSqliteDescriptor::SqliteFile { + path: std::env::temp_dir() + .join(format!( + "agentos-opencode-session-{}.sqlite", + std::process::id() + )) + .to_string_lossy() + .into_owned(), + }), loopback_exempt_ports: vec![llmock.port()], packages: vec![ PackageRef { @@ -298,12 +308,14 @@ catch (error) { process.stderr.write(String(error)); process.exitCode = 1; }"# model_count > 1, "native OpenCode ACP should expose more than one model; got {model_options:?}" ); - assert_eq!( - model_options - .get("currentValue") - .and_then(|value| value.as_str()), - Some("anthropic/claude-sonnet-4-6"), - "native OpenCode ACP should honor the configured current model" + let current_model = model_options + .get("currentValue") + .and_then(|value| value.as_str()) + .expect("native OpenCode ACP should expose the current model"); + assert!( + current_model == "anthropic/claude-sonnet-4-6" + || current_model.starts_with("anthropic/claude-sonnet-4-6/"), + "native OpenCode ACP should honor the configured current model, optionally with an ACP reasoning variant; got {current_model:?}" ); let prompt = tokio::time::timeout( diff --git a/crates/client/tests/os_instructions_e2e.rs b/crates/client/tests/os_instructions_e2e.rs index f8b3a40113..c9d15edfda 100644 --- a/crates/client/tests/os_instructions_e2e.rs +++ b/crates/client/tests/os_instructions_e2e.rs @@ -9,6 +9,7 @@ mod common; use std::collections::BTreeMap; +use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::sync::Arc; @@ -97,8 +98,11 @@ fn write_mock_pi_adapter(module_root: &std::path::Path) -> std::path::PathBuf { "__MOCK_SESSION_ID__", &format!("mock-session-{}", Uuid::new_v4()), ); - std::fs::write(package_dir.join("adapter.mjs"), adapter) + let adapter_path = package_dir.join("adapter.mjs"); + std::fs::write(&adapter_path, format!("#!/usr/bin/env node\n{adapter}")) .expect("write mock adapter entrypoint"); + std::fs::set_permissions(&adapter_path, std::fs::Permissions::from_mode(0o755)) + .expect("make mock adapter executable"); package_dir } diff --git a/crates/client/tests/process_e2e.rs b/crates/client/tests/process_e2e.rs index ff7242f055..c842206ed9 100644 --- a/crates/client/tests/process_e2e.rs +++ b/crates/client/tests/process_e2e.rs @@ -36,14 +36,15 @@ async fn process_surface_exec_spawn_and_snapshot() { ); assert!( matches!( - os.write_process_stdin(MISSING_PID, StdinInput::Text("x".to_string())), + os.write_process_stdin(MISSING_PID, StdinInput::Text("x".to_string())) + .await, Err(ClientError::ProcessNotFound(_)) ), "write_process_stdin(unknown) must return ProcessNotFound" ); assert!( matches!( - os.close_process_stdin(MISSING_PID), + os.close_process_stdin(MISSING_PID).await, Err(ClientError::ProcessNotFound(_)) ), "close_process_stdin(unknown) must return ProcessNotFound" @@ -193,8 +194,11 @@ async fn process_surface_exec_spawn_and_snapshot() { // Write to stdin, then close it so `cat` sees EOF and exits. os.write_process_stdin(handle.pid, StdinInput::Text("spawned-input".to_string())) + .await .expect("write stdin"); - os.close_process_stdin(handle.pid).expect("close stdin"); + os.close_process_stdin(handle.pid) + .await + .expect("close stdin"); // Collect the expected stdout bytes. The stdout subscription is a live multi-subscriber stream, // so process exit is observed through wait_process rather than channel closure. @@ -217,13 +221,21 @@ async fn process_surface_exec_spawn_and_snapshot() { ); // wait_process resolves with the exit code (cat exits 0 on clean EOF). - let exit_code = tokio::time::timeout( + let exit_code = match tokio::time::timeout( std::time::Duration::from_secs(10), os.wait_process(handle.pid), ) .await - .expect("wait_process timed out") - .expect("wait_process"); + { + Ok(result) => result.expect("wait_process"), + Err(error) => { + let sdk_process = os.get_process(handle.pid); + let kernel_processes = os.all_processes().await; + panic!( + "wait_process timed out: {error}; sdk_process={sdk_process:?}; kernel_processes={kernel_processes:?}" + ); + } + }; assert_eq!(exit_code, 0, "cat should exit 0 after EOF"); // --- kernel snapshot: all_processes / process_tree ------------------------------------------- diff --git a/crates/execution/assets/runners/wasi-module.js b/crates/execution/assets/runners/wasi-module.js index 52fc521463..5c38397f87 100644 --- a/crates/execution/assets/runners/wasi-module.js +++ b/crates/execution/assets/runners/wasi-module.js @@ -1745,10 +1745,20 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = const waitMs = nonblocking ? 0 : 10; let chunk = null; while (true) { - const response = syncRpc.callSync("__kernel_stdin_read", [ - totalLength, - waitMs, - ]); + let response; + try { + response = syncRpc.callSync("__kernel_stdin_read", [ + totalLength, + waitMs, + ]); + } catch (error) { + if (error?.code === "EINTR") { + // Node/libuv retries interrupted stream reads. A caught + // guest signal must not turn an open kernel PTY into EOF. + continue; + } + throw error; + } if ( response && typeof response === "object" && diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index e70b813227..2ee23dbc09 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -77,12 +77,18 @@ const WASI_FILETYPE_DIRECTORY = 3; const WASI_FILETYPE_REGULAR_FILE = 4; const WASI_FILETYPE_SOCKET_DGRAM = 5; const WASI_FILETYPE_SOCKET_STREAM = 6; +const AGENTOS_RDEV_ZERO = (1 << 8) | 5; +const AGENTOS_RDEV_URANDOM = (1 << 8) | 9; const WASI_OFLAGS_CREAT = 1; const WASI_OFLAGS_DIRECTORY = 2; const WASI_OFLAGS_EXCL = 4; const WASI_OFLAGS_TRUNC = 8; const WASI_FDFLAGS_APPEND = 1; const WASI_FDFLAGS_NONBLOCK = 4; +// agentOS extends the spare Preview 1 fdflags space so libc can round-trip +// O_DIRECT through fcntl(F_GETFL/F_SETFL). This is an agentOS ABI bit, not a +// stock WASI capability. +const WASI_FDFLAGS_AGENTOS_DIRECT = 0x20; const WASI_LIBC_O_DIRECT = 0x20000000; const WASI_LIBC_O_RDONLY = 0x04000000; const WASI_LIBC_O_WRONLY = 0x10000000; @@ -141,6 +147,28 @@ function boundedWasmSyncRpcReadLength(length) { __agentOSWasmSyncRpcReadPayloadBytes, ); } + +function boundedWasmGuestReadLength(length) { + return Math.min( + Number(length) >>> 0, + __agentOSWasmSyncReadLimitBytes, + ); +} + +function kernelFdReadFillsRequestedLength(kernelFd, stat) { + const filetype = Number(stat?.filetype) >>> 0; + if (filetype === WASI_FILETYPE_REGULAR_FILE) return true; + if (filetype !== WASI_FILETYPE_CHARACTER_DEVICE) return false; + + // The V8 sync bridge has a smaller per-response payload than a legal guest + // read. agentOS's deterministic fill devices always satisfy the requested + // length, so aggregate bridge-sized host reads rather than leaking the + // transport boundary as a guest-visible short read. TTYs and other character + // devices remain single-read because their short reads are meaningful. + const filestat = callSyncRpc('process.fd_filestat', [kernelFd]); + const rdev = Number(filestat?.rdev); + return rdev === AGENTOS_RDEV_ZERO || rdev === AGENTOS_RDEV_URANDOM; +} const POSIX_SPAWN_RESETIDS = 1; const POSIX_SPAWN_SETPGROUP = 2; const POSIX_SPAWN_SETSIGDEF = 4; @@ -1075,7 +1103,13 @@ function loadExecImageFromPath(command, argv, interpreterDepth = 0) { if (bytes.equals(INTERNAL_KERNEL_COMMAND_STUB)) { bytes = projectedCommandImageBytes(command); if (bytes === null) { - throw execError('ENOENT', `registered command image for ${command} is unavailable`); + // The kernel registry is authoritative for generated stubs. A stub with + // no projected WASM image can still be a sidecar-owned binding (or + // another virtual command), so hand classification to process.exec. + throw execError( + 'ENOEXEC', + `registered command ${command} requires sidecar executable resolution`, + ); } } return compileExecImage( @@ -2417,6 +2451,16 @@ if (typeof wasiImport.path_filestat_set_times !== 'function') { if (typeof wasiImport.fd_filestat_set_times !== 'function') { wasiImport.fd_filestat_set_times = (fd, atimNs, mtimNs, fstFlags) => { try { + const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd' && SIDECAR_MANAGED_PROCESS) { + callSyncRpc('process.fd_utimes', [ + Number(handle.targetFd) >>> 0, + String(atimNs), + String(mtimNs), + Number(fstFlags) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; + } const guestPath = guestPathForManagedFd(fd); if (typeof guestPath !== 'string') { return WASI_ERRNO_BADF; @@ -2938,6 +2982,8 @@ function mapSyntheticFsError(error) { return WASI_ERRNO_EXIST; case 'EFAULT': return WASI_ERRNO_FAULT; + case 'EFBIG': + return WASI_ERRNO_FBIG; case 'EISDIR': return WASI_ERRNO_ISDIR; case 'ELOOP': @@ -3039,6 +3085,8 @@ function mapHostProcessError(error) { return WASI_ERRNO_NOEXEC; case 'ENOMEM': return WASI_ERRNO_NOMEM; + case 'ENOSPC': + return WASI_ERRNO_NOSPC; case 'ENOTDIR': return WASI_ERRNO_NOTDIR; case 'ENOTEMPTY': @@ -5810,6 +5858,12 @@ function managedHostNetTargetFd(socket) { return Number(socket.targetFd) >>> 0; } +function managedHostNetOperationIsNonblocking(socket, messageFlags = 0) { + if (((Number(messageFlags) >>> 0) & HOST_NET_MSG_DONTWAIT) !== 0) return true; + const stat = callSyncRpc('process.fd_stat', [managedHostNetTargetFd(socket)]); + return (Number(stat?.flags) & KERNEL_O_NONBLOCK) !== 0; +} + function managedHostNetAddressText(value) { if (typeof value?.address === 'string' && Number.isInteger(Number(value?.port))) { return formatHostNetAddressInfo(value); @@ -5883,7 +5937,7 @@ function formatHostNetAddressInfo(info) { return `${address}:${port}`; } -// These are the AgentOS wasi-libc p1 ABI values, not Linux's numeric values. +// These are the agentOS wasi-libc p1 ABI values, not Linux's numeric values. // libc serializes Linux-compatible socket behavior over host_net, while the // private guest/runner boundary retains wasi-libc's AF_INET=1, AF_INET6=2, // AF_UNIX=3 assignments. @@ -5901,6 +5955,7 @@ const HOST_NET_SOCK_NONBLOCK = 0x4000; const HOST_NET_SOCK_CLOEXEC = 0x2000; const HOST_NET_SOL_SOCKET = 1; const HOST_NET_WASI_SOL_SOCKET = 0x7fffffff; +const HOST_NET_SO_REUSEADDR = 2; const HOST_NET_SO_ERROR = 4; const HOST_NET_SO_RCVTIMEO_64 = 20; const HOST_NET_SO_RCVTIMEO_32 = 66; @@ -5914,6 +5969,7 @@ const HOST_NET_TIMEVAL_BYTES = 16; // every connection (ssh_packet_set_tos / set_nodelay in opacket/misc) and // treats failure as per-connection stderr noise. const HOST_NET_SO_KEEPALIVE = 9; // SOL_SOCKET, socket(7) +const HOST_NET_SO_LINGER = 13; const HOST_NET_IPPROTO_IP = 0; const HOST_NET_IP_TOS = 1; // ip(7) const HOST_NET_IPPROTO_TCP = 6; @@ -5977,6 +6033,12 @@ function hostNetSockoptKind(level, optname, optvalLen) { ) { return null; } + if (normalizedOptname === HOST_NET_SO_REUSEADDR && normalizedOptvalLen === 4) { + return 'reuse-address'; + } + if (normalizedOptname === HOST_NET_SO_LINGER && normalizedOptvalLen === 8) { + return 'linger'; + } if (normalizedOptvalLen !== HOST_NET_TIMEVAL_BYTES) { return null; } @@ -7534,10 +7596,11 @@ const hostNetImport = { try { const recvFlags = Number(flags) >>> 0; if (socket.managed === true) { - const nonblocking = (recvFlags & HOST_NET_MSG_DONTWAIT) !== 0; + const targetFd = managedHostNetTargetFd(socket); + const nonblocking = managedHostNetOperationIsNonblocking(socket, recvFlags); const configured = nonblocking ? null : callSyncRpc( 'process.hostnet_get_option', - [managedHostNetTargetFd(socket), 'receive-timeout'], + [targetFd, 'receive-timeout'], ); const configuredTimeout = Number(configured?.durationMs); const hasConfiguredTimeout = configured?.durationMs != null @@ -7557,7 +7620,7 @@ const hostNetImport = { } } result = callSyncRpc('process.hostnet_recv', [ - managedHostNetTargetFd(socket), + targetFd, Number(bufLen) >>> 0, recvFlags, 0, @@ -7767,10 +7830,11 @@ const hostNetImport = { return WASI_ERRNO_INVAL; } if (socket.managed === true) { - const nonblocking = (recvFlags & HOST_NET_MSG_DONTWAIT) !== 0; + const targetFd = managedHostNetTargetFd(socket); + const nonblocking = managedHostNetOperationIsNonblocking(socket, recvFlags); const configured = nonblocking ? null : callSyncRpc( 'process.hostnet_get_option', - [managedHostNetTargetFd(socket), 'receive-timeout'], + [targetFd, 'receive-timeout'], ); const configuredTimeout = Number(configured?.durationMs); const hasConfiguredTimeout = configured?.durationMs != null @@ -7790,7 +7854,7 @@ const hostNetImport = { } } event = callSyncRpc('process.hostnet_recv', [ - managedHostNetTargetFd(socket), + targetFd, Number(bufLen) >>> 0, recvFlags, 0, @@ -7865,6 +7929,25 @@ const hostNetImport = { return WASI_ERRNO_SUCCESS; } try { + if (sockoptKind === 'reuse-address') { + callSyncRpc('process.hostnet_set_option', [ + managedHostNetTargetFd(socket), + 'reuse-address', + readGuestUint32(optvalPtr) !== 0, + ]); + return WASI_ERRNO_SUCCESS; + } + if (sockoptKind === 'linger') { + callSyncRpc('process.hostnet_set_option', [ + managedHostNetTargetFd(socket), + 'linger', + { + enabled: readGuestUint32(optvalPtr) !== 0, + seconds: readGuestUint32(Number(optvalPtr) + 4), + }, + ]); + return WASI_ERRNO_SUCCESS; + } const timeoutMs = parseHostNetTimevalMs(readGuestBytes(optvalPtr, optvalLen)); if (timeoutMs == null && readGuestBytes(optvalPtr, optvalLen).some((byte) => byte !== 0)) { return WASI_ERRNO_INVAL; @@ -8154,7 +8237,7 @@ const hostProcessImport = { (flags & POSIX_SPAWN_SETSCHEDULER) !== 0 && requestedPolicy !== 0 ) { - // AgentOS exposes SCHED_OTHER. Real-time policies require host + // agentOS exposes SCHED_OTHER. Real-time policies require host // scheduling privileges and are deliberately not virtualized. return WASI_ERRNO_PERM; } @@ -8489,7 +8572,7 @@ const hostProcessImport = { try { replacement = loadExecImageFromPath(command, argv); } catch (loadError) { - // The trusted sidecar owns AgentOS image selection. The local + // The trusted sidecar owns agentOS image selection. The local // runner only replaces WASM->WASM after successful compilation; // an otherwise valid non-WASM image is prepared and committed // by the sidecar without ever resuming this image. @@ -10764,20 +10847,15 @@ function mutateGuestFileRange(fd, offset, length, method, extraArgs = []) { if (handle?.kind !== 'guest-file' && handle?.kind !== 'kernel-fd' || typeof handle.targetFd !== 'number') { return WASI_ERRNO_BADF; } - const rangeOffset = Number(offset); - const rangeLength = Number(length); - if ( - !Number.isSafeInteger(rangeOffset) || - !Number.isSafeInteger(rangeLength) || - rangeOffset < 0 || - rangeLength < 0 - ) { + const rangeOffset = BigInt(offset); + const rangeLength = BigInt(length); + if (rangeOffset < 0n || rangeLength < 0n) { return WASI_ERRNO_INVAL; } const args = [ Number(handle.targetFd) >>> 0, - rangeOffset, - rangeLength, + rangeOffset.toString(), + rangeLength.toString(), ...extraArgs, ]; switch (method) { @@ -10964,13 +11042,31 @@ const hostFsImport = { return WASI_ERRNO_FAULT; } const rawTarget = readGuestString(pathPtr, pathLen); - target = Number(fd) >>> 0 === 0xffffffff - ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) - : resolvePathOpenGuestPath(fd, pathPtr, pathLen); - if (typeof target !== 'string') { - return WASI_ERRNO_BADF; + const numericFd = Number(fd) >>> 0; + let stats; + if (rawTarget.length === 0 && numericFd !== 0xffffffff) { + const handle = lookupFdHandle(numericFd); + if (!handle) return WASI_ERRNO_BADF; + if (SIDECAR_MANAGED_PROCESS && handle.kind === 'kernel-fd') { + stats = callSyncRpc('process.path_statfs_at', [ + Number(handle.targetFd) >>> 0, + '', + ]); + } else if (typeof handle.guestPath === 'string') { + target = handle.guestPath; + stats = callSyncRpc('fs.statfsSync', [target]); + } else { + return WASI_ERRNO_BADF; + } + } else { + target = numericFd === 0xffffffff + ? path.posix.resolve(HOST_FS_GUEST_CWD, rawTarget) + : resolvePathOpenGuestPath(fd, pathPtr, pathLen); + if (typeof target !== 'string') { + return WASI_ERRNO_BADF; + } + stats = callSyncRpc('fs.statfsSync', [target]); } - const stats = callSyncRpc('fs.statfsSync', [target]); return ( writeGuestUint64(retTotalBytesPtr, BigInt(stats.totalBytes)) || writeGuestUint64(retUsedBytesPtr, BigInt(stats.usedBytes)) || @@ -11021,20 +11117,15 @@ const hostFsImport = { if (handle?.kind !== 'guest-file' && handle?.kind !== 'kernel-fd' || typeof handle.targetFd !== 'number') { return WASI_ERRNO_BADF; } - const punchOffset = Number(offset); - const punchLength = Number(length); - if ( - !Number.isSafeInteger(punchOffset) || - !Number.isSafeInteger(punchLength) || - punchOffset < 0 || - punchLength < 0 - ) { + const punchOffset = BigInt(offset); + const punchLength = BigInt(length); + if (punchOffset < 0n || punchLength < 0n) { return WASI_ERRNO_INVAL; } callSyncRpc('fs.punchHoleSync', [ Number(handle.targetFd) >>> 0, - punchOffset, - punchLength, + punchOffset.toString(), + punchLength.toString(), ]); return WASI_ERRNO_SUCCESS; } catch (error) { @@ -11187,11 +11278,12 @@ const hostFsImport = { retSizePtr, ) { try { + if ((Number(pathLen) >>> 0) === 0) return WASI_ERRNO_NOENT; const nameErrno = checkFixedRequestLimit( 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const capacity = Number(size) >>> 0; @@ -11225,7 +11317,7 @@ const hostFsImport = { 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const capacity = Number(size) >>> 0; @@ -11251,6 +11343,7 @@ const hostFsImport = { }, path_listxattr(fd, pathPtr, pathLen, listPtr, size, followSymlinks, retSizePtr) { try { + if ((Number(pathLen) >>> 0) === 0) return WASI_ERRNO_NOENT; const capacity = Number(size) >>> 0; if (!guestRangesAreValid([retSizePtr, 4], [listPtr, capacity])) { return WASI_ERRNO_FAULT; @@ -11305,11 +11398,12 @@ const hostFsImport = { ) { let target; try { + if ((Number(pathLen) >>> 0) === 0) return WASI_ERRNO_NOENT; const nameErrno = checkFixedRequestLimit( 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const valueErrno = checkFixedRequestLimit( @@ -11347,7 +11441,7 @@ const hostFsImport = { 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const valueErrno = checkFixedRequestLimit( @@ -11372,11 +11466,12 @@ const hostFsImport = { }, path_removexattr(fd, pathPtr, pathLen, namePtr, nameLen, followSymlinks) { try { + if ((Number(pathLen) >>> 0) === 0) return WASI_ERRNO_NOENT; const nameErrno = checkFixedRequestLimit( 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const rawTarget = readGuestString(pathPtr, pathLen); @@ -11400,7 +11495,7 @@ const hostFsImport = { 'wasm.abi.maxXattrNameBytes', nameLen, XATTR_NAME_MAX, - WASI_ERRNO_RANGE, + WASI_ERRNO_INVAL, ); if (nameErrno !== WASI_ERRNO_SUCCESS) return nameErrno; const handle = lookupFdHandle(fd); @@ -11988,7 +12083,7 @@ function kernelPathOperand(fd, pathPtr, pathLen) { const numericFd = Number(fd) >>> 0; if (numericFd === NODE_CWD_FD) { return { - // AgentOS extension imports use this sentinel for an ordinary pathname + // agentOS extension imports use this sentinel for an ordinary pathname // resolved from the process cwd. WASI's private tagged preopens still go // through lookupFdHandle so they retain their capability-root identity. dirFd: numericFd, @@ -12321,57 +12416,97 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { ); if (handle?.kind === 'kernel-fd') { try { - const requestedLength = boundedWasmSyncRpcReadLength( + const requestedLength = boundedWasmGuestReadLength( guestIovByteLength(iovs, iovsLen, iovRequest), ); const kernelFd = Number(handle.targetFd) >>> 0; - let bytes; const stat = callSyncRpc('process.fd_stat', [kernelFd]); + const fillsRequestedLength = kernelFdReadFillsRequestedLength(kernelFd, stat); + traceHostProcess('fd-read-managed-start', { + guestFd: numericFd, + kernelFd, + filetype: Number(stat?.filetype) >>> 0, + fillsRequestedLength, + requestedLength, + }); const nonblocking = (Number(stat?.flags) & KERNEL_O_NONBLOCK) !== 0; const deadline = nonblocking ? Date.now() : maxBlockingReadMs == null ? null : Date.now() + maxBlockingReadMs; - while (bytes == null) { - try { - // A process with local descendants must return to its own event pump - // between zero-time probes. A leaf process can instead issue a - // bounded wait: the sidecar parks descendant reads by reply token, - // freeing the parent/sibling dispatcher until data or EOF arrives. - const pumpsLocalChildren = hasActiveSpawnedChildren(); - const remainingMs = deadline == null - ? KERNEL_WAIT_SLICE_MS - : Math.max(0, deadline - Date.now()); - const waitMs = nonblocking || pumpsLocalChildren - ? 0 - : Math.min(KERNEL_WAIT_SLICE_MS, remainingMs); - bytes = Buffer.from(callSyncRpc('process.fd_read', [ - kernelFd, - requestedLength, - waitMs, - ]) ?? []); - } catch (error) { - if (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK') { - throw error; - } - if (nonblocking) { - throw error; - } - if (deadline != null && Date.now() >= deadline) { - const timeout = new Error( - 'blocking file descriptor read timed out; raise limits.resources.maxBlockingReadMs', - ); - timeout.code = 'EAGAIN'; - throw timeout; + const readChunk = (chunkLength) => { + let chunk; + while (chunk == null) { + try { + // A process with local descendants must return to its own event pump + // between zero-time probes. A leaf process can instead issue a + // bounded wait: the sidecar parks descendant reads by reply token, + // freeing the parent/sibling dispatcher until data or EOF arrives. + const pumpsLocalChildren = hasActiveSpawnedChildren(); + const remainingMs = deadline == null + ? KERNEL_WAIT_SLICE_MS + : Math.max(0, deadline - Date.now()); + const waitMs = nonblocking || pumpsLocalChildren + ? 0 + : Math.min(KERNEL_WAIT_SLICE_MS, remainingMs); + chunk = Buffer.from(callSyncRpc('process.fd_read', [ + kernelFd, + chunkLength, + waitMs, + ]) ?? []); + } catch (error) { + if (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK') { + throw error; + } + if (nonblocking) { + throw error; + } + if (deadline != null && Date.now() >= deadline) { + const timeout = new Error( + 'blocking file descriptor read timed out; raise limits.resources.maxBlockingReadMs', + ); + timeout.code = 'EAGAIN'; + throw timeout; + } + const progressed = pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + dispatchPendingWasmSignals(); + if (!progressed && !hasActiveSpawnedChildren()) { + Atomics.wait(syntheticWaitArray, 0, 0, 1); + } } - const progressed = pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); - dispatchPendingWasmSignals(); - if (!progressed && !hasActiveSpawnedChildren()) { - Atomics.wait(syntheticWaitArray, 0, 0, 1); + } + if (chunk.byteLength > chunkLength) { + const invalid = new Error('descriptor read exceeded its admitted chunk length'); + invalid.code = 'EIO'; + throw invalid; + } + return chunk; + }; + + let bytes; + if (fillsRequestedLength) { + const chunks = []; + let totalLength = 0; + while (totalLength < requestedLength) { + const chunkLength = boundedWasmSyncRpcReadLength(requestedLength - totalLength); + const chunk = readChunk(chunkLength); + if (chunk.byteLength > 0) { + chunks.push(chunk); + totalLength += chunk.byteLength; } + if (chunk.byteLength < chunkLength) break; } + bytes = Buffer.concat(chunks, totalLength); + } else { + bytes = readChunk(boundedWasmSyncRpcReadLength(requestedLength)); } + traceHostProcess('fd-read-managed-complete', { + guestFd: numericFd, + kernelFd, + requestedLength, + returnedLength: bytes.byteLength, + }); const written = writeBytesToGuestIovs(iovs, iovsLen, bytes, iovRequest); return writeGuestUint32(nreadPtr, written); } catch (error) { @@ -12457,7 +12592,14 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } } - if (handle?.kind === 'passthrough' && typeof handle.ioFd === 'number') { + if ( + handle?.kind === 'passthrough' && + typeof handle.ioFd === 'number' && + !( + handle.targetFd === 0 && + (SIDECAR_MANAGED_PROCESS || KERNEL_STDIO_SYNC_RPC) + ) + ) { try { const requestedLength = boundedWasmSyncRpcReadLength( __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => iovRequest.totalLength), @@ -12642,14 +12784,44 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { const handle = lookupFdHandle(fd); if (handle?.kind === 'kernel-fd') { try { - const requestedLength = boundedWasmSyncRpcReadLength( + const requestedLength = boundedWasmGuestReadLength( guestIovByteLength(iovs, iovsLen, iovRequest), ); - const bytes = Buffer.from(callSyncRpc('process.fd_pread', [ - Number(handle.targetFd) >>> 0, - requestedLength, - BigInt(offset).toString(), - ]) ?? []); + const kernelFd = Number(handle.targetFd) >>> 0; + const readOffset = BigInt(offset); + const stat = callSyncRpc('process.fd_stat', [kernelFd]); + const fillsRequestedLength = kernelFdReadFillsRequestedLength(kernelFd, stat); + let bytes; + if (fillsRequestedLength) { + const chunks = []; + let totalLength = 0; + while (totalLength < requestedLength) { + const chunkLength = boundedWasmSyncRpcReadLength(requestedLength - totalLength); + const chunk = Buffer.from(callSyncRpc('process.fd_pread', [ + kernelFd, + chunkLength, + (readOffset + BigInt(totalLength)).toString(), + ]) ?? []); + if (chunk.byteLength > chunkLength) { + const invalid = new Error('positioned read exceeded its admitted chunk length'); + invalid.code = 'EIO'; + throw invalid; + } + if (chunk.byteLength > 0) { + chunks.push(chunk); + totalLength += chunk.byteLength; + } + if (chunk.byteLength < chunkLength) break; + } + bytes = Buffer.concat(chunks, totalLength); + } else { + const chunkLength = boundedWasmSyncRpcReadLength(requestedLength); + bytes = Buffer.from(callSyncRpc('process.fd_pread', [ + kernelFd, + chunkLength, + readOffset.toString(), + ]) ?? []); + } return writeGuestUint32( nreadPtr, writeBytesToGuestIovs(iovs, iovsLen, bytes, iovRequest), @@ -12978,7 +13150,8 @@ wasiImport.fd_fdstat_get = (fd, statPtr) => { const stat = callSyncRpc('process.fd_stat', [Number(handle.targetFd) >>> 0]); const kernelFlags = Number(stat?.flags) >>> 0; const wasiFlags = (kernelFlags & KERNEL_O_APPEND ? WASI_FDFLAGS_APPEND : 0) - | (kernelFlags & KERNEL_O_NONBLOCK ? WASI_FDFLAGS_NONBLOCK : 0); + | (kernelFlags & KERNEL_O_NONBLOCK ? WASI_FDFLAGS_NONBLOCK : 0) + | (kernelFlags & KERNEL_O_DIRECT ? WASI_FDFLAGS_AGENTOS_DIRECT : 0); return writeGuestFdstat( statPtr, Number(stat?.filetype) >>> 0, @@ -13136,7 +13309,8 @@ wasiImport.fd_fdstat_set_flags = (fd, flags) => { try { const wasiFlags = Number(flags) >>> 0; const kernelFlags = (wasiFlags & WASI_FDFLAGS_APPEND ? KERNEL_O_APPEND : 0) - | (wasiFlags & WASI_FDFLAGS_NONBLOCK ? KERNEL_O_NONBLOCK : 0); + | (wasiFlags & WASI_FDFLAGS_NONBLOCK ? KERNEL_O_NONBLOCK : 0) + | (wasiFlags & WASI_FDFLAGS_AGENTOS_DIRECT ? KERNEL_O_DIRECT : 0); callSyncRpc('process.fd_set_flags', [Number(handle.targetFd) >>> 0, kernelFlags]); const hostNetSocket = getHostNetSocket(fd); if (hostNetSocket && !hostNetSocket.closed) { @@ -13511,6 +13685,12 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { const written = stdioStream != null ? Number(callSyncRpc('__kernel_stdio_write', [kernelFd, bytes])) >>> 0 : writeKernelFdCooperatively(kernelFd, bytes); + traceHostProcess('fd-write-managed-complete', { + guestFd: numericFd, + kernelFd, + requestedLength: bytes.length, + written, + }); if (!Number.isSafeInteger(written) || written < 0 || written > bytes.length) { return WASI_ERRNO_FAULT; } @@ -14393,7 +14573,7 @@ function initializeSignalMaskForInstance(targetInstance) { const setter = targetInstance?.exports?.__agentos_set_initial_sigmask; if (typeof setter !== 'function') { throw new Error( - 'spawned WASM image cannot initialize its inherited signal mask; rebuild it with the current AgentOS sysroot', + 'spawned WASM image cannot initialize its inherited signal mask; rebuild it with the current agentOS sysroot', ); } setter(mask.lo, mask.hi); diff --git a/crates/execution/assets/undici-shims/stream.js b/crates/execution/assets/undici-shims/stream.js index 875ee53a21..15db2e62f1 100644 --- a/crates/execution/assets/undici-shims/stream.js +++ b/crates/execution/assets/undici-shims/stream.js @@ -65,13 +65,12 @@ export const finished = (stream, options, callback) => { const readableEnabled = normalizedOptions?.readable !== false; const writableEnabled = normalizedOptions?.writable !== false; let cancelled = false; - let timer = null; + const restoreHooks = []; const cleanup = () => { cancelled = true; - if (timer !== null) { - clearTimeout(timer); - timer = null; + while (restoreHooks.length > 0) { + restoreHooks.pop()(); } }; @@ -83,27 +82,87 @@ export const finished = (stream, options, callback) => { queueMicrotask(() => done(error)); }; - const poll = () => { - if (cancelled) { - return; - } - const state = stream?._state; - if (state === "errored") { - complete(normalizeStreamError(stream?._storedError)); - return; - } - if ( - state === "closed" || - (isWebReadableStream(stream) && !readableEnabled) || - (isWebWritableStream(stream) && !writableEnabled) - ) { - complete(); - return; + const observeClosedPromise = (owner) => { + const closed = owner?._closedPromise ?? owner?.closed; + if (!closed || typeof closed.then !== "function") { + return false; } - timer = setTimeout(poll, 0); + Promise.resolve(closed).then( + () => complete(), + (error) => complete(normalizeStreamError(error)), + ); + return true; }; - poll(); + const state = stream?._state; + if (state === "errored") { + complete(normalizeStreamError(stream?._storedError)); + return cleanup; + } + if ( + state === "closed" || + (isWebReadableStream(stream) && !readableEnabled) || + (isWebWritableStream(stream) && !writableEnabled) + ) { + complete(); + return cleanup; + } + + const existingOwner = isWebReadableStream(stream) + ? stream?._reader + : stream?._writer; + if (observeClosedPromise(existingOwner)) { + return cleanup; + } + + const acquisitionMethod = isWebReadableStream(stream) + ? "getReader" + : "getWriter"; + const originalAcquire = stream?.[acquisitionMethod]; + if (typeof originalAcquire === "function") { + const observedAcquire = function (...args) { + const owner = originalAcquire.apply(this, args); + observeClosedPromise(owner); + return owner; + }; + stream[acquisitionMethod] = observedAcquire; + restoreHooks.push(() => { + if (stream[acquisitionMethod] === observedAcquire) { + stream[acquisitionMethod] = originalAcquire; + } + }); + } + + // agentOS's Web Streams implementation exposes its controller. Hook its + // terminal transitions so `finished()` remains event-driven even when no + // reader or writer has been acquired yet. + const controller = isWebReadableStream(stream) + ? stream?._readableStreamController + : stream?._writableStreamController; + for (const [method, errorResult] of [ + ["close", false], + ["error", true], + ]) { + const original = controller?.[method]; + if (typeof original !== "function") { + continue; + } + const observed = function (...args) { + const result = original.apply(this, args); + if (errorResult) { + complete(normalizeStreamError(args[0])); + } else { + complete(); + } + return result; + }; + controller[method] = observed; + restoreHooks.push(() => { + if (controller[method] === observed) { + controller[method] = original; + } + }); + } return cleanup; }; diff --git a/crates/execution/src/backend/reply.rs b/crates/execution/src/backend/reply.rs index b49b9a0cbe..8294b06088 100644 --- a/crates/execution/src/backend/reply.rs +++ b/crates/execution/src/backend/reply.rs @@ -368,6 +368,19 @@ impl DirectHostReplyHandle { self.settle(Err(error)) } + /// Whether this exact response lane has reached a terminal state. + /// + /// Readiness-driven host calls can have a retry event already queued when + /// a signal or teardown settles the lane. The owner uses this observation + /// to discard only that stale retry; settlement methods remain strict and + /// still return `EALREADY` for every attempted duplicate response. + pub fn is_terminal(&self) -> bool { + matches!( + self.inner.state.load(Ordering::Acquire), + REPLY_SETTLED | REPLY_DELIVERY_FAILED + ) + } + /// Mark a successfully claimed exec request complete without sending a /// response into the replaced image. Ordinary operations must settle with /// `succeed` or `fail`; using this on an open or settled lane is an error. diff --git a/crates/execution/src/host/filesystem.rs b/crates/execution/src/host/filesystem.rs index a0a80e13a5..bfcd4fcc80 100644 --- a/crates/execution/src/host/filesystem.rs +++ b/crates/execution/src/host/filesystem.rs @@ -5,6 +5,8 @@ pub enum DescriptorWhence { Set, Current, End, + Data, + Hole, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -366,6 +368,9 @@ pub enum FilesystemOperation { dir_fd: u32, path: BoundedString, }, + DescriptorFilesystemStats { + fd: u32, + }, Remount { path: BoundedString, options: BoundedString, diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index c4a0489877..cbbfaf753e 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -739,6 +739,11 @@ struct LocalBridgeState { /// `None` means "route module resolution to the service loop" (the kernel-VFS /// fallback for callers that supply no reader). module_reader: Option>, + /// The installed reader only exposes trusted host mappings; every other + /// path is owned by the live kernel VFS. Package-origin resolution must + /// bypass that partial reader before its `/root/node_modules` compatibility + /// mapping can shadow the package's dependency closure under `/opt/agentos`. + module_reader_forwards_to_kernel: bool, } impl Default for LocalBridgeState { @@ -761,6 +766,7 @@ impl Default for LocalBridgeState { forward_kernel_stdin_rpc: false, v8_session: None, module_reader: None, + module_reader_forwards_to_kernel: false, } } } @@ -812,6 +818,10 @@ struct GuestPathTranslator { implicit_host_cwd: PathBuf, sandbox_root: Option, mappings: Vec, + /// Explicit, trusted host projections supplied by the sidecar. A live + /// kernel-VFS module reader may fall back only to these roots, never to the + /// implicit host cwd or sandbox root. + explicit_mapping_guest_roots: Vec, } #[derive(Debug, Clone, Deserialize, Default)] @@ -1237,6 +1247,10 @@ impl GuestPathTranslator { .into_iter() .filter(|mapping| mapping.guest_path.starts_with('/')) .collect::>(); + let explicit_mapping_guest_roots = mappings + .iter() + .map(|mapping| mapping.guest_path.clone()) + .collect(); if !mappings .iter() @@ -1258,9 +1272,17 @@ impl GuestPathTranslator { .filter(|value| Path::new(value.as_str()).is_absolute()) .map(PathBuf::from), mappings, + explicit_mapping_guest_roots, } } + fn explicitly_maps_guest_path(&self, guest_path: &str) -> bool { + let normalized = normalize_guest_path(guest_path); + self.explicit_mapping_guest_roots + .iter() + .any(|root| strip_guest_prefix(&normalized, root).is_some()) + } + fn is_known_host_path(&self, host_path: &Path) -> bool { if host_path.starts_with(&self.implicit_host_cwd) { return true; @@ -1767,6 +1789,10 @@ impl ModuleResolutionTestHarness { implicit_guest_cwd: String::from("/root"), implicit_host_cwd: host_root, sandbox_root: None, + explicit_mapping_guest_roots: mappings + .iter() + .map(|mapping| mapping.guest_path.clone()) + .collect(), mappings, }; Self { local_bridge } @@ -2122,6 +2148,7 @@ pub struct JavascriptExecution { events: EventReceiver, pending_sync_rpc: Arc>, exited: Arc, + termination_requested: AtomicBool, kernel_stdin: Arc, _import_cache_guard: Arc, v8_session: V8SessionHandle, @@ -2258,15 +2285,21 @@ impl JavascriptExecution { } pub fn terminate(&self) -> Result<(), JavascriptExecutionError> { - // Completion may race an idempotent child-process cleanup kill. Once - // the terminal frame is published, preserve that result and avoid - // enqueueing TerminateExecution into an already-completed V8 session. - if self.has_exited() { + // Kernel control delivery, explicit cleanup, and the terminal frame can + // race. Exactly one path may request isolate termination; later + // idempotent shutdown attempts preserve the first result instead of + // enqueueing a late TerminateExecution diagnostic into guest stderr. + if self.has_exited() || self.termination_requested.swap(true, Ordering::AcqRel) { return Ok(()); } - self.v8_session + let result = self + .v8_session .terminate() - .map_err(JavascriptExecutionError::Terminate) + .map_err(JavascriptExecutionError::Terminate); + if result.is_err() { + self.termination_requested.store(false, Ordering::Release); + } + result } pub fn pause(&self) -> Result<(), JavascriptExecutionError> { @@ -3493,6 +3526,22 @@ impl JavascriptExecutionEngine { local_bridge.kernel_stdin = kernel_stdin.clone(); local_bridge.v8_session = Some(v8_session.clone()); local_bridge.module_reader = module_reader; + if local_bridge.module_reader.is_none() + && !local_bridge + .translator + .explicit_mapping_guest_roots + .is_empty() + { + // Production normally forwards module calls to the kernel when no + // VFS reader is installed. Trusted runtime projections (npm, + // Pyodide assets, and similar sidecar-created mappings) need a + // narrowly scoped host fallback first. This always-missing reader + // activates the layered resolver: explicit mappings resolve + // locally, while every other miss still falls through to the + // kernel service loop. + local_bridge.module_reader = Some(Box::new(ForwardingModuleFsReader)); + local_bridge.module_reader_forwards_to_kernel = true; + } local_bridge.module_resolution = GuestModuleResolution::from_env(&request.env); local_bridge.forward_kernel_stdin_rpc = request .env @@ -3558,6 +3607,7 @@ impl JavascriptExecutionEngine { events, pending_sync_rpc, exited, + termination_requested: AtomicBool::new(false), kernel_stdin, _import_cache_guard: import_cache_guard, v8_session, @@ -5020,6 +5070,14 @@ fn send_single_javascript_event( /// Handle internal bridge calls that don't need to go to the sidecar. /// Returns Some(response) if handled locally, None if it should be forwarded. impl LocalBridgeState { + fn package_module_request_requires_kernel(&self, parent: &str) -> bool { + if !self.module_reader_forwards_to_kernel { + return false; + } + let parent = guest_path_from_file_url(parent).unwrap_or_else(|| parent.to_owned()); + normalize_guest_path(&parent).starts_with("/opt/agentos/pkgs/") + } + fn handle_internal_bridge_call( &mut self, call_id: u64, @@ -5039,6 +5097,9 @@ impl LocalBridgeState { if self.js_runtime_denies_specifier(specifier) { return Some(LocalBridgeCallResult::Immediate(Value::Null)); } + if self.package_module_request_requires_kernel(parent) { + return None; + } let resolved = self.with_module_resolver(|resolver| { resolver.resolve_module(specifier, parent, mode) }); @@ -5054,11 +5115,23 @@ impl LocalBridgeState { )) } "_moduleFormat" => { - let format = - match self.module_format(args.first().and_then(Value::as_str).unwrap_or("")) { - Ok(format) => format, - Err(error) => return Some(LocalBridgeCallResult::Error(error)), - }; + let path = args.first().and_then(Value::as_str).unwrap_or(""); + // A forwarding reader means the live kernel VFS owns paths + // outside the explicitly admitted host projections. The local + // resolver cannot distinguish a missing package.json there + // from Node's real default-CommonJS classification, so a + // locally inferred `commonjs` result would hide the + // authoritative package scope (notably projected `.aospkg` + // packages). Forward those paths to the kernel just like + // resolve/load misses; explicit trusted host mappings remain + // eligible for the fast local path. + if self.has_module_reader() && !self.translator.explicitly_maps_guest_path(path) { + return None; + } + let format = match self.module_format(path) { + Ok(format) => format, + Err(error) => return Some(LocalBridgeCallResult::Error(error)), + }; if format.is_none() && self.has_module_reader() { return None; } @@ -5082,6 +5155,23 @@ impl LocalBridgeState { )) } "_batchResolveModules" => { + if args + .first() + .and_then(Value::as_array) + .is_some_and(|requests| { + requests.iter().any(|request| { + request + .as_array() + .and_then(|pair| pair.get(1)) + .and_then(Value::as_str) + .is_some_and(|parent| { + self.package_module_request_requires_kernel(parent) + }) + }) + }) + { + return None; + } let resolved = match self.batch_resolve_modules(args) { Ok(resolved) => resolved, Err(error) => return Some(LocalBridgeCallResult::Error(error)), @@ -5532,7 +5622,11 @@ impl LocalBridgeState { ) -> T { let cache = &mut self.resolution_cache; if let Some(reader) = self.module_reader.as_deref_mut() { - let reader: &mut dyn ModuleFsReader = reader; + let mut layered = LayeredModuleFsReader { + primary: reader, + explicit_host_mappings: &mut self.translator, + }; + let reader: &mut dyn ModuleFsReader = &mut layered; let mut resolver = ModuleResolver { reader, cache }; f(&mut resolver) } else { @@ -5544,6 +5638,92 @@ impl LocalBridgeState { } } +struct LayeredModuleFsReader<'a> { + primary: &'a mut dyn ModuleFsReader, + explicit_host_mappings: &'a mut GuestPathTranslator, +} + +struct ForwardingModuleFsReader; + +impl ModuleFsReader for ForwardingModuleFsReader { + fn canonical_guest_path( + &mut self, + _guest_path: &str, + ) -> Result, HostServiceError> { + Ok(None) + } + + fn read_to_string(&mut self, _guest_path: &str) -> Result, HostServiceError> { + Ok(None) + } + + fn path_is_dir(&mut self, _guest_path: &str) -> Result, HostServiceError> { + Ok(None) + } + + fn path_exists(&mut self, _guest_path: &str) -> Result { + Ok(false) + } +} + +impl ModuleFsReader for LayeredModuleFsReader<'_> { + fn canonical_guest_path( + &mut self, + guest_path: &str, + ) -> Result, HostServiceError> { + if let Some(path) = self.primary.canonical_guest_path(guest_path)? { + return Ok(Some(path)); + } + if !self + .explicit_host_mappings + .explicitly_maps_guest_path(guest_path) + { + return Ok(None); + } + self.explicit_host_mappings + .canonical_module_guest_path(guest_path) + } + + fn read_to_string(&mut self, guest_path: &str) -> Result, HostServiceError> { + if let Some(source) = self.primary.read_to_string(guest_path)? { + return Ok(Some(source)); + } + if !self + .explicit_host_mappings + .explicitly_maps_guest_path(guest_path) + { + return Ok(None); + } + ModuleFsReader::read_to_string(&mut self.explicit_host_mappings, guest_path) + } + + fn path_is_dir(&mut self, guest_path: &str) -> Result, HostServiceError> { + if let Some(is_dir) = self.primary.path_is_dir(guest_path)? { + return Ok(Some(is_dir)); + } + if !self + .explicit_host_mappings + .explicitly_maps_guest_path(guest_path) + { + return Ok(None); + } + ModuleFsReader::path_is_dir(&mut self.explicit_host_mappings, guest_path) + } + + fn path_exists(&mut self, guest_path: &str) -> Result { + if self.primary.path_exists(guest_path)? { + return Ok(true); + } + if !self + .explicit_host_mappings + .explicitly_maps_guest_path(guest_path) + { + return Ok(false); + } + ModuleFsReader::path_exists(&mut self.explicit_host_mappings, guest_path) + } +} + impl ModuleFsReader for &mut dyn ModuleFsReader { fn canonical_guest_path( &mut self, @@ -8325,6 +8505,32 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::tempdir; + struct MissingModuleReader; + + impl ModuleFsReader for MissingModuleReader { + fn canonical_guest_path( + &mut self, + _guest_path: &str, + ) -> Result, HostServiceError> { + Ok(None) + } + + fn read_to_string( + &mut self, + _guest_path: &str, + ) -> Result, HostServiceError> { + Ok(None) + } + + fn path_is_dir(&mut self, _guest_path: &str) -> Result, HostServiceError> { + Ok(None) + } + + fn path_exists(&mut self, _guest_path: &str) -> Result { + Ok(false) + } + } + #[test] fn malformed_bridge_payload_is_a_typed_decode_error() { let error = decode_bridge_call_args("_resolveModule", 17, &[0xff]) @@ -8353,6 +8559,128 @@ mod tests { .contains("ERR_AGENTOS_BRIDGE_DISPATCH_DECODE")); } + #[test] + fn live_module_reader_falls_back_only_to_explicit_host_mappings() { + let root = tempdir().expect("create explicit mapping root"); + let mapped_root = root.path().join("npm"); + fs::create_dir_all(mapped_root.join("lib/utils")).expect("create mapped module tree"); + fs::write( + mapped_root.join("lib/utils/display.js"), + "module.exports = 1;\n", + ) + .expect("write mapped module"); + fs::write(root.path().join("hidden.js"), "module.exports = 2;\n") + .expect("write implicit-cwd module"); + let env = BTreeMap::from([( + String::from(NODE_GUEST_PATH_MAPPINGS_ENV), + serde_json::to_string(&vec![json!({ + "guestPath": "/__secure_exec/node-runtime/npm", + "hostPath": mapped_root, + })]) + .expect("serialize explicit mapping"), + )]); + let mut bridge = LocalBridgeState::default(); + bridge.translator = GuestPathTranslator::from_host_context( + &env, + root.path().to_path_buf(), + String::from("/workspace"), + ); + bridge.module_reader = Some(Box::new(MissingModuleReader)); + + assert_eq!( + bridge + .resolve_module( + "/__secure_exec/node-runtime/npm/lib/utils/display.js", + "/workspace/[eval]", + ModuleResolveMode::Require, + ) + .expect("resolve explicit host mapping"), + Some(String::from( + "/__secure_exec/node-runtime/npm/lib/utils/display.js" + )) + ); + assert_eq!( + bridge + .resolve_module( + "/workspace/hidden.js", + "/workspace/[eval]", + ModuleResolveMode::Require, + ) + .expect("implicit host cwd remains unavailable"), + None + ); + } + + #[test] + fn partial_host_reader_defers_agentos_package_resolution_to_kernel_vfs() { + let mut bridge = LocalBridgeState::default(); + bridge.module_reader = Some(Box::new(MissingModuleReader)); + bridge.module_reader_forwards_to_kernel = true; + let parent = + "/opt/agentos/pkgs/codex/0.0.1/node_modules/@agentos-software/codex/dist/adapter.js"; + + assert!( + bridge + .handle_internal_bridge_call( + 1, + "_resolveModule", + &[ + Value::String(String::from("@agentclientprotocol/sdk")), + Value::String(parent.to_owned()), + Value::String(String::from("import")), + ], + ) + .is_none(), + "a /root/node_modules host mapping must not shadow package-local dependencies" + ); + assert!( + bridge + .handle_internal_bridge_call( + 2, + "_resolveModule", + &[ + Value::String(String::from("@agentclientprotocol/sdk")), + Value::String(format!("file://{parent}")), + Value::String(String::from("import")), + ], + ) + .is_none(), + "file URL referrers inside projected packages must also use the kernel VFS" + ); + assert!( + bridge + .handle_internal_bridge_call( + 3, + "_batchResolveModules", + &[Value::Array(vec![Value::Array(vec![ + Value::String(String::from("@agentclientprotocol/sdk")), + Value::String(parent.to_owned()), + ])])], + ) + .is_none(), + "batched package resolution must preserve the same kernel ownership" + ); + } + + #[test] + fn vfs_owned_module_format_falls_through_to_the_authoritative_reader() { + let mut bridge = LocalBridgeState::default(); + bridge.module_reader = Some(Box::new(MissingModuleReader)); + + assert!( + bridge + .handle_internal_bridge_call( + 1, + "_moduleFormat", + &[Value::String(String::from( + "/opt/agentos/pkgs/demo/0.0.1/node_modules/demo/index.js", + ))], + ) + .is_none(), + "a local default-CommonJS guess must not hide live VFS package metadata" + ); + } + #[test] fn dispose_context_reclaims_one_shot_metadata_without_reusing_ids() { let mut engine = JavascriptExecutionEngine::default(); diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index 737fc80e3c..1e71d6419d 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -367,7 +367,7 @@ pub enum NativeBinaryFormat { } impl NativeBinaryFormat { - fn display_name(self) -> &'static str { + pub fn display_name(self) -> &'static str { match self { Self::Elf => "ELF", Self::MachO => "Mach-O", @@ -2297,7 +2297,7 @@ impl WasmExecutionEngine { verify_wasm_module_header(&resolved_module)?; // Enforce bounded structural parsing before the complete feature // validator. Oversized section counts and pathological varuints must - // fail at AgentOS's explicit parser limits rather than being obscured + // fail at agentOS's explicit parser limits rather than being obscured // by a later engine/validator EOF diagnostic. validate_module_limits(&resolved_module, &request)?; validate_module_profile(&resolved_module)?; @@ -4262,6 +4262,7 @@ fn build_wasm_runner_bootstrap( format!( r#"const __agentOSWasmInternalEnv = {internal_env_json}; const __agentOSWasmSyncRpcReadPayloadBytes = {wasm_sync_rpc_read_payload_bytes}; + const __agentOSWasmSyncReadLimitBytes = {WASM_SYNC_READ_LIMIT_BYTES}; const __agentOSWasmEntropyLimitBytes = {WASM_SYNC_READ_LIMIT_BYTES}; const __agentOSRequireBuiltin = (specifier) => {{ if (typeof globalThis.require === "function") {{ @@ -4645,6 +4646,7 @@ if (typeof globalThis !== "undefined") {{ case "process.path_open_at": case "process.path_mkdir_at": case "process.path_stat_at": + case "process.path_statfs_at": case "process.path_chmod_at": case "process.path_utimes_at": case "process.path_chown_at": @@ -4689,6 +4691,7 @@ if (typeof globalThis !== "undefined") {{ case "process.fd_chmod": case "process.fd_chown": case "process.fd_truncate": + case "process.fd_utimes": case "process.fd_set_flags": case "process.fd_getfd": case "process.fd_setfd": @@ -5520,7 +5523,7 @@ fn validate_module_profile(resolved_module: &ResolvedWasmModule) -> Result<(), W profile::validate_locked_profile(bytes.as_slice()).map_err(WasmExecutionError::Host) } -fn detect_native_binary_format(header: &[u8]) -> Option { +pub fn detect_native_binary_format(header: &[u8]) -> Option { if header.len() >= 4 && &header[..4] == b"\x7fELF" { return Some(NativeBinaryFormat::Elf); } @@ -6299,6 +6302,9 @@ mod tests { assert!(bootstrap.contains(&format!( "const __agentOSWasmSyncRpcReadPayloadBytes = {raw_limit};" ))); + assert!(bootstrap.contains(&format!( + "const __agentOSWasmSyncReadLimitBytes = {WASM_SYNC_READ_LIMIT_BYTES};" + ))); for method in [ "process.exec_image_open", "process.exec_image_open_fd", @@ -6312,6 +6318,11 @@ mod tests { } let runner = include_str!("../assets/runners/wasm-runner.mjs"); assert!(runner.contains("boundedWasmSyncRpcReadLength(")); + assert!(runner.contains("boundedWasmGuestReadLength(")); + assert!(runner.contains("kernelFdReadFillsRequestedLength(kernelFd, stat)")); + assert!(runner.contains("rdev === AGENTOS_RDEV_ZERO || rdev === AGENTOS_RDEV_URANDOM")); + assert!(runner.contains("Buffer.concat(chunks, totalLength)")); + assert!(runner.contains("readOffset + BigInt(totalLength)")); assert!(runner.contains("callSyncRpc('process.fd_read'")); assert!(runner.contains("callSyncRpc('process.fd_pread'")); assert!(runner.contains("callSyncRpc('process.exec_image_open'")); @@ -6451,6 +6462,25 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet ); } + #[test] + fn sidecar_managed_passthrough_stdin_uses_the_kernel_pipe() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let fd_read_start = runner + .find("wasiImport.fd_read = (fd, iovs") + .expect("wrapped fd_read must exist"); + let fd_read_end = runner[fd_read_start..] + .find("wasiImport.fd_readdir = (fd") + .map(|offset| fd_read_start + offset) + .expect("fd_read must precede fd_readdir"); + let fd_read = &runner[fd_read_start..fd_read_end]; + + assert!(fd_read.contains("typeof handle.ioFd === 'number'")); + assert!(fd_read.contains( + "handle.targetFd === 0 &&\n (SIDECAR_MANAGED_PROCESS || KERNEL_STDIO_SYNC_RPC)" + )); + assert!(fd_read.contains("readKernelStdinChunk(requestedLength, nonblocking)")); + } + #[test] fn wasm_preview1_memory_is_prevalidated_before_host_side_effects() { let runner = include_str!("../assets/runners/wasm-runner.mjs"); @@ -6851,6 +6881,34 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet assert!(!section[managed..projection].contains("fsModule.")); } + #[test] + fn standalone_registered_exec_stubs_defer_to_sidecar_classification() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let loader_start = runner + .find("function loadExecImageFromPath(command, argv, interpreterDepth = 0) {") + .expect("path exec loader"); + let loader_end = runner[loader_start..] + .find("function executableTargetForHandle") + .map(|offset| loader_start + offset) + .expect("path exec loader end"); + let loader = &runner[loader_start..loader_end]; + assert!(loader.contains("bytes.equals(INTERNAL_KERNEL_COMMAND_STUB)")); + assert!(loader.contains( + "throw execError(\n 'ENOEXEC',\n `registered command ${command} requires sidecar executable resolution`," + )); + + let exec_start = runner + .find(" proc_exec(\n") + .expect("proc_exec import"); + let exec_end = runner[exec_start..] + .find(" proc_fexec(") + .map(|offset| exec_start + offset) + .expect("proc_exec import end"); + let exec_import = &runner[exec_start..exec_end]; + assert!(exec_import.contains("loadError?.code === 'ENOEXEC'")); + assert!(exec_import.contains("callSyncRpc('process.exec'")); + } + #[test] fn managed_closefrom_uses_one_bulk_kernel_mutation() { let runner = include_str!("../assets/runners/wasm-runner.mjs"); @@ -7397,7 +7455,7 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet #[test] fn wasi_preview1_import_manifest_matches_native_runner() { let manifest: Value = serde_json::from_str(include_str!("../assets/agentos-wasm-abi.json")) - .expect("parse AgentOS WASM ABI manifest"); + .expect("parse agentOS WASM ABI manifest"); let expected = manifest["imports"] .as_array() .expect("ABI imports array") @@ -7993,6 +8051,14 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet assert!(runner.contains("attachManagedHostNetDescription(guestFd, descriptionId);")); assert!(runner.contains("descriptionId: handle?.hostNetDescriptionId ?? null")); assert!(runner.contains("fd = registerKernelDelegateFd(received.fd);")); + assert!(runner.contains("function managedHostNetOperationIsNonblocking(")); + assert_eq!( + runner + .matches("managedHostNetOperationIsNonblocking(socket, recvFlags)") + .count(), + 2, + "recv and recvfrom must both honor kernel-owned O_NONBLOCK" + ); assert!(runner.contains("const managedHostNetKernelGuestFds = new Set(")); assert!(runner.contains( "initialKernelGuestFds.has(guestFd) && !managedHostNetKernelGuestFds.has(guestFd)" @@ -8084,11 +8150,27 @@ process.stdout.write(JSON.stringify({{ overflow, exactLine: exact.line, exactRet let mapper = &runner[start..end]; assert!(mapper.contains("case 'ENOENT':\n return WASI_ERRNO_NOENT;")); assert!(mapper.contains("case 'EINTR':\n return WASI_ERRNO_INTR;")); + assert!(mapper.contains("case 'ENOSPC':\n return WASI_ERRNO_NOSPC;")); assert!(mapper.contains("default:\n return WASI_ERRNO_FAULT;")); assert!(!mapper.contains("command not found")); assert!(!mapper.contains("error?.message")); } + #[test] + fn synthetic_filesystem_errno_mapping_preserves_file_size_limit_errors() { + let runner = include_str!("../assets/runners/wasm-runner.mjs"); + let start = runner + .find("function mapSyntheticFsError(error) {") + .expect("synthetic filesystem errno mapper"); + let end = runner[start..] + .find("function mapHostProcessError(error) {") + .map(|offset| start + offset) + .expect("synthetic filesystem errno mapper end"); + assert!(runner[start..end].contains("case 'EFBIG':\n return WASI_ERRNO_FBIG;")); + assert!(runner.contains("const rangeOffset = BigInt(offset);")); + assert!(runner.contains("rangeOffset.toString(),\n rangeLength.toString(),")); + } + #[test] fn managed_signal_dispatch_drains_the_published_delivery_without_double_claiming() { let runner = include_str!("../assets/runners/wasm-runner.mjs"); diff --git a/crates/execution/src/wasm/wasmtime/engine.rs b/crates/execution/src/wasm/wasmtime/engine.rs index 05c30de2d3..abdc6b96ba 100644 --- a/crates/execution/src/wasm/wasmtime/engine.rs +++ b/crates/execution/src/wasm/wasmtime/engine.rs @@ -31,10 +31,10 @@ pub struct WasmtimeMetricsSnapshot { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum WasmtimeFeatureProfile { - /// AgentOS-owned Preview1/POSIX ABI with the proposal switches configured + /// agentOS-owned Preview1/POSIX ABI with the proposal switches configured /// in `build_engine`; changing any switch requires a new keyed variant. AgentOsOwnedWasiV1, - /// AgentOS-owned Preview1/POSIX ABI plus the core WebAssembly threads + /// agentOS-owned Preview1/POSIX ABI plus the core WebAssembly threads /// proposal. This profile is selected explicitly and never inferred. AgentOsOwnedWasiV1Threads, } @@ -43,23 +43,44 @@ pub enum WasmtimeFeatureProfile { pub struct WasmtimeEngineProfile { pub feature_profile: WasmtimeFeatureProfile, pub wasm_stack_bytes: usize, + pub deterministic_fuel: bool, } impl WasmtimeEngineProfile { pub fn new(wasm_stack_bytes: Option) -> Result { - Self::with_feature_profile(wasm_stack_bytes, WasmtimeFeatureProfile::AgentOsOwnedWasiV1) + Self::new_with_deterministic_fuel(wasm_stack_bytes, false) + } + + pub fn new_with_deterministic_fuel( + wasm_stack_bytes: Option, + deterministic_fuel: bool, + ) -> Result { + Self::with_feature_profile( + wasm_stack_bytes, + WasmtimeFeatureProfile::AgentOsOwnedWasiV1, + deterministic_fuel, + ) } pub fn new_threaded(wasm_stack_bytes: Option) -> Result { + Self::new_threaded_with_deterministic_fuel(wasm_stack_bytes, false) + } + + pub fn new_threaded_with_deterministic_fuel( + wasm_stack_bytes: Option, + deterministic_fuel: bool, + ) -> Result { Self::with_feature_profile( wasm_stack_bytes, WasmtimeFeatureProfile::AgentOsOwnedWasiV1Threads, + deterministic_fuel, ) } fn with_feature_profile( wasm_stack_bytes: Option, feature_profile: WasmtimeFeatureProfile, + deterministic_fuel: bool, ) -> Result { let wasm_stack_bytes = wasm_stack_bytes .map(usize::try_from) @@ -75,6 +96,7 @@ impl WasmtimeEngineProfile { Ok(Self { feature_profile, wasm_stack_bytes, + deterministic_fuel, }) } @@ -293,7 +315,7 @@ fn build_engine(profile: WasmtimeEngineProfile) -> Result