diff --git a/crates/native-sidecar-core/src/lib.rs b/crates/native-sidecar-core/src/lib.rs index 6c581349e9..e4172a77da 100644 --- a/crates/native-sidecar-core/src/lib.rs +++ b/crates/native-sidecar-core/src/lib.rs @@ -84,10 +84,9 @@ pub use root_fs::{ root_snapshot_from_entries, SidecarCoreError, }; pub use router::{ - connection_id_of, generated_wire_blocking_extension_interrupt, request_dispatch_mode, - request_is_unsupported_host_callback_direction, route_request_payload, session_scope_of, - unsupported_host_callback_direction_dispatch, vm_id_of, BlockingExtensionInterrupt, - RequestDispatchMode, RequestRoute, UNSUPPORTED_HOST_CALLBACK_DIRECTION_CODE, + connection_id_of, request_dispatch_mode, request_is_unsupported_host_callback_direction, + route_request_payload, session_scope_of, unsupported_host_callback_direction_dispatch, + vm_id_of, RequestDispatchMode, RequestRoute, UNSUPPORTED_HOST_CALLBACK_DIRECTION_CODE, UNSUPPORTED_HOST_CALLBACK_DIRECTION_MESSAGE, }; pub use signals::{ diff --git a/crates/native-sidecar-core/src/router.rs b/crates/native-sidecar-core/src/router.rs index f317924511..e7651b9e56 100644 --- a/crates/native-sidecar-core/src/router.rs +++ b/crates/native-sidecar-core/src/router.rs @@ -10,7 +10,6 @@ use agentos_sidecar_protocol::protocol::{ RequestFrame, RequestPayload, ResizePtyRequest, SealLayerRequest, SnapshotRootFilesystemRequest, VmFetchRequest, WriteStdinRequest, }; -use agentos_sidecar_protocol::wire as generated_wire; pub const UNSUPPORTED_HOST_CALLBACK_DIRECTION_CODE: &str = "unsupported_direction"; pub const UNSUPPORTED_HOST_CALLBACK_DIRECTION_MESSAGE: &str = @@ -63,12 +62,6 @@ pub enum RequestRoute { UnsupportedHostCallbackDirection, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlockingExtensionInterrupt<'a> { - ExtensionPayload(&'a [u8]), - KillProcess, -} - pub fn route_request_payload(request: &RequestFrame) -> RequestRoute { match request.payload.clone() { RequestPayload::Authenticate(payload) => RequestRoute::Authenticate(payload), @@ -145,31 +138,6 @@ pub fn route_request_payload(request: &RequestFrame) -> RequestRoute { } } -pub fn generated_wire_blocking_extension_interrupt<'a>( - active_request: &generated_wire::RequestFrame, - blocking_namespace: &str, - interrupting_request: &'a generated_wire::RequestFrame, -) -> Option> { - if interrupting_request.ownership != active_request.ownership { - return None; - } - - match &interrupting_request.payload { - generated_wire::RequestPayload::ExtEnvelope(envelope) - if envelope.namespace == blocking_namespace => - { - Some(BlockingExtensionInterrupt::ExtensionPayload( - &envelope.payload, - )) - } - generated_wire::RequestPayload::ExtEnvelope(_) => None, - generated_wire::RequestPayload::KillProcessRequest(_) => { - Some(BlockingExtensionInterrupt::KillProcess) - } - _ => None, - } -} - pub fn request_dispatch_mode(request: &RequestFrame) -> RequestDispatchMode { match request.payload { RequestPayload::DisposeVm(_) | RequestPayload::Ext(_) => RequestDispatchMode::Async, @@ -297,25 +265,11 @@ mod tests { OwnershipScope, PersistenceFlushRequest, PersistenceLoadRequest, ResponsePayload, PROTOCOL_VERSION, }; - use agentos_sidecar_protocol::wire as generated_wire; fn request(payload: RequestPayload) -> RequestFrame { RequestFrame::new(7, OwnershipScope::connection("conn"), payload) } - fn generated_request( - request_id: i64, - ownership: generated_wire::OwnershipScope, - payload: generated_wire::RequestPayload, - ) -> generated_wire::RequestFrame { - generated_wire::RequestFrame { - schema: generated_wire::protocol_schema(), - request_id, - ownership, - payload, - } - } - fn reverse_host_callback_payloads() -> Vec { vec![ RequestPayload::HostFilesystemCall(HostFilesystemCallRequest { @@ -423,83 +377,6 @@ mod tests { } } - #[test] - fn generated_wire_prompt_interrupt_classifier_matches_only_same_scope_interrupts() { - let ownership = generated_wire::OwnershipScope::VmOwnership(generated_wire::VmOwnership { - connection_id: String::from("conn"), - session_id: String::from("session"), - vm_id: String::from("vm"), - }); - let active = generated_request( - 1, - ownership.clone(), - generated_wire::RequestPayload::ExtEnvelope(generated_wire::ExtEnvelope { - namespace: String::from("prompt"), - payload: b"active".to_vec(), - }), - ); - - let same_namespace = generated_request( - 2, - ownership.clone(), - generated_wire::RequestPayload::ExtEnvelope(generated_wire::ExtEnvelope { - namespace: String::from("prompt"), - payload: b"cancel".to_vec(), - }), - ); - assert_eq!( - generated_wire_blocking_extension_interrupt(&active, "prompt", &same_namespace), - Some(BlockingExtensionInterrupt::ExtensionPayload(b"cancel")) - ); - - let kill = generated_request( - 3, - ownership.clone(), - generated_wire::RequestPayload::KillProcessRequest( - generated_wire::KillProcessRequest { - process_id: String::from("proc"), - signal: String::from("SIGTERM"), - }, - ), - ); - assert_eq!( - generated_wire_blocking_extension_interrupt(&active, "prompt", &kill), - Some(BlockingExtensionInterrupt::KillProcess) - ); - - let other_namespace = generated_request( - 4, - ownership.clone(), - generated_wire::RequestPayload::ExtEnvelope(generated_wire::ExtEnvelope { - namespace: String::from("other"), - payload: b"cancel".to_vec(), - }), - ); - assert_eq!( - generated_wire_blocking_extension_interrupt(&active, "prompt", &other_namespace), - None - ); - - let other_scope = generated_request( - 5, - generated_wire::OwnershipScope::VmOwnership(generated_wire::VmOwnership { - connection_id: String::from("conn"), - session_id: String::from("session"), - vm_id: String::from("other-vm"), - }), - generated_wire::RequestPayload::KillProcessRequest( - generated_wire::KillProcessRequest { - process_id: String::from("proc"), - signal: String::from("SIGTERM"), - }, - ), - ); - assert_eq!( - generated_wire_blocking_extension_interrupt(&active, "prompt", &other_scope), - None - ); - } - #[test] fn ownership_scope_helpers_extract_shared_ids() { let connection = OwnershipScope::connection("conn-1"); diff --git a/crates/native-sidecar/CLAUDE.md b/crates/native-sidecar/CLAUDE.md index e44c153544..c74bd73816 100644 --- a/crates/native-sidecar/CLAUDE.md +++ b/crates/native-sidecar/CLAUDE.md @@ -17,7 +17,7 @@ Migration status: **resource limits** (typed `*ExecutionLimits` on the execution - `RequestPayload::Ext`, `ResponsePayload::ExtResult`, `EventPayload::Ext`, and sidecar callback `Ext` payloads are opaque to core sidecar code; dispatch only by namespace and leave inner payload decoding to the registered extension. - `ExtensionContext` primitives should delegate to existing `NativeSidecar` ownership, process, event, and callback paths instead of giving extensions direct access to internal maps such as VM tables or ACP session state. - Extension callbacks and events must stay transport-agnostic: do not expose stdio, socket, or browser `postMessage` details through the `Extension` trait or `ExtensionContext`. -- Stdio blocking-request interruption must stay extension-owned. Core stdio may call generic `Extension` hooks, but production agentos-native-sidecar code must not decode ACP payloads or depend on `agentos-protocol`. +- Progress classification must stay extension-owned and opaque. Core stdio may call the generic `Extension::request_class` hook, but production native-sidecar code must not decode ACP payloads, depend on `agentos-protocol`, or reintroduce prompt-specific interruption hooks/pending-frame slots. - Sidecar-to-host callback protocol must stay agent-agnostic: use `HostCallback{callback_key}` for generic host callbacks, and keep binding collection-specific naming and schemas out of the core callback frame. - Legacy ACP helpers under `tests/acp_legacy/` are fixtures only; production ACP behavior belongs in `crates/agentos-sidecar`, not `crates/sidecar/src`. - Binding CLI `--json` and `--json-file` payloads in `src/bindings.rs` must be validated against the registered host callback `input_schema` before building `HostCallbackRequest`; relying on the host callback to fail closed leaves non-TypeScript hosts and any pre-dispatch checks exposed to raw, unvalidated payload shapes. diff --git a/crates/native-sidecar/src/extension.rs b/crates/native-sidecar/src/extension.rs index 4f28a8181c..eff93b8ec7 100644 --- a/crates/native-sidecar/src/extension.rs +++ b/crates/native-sidecar/src/extension.rs @@ -849,25 +849,6 @@ pub enum ExtensionOrderingPolicy { ExtensionManaged, } -/// Compatibility input for the legacy blocking-extension stdio path. The -/// routed protocol engine no longer uses this hook; it remains present only in -/// the lower ownership-partition revision until that caller is removed later -/// in the stack. -pub enum ExtensionInterruptRequest<'a> { - ExtensionPayload { - payload: &'a [u8], - ownership: &'a OwnershipScope, - }, - KillProcess, -} - -#[derive(Debug, Clone)] -pub struct ExtensionInterruptResponse { - pub interrupt_active: bool, - pub interrupted_response_payload: Vec, - pub interrupting_response_payload: Option>, -} - pub trait Extension: Send + Sync { fn namespace(&self) -> &str; @@ -930,18 +911,6 @@ pub trait Extension: Send + Sync { Box::pin(async { Ok(()) }) } - fn is_blocking_request(&self, _payload: &[u8]) -> bool { - false - } - - fn interrupt_blocking_request( - &self, - _blocking_payload: &[u8], - _interrupt: ExtensionInterruptRequest<'_>, - ) -> Option { - None - } - fn on_dispose<'a>(&'a self) -> ExtensionFuture<'a, ()> { Box::pin(async { Ok(()) }) } diff --git a/crates/native-sidecar/src/lib.rs b/crates/native-sidecar/src/lib.rs index 2bc5031f86..86a570c507 100644 --- a/crates/native-sidecar/src/lib.rs +++ b/crates/native-sidecar/src/lib.rs @@ -29,8 +29,8 @@ pub mod vm_sqlite; pub use agentos_sidecar_protocol::{generated_protocol, protocol, wire}; pub use extension::{ - Extension, ExtensionContext, ExtensionFuture, ExtensionInterruptRequest, - ExtensionInterruptResponse, ExtensionOrderingPolicy, ExtensionRequestClass, ExtensionResponse, + Extension, ExtensionContext, ExtensionFuture, ExtensionOrderingPolicy, ExtensionRequestClass, + ExtensionResponse, }; pub use service::{DispatchResult, NativeSidecar, NativeSidecarConfig, SidecarError}; pub use state::EventSinkTransport; diff --git a/crates/native-sidecar/tests/architecture_guards.rs b/crates/native-sidecar/tests/architecture_guards.rs index 560e26e393..f656c3fae2 100644 --- a/crates/native-sidecar/tests/architecture_guards.rs +++ b/crates/native-sidecar/tests/architecture_guards.rs @@ -665,6 +665,316 @@ fn generic_runtime_layers_do_not_depend_on_product_or_acp_layers() { ); } +#[test] +fn native_sidecar_has_no_prompt_specific_interrupt_workaround() { + let root = repo_root(); + let sources = [ + "crates/native-sidecar/src/stdio.rs", + "crates/native-sidecar/src/extension.rs", + "crates/native-sidecar-core/src/router.rs", + "crates/native-sidecar-core/src/lib.rs", + ]; + let obsolete = [ + "pending_frame", + "dispatch_with_prompt_interrupt", + "BlockingExtensionInterrupt", + "ExtensionInterruptRequest", + "interrupt_blocking_request", + "is_blocking_request", + ]; + + for relative_path in sources { + let source = std::fs::read_to_string(root.join(relative_path)) + .unwrap_or_else(|error| panic!("read {relative_path}: {error}")); + for marker in obsolete { + assert!( + !source.contains(marker), + "generic native-sidecar routing must not restore ACP prompt workaround marker {marker} in {relative_path}" + ); + } + } + + let native_manifest = dependency_keys(&root.join("crates/native-sidecar/Cargo.toml")); + assert!( + !native_manifest.contains("agentos-protocol"), + "native-sidecar must classify extension progress without depending on ACP protocol types" + ); + + let extension_contract = + std::fs::read_to_string(root.join("crates/native-sidecar/src/extension.rs")) + .expect("read generic extension contract"); + assert!( + extension_contract.contains("fn request_ordering_key("), + "generic extension contract must expose an opaque ordering-key hook" + ); + assert!( + !extension_contract.contains("agentos_protocol"), + "generic extension ordering must not depend on ACP protocol types" + ); +} + +#[test] +fn extension_context_cannot_borrow_the_whole_sidecar() { + let root = repo_root(); + let extension = std::fs::read_to_string(root.join("crates/native-sidecar/src/extension.rs")) + .expect("read extension contract"); + for forbidden in [ + "ExtensionHostBackend::Borrowed", + "Borrowed(&'a mut dyn ExtensionHost)", + "ExtensionContext::new", + ] { + assert!( + !extension.contains(forbidden), + "extension requests must use cloneable owned services, not whole-sidecar borrow marker {forbidden}" + ); + } + assert!( + extension.contains("services: Arc") + && extension.contains("fn with_services("), + "ExtensionContext must retain only cloneable transport-agnostic services" + ); +} + +#[test] +fn owned_javascript_event_preparation_does_not_execute_business_work_inline() { + let root = repo_root(); + let source = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/child_process.rs")) + .expect("read JavaScript process-event service source"); + let start = source + .find("pub(crate) fn prepare_owned_javascript_process_event_service(") + .expect("owned JavaScript event preparation function"); + let tail = &source[start..]; + let end = tail + .find("pub(crate) async fn spawn_descendant_javascript_child_process_for_test(") + .expect("function following owned JavaScript event preparation"); + let body = &tail[..end]; + + assert!( + body.contains("Pin> + 'static>>"), + "owned JavaScript event preparation must return detached supervised work" + ); + assert!( + !body.contains("poll_descendant_javascript_child_process(") + && !body.contains("handle_javascript_process_rpc("), + "owned preparation must not restore a legacy whole-sidecar async RPC handler" + ); + + let special_setup = body + .find("let cache_root = self.cache_root.clone();") + .expect("special RPC setup boundary"); + let supervised_start = body[special_setup..] + .find("Box::pin(async move {") + .map(|offset| special_setup + offset) + .expect("special RPC supervised future boundary"); + let inline_preparation = &body[..supervised_start]; + for business_operation in [ + "poll_owned_descendant_javascript_child_process(", + "write_descendant_javascript_child_process_stdin_owned(", + "close_descendant_javascript_child_process_stdin_owned(", + "kill_descendant_javascript_child_process_owned(", + "commit_wasm_fd_process_image_owned(", + "exec_javascript_process_image_owned(", + "handle_owned_process_kill_rpc(", + ] { + assert!( + !inline_preparation.contains(business_operation), + "business operation {business_operation} must start only after the supervised owned future is polled" + ); + assert!( + body[supervised_start..].contains(business_operation), + "owned JavaScript service lost supervised operation {business_operation}" + ); + } +} + +#[test] +fn owned_python_event_preparation_does_not_execute_business_work_inline() { + let root = repo_root(); + let subprocess = std::fs::read_to_string( + root.join("crates/native-sidecar/src/execution/python/subprocess.rs"), + ) + .expect("read Python process-event service source"); + let start = subprocess + .find("pub(crate) fn prepare_owned_python_process_event_service") + .expect("owned Python event preparation function"); + let tail = &subprocess[start..]; + let end = tail + .find("pub(crate) fn prepare_owned_python_subprocess_run") + .expect("function following owned Python event preparation"); + let body = &tail[..end]; + + assert!( + body.contains("Pin> + 'static>>"), + "owned Python event preparation must return detached supervised work" + ); + let supervised_start = body + .find("Box::pin(async move {") + .expect("Python event supervised future boundary"); + let inline_preparation = &body[..supervised_start]; + assert!( + !inline_preparation.contains("try_command("), + "Python event preparation must not touch VM state inline" + ); + for business_operation in [ + "prepare_owned_python_subprocess_run", + "service_owned_python_vfs_rpc_request(", + ] { + assert!( + !inline_preparation.contains(business_operation), + "Python business operation {business_operation} must start only after the supervised owned future is polled" + ); + assert!( + body[supervised_start..].contains(business_operation), + "owned Python service lost supervised operation {business_operation}" + ); + } + + let extension_services = + std::fs::read_to_string(root.join("crates/native-sidecar/src/extension_services.rs")) + .expect("read owned extension services"); + let start = extension_services + .find("pub(crate) fn prepare_owned_python_event_service(") + .expect("owned Python extension-service preparation"); + let tail = &extension_services[start..]; + let end = tail + .find("pub(crate) fn prepare_owned_child_bridge_event_service(") + .expect("function following owned Python extension service"); + let body = &tail[..end]; + let supervised_start = body + .find("future: Box::pin(async move {") + .expect("owned Python extension-service future boundary"); + assert!( + !body[..supervised_start].contains("try_command("), + "owned Python extension-service preparation must not touch VM state inline" + ); + + let child_process = + std::fs::read_to_string(root.join("crates/native-sidecar/src/execution/child_process.rs")) + .expect("read child process event routing"); + assert!( + !child_process.contains("ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE"), + "attached Python VFS requests must be claimed as owned work, not answered by an inline fallback" + ); +} + +#[test] +fn protocol_ingress_router_only_registers_and_starts_owned_work() { + let root = repo_root(); + let source = std::fs::read_to_string(root.join("crates/native-sidecar/src/stdio.rs")) + .expect("read protocol engine source"); + let start = source + .find("fn route_protocol_frame(") + .expect("protocol ingress router"); + let tail = &source[start..]; + let end = tail + .find("fn reap_protocol_tasks_nowait(") + .expect("function following protocol ingress router"); + let router = &tail[..end]; + + assert!( + !source.contains("async fn route_protocol_frame("), + "protocol ingress routing must remain synchronous; any await must stay inside independently supervised task bodies" + ); + for forbidden in [ + "dispatch_wire(", + "dispatch_wire_blocking(", + "dispatch_blocking(", + "block_on(", + ] { + assert!( + !router.contains(forbidden), + "protocol ingress router must not execute whole-sidecar business path {forbidden}" + ); + } + assert!( + router.contains("operations.admit(") + && router.contains("progress_requests.admit_owned(") + && router.contains("schedule_prepared_request("), + "protocol ingress router must reserve, register, and start independently supervised work" + ); + for forbidden_global_dispatch in [ + "Arc", + "VecDeque", + ] { + assert!( + !source.contains(forbidden_global_dispatch), + "protocol engine must not restore a global sidecar lock or ordinary request backlog: {forbidden_global_dispatch}" + ); + } +} + +#[test] +fn generic_request_preparation_defers_business_handlers() { + let root = repo_root(); + let service = std::fs::read_to_string(root.join("crates/native-sidecar/src/service.rs")) + .expect("read native sidecar service source"); + let start = service + .find("pub(crate) fn prepare_request_wire(") + .expect("generic request preparation function"); + let tail = &service[start..]; + let end = tail + .find("pub(crate) fn complete_request(") + .expect("function following generic request preparation"); + let preparation = &tail[..end]; + + for forbidden in [ + "PreparedRequest::ready(", + "let result = match route", + "self.authenticate_connection(&request", + "self.open_session(&request", + "self.commit_prepared_membership(", + ] { + assert!( + !preparation.contains(forbidden), + "generic request preparation must stage or own work instead of executing ingress handler {forbidden}" + ); + } + for owned_route in [ + "let future = register_host_callbacks(self, &request, payload);", + "let future = self.get_process_snapshot(&request, payload);", + "let future = self.get_resource_snapshot(&request, payload);", + "let future = self.get_zombie_timer_count(&request, payload);", + "let future = self.provided_commands(&request, payload);", + "let future = self.list_mounts(&request, payload);", + ] { + assert!( + preparation.contains(owned_route), + "generic prepared route lost its owned deferred future: {owned_route}" + ); + } + assert!( + preparation.contains("PreparedRequest::from_future_with_membership(") + && preparation.contains("PreparedMembershipCommit::Connection") + && preparation.contains("PreparedMembershipCommit::Session"), + "connection/session requests must stage bounded central membership mutations" + ); + + let bindings = std::fs::read_to_string(root.join("crates/native-sidecar/src/bindings.rs")) + .expect("read host callback registration source"); + let start = bindings + .find("pub(crate) fn register_host_callbacks") + .expect("host callback preparation function"); + let tail = &bindings[start..]; + let future_boundary = tail + .find("async move {") + .expect("owned host callback future boundary"); + let inline_preparation = &tail[..future_boundary]; + for forbidden in [ + "validate_bindings_registration(", + "set_vm_permissions(", + "try_command(", + "refresh_binding_registry(", + ] { + assert!( + !inline_preparation.contains(forbidden), + "host callback preparation must not execute {forbidden} before its owned future is polled" + ); + } +} + #[test] fn shared_acp_runtime_has_no_adapter_name_policy() { let root = repo_root(); @@ -946,29 +1256,59 @@ 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 owned_execute = source + .split("pub(crate) async fn execute_owned") + .nth(1) + .expect("owned top-level execution path"); + let compact: String = owned_execute + .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(") + && compact.contains(").await.map_err(python_error)?;"), "top-level Python startup must await cache materialization and prewarm instead of blocking a Tokio worker" ); assert!( - source.contains(".bundled_pyodide_dist_path_for_vm_async(&vm_id, &vm.runtime_context)"), + compact.contains( + "python_engine.bundled_pyodide_dist_path_for_vm_async(&vm_id,&runtime_context).await" + ), "top-level Pyodide cache materialization must not run synchronously before the async Python start" ); + assert!( + compact.contains("drop(vm);letmutpython_engine=execution_engines.python("), + "top-level Python startup must release mutable VM state before awaiting runtime warmup" + ); } #[test] fn nested_child_start_never_blocks_the_shared_runtime_worker() { - let source = native_execution_source(&repo_root()); + let root = repo_root(); + let source = native_execution_source(&root); + let child_path = root.join("crates/native-sidecar/src/execution/child_process.rs"); + let child_source = std::fs::read_to_string(&child_path) + .unwrap_or_else(|error| panic!("read {child_path:?}: {error}")); + let compact_child: String = child_source + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); assert!( source.contains("pub(crate) async fn spawn_javascript_child_process("), "root child startup must be an async sidecar dispatch path" ); assert!( - source.contains("async fn spawn_descendant_javascript_child_process("), - "descendant child startup must be an async sidecar dispatch path" + compact_child.contains("pub(crate)fnspawn_descendant_javascript_child_process(") + && compact_child + .contains(")->Pin>+'static>>") + && compact_child + .contains("Self::build_owned_descendant_javascript_child_process_spawn("), + "descendant child startup must detach an owned static future from the sidecar dispatcher" + ); + assert!( + compact_child.contains("Box::pin(asyncmove{") + && compact_child.contains("drop(vm);letexecution=execution_start.await?;"), + "descendant child startup must release mutable VM state before awaiting runtime startup" ); assert!( source @@ -1160,6 +1500,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(); @@ -1447,23 +1788,6 @@ fn browser_sources_are_retained_but_disabled_from_native_build_and_publish_gates ); } } - - let mirror_generator = - std::fs::read_to_string(root.join("scripts/generate-agentos-mirror.mjs")) - .expect("read compatibility mirror generator"); - assert!( - mirror_generator.contains("browserShim ? { private: true } : {}") - && mirror_generator.contains("browserShim ? \"publish = false\" : \"\""), - "generated browser compatibility shims must remain private and unpublishable" - ); - for relative_path in [".github/workflows/ci.yml", "scripts/ci.sh"] { - let source = std::fs::read_to_string(root.join(relative_path)) - .unwrap_or_else(|error| panic!("read {relative_path}: {error}")); - assert!( - source.contains("node --test scripts/generate-agentos-mirror.test.mjs"), - "{relative_path} must enforce compatibility-mirror reproducibility" - ); - } } #[test] diff --git a/crates/native-sidecar/tests/crash_isolation.rs b/crates/native-sidecar/tests/crash_isolation.rs index d39090f4a1..0cb290b3c4 100644 --- a/crates/native-sidecar/tests/crash_isolation.rs +++ b/crates/native-sidecar/tests/crash_isolation.rs @@ -132,6 +132,8 @@ fn guest_failure_in_one_vm_does_not_break_peer_vm_execution() { } EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } @@ -219,6 +221,8 @@ fn collect_crash_process_output( | EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } diff --git a/crates/native-sidecar/tests/extension.rs b/crates/native-sidecar/tests/extension.rs index a0ba3b33c4..d26ef5450f 100644 --- a/crates/native-sidecar/tests/extension.rs +++ b/crates/native-sidecar/tests/extension.rs @@ -31,7 +31,7 @@ impl Extension for EchoExtension { fn handle_request<'a>( &'a self, - mut ctx: ExtensionContext<'a>, + mut ctx: ExtensionContext, payload: Vec, ) -> ExtensionFuture<'a, ExtensionResponse> { Box::pin(async move { @@ -168,6 +168,8 @@ impl Extension for EchoExtension { | EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } @@ -196,7 +198,7 @@ impl Extension for VmLifetimeExtension { fn handle_request<'a>( &'a self, - mut ctx: ExtensionContext<'a>, + mut ctx: ExtensionContext, _payload: Vec, ) -> ExtensionFuture<'a, ExtensionResponse> { Box::pin(async move { diff --git a/crates/native-sidecar/tests/kill_cleanup.rs b/crates/native-sidecar/tests/kill_cleanup.rs index 88af875c1d..a88f8b3148 100644 --- a/crates/native-sidecar/tests/kill_cleanup.rs +++ b/crates/native-sidecar/tests/kill_cleanup.rs @@ -275,6 +275,8 @@ fn collect_kill_cleanup_process_output( | EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } diff --git a/crates/native-sidecar/tests/process_isolation.rs b/crates/native-sidecar/tests/process_isolation.rs index 00a5bed1c4..9109978b46 100644 --- a/crates/native-sidecar/tests/process_isolation.rs +++ b/crates/native-sidecar/tests/process_isolation.rs @@ -118,6 +118,8 @@ fn concurrent_vm_processes_stay_isolated_with_vm_scoped_events() { } EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } diff --git a/crates/native-sidecar/tests/python.rs b/crates/native-sidecar/tests/python.rs index fbdbd9c667..d144cad632 100644 --- a/crates/native-sidecar/tests/python.rs +++ b/crates/native-sidecar/tests/python.rs @@ -151,6 +151,8 @@ fn collect_process_output_with_timeout( EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } @@ -920,6 +922,8 @@ fn wait_for_stdout_chunk( EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } @@ -1253,6 +1257,8 @@ fn concurrent_python_processes_stay_isolated_across_vms() { } EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } diff --git a/crates/native-sidecar/tests/security_hardening.rs b/crates/native-sidecar/tests/security_hardening.rs index 7512636cea..0d1351ee80 100644 --- a/crates/native-sidecar/tests/security_hardening.rs +++ b/crates/native-sidecar/tests/security_hardening.rs @@ -142,6 +142,8 @@ fn collect_process_output_bounded( | EventPayload::ProcessExitedEvent(_) | EventPayload::VmLifecycleEvent(_) | EventPayload::StructuredEvent(_) + | EventPayload::ExecutionOutputEvent(_) + | EventPayload::ExecutionCompletedEvent(_) | EventPayload::ExtEnvelope(_) => {} } } diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index 849ab9b39f..3b36f0422c 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -63,13 +63,22 @@ mod wire { // The unit tests include!d from src/service.rs reference crate::stdio::LocalBridge, // and stdio.rs in turn uses these crate-root re-exports (mirrored from lib.rs) so it // compiles inside this integration-test crate too. -use extension::{ - Extension, ExtensionContext, ExtensionFuture, ExtensionInterruptRequest, - ExtensionInterruptResponse, ExtensionResponse, -}; +use extension::{Extension, ExtensionContext, ExtensionFuture, ExtensionResponse}; use service::NativeSidecarConfig; use state::{EventSinkTransport, SidecarRequestTransport}; +#[allow(dead_code)] +#[path = "../src/extension_services.rs"] +mod extension_services; +#[allow(dead_code)] +#[path = "../src/ownership_coordinator.rs"] +mod ownership_coordinator; +#[allow(dead_code)] +#[path = "../src/process_event_broker.rs"] +mod process_event_broker; +#[allow(dead_code)] +#[path = "../src/request_operations.rs"] +mod request_operations; #[allow(dead_code)] #[path = "../src/stdio.rs"] mod stdio; @@ -451,10 +460,19 @@ ykAheWCsAteSEWVc0w==\n\ session_id: String::from("session-queue"), vm_id: String::from("vm-queue"), process_id: format!("proc-queue-{index}"), + child_path: Vec::new(), event: ActiveExecutionEvent::Stdout(Vec::new()), } } + fn process_event_target_label(envelope: &ProcessEventEnvelope) -> String { + if envelope.child_path.is_empty() { + envelope.process_id.clone() + } else { + format!("{}/{}", envelope.process_id, envelope.child_path.join("/")) + } + } + fn insert_binding_process( sidecar: &mut NativeSidecar, vm_id: &str, @@ -1134,10 +1152,8 @@ ykAheWCsAteSEWVc0w==\n\ .expect("create vm"); insert_binding_process(&mut sidecar, &vm_id, "proc-single-event"); - let process = sidecar - .vms - .get_mut(&vm_id) - .expect("test vm") + let mut vm = sidecar.vms.get_mut(&vm_id).expect("test vm"); + let process = vm .active_processes .get_mut("proc-single-event") .expect("test process"); @@ -1149,6 +1165,7 @@ ykAheWCsAteSEWVc0w==\n\ .lock() .expect("binding event queue") .push_back(ActiveExecutionEvent::Stdout(b"single-edge".to_vec())); + drop(vm); // Deliberately do not notify. poll_event's first probe pumps this // already-durable execution event into the sidecar queue; it must @@ -1209,7 +1226,8 @@ ykAheWCsAteSEWVc0w==\n\ connection_id: connection_id.clone(), session_id: session_id.clone(), vm_id: vm_id.clone(), - process_id: String::from("root-proc/child-1"), + process_id: String::from("root-proc"), + child_path: vec![String::from("child-1")], event: ActiveExecutionEvent::Stdout(b"preserve".to_vec()), }) .expect("queue descendant event"); @@ -1226,14 +1244,11 @@ ykAheWCsAteSEWVc0w==\n\ "unexpected overflow error: {error}" ); assert_eq!(sidecar.pending_process_events.len(), 1); - assert_eq!( - sidecar - .pending_process_events - .front() - .expect("preserved global event") - .process_id, - "root-proc/child-1" - ); + let preserved = sidecar + .pending_process_events + .front() + .expect("preserved global event"); + assert_eq!(process_event_target_label(preserved), "root-proc/child-1"); } fn descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes() { @@ -1276,12 +1291,19 @@ ykAheWCsAteSEWVc0w==\n\ .child_processes .insert(String::from("child-1"), child); - let envelope = |process_id: &str, marker: u8| ProcessEventEnvelope { - connection_id: connection_id.clone(), - session_id: session_id.clone(), - vm_id: vm_id.clone(), - process_id: process_id.to_owned(), - event: ActiveExecutionEvent::Stdout(vec![marker]), + let envelope = |process_id: &str, marker: u8| { + let (process_id, child_path) = process_id + .split_once('/') + .map(|(root, child)| (root.to_owned(), vec![child.to_owned()])) + .unwrap_or_else(|| (process_id.to_owned(), Vec::new())); + ProcessEventEnvelope { + connection_id: connection_id.clone(), + session_id: session_id.clone(), + vm_id: vm_id.clone(), + process_id, + child_path, + event: ActiveExecutionEvent::Stdout(vec![marker]), + } }; let expected = vec![ (String::from("other-before"), 1u8), @@ -1296,7 +1318,7 @@ ykAheWCsAteSEWVc0w==\n\ let ActiveExecutionEvent::Stdout(bytes) = &envelope.event else { panic!("expected stdout envelope"); }; - (envelope.process_id.clone(), bytes[0]) + (process_event_target_label(envelope), bytes[0]) }) .collect::>() }; @@ -1368,6 +1390,7 @@ ykAheWCsAteSEWVc0w==\n\ session_id: session_id.clone(), vm_id: vm_id.clone(), process_id: String::from("proc-exit"), + child_path: Vec::new(), event: ActiveExecutionEvent::Stdout(b"trailing".to_vec()), }) .expect("queue trailing process event"); @@ -1379,6 +1402,7 @@ ykAheWCsAteSEWVc0w==\n\ session_id, vm_id: vm_id.clone(), process_id: String::from("proc-exit"), + child_path: Vec::new(), event: ActiveExecutionEvent::Exited(0), }) ) @@ -1512,10 +1536,8 @@ ykAheWCsAteSEWVc0w==\n\ fn sqlite_database_handles_are_bounded() { let (mut sidecar, vm_id) = create_sqlite_handle_test_sidecar(); { - let process = sidecar - .vms - .get_mut(&vm_id) - .expect("sqlite vm") + let mut vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); + let process = vm .active_processes .get_mut("proc-sqlite-handles") .expect("sqlite process"); @@ -1553,10 +1575,8 @@ ykAheWCsAteSEWVc0w==\n\ fn sqlite_statement_handles_are_bounded() { let (mut sidecar, vm_id) = create_sqlite_handle_test_sidecar(); { - let process = sidecar - .vms - .get_mut(&vm_id) - .expect("sqlite vm") + let mut vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); + let process = vm .active_processes .get_mut("proc-sqlite-handles") .expect("sqlite process"); @@ -1670,6 +1690,123 @@ ykAheWCsAteSEWVc0w==\n\ ) } + fn vm_execution_context_counts( + sidecar: &NativeSidecar, + vm_id: &str, + ) -> (usize, usize, usize, usize, usize) { + let engines = sidecar + .vms + .get(vm_id) + .expect("test VM") + .execution_engines + .clone(); + let javascript = engines + .javascript("inspect test context count") + .expect("borrow VM JavaScript engine") + .context_count_for_test(); + let (wasm, wasm_javascript) = { + let wasm_engine = engines + .wasm("inspect test context count") + .expect("borrow VM WebAssembly engine"); + ( + wasm_engine.context_count_for_test(), + wasm_engine.javascript_context_count_for_test(), + ) + }; + let (python, python_javascript) = { + let python_engine = engines + .python("inspect test context count") + .expect("borrow VM Python engine"); + ( + python_engine.context_count_for_test(), + python_engine.javascript_context_count_for_test(), + ) + }; + (javascript, wasm, wasm_javascript, python, python_javascript) + } + + fn create_javascript_context_for_vm_test( + sidecar: &NativeSidecar, + vm_id: &str, + ) -> agentos_execution::JavascriptContext { + let engines = sidecar + .vms + .get(vm_id) + .expect("JavaScript test VM") + .execution_engines + .clone(); + let context = engines + .javascript("create test JavaScript context") + .expect("borrow VM JavaScript engine") + .create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: None, + }); + context + } + + fn start_javascript_execution_for_vm_test( + sidecar: &NativeSidecar, + vm_id: &str, + request: StartJavascriptExecutionRequest, + ) -> Result< + agentos_execution::JavascriptExecution, + agentos_execution::JavascriptExecutionError, + > { + let engines = sidecar + .vms + .get(vm_id) + .expect("JavaScript test VM") + .execution_engines + .clone(); + let result = engines + .javascript("start test JavaScript execution") + .expect("borrow VM JavaScript engine") + .start_execution(request); + result + } + + fn create_python_context_for_vm_test( + sidecar: &NativeSidecar, + vm_id: &str, + pyodide_dist_path: PathBuf, + ) -> agentos_execution::PythonContext { + let engines = sidecar + .vms + .get(vm_id) + .expect("Python test VM") + .execution_engines + .clone(); + let context = engines + .python("create test Python context") + .expect("borrow VM Python engine") + .create_context(CreatePythonContextRequest { + vm_id: vm_id.to_owned(), + pyodide_dist_path, + }); + context + } + + fn start_python_execution_for_vm_test( + sidecar: &NativeSidecar, + vm_id: &str, + request: StartPythonExecutionRequest, + ) -> Result + { + let engines = sidecar + .vms + .get(vm_id) + .expect("Python test VM") + .execution_engines + .clone(); + let result = engines + .python("start test Python execution") + .expect("borrow VM Python engine") + .start_execution(request); + result + } + #[allow(dead_code)] fn create_active_execution_for_tests() -> ActiveExecution { let mut sidecar = create_test_sidecar(); @@ -1685,15 +1822,23 @@ ykAheWCsAteSEWVc0w==\n\ .expect("create vm"); let cwd = temp_dir("agentos-native-sidecar-js-crypto-rpc"); write_fixture(&cwd.join("entry.mjs"), "export {};\n"); - let context = sidecar.javascript_engine.create_context( + let engines = sidecar + .vms + .get(&vm_id) + .expect("javascript vm") + .execution_engines + .clone(); + let mut javascript_engine = engines + .javascript("create active execution test fixture") + .expect("borrow javascript execution engine"); + let context = javascript_engine.create_context( agentos_execution::CreateJavascriptContextRequest { vm_id: vm_id.clone(), bootstrap_module: None, compile_cache_root: None, }, ); - let execution = sidecar - .javascript_engine + let execution = javascript_engine .start_execution(agentos_execution::StartJavascriptExecutionRequest { guest_runtime: Default::default(), vm_id, @@ -2614,18 +2759,18 @@ console.log(JSON.stringify({ status: "ok", summary })); progressed = true; continue; } - let event = sidecar - .vms - .get_mut(&workload.vm_id) - .and_then(|vm| vm.active_processes.get_mut(&workload.process_id)) - .and_then(|process| { - if let Some(event) = process.pending_execution_events.pop_front() { - Some(event) - } else { - poll_test_execution_event(process, Duration::from_millis(5)) - .expect("poll concurrent VM execution") - } - }); + let event = sidecar.vms.get_mut(&workload.vm_id).and_then(|mut vm| { + vm.active_processes + .get_mut(&workload.process_id) + .and_then(|process| { + if let Some(event) = process.pending_execution_events.pop_front() { + Some(event) + } else { + poll_test_execution_event(process, Duration::from_millis(5)) + .expect("poll concurrent VM execution") + } + }) + }); let Some(event) = event else { output.settled = output.exit_code.is_some(); continue; @@ -2748,6 +2893,7 @@ console.log(JSON.stringify({ status: "ok", summary })); runtime_context: vm.runtime_context.clone(), capabilities: vm.capabilities.clone(), }); + drop(vm); start_javascript_entry_with_env( sidecar, &vm_id, @@ -3202,16 +3348,21 @@ console.log(JSON.stringify({ status: "ok", summary })); process_id: &str, env: BTreeMap, ) { - 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 + let engines = sidecar + .vms + .get(vm_id) + .expect("javascript vm") + .execution_engines + .clone(); + let mut javascript_engine = engines + .javascript("start JavaScript entry test fixture") + .expect("borrow javascript execution engine"); + let context = javascript_engine.create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: None, + }); + let execution = javascript_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), @@ -3227,7 +3378,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -3242,14 +3393,16 @@ console.log(JSON.stringify({ status: "ok", summary })); }; { - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -3480,7 +3633,7 @@ console.log(JSON.stringify({ status: "ok", summary })); continue; } let next_event = { - let vm = sidecar.vms.get_mut(vm_id).expect("active vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("active vm"); vm.active_processes.get_mut(process_id).and_then(|process| { if let Some(event) = process.pop_pending_execution_event() { Some(event) @@ -3549,7 +3702,7 @@ console.log(JSON.stringify({ status: "ok", summary })); continue; } let event = { - let Some(vm) = sidecar.vms.get_mut(vm_id) else { + let Some(mut vm) = sidecar.vms.get_mut(vm_id) else { continue; }; let Some(process) = vm.active_processes.get_mut(&process_id) else { @@ -3579,14 +3732,12 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("handle sibling internal process event"); progressed = true; - } else if let Some(process) = sidecar - .vms - .get_mut(vm_id) - .and_then(|vm| vm.active_processes.get_mut(&process_id)) - { - process - .queue_pending_execution_event(event) - .expect("requeue sibling public process event"); + } else if let Some(mut vm) = sidecar.vms.get_mut(vm_id) { + if let Some(process) = vm.active_processes.get_mut(&process_id) { + process + .queue_pending_execution_event(event) + .expect("requeue sibling public process event"); + } } } @@ -3605,7 +3756,7 @@ console.log(JSON.stringify({ status: "ok", summary })); let mut stdout = Vec::new(); for _ in 0..64 { let next_event = { - let vm = sidecar.vms.get_mut(vm_id).expect("active vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("active vm"); vm.active_processes.get_mut(process_id).and_then(|process| { if let Some(event) = process.pop_pending_execution_event() { Some(event) @@ -3799,12 +3950,19 @@ console.log(JSON.stringify({ status: "ok", summary })); process_id: &str, attach_stdout_pty: bool, ) -> Option { - let context = sidecar - .wasm_engine - .create_context(CreateWasmContextRequest { - vm_id: vm_id.to_owned(), - module_path: Some(String::from("./guest.wasm")), - }); + let engines = sidecar + .vms + .get(vm_id) + .expect("wasm vm") + .execution_engines + .clone(); + let mut wasm_engine = engines + .wasm("start WebAssembly test fixture") + .expect("borrow WebAssembly execution engine"); + let context = wasm_engine.create_context(CreateWasmContextRequest { + vm_id: vm_id.to_owned(), + module_path: Some(String::from("./guest.wasm")), + }); let env = { let vm = sidecar.vms.get(vm_id).expect("wasm vm"); @@ -3817,8 +3975,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ]) }; - let execution = sidecar - .wasm_engine + let execution = wasm_engine .start_execution(StartWasmExecutionRequest { guest_runtime: Default::default(), limits: Default::default(), @@ -3832,7 +3989,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("start fake wasm execution"); let (kernel_handle, master_fd) = { - let vm = sidecar.vms.get_mut(vm_id).expect("wasm vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("wasm vm"); let kernel_handle = vm .kernel .spawn_process( @@ -3864,7 +4021,7 @@ console.log(JSON.stringify({ status: "ok", summary })); (kernel_handle, master_fd) }; - let vm = sidecar.vms.get_mut(vm_id).expect("wasm vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("wasm vm"); let kernel_pid = kernel_handle.pid(); vm.active_processes.insert( process_id.to_owned(), @@ -3888,16 +4045,21 @@ 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 + let engines = sidecar + .vms + .get(vm_id) + .expect("javascript vm") + .execution_engines + .clone(); + let mut javascript_engine = engines + .javascript("start fake JavaScript process") + .expect("borrow javascript execution engine"); + let context = javascript_engine.create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: None, + }); + let execution = javascript_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), @@ -3913,7 +4075,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -3927,14 +4089,16 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("spawn kernel javascript process") }; - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -3949,7 +4113,8 @@ console.log(JSON.stringify({ status: "ok", summary })); process_id: &str, ) { let (kernel_handle, guest_env) = { - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let guest_env = vm.guest_env.clone(); let handle = vm .kernel .create_virtual_process( @@ -3958,16 +4123,16 @@ console.log(JSON.stringify({ status: "ok", summary })); JAVASCRIPT_COMMAND, vec![String::from(JAVASCRIPT_COMMAND)], VirtualProcessOptions { - env: vm.guest_env.clone(), + env: guest_env.clone(), cwd: Some(String::from("/")), ..VirtualProcessOptions::default() }, ) .expect("create virtual javascript parent"); - (handle, vm.guest_env.clone()) + (handle, guest_env) }; - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); vm.active_processes.insert( process_id.to_owned(), active_process_for_tests( @@ -3998,15 +4163,16 @@ console.log(JSON.stringify({ status: "ok", summary })); 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_javascript_socket_path_context(&vm).expect("build socket path context"), vm.capabilities.clone(), vm.kernel_socket_readiness.clone(), ) }; - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut(process_id) .expect("javascript process"); runtime_handle.block_on(service_javascript_sync_rpc( @@ -4015,7 +4181,7 @@ console.log(JSON.stringify({ status: "ok", summary })); vm_id, dns: &dns, socket_paths: &socket_paths, - kernel: &mut vm.kernel, + kernel, kernel_readiness, process, sync_request: &request, @@ -4035,14 +4201,15 @@ console.log(JSON.stringify({ status: "ok", summary })); 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_javascript_socket_path_context(&vm).expect("build socket path context"), vm.capabilities.clone(), vm.kernel_socket_readiness.clone(), ) }; - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut(process_id) .expect("javascript process"); service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { @@ -4050,7 +4217,7 @@ console.log(JSON.stringify({ status: "ok", summary })); vm_id, dns: &dns, socket_paths: &socket_paths, - kernel: &mut vm.kernel, + kernel, kernel_readiness, process, sync_request: &request, @@ -4088,7 +4255,7 @@ console.log(JSON.stringify({ status: "ok", summary })); if method != "net.connect" { return Ok(result); } - let vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("javascript vm"); let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); let process = vm .active_processes @@ -4287,7 +4454,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("bind kernel-backed udp socket"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("vm state"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("vm state"); let process = vm .active_processes .get_mut("proc-js-kernel-query") @@ -4771,8 +4938,8 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("bind kernel-backed udp socket"); - let vm = sidecar.vms.get_mut(&vm_id).expect("vm state"); - let before = vm_network_resource_snapshot(vm); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("vm state"); + let before = vm_network_resource_snapshot(&vm); assert_eq!(before.capabilities.len(), 2); assert_eq!(before.capability_usage, 2); assert_eq!(before.sockets, 2); @@ -4844,7 +5011,7 @@ console.log(JSON.stringify({ status: "ok", summary })); assert_eq!(kernel_snapshot.sockets, 2); assert_eq!(kernel_snapshot.socket_connections, 0); - let after = vm_network_resource_snapshot(vm); + let after = vm_network_resource_snapshot(&vm); assert_eq!(after, before); } @@ -7170,16 +7337,21 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("create vm"); let cwd = temp_dir("agentos-native-sidecar-js-kernel-stdin-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 + let execution_engines = sidecar + .vms + .get(&vm_id) + .expect("javascript vm") + .execution_engines + .clone(); + let mut javascript_engine = execution_engines + .javascript("prepare kernel stdin test execution") + .expect("borrow VM JavaScript engine"); + let context = javascript_engine.create_context(CreateJavascriptContextRequest { + vm_id: vm_id.clone(), + bootstrap_module: None, + compile_cache_root: None, + }); + let execution = javascript_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), @@ -7198,8 +7370,9 @@ console.log(JSON.stringify({ status: "ok", summary })); wasm_module_bytes: None, }) .expect("start fake javascript execution"); + drop(javascript_engine); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -7213,7 +7386,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("spawn kernel javascript process") }; let kernel_stdin_writer_fd = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let (read_fd, write_fd) = vm .kernel .open_pipe(EXECUTION_DRIVER_NAME, kernel_handle.pid()) @@ -7227,7 +7400,7 @@ console.log(JSON.stringify({ status: "ok", summary })); write_fd }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-stdin"), active_process_for_tests( @@ -7341,16 +7514,21 @@ console.log(JSON.stringify({ status: "ok", summary })); let cwd = temp_dir("agentos-native-sidecar-js-pty-raw-mode"); write_fixture(&cwd.join("entry.mjs"), "export {};\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 + let execution_engines = sidecar + .vms + .get(&vm_id) + .expect("javascript vm") + .execution_engines + .clone(); + let mut javascript_engine = execution_engines + .javascript("prepare PTY raw-mode test execution") + .expect("borrow VM JavaScript engine"); + let context = javascript_engine.create_context(CreateJavascriptContextRequest { + vm_id: vm_id.clone(), + bootstrap_module: None, + compile_cache_root: None, + }); + let execution = javascript_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), @@ -7364,8 +7542,9 @@ console.log(JSON.stringify({ status: "ok", summary })); wasm_module_bytes: None, }) .expect("start fake javascript execution"); + drop(javascript_engine); let (terminal_owner_handle, terminal_owner_pid, terminal_master_fd, kernel_handle) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let terminal_owner_handle = vm .kernel .spawn_process( @@ -7418,7 +7597,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ) }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let mut child = active_process_for_tests( kernel_handle.pid(), kernel_handle, @@ -7527,7 +7706,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("child exit should not be stale"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let termios = vm .kernel .tcgetattr( @@ -7566,13 +7745,27 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("create vm b"); - let cache_path_a = sidecar - .javascript_engine + let engines_a = sidecar + .vms + .get(&vm_a) + .expect("vm a") + .execution_engines + .clone(); + let engines_b = sidecar + .vms + .get(&vm_b) + .expect("vm b") + .execution_engines + .clone(); + let cache_path_a = engines_a + .javascript("materialize vm a import cache") + .expect("borrow vm a JavaScript engine") .materialize_import_cache_for_vm(&vm_a) .expect("materialize vm a import cache") .to_path_buf(); - let cache_path_b = sidecar - .javascript_engine + let cache_path_b = engines_b + .javascript("materialize vm b import cache") + .expect("borrow vm b JavaScript engine") .materialize_import_cache_for_vm(&vm_b) .expect("materialize vm b import cache") .to_path_buf(); @@ -7589,6 +7782,8 @@ console.log(JSON.stringify({ status: "ok", summary })); assert!(cache_root_a.exists(), "vm a cache root should exist"); assert!(cache_root_b.exists(), "vm b cache root should exist"); + drop(engines_a); + sidecar .dispose_vm_internal_blocking( &connection_id, @@ -7606,18 +7801,16 @@ console.log(JSON.stringify({ status: "ok", summary })); cache_root_b.exists(), "vm b cache root should remain until that VM is disposed" ); - assert!( - sidecar - .javascript_engine - .import_cache_path_for_vm(&vm_a) - .is_none(), - "vm a cache entry should be removed from the engine" - ); assert_eq!( - sidecar.javascript_engine.import_cache_path_for_vm(&vm_b), + engines_b + .javascript("inspect vm b import cache") + .expect("borrow vm b JavaScript engine") + .import_cache_path_for_vm(&vm_b), Some(cache_path_b.as_path()) ); + drop(engines_b); + sidecar .dispose_vm_internal_blocking( &connection_id, @@ -7688,8 +7881,9 @@ console.log(JSON.stringify({ status: "ok", summary })); PermissionsPolicy::allow_all(), ) .expect("create live vm"); - let vm = sidecar.vms.get_mut(&live_vm_id).expect("live vm"); + let mut vm = sidecar.vms.get_mut(&live_vm_id).expect("live vm"); vm.active_processes.remove("proc-js-race"); + drop(vm); assert!(block_on_sidecar!( sidecar, sidecar.handle_execution_event( @@ -7783,6 +7977,7 @@ console.log(JSON.stringify({ status: "ok", summary })); session_id: session_id.clone(), vm_id: vm_id.clone(), process_id: String::from("proc-js-race"), + child_path: Vec::new(), event: crate::state::ActiveExecutionEvent::Stdout( b"stale stdout".to_vec(), ), @@ -7795,6 +7990,7 @@ console.log(JSON.stringify({ status: "ok", summary })); session_id: session_id.clone(), vm_id: vm_id.clone(), process_id: String::from("proc-js-race"), + child_path: Vec::new(), event: crate::state::ActiveExecutionEvent::Exited(0), }) .expect("queue stale exited envelope"); @@ -7852,6 +8048,7 @@ console.log(JSON.stringify({ status: "ok", summary })); session_id: sender_session_id, vm_id: sender_vm_id, process_id: String::from("proc-js-race"), + child_path: Vec::new(), event: crate::state::ActiveExecutionEvent::Stdout( b"stale stdout".to_vec(), ), @@ -8145,7 +8342,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("create vm"); let zombie_pid = { - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .register_driver(CommandDriver::new("test-driver", ["test-zombie"])) .expect("register test driver"); @@ -8178,7 +8375,7 @@ console.log(JSON.stringify({ status: "ok", summary })); } { - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let waited = vm.kernel.waitpid(zombie_pid).expect("waitpid"); assert_eq!(waited.pid, zombie_pid); assert_eq!(waited.status, 17); @@ -8387,7 +8584,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure mounts"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let hidden = vm .kernel .filesystem_mut() @@ -8474,7 +8671,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure readonly mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let error = vm .kernel .filesystem_mut() @@ -8553,7 +8750,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure host_dir mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let hidden = vm .kernel .filesystem_mut() @@ -8630,7 +8827,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure host_dir mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let error = vm .kernel .filesystem_mut() @@ -8689,7 +8886,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure module_access mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let error = vm .kernel .filesystem_mut() @@ -8988,7 +9185,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure js_bridge mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .filesystem_mut() .link("/workspace/original.txt", "/workspace/linked.txt") @@ -9127,7 +9324,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure js_bridge mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let read_error = vm .kernel .filesystem_mut() @@ -9219,7 +9416,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure js_bridge mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); assert_eq!( vm.kernel .filesystem_mut() @@ -9340,7 +9537,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure js_bridge mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let read_error = vm .kernel .filesystem_mut() @@ -9427,7 +9624,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure js_bridge mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let entries = vm .kernel .filesystem_mut() @@ -9528,7 +9725,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure sandbox_agent mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let hidden = vm .kernel .filesystem_mut() @@ -9637,7 +9834,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure s3 mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let hidden = vm .kernel .filesystem_mut() @@ -9656,6 +9853,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("read s3-backed file"), b"native s3 mount".to_vec() ); + drop(vm); drop(sidecar); let requests = server.requests(); @@ -9742,7 +9940,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure object_s3 mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .filesystem_mut() .write_file("/objects/file.txt", b"native object mount".to_vec()) @@ -9754,6 +9952,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("read object s3-backed file"), b"native object mount".to_vec() ); + drop(vm); drop(sidecar); assert!(server @@ -9838,7 +10037,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure chunked_local mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .filesystem_mut() .write_file("/local/file.txt", b"native local mount".to_vec()) @@ -9850,6 +10049,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("read chunked local file"), b"native local mount".to_vec() ); + drop(vm); drop(sidecar); assert!(metadata_path.exists()); @@ -10288,7 +10488,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("create vm"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .filesystem_mut() .write_file("/blocked.txt", b"nope".to_vec()) @@ -10530,6 +10730,75 @@ console.log(JSON.stringify({ status: "ok", summary })); assert_eq!(vm.bindings, bindings_before); assert_eq!(vm.command_guest_paths, command_paths_before); } + fn binding_registration_success_restore_failure_rolls_back_owned_mutation() { + 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 (original_permissions, bindings_before, command_paths_before) = { + let vm = sidecar.vms.get(&vm_id).expect("configured vm"); + ( + vm.configuration.permissions.clone(), + vm.bindings.clone(), + vm.command_guest_paths.clone(), + ) + }; + + sidecar + .bridge + .queue_set_vm_permissions_result(Ok(())) + .expect("queue temporary allow-all permission set"); + sidecar + .bridge + .queue_set_vm_permissions_result(Err(SidecarError::Bridge(String::from( + "injected successful-registration restore failure", + )))) + .expect("queue original permission restore failure"); + + let response = sidecar + .dispatch_blocking(request( + 6, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::RegisterHostCallbacks(test_bindings_payload( + "new-binding", + "new binding", + "new-command", + )), + )) + .expect("dispatch owned binding registration restore failure"); + match response.response.payload { + ResponsePayload::Rejected(rejected) => { + assert!(rejected + .message + .contains("injected successful-registration restore failure")); + } + other => panic!("expected rejected response, got {other:?}"), + } + + 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!( + !vm.kernel.commands().contains_key("new-command"), + "failed registration alias must be removed from the kernel command driver" + ); + drop(vm); + assert_eq!( + sidecar + .bridge + .permissions + .lock() + .expect("read stored permissions") + .get(&vm_id), + Some(&original_permissions), + ); + } fn create_vm_rejects_permission_rules_with_empty_operations() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = @@ -10872,7 +11141,7 @@ console.log(JSON.stringify({ status: "ok", summary })); .expect("dispatch guest filesystem request"); } - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let note_stat = vm .kernel .stat("/workspace/note.txt") @@ -11186,7 +11455,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )) .expect("configure host_dir mount"); - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); let error = vm .kernel .filesystem_mut() @@ -11704,7 +11973,7 @@ console.log(JSON.stringify({ status: "ok", summary })); for _ in 0..64 { let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("active vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("active vm"); vm.active_processes .get_mut("proc-wasm-pty") .and_then(|process| { @@ -11735,7 +12004,7 @@ console.log(JSON.stringify({ status: "ok", summary })); if pty_text.is_none() { let maybe_pty = { - let vm = sidecar.vms.get_mut(&vm_id).expect("wasm vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("wasm vm"); let kernel_pid = vm .active_processes .get("proc-wasm-pty") @@ -11920,9 +12189,9 @@ console.log(JSON.stringify({ status: "ok", summary })); ], ), ] { - let resolved = sidecar - .resolve_javascript_child_process_execution( - vm, + let resolved = + NativeSidecar::::resolve_javascript_child_process_execution( + &vm, &vm.guest_env, &vm.guest_cwd, &vm.host_cwd, @@ -11945,17 +12214,18 @@ console.log(JSON.stringify({ status: "ok", summary })); ); } - let missing = sidecar.resolve_javascript_child_process_execution( - vm, - &vm.guest_env, - &vm.guest_cwd, - &vm.host_cwd, - &crate::protocol::JavascriptChildProcessSpawnRequest { - command: String::from("definitely-not-a-command"), - args: Vec::new(), - options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), - }, - ); + let missing = + NativeSidecar::::resolve_javascript_child_process_execution( + &vm, + &vm.guest_env, + &vm.guest_cwd, + &vm.host_cwd, + &crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("definitely-not-a-command"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + }, + ); let error = missing.expect_err("missing command should fail"); assert!( error @@ -11967,8 +12237,8 @@ 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( - vm, + let exact_missing = NativeSidecar::::resolve_javascript_child_process_execution_with_mode( + &vm, &BTreeMap::new(), &vm.guest_cwd, &vm.host_cwd, @@ -12001,7 +12271,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("create vm"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); vm.kernel .write_file("/replacement.wasm", b"\0asm\x01\0\0\0".to_vec()) .expect("write replacement module"); @@ -12095,7 +12365,7 @@ console.log(JSON.stringify({ status: "ok", summary })); ) .expect("commit local WASM shebang exec"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); assert_eq!( vm.kernel .read_file_for_process( @@ -12155,7 +12425,7 @@ console.log(JSON.stringify({ status: "ok", summary })); )); } - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); let kernel_process = vm .kernel .list_processes() @@ -12186,6 +12456,7 @@ console.log(JSON.stringify({ status: "ok", summary })); let fd_replacement_env = BTreeMap::from([(String::from("FD_ONLY"), String::from("yes"))]); + drop(vm); sidecar .commit_wasm_fd_process_image( &vm_id, @@ -12207,7 +12478,7 @@ console.log(JSON.stringify({ status: "ok", summary })); }, ) .expect("commit prevalidated runner-owned fd image"); - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); assert_eq!( vm.active_processes .get("exec-process") @@ -12240,13 +12511,18 @@ console.log(JSON.stringify({ status: "ok", summary })); PermissionsPolicy::allow_all(), ) .expect("create vm"); - sidecar.javascript_engine.set_import_cache_base_dir( - vm_id.clone(), - sidecar.cache_root.join("cross-runtime-exec-import-cache"), - ); + let import_cache_root = sidecar.cache_root.join("cross-runtime-exec-import-cache"); + sidecar + .vms + .get(&vm_id) + .expect("created vm") + .execution_engines + .javascript("configure cross-runtime import cache") + .expect("borrow VM JavaScript engine") + .set_import_cache_base_dir(vm_id.clone(), import_cache_root); let (kernel_handle, host_cwd) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); vm.kernel.mkdir("/work", true).expect("create work dir"); vm.kernel .write_file( @@ -12441,7 +12717,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("post-commit start failure must not return into the old image"); assert!(!sidecar.fail_next_exec_start_after_commit); - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); let kernel_process = vm .kernel .list_processes() @@ -12494,7 +12770,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let cwd = temp_dir("agentos-native-sidecar-posix-spawn-order"); insert_fake_javascript_parent_process(&mut sidecar, &vm_id, &cwd, "posix-spawn-parent"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); vm.kernel .write_file("/truncated-script", b"#!/missing-interpreter\n".to_vec()) .expect("write exact script fixture"); @@ -12683,7 +12959,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("mark staged successful nested module executable"); let (nested_handle, nested_env) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); let root_pid = vm .active_processes .get("posix-spawn-shadow-parent") @@ -12761,7 +13037,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ) { let contents = contents.as_ref(); let host_path = { - let vm = sidecar.vms.get_mut(vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("created vm"); let parent = Path::new(guest_path) .parent() .and_then(Path::to_str) @@ -12797,7 +13073,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); link_path: &str, ) { let (host_path, host_target) = { - let vm = sidecar.vms.get_mut(vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(vm_id).expect("created vm"); let parent = Path::new(link_path) .parent() .and_then(Path::to_str) @@ -12884,7 +13160,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ); let (nested_handle, nested_env) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); let root_pid = vm .active_processes .get("posix-spawnp-parent") @@ -13183,7 +13459,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); let successful_wasm = wat::parse_str(r#"(module (func (export "_start")))"#) .expect("compile successful WASM fixture"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut 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(&successful_host_path, &successful_wasm); @@ -13221,13 +13497,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("created vm") .kernel .resource_snapshot(); - let context_baseline = ( - sidecar.javascript_engine.context_count_for_test(), - sidecar.wasm_engine.context_count_for_test(), - sidecar.wasm_engine.javascript_context_count_for_test(), - sidecar.python_engine.context_count_for_test(), - sidecar.python_engine.javascript_context_count_for_test(), - ); + let context_baseline = vm_execution_context_counts(&sidecar, &vm_id); for iteration in 0..8 { if spawn_javascript_child_process_for_test( &mut sidecar, @@ -13254,20 +13524,14 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "failed top-level spawn must not register a child" ); assert_eq!( - ( - sidecar.javascript_engine.context_count_for_test(), - sidecar.wasm_engine.context_count_for_test(), - sidecar.wasm_engine.javascript_context_count_for_test(), - sidecar.python_engine.context_count_for_test(), - sidecar.python_engine.javascript_context_count_for_test(), - ), + vm_execution_context_counts(&sidecar, &vm_id), context_baseline, "top-level iteration {iteration} leaked an execution context" ); } let (nested_handle, nested_env, nested_host_cwd) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); let root_pid = vm .active_processes .get("malformed-wasm-parent") @@ -13346,13 +13610,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "failed nested spawn must not register a child" ); assert_eq!( - ( - sidecar.javascript_engine.context_count_for_test(), - sidecar.wasm_engine.context_count_for_test(), - sidecar.wasm_engine.javascript_context_count_for_test(), - sidecar.python_engine.context_count_for_test(), - sidecar.python_engine.javascript_context_count_for_test(), - ), + vm_execution_context_counts(&sidecar, &vm_id), context_baseline, "nested iteration {iteration} leaked an execution context" ); @@ -13381,13 +13639,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("successful spawn child id") .to_owned(); assert_eq!( - ( - sidecar.javascript_engine.context_count_for_test(), - sidecar.wasm_engine.context_count_for_test(), - sidecar.wasm_engine.javascript_context_count_for_test(), - sidecar.python_engine.context_count_for_test(), - sidecar.python_engine.javascript_context_count_for_test(), - ), + vm_execution_context_counts(&sidecar, &vm_id), context_baseline, "successful spawn iteration {iteration} retained one-shot context metadata" ); @@ -13415,13 +13667,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); "successful spawn iteration {iteration} did not exit" ); assert_eq!( - ( - sidecar.javascript_engine.context_count_for_test(), - sidecar.wasm_engine.context_count_for_test(), - sidecar.wasm_engine.javascript_context_count_for_test(), - sidecar.python_engine.context_count_for_test(), - sidecar.python_engine.javascript_context_count_for_test(), - ), + vm_execution_context_counts(&sidecar, &vm_id), context_baseline, "successful spawn/reap iteration {iteration} leaked an execution context" ); @@ -13454,9 +13700,9 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); ..Default::default() }, }; - let error = sidecar - .resolve_javascript_child_process_execution( - vm, + let error = + NativeSidecar::::resolve_javascript_child_process_execution( + &vm, &vm.guest_env, &vm.guest_cwd, &vm.host_cwd, @@ -13548,9 +13794,9 @@ 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( - vm, + let resolved = + NativeSidecar::::resolve_javascript_child_process_execution( + &vm, &vm.guest_env, &vm.guest_cwd, &vm.host_cwd, @@ -13665,9 +13911,9 @@ 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( - vm, + let resolved = + NativeSidecar::::resolve_javascript_child_process_execution( + &vm, &vm.guest_env, &vm.guest_cwd, &vm.host_cwd, @@ -14117,7 +14363,7 @@ process.stdout.write(`${JSON.stringify(snapshot)}\n`); .expect("register math binding collection"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); vm.kernel .write_file( "/workspace/invalid-binding-input.json", @@ -14765,15 +15011,11 @@ export async function loadPyodide() { write_fixture(&pyodide_dir.join("pyodide.asm.js"), ""); write_fixture(&pyodide_dir.join("pyodide.asm.wasm"), ""); - let context = sidecar - .python_engine - .create_context(CreatePythonContextRequest { - vm_id: vm_id.clone(), - pyodide_dist_path: pyodide_dir, - }); - let execution = sidecar - .python_engine - .start_execution(StartPythonExecutionRequest { + let context = create_python_context_for_vm_test(&sidecar, &vm_id, pyodide_dir); + let execution = start_python_execution_for_vm_test( + &sidecar, + &vm_id, + StartPythonExecutionRequest { guest_runtime: Default::default(), limits: Default::default(), vm_id: vm_id.clone(), @@ -14782,11 +15024,12 @@ export async function loadPyodide() { file_path: None, env: BTreeMap::new(), cwd: cwd.clone(), - }) - .expect("start fake python execution"); + }, + ) + .expect("start fake python execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); vm.kernel .spawn_process( PYTHON_COMMAND, @@ -14801,7 +15044,7 @@ export async function loadPyodide() { }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); vm.active_processes.insert( String::from("proc-python-vfs"), active_process_for_tests( @@ -14815,7 +15058,7 @@ export async function loadPyodide() { for _ in 0..16 { let event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); let process = vm .active_processes .get_mut("proc-python-vfs") @@ -14918,7 +15161,7 @@ export async function loadPyodide() { ); let content = { - let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); String::from_utf8( vm.kernel .read_file("/workspace/note.txt") @@ -14929,7 +15172,7 @@ export async function loadPyodide() { assert_eq!(content, "hello from sidecar rpc"); let process = { - let vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("python vm"); vm.active_processes .remove("proc-python-vfs") .expect("remove fake python process") @@ -14971,17 +15214,11 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test( + &sidecar, + &vm_id, + StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -14995,11 +15232,12 @@ await new Promise(() => {}); cwd: cwd.clone(), inline_code: None, wasm_module_bytes: None, - }) - .expect("start fake javascript execution"); + }, + ) + .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -15014,7 +15252,7 @@ await new Promise(() => {}); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-sync"), active_process_for_tests( @@ -15030,7 +15268,7 @@ await new Promise(() => {}); let mut saw_stdout = false; for _ in 0..16 { let event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let process = vm .active_processes .get_mut("proc-js-sync") @@ -15060,7 +15298,7 @@ await new Promise(() => {}); } let content = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); String::from_utf8( vm.kernel .read_file("/rpc/note.txt") @@ -15070,14 +15308,14 @@ await new Promise(() => {}); }; assert_eq!(content, "hello from sidecar rpc"); let link_target = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = sidecar.vms.get(&vm_id).expect("javascript vm"); vm.kernel .read_link("/rpc/link.txt") .expect("read bridged symlink") }; assert_eq!(link_target, "/rpc/note.txt"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = sidecar.vms.get(&vm_id).expect("javascript vm"); assert!( !vm.kernel .exists("/rpc/renamed.txt") @@ -15094,7 +15332,7 @@ await new Promise(() => {}); assert!(saw_stdout, "expected guest stdout after sync fs round-trip"); let process = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .remove("proc-js-sync") .expect("remove fake javascript process") @@ -15353,7 +15591,7 @@ await new Promise(() => {}); ) .expect("create vm"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .write_file("/rpc/input.txt", b"abcdefg") .expect("seed input file"); @@ -15476,17 +15714,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -15506,7 +15735,7 @@ console.log( .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -15521,7 +15750,7 @@ console.log( }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-fd"), active_process_for_tests( @@ -15539,7 +15768,7 @@ console.log( let mut exit_code = None; for _ in 0..64 { let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .get_mut("proc-js-fd") .and_then(|process| { @@ -15620,7 +15849,7 @@ console.log( "stdout: {stdout}" ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let output = String::from_utf8( vm.kernel .read_file("/rpc/output.txt") @@ -16117,7 +16346,7 @@ fs.writeFileSync("/tmp/z/a.txt", "a\n"); assert_eq!(exit_code, Some(0), "stderr: {stderr}"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .mkdir("/tmp/z", true) .expect("create kernel merge dir"); @@ -16310,7 +16539,7 @@ fs.symlinkSync("file.txt", `${dir}/link-file`); assert_eq!(exit_code, Some(0), "stdout: {_stdout}\nstderr: {stderr}"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .mkdir("/tmp/readdir-raw-dirents", true) .expect("create kernel merge dir"); @@ -16562,7 +16791,7 @@ process.stdout.write(`${JSON.stringify({ plain, typed, empty })}\n`); ) .expect("create vm"); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel.mkdir("/app", true).expect("create app dir"); vm.kernel .mkdir("/fixtures", true) @@ -16685,17 +16914,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -16715,7 +16935,7 @@ await new Promise(() => {}); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -16730,7 +16950,7 @@ await new Promise(() => {}); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut 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. @@ -16753,7 +16973,7 @@ await new Promise(() => {}); for _ in 0..40 { let event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); let process = vm .active_processes .get_mut("proc-js-promises") @@ -16847,7 +17067,7 @@ await new Promise(() => {}); } let content = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); (0..10) .map(|index| { String::from_utf8( @@ -16879,7 +17099,7 @@ await new Promise(() => {}); ); let process = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .remove("proc-js-promises") .expect("remove fake javascript process") @@ -18090,7 +18310,7 @@ await new Promise(() => {}); let process_id = "proc-js-sqlite-rpc"; let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -18103,7 +18323,7 @@ await new Promise(() => {}); ) .expect("spawn sqlite kernel process") }; - let vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("sqlite vm"); vm.active_processes.insert( String::from(process_id), active_process_for_tests( @@ -18114,6 +18334,7 @@ await new Promise(() => {}); ) .with_host_cwd(cwd.clone()), ); + drop(vm); let database_id = call_javascript_sync_rpc( &mut sidecar, @@ -18391,7 +18612,7 @@ console.log("sqlite-ok"); assert!(stderr.trim().is_empty(), "stderr: {stderr}"); assert_eq!(stdout.trim(), "sqlite-ok"); let database_bytes = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .read_file("/workspace/sqlite-builtins.db") .expect("read sqlite builtins database file") @@ -19170,17 +19391,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -19200,7 +19412,7 @@ console.log(JSON.stringify({ lookup, resolve4 })); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -19215,7 +19427,7 @@ console.log(JSON.stringify({ lookup, resolve4 })); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-dns"), active_process_for_tests( @@ -19233,7 +19445,7 @@ console.log(JSON.stringify({ lookup, resolve4 })); let mut exit_code = None; for _ in 0..64 { let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .get_mut("proc-js-dns") .and_then(|process| { @@ -19366,17 +19578,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -19396,7 +19599,7 @@ process.exit(0); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -19411,7 +19614,7 @@ process.exit(0); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-ssrf-protection"), active_process_for_tests( @@ -19429,7 +19632,7 @@ process.exit(0); let mut exit_code = None; for _ in 0..64 { let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .get_mut("proc-js-ssrf-protection") .and_then(|process| { @@ -20071,17 +20274,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -20101,7 +20295,7 @@ process.exit(0); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -20116,14 +20310,16 @@ process.exit(0); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -20146,7 +20342,7 @@ process.exit(0); continue; } let next_event = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes .get_mut("proc-js-tls") .and_then(|process| { @@ -20247,11 +20443,11 @@ process.exit(0); "payload: {payload}" ); assert!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-http-listen")) - .is_some_and(|process| process.http_servers.contains_key(&7)), + sidecar.vms.get(&vm_id).is_some_and(|vm| { + vm.active_processes + .get("proc-js-http-listen") + .is_some_and(|process| process.http_servers.contains_key(&7)) + }), "HTTP server was not registered", ); @@ -20269,11 +20465,11 @@ process.exit(0); .expect("close http bridge server"); assert_eq!(close, Value::Null); assert!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-http-listen")) - .is_some_and(|process| process.http_servers.is_empty()), + sidecar.vms.get(&vm_id).is_some_and(|vm| { + vm.active_processes + .get("proc-js-http-listen") + .is_some_and(|process| process.http_servers.is_empty()) + }), "HTTP server should be removed after close", ); } @@ -20296,7 +20492,7 @@ process.exit(0); "{\"status\":200,\"headers\":[[\"content-type\",\"text/plain\"]],\"body\":\"cG9uZw==\",\"bodyEncoding\":\"base64\"}", ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("vm"); let process = vm .active_processes .get_mut("proc-js-http-respond") @@ -20320,15 +20516,15 @@ process.exit(0); .expect("record http response"); assert_eq!(response, Value::Null); assert_eq!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-http-respond")) - .and_then(|process| process.pending_http_requests.get(&(7, 9))) - .and_then(|pending| match pending { - PendingHttpRequest::Buffered(response) => response.clone(), - PendingHttpRequest::Deferred(_) => None, - }), + sidecar.vms.get(&vm_id).and_then(|vm| { + vm.active_processes + .get("proc-js-http-respond") + .and_then(|process| process.pending_http_requests.get(&(7, 9))) + .and_then(|pending| match pending { + PendingHttpRequest::Buffered(response) => response.clone(), + PendingHttpRequest::Deferred(_) => None, + }) + }), Some(response_json), ); } @@ -20357,7 +20553,7 @@ process.exit(0); let response_json = format!(r#"{{"status":200,"body":"{oversized_body}"}}"#); assert!(response_json.len() > crate::wire::DEFAULT_MAX_FRAME_BYTES); { - let vm = sidecar.vms.get_mut(&vm_id).expect("vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("vm"); let process = vm .active_processes .get_mut("proc-js-http-respond-oversized") @@ -20384,12 +20580,12 @@ process.exit(0); "unexpected error: {error}" ); assert_eq!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-http-respond-oversized")) - .and_then(|process| process.pending_http_requests.get(&(7, 10))) - .map(|pending| matches!(pending, PendingHttpRequest::Buffered(None))), + sidecar.vms.get(&vm_id).and_then(|vm| { + vm.active_processes + .get("proc-js-http-respond-oversized") + .and_then(|process| process.pending_http_requests.get(&(7, 10))) + .map(|pending| matches!(pending, PendingHttpRequest::Buffered(None))) + }), Some(true), ); } @@ -21451,7 +21647,7 @@ setTimeout(() => { "{\"status\":200,\"headers\":[[\"content-type\",\"text/plain\"]],\"body\":\"c2VjdXJlLXBvbmc=\",\"bodyEncoding\":\"base64\"}", ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("vm"); let process = vm .active_processes .get_mut("proc-js-http2-respond") @@ -21475,15 +21671,15 @@ setTimeout(() => { .expect("record http2 response"); assert_eq!(response, Value::Bool(true)); assert_eq!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-http2-respond")) - .and_then(|process| process.pending_http_requests.get(&(33, 44))) - .and_then(|pending| match pending { - PendingHttpRequest::Buffered(response) => response.clone(), - PendingHttpRequest::Deferred(_) => None, - }), + sidecar.vms.get(&vm_id).and_then(|vm| { + vm.active_processes + .get("proc-js-http2-respond") + .and_then(|process| process.pending_http_requests.get(&(33, 44))) + .and_then(|pending| match pending { + PendingHttpRequest::Buffered(response) => response.clone(), + PendingHttpRequest::Deferred(_) => None, + }) + }), Some(response_json), ); } @@ -21886,7 +22082,7 @@ console.log(JSON.stringify({ sidecar: &NativeSidecar, vm_id: &str, ) -> VmNetworkResourceSnapshot { - vm_network_resource_snapshot(sidecar.vms.get(vm_id).expect("vm state")) + vm_network_resource_snapshot(&sidecar.vms.get(vm_id).expect("vm state")) } fn assert_network_resources_unchanged( @@ -22010,10 +22206,10 @@ 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 process = sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("proc-js-server")) + let vm = sidecar.vms.get(&vm_id).expect("server VM"); + let process = vm + .active_processes + .get("proc-js-server") .expect("server process"); assert!( process.http_servers.is_empty(), @@ -22026,6 +22222,7 @@ await new Promise(() => {}); .any(|listener| listener.kernel_socket_id.is_some()), "http.createServer should register a kernel TCP listener", ); + drop(vm); let response = sidecar .dispatch_blocking(request( @@ -22731,7 +22928,7 @@ await new Promise(() => {}); !vm.active_processes.contains_key("proc-js-server"), "target process should be cleaned up after exit" ); - let after = vm_network_resource_snapshot(vm); + let after = vm_network_resource_snapshot(&vm); assert!( after.capabilities.is_empty(), "target exit should release capabilities" @@ -23951,17 +24148,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -23981,7 +24169,7 @@ console.log(`BODY:${{body}}`); .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -23996,14 +24184,16 @@ console.log(`BODY:${{body}}`); }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -24020,15 +24210,16 @@ console.log(`BODY:${{body}}`); .capabilities .clone(); let socket_paths = build_javascript_socket_path_context( - sidecar.vms.get(&vm_id).expect("javascript vm"), + &sidecar.vms.get(&vm_id).expect("javascript vm"), ) .expect("build Unix socket path context"); let socket_path = "/tmp/agentos.sock"; let listen = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24036,7 +24227,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24057,7 +24248,7 @@ console.log(`BODY:${{body}}`); Value::String(String::from(socket_path)) ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = sidecar.vms.get(&vm_id).expect("javascript vm"); assert!( vm.kernel .exists(socket_path) @@ -24129,9 +24320,10 @@ console.log(`BODY:${{body}}`); let accept_deadline = Instant::now() + Duration::from_secs(1); let accepted = loop { let accepted = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24139,7 +24331,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24170,9 +24362,10 @@ console.log(`BODY:${{body}}`); ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); let connections = service_javascript_net_sync_rpc( @@ -24180,7 +24373,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24195,9 +24388,10 @@ console.log(`BODY:${{body}}`); } { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24205,7 +24399,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24225,9 +24419,10 @@ console.log(`BODY:${{body}}`); } { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24235,7 +24430,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24274,9 +24469,10 @@ console.log(`BODY:${{body}}`); assert_eq!(server_end["type"], Value::String(String::from("end"))); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24284,7 +24480,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24304,9 +24500,10 @@ console.log(`BODY:${{body}}`); } { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24314,7 +24511,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24353,9 +24550,10 @@ console.log(`BODY:${{body}}`); assert_eq!(client_end["type"], Value::String(String::from("end"))); for (id, request_id) in [(&client_socket_id, 14_u64), (&server_socket_id, 15_u64)] { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24363,7 +24561,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24377,9 +24575,10 @@ console.log(`BODY:${{body}}`); } { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); - let process = vm - .active_processes + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let vm = &mut *vm; + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let process = active_processes .get_mut("proc-js-unix") .expect("unix process"); service_javascript_net_sync_rpc( @@ -24387,7 +24586,7 @@ console.log(`BODY:${{body}}`); &vm_id, &dns, &socket_paths, - &mut vm.kernel, + kernel, process, &JavascriptSyncRpcRequest { raw_bytes_args: std::collections::HashMap::new(), @@ -24547,7 +24746,7 @@ console.log(JSON.stringify({ ); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .write_file("/rpc/note.txt", b"hello from nested child".to_vec()) .expect("seed rpc note"); @@ -24559,17 +24758,8 @@ console.log(JSON.stringify({ .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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -24589,7 +24779,7 @@ console.log(JSON.stringify({ .expect("start fake javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .spawn_process( JAVASCRIPT_COMMAND, @@ -24604,14 +24794,16 @@ console.log(JSON.stringify({ }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -24737,17 +24929,8 @@ 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 { + let context = create_javascript_context_for_vm_test(&sidecar, &vm_id); + let execution = start_javascript_execution_for_vm_test(&sidecar, &vm_id, StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: vm_id.clone(), @@ -24767,7 +24950,7 @@ console.log(JSON.stringify({ .expect("start nested SIGCHLD javascript execution"); let kernel_handle = { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.kernel .write_file( "/root/child.mjs", @@ -24794,14 +24977,16 @@ console.log(JSON.stringify({ }; { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); 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(), + runtime_context, + limits, GuestRuntimeKind::JavaScript, ActiveExecution::Javascript(execution), ) @@ -24850,7 +25035,7 @@ console.log(JSON.stringify({ let kernel_handle = create_kernel_process_handle_for_tests(); { - let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); vm.active_processes.insert( String::from("proc-js-child-gone"), active_process_for_tests( @@ -24868,7 +25053,8 @@ console.log(JSON.stringify({ connection_id: connection_id.clone(), session_id: session_id.clone(), vm_id: vm_id.clone(), - process_id: String::from("proc-js-child-gone/ghost-child"), + process_id: String::from("proc-js-child-gone"), + child_path: vec![String::from("ghost-child")], event: ActiveExecutionEvent::Stdout(b"queued-but-undeliverable".to_vec()), }); @@ -24955,10 +25141,10 @@ console.log(JSON.stringify({ .expect("pump child process events"), "the proactive JavaScript event pump must not consume WASM-owned child output" ); - let queued = sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("wasm-root")) + let vm = sidecar.vms.get(&vm_id).expect("test vm"); + let queued = vm + .active_processes + .get("wasm-root") .and_then(|root| root.child_processes.get("child-1")) .and_then(|child| child.pending_execution_events.front()) .expect("WASM child output should remain available to child_process.poll"); @@ -24999,8 +25185,9 @@ try { write_fixture(&cwd.join("writer.mjs"), writer); let (parent_pid, read_fd, write_fd, baseline) = { - let vm = sidecar.vms.get_mut(&vm_id).expect("test vm"); + let mut vm = sidecar.vms.get_mut(&vm_id).expect("test vm"); vm.limits.reactor.operation_deadline_ms = 50; + let guest_env = vm.guest_env.clone(); vm.kernel .write_file("/writer.mjs", writer.to_vec()) .expect("stage guest writer"); @@ -25012,7 +25199,7 @@ try { WASM_COMMAND, vec![String::from(WASM_COMMAND)], VirtualProcessOptions { - env: vm.guest_env.clone(), + env: guest_env.clone(), cwd: Some(String::from("/")), ..VirtualProcessOptions::default() }, @@ -25035,18 +25222,20 @@ try { .expect("fill child pipe"), capacity ); - let mut env = vm.guest_env.clone(); + let mut env = guest_env; env.insert( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from("[\"fs\"]"), ); + let runtime_context = vm.runtime_context.clone(); + let limits = vm.limits.clone(); vm.active_processes.insert( String::from("wasm-write-parent"), active_process_for_vm_tests( parent_pid, handle, - vm.runtime_context.clone(), - vm.limits.clone(), + runtime_context, + limits, GuestRuntimeKind::WebAssembly, ActiveExecution::Binding(BindingExecution::default()), ) @@ -25112,12 +25301,12 @@ try { "WASM parent failed before the child write parked: {error}; events: {prepark_events:?}" ), } - let parked = sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("wasm-write-parent")) - .and_then(|parent| parent.child_processes.get(&child_id)) - .is_some_and(|child| child.deferred_kernel_wait_rpc.is_some()); + let parked = sidecar.vms.get(&vm_id).is_some_and(|vm| { + vm.active_processes + .get("wasm-write-parent") + .and_then(|parent| parent.child_processes.get(&child_id)) + .is_some_and(|child| child.deferred_kernel_wait_rpc.is_some()) + }); if parked { break; } @@ -25161,19 +25350,19 @@ try { runtime_handle .block_on(sidecar.pump_process_events(&ownership)) .expect("service deadline wake"); - let parked = sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("wasm-write-parent")) - .and_then(|parent| parent.child_processes.get(&child_id)) - .is_some_and(|child| child.deferred_kernel_wait_rpc.is_some()); - if !parked { - let timer_cleared = sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("wasm-write-parent")) + let parked = sidecar.vms.get(&vm_id).is_some_and(|vm| { + vm.active_processes + .get("wasm-write-parent") .and_then(|parent| parent.child_processes.get(&child_id)) - .is_some_and(|child| child.deferred_child_write_timer.is_none()); + .is_some_and(|child| child.deferred_kernel_wait_rpc.is_some()) + }); + if !parked { + let timer_cleared = sidecar.vms.get(&vm_id).is_some_and(|vm| { + vm.active_processes + .get("wasm-write-parent") + .and_then(|parent| parent.child_processes.get(&child_id)) + .is_some_and(|child| child.deferred_child_write_timer.is_none()) + }); assert!(timer_cleared, "settled write must release its timer task"); break; } @@ -25206,11 +25395,11 @@ try { "timed-out child must restore the parent process/fd baseline" ); assert_eq!( - sidecar - .vms - .get(&vm_id) - .and_then(|vm| vm.active_processes.get("wasm-write-parent")) - .map(|parent| parent.kernel_pid), + sidecar.vms.get(&vm_id).and_then(|vm| { + vm.active_processes + .get("wasm-write-parent") + .map(|parent| parent.kernel_pid) + }), Some(parent_pid) ); } @@ -25330,6 +25519,7 @@ try { 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(); + binding_registration_success_restore_failure_rolls_back_owned_mutation(); 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(); @@ -25518,6 +25708,11 @@ try { object_s3_mount_plugin_is_not_registered(); } + #[test] + fn binding_registration_owned_rollback_regression() { + binding_registration_success_restore_failure_rolls_back_owned_mutation(); + } + #[test] fn service_javascript_sqlite_persists_multiple_handles_and_wal() { javascript_sqlite_sync_rpcs_round_trip_and_persist_vm_files(); diff --git a/packages/core/package.json b/packages/core/package.json index 3a11f94756..c0e17fcb83 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,7 +57,7 @@ "build:protocols": "pnpm run build:agentos-protocol", "test": "vitest run --exclude '**/*.nightly.test.ts' --reporter=verbose", "test:unit": "vitest run tests/agent-exit-event.test.ts tests/agentos-package.test.ts tests/agentos-protocol.test.ts tests/allowed-node-builtins.test.ts tests/bindings-zod.test.ts tests/bindings.test.ts tests/cron-manager.test.ts tests/cron-timer-driver.test.ts tests/generated-protocol.test.ts tests/leak-agent-os-processes.test.ts tests/leak-rpc-client.test.ts tests/mount-descriptors.test.ts tests/mount-reconfigure.test.ts tests/options-schema.test.ts tests/public-api-exports.test.ts tests/root-filesystem-descriptors.test.ts tests/runtime-compat-mount.test.ts tests/session-event-ordering.test.ts tests/session-permission-surface.test.ts tests/sidecar-client.test.ts tests/sidecar-permission-descriptors.test.ts tests/wasm-permission-tiers.test.ts --fileParallelism=false", - "test:pr": "pnpm test:unit && vitest run tests/migration-parity.test.ts --fileParallelism=false --reporter=verbose", + "test:pr": "pnpm test:unit && vitest run tests/migration-parity.test.ts tests/acp-reactor-regression.test.ts --fileParallelism=false --reporter=verbose", "test:nightly": "vitest run tests/*.nightly.test.ts --reporter=verbose --passWithNoTests" }, "dependencies": { diff --git a/packages/core/tests/acp-reactor-regression.test.ts b/packages/core/tests/acp-reactor-regression.test.ts index 98049a1db9..a584fe3ab2 100644 --- a/packages/core/tests/acp-reactor-regression.test.ts +++ b/packages/core/tests/acp-reactor-regression.test.ts @@ -164,6 +164,114 @@ process.stdin.on("data", (chunk) => { }); `.trim(); +// Keeps a prompt open until the host sends the ACP cancellation notification. +// Each public session owns a separate adapter process, so two sessions using +// this package provide deterministic, independently gated prompt operations. +const SLEEPING_PROMPT_ADAPTER = String.raw` +let input = ""; +let activePromptId; + +function writeMessage(message) { + process.stdout.write(JSON.stringify(message) + "\n"); +} + +function writeResponse(id, result) { + writeMessage({ jsonrpc: "2.0", id, result }); +} + +function writeUpdate(text) { + writeMessage({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "sleeping-session", + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text }, + }, + }, + }); +} + +async function handleMessage(msg) { + if (msg.method === "session/cancel" && msg.id === undefined) { + if (activePromptId !== undefined) { + const promptId = activePromptId; + activePromptId = undefined; + writeResponse(promptId, { stopReason: "cancelled" }); + } + return; + } + if (msg.id === undefined) return; + + switch (msg.method) { + case "initialize": + writeResponse(msg.id, { + protocolVersion: 1, + agentInfo: { name: "sleeping-prompt", version: "1.0.0" }, + agentCapabilities: { + plan_mode: false, + tool_calls: false, + promptCapabilities: {}, + }, + modes: { + currentModeId: "default", + availableModes: [{ id: "default", label: "Default" }], + }, + configOptions: [], + }); + return; + case "session/new": + writeResponse(msg.id, { + sessionId: "sleeping-session", + modes: { + currentModeId: "default", + availableModes: [{ id: "default", label: "Default" }], + }, + configOptions: [], + }); + return; + case "session/prompt": + if (activePromptId !== undefined) { + throw new Error("adapter received a second prompt before cancellation"); + } + activePromptId = msg.id; + writeUpdate("prompt-started"); + return; + case "session/close": + writeResponse(msg.id, {}); + return; + default: + writeMessage({ + jsonrpc: "2.0", + id: msg.id, + error: { + code: -32601, + message: "Method not found", + data: { method: msg.method }, + }, + }); + } +} + +process.stdin.resume(); +process.stdin.on("data", (chunk) => { + input += String(chunk); + while (true) { + const newline = input.indexOf("\n"); + if (newline === -1) break; + const line = input.slice(0, newline); + input = input.slice(newline + 1); + if (!line.trim()) continue; + const message = JSON.parse(line); + void handleMessage(message).catch((error) => { + process.stderr.write(String(error && error.stack ? error.stack : error) + "\n"); + process.exitCode = 1; + }); + } +}); +`.trim(); + async function waitFor( predicate: () => boolean, timeoutMs = 10_000, @@ -309,4 +417,93 @@ describe("ACP adapter reactor regression", () => { await vm.unloadSession({ sessionId }); } }, 120_000); + + test("keeps two prompts in flight while public filesystem calls complete on the shared sidecar", async () => { + const agentPackage = createProjectedAgentPackage({ + name: "sleeping-prompt-concurrency", + adapterScript: SLEEPING_PROMPT_ADAPTER, + }); + cleanups.add(async () => agentPackage.cleanup()); + + const vm = await AgentOs.create({ + sidecar: { kind: "shared", pool: "acp-prompt-concurrency" }, + mounts: moduleAccessMounts(MODULE_ACCESS_CWD), + defaultSoftware: false, + software: [common, agentPackage.software], + permissions: { + fs: "allow", + childProcess: "allow", + }, + }); + cleanups.add(async () => vm.dispose()); + + const sessionIds = ["sleeping-a", "sleeping-b"] as const; + for (const sessionId of sessionIds) { + await vm.openSession({ sessionId, agent: "sleeping-prompt-concurrency" }); + } + + const started = new Set(); + const unsubscribes = sessionIds.map((sessionId) => + vm.onSessionEvent(sessionId, (event) => { + if ( + event.durability === "ephemeral" && + event.type === "agent_message_chunk" && + event.content.type === "text" && + event.content.text === "prompt-started" + ) { + started.add(sessionId); + } + }), + ); + const prompts = sessionIds.map((sessionId) => + vm.prompt({ + sessionId, + content: [{ type: "text" as const, text: `Sleep ${sessionId}` }], + }), + ); + + try { + await waitFor(() => started.size === sessionIds.length); + + const filesystemWork = (async () => { + await vm.writeFile("/prompt-concurrency.txt", "filesystem-progress"); + return textDecoder.decode(await vm.readFile("/prompt-concurrency.txt")); + })(); + const fileContents = await Promise.race([ + filesystemWork, + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error("filesystem calls were blocked by active prompts"), + ), + 5_000, + ), + ), + ]); + expect(fileContents).toBe("filesystem-progress"); + + const cancellations = await Promise.all( + sessionIds.map((sessionId) => vm.cancelPrompt({ sessionId })), + ); + expect(cancellations).toEqual([ + { status: "cancelled" }, + { status: "cancelled" }, + ]); + const promptResults = await Promise.all(prompts); + expect(promptResults.map((result) => result.stopReason)).toEqual([ + "cancelled", + "cancelled", + ]); + } finally { + for (const unsubscribe of unsubscribes) unsubscribe(); + await Promise.allSettled( + sessionIds.map((sessionId) => vm.cancelPrompt({ sessionId })), + ); + await Promise.allSettled(prompts); + for (const sessionId of sessionIds) { + await vm.unloadSession({ sessionId }); + } + } + }, 120_000); }); diff --git a/request-concurrency-fix-prompt.md b/request-concurrency-fix-prompt.md new file mode 100644 index 0000000000..80c0229913 --- /dev/null +++ b/request-concurrency-fix-prompt.md @@ -0,0 +1,1004 @@ +# Native-sidecar P0 request concurrency and protocol progress + +Status: implemented; final P0 validation passed on 2026-08-05 + +Owner: agentOS native sidecar + +Scope: P0 protocol correctness and guaranteed progress +Working revision: `zid-sleeper-agent` + +Checkbox legend: `[x]` means the implementation exists and its mapped active +test passed in this working copy. `[ ]` means incomplete; parenthetical text +calls out partial implementation or coverage that must not be mistaken for P0 +acceptance. + +## Current audited status + +- [x] A sleeping ACP prompt no longer retains the native protocol coordinator. +- [x] ACP cancel, permission response, machine callback response, and bounded + unload/delete teardown can progress while a prompt is active. +- [x] Different ACP routes can have prompts in flight concurrently. +- [x] The public-client regression for two prompts plus concurrent file + write/read is checked in and active. +- [x] Output producers no longer synchronously wait for stdout/fd-3 capacity. +- [x] The public-client two-prompt/filesystem regression passes against a + freshly built shared sidecar. +- [x] Ordinary extension and generic requests are synchronously admitted and + prepared, then started under independently tracked `JoinSet` tasks. +- [x] Production stdio ingress never calls `dispatch_wire(...).await`; that + API remains only for direct in-process compatibility. +- [x] Connection/session/VM ownership, cloneable `VmHandle`s, + `RoutedExtensionServices`, internal-event admission, and bounded + shutdown drain/flush satisfy the complete P0 contract. + +## Completion contract + +This document is both the design contract and the implementation checklist. A +checkbox may be marked complete only when the code exists, the corresponding +test is active, and the listed acceptance command passes. + +The completed implementation must maintain this invariant: + +> The native stdio ingress router may decode, validate, reserve bounded +> admission, register a request, and route a frame. It must never await business +> execution, output capacity, guest/runtime activity, a host callback, or event +> drainage. + +The reader, router, operation execution, state coordination, output broker, and +physical writers are separate progress domains. Backpressure in one domain must +not stop cancellation, registered responses, shutdown, or unrelated admitted +work in another. + +## Why this is required + +Before this change, the protocol loop awaited +`NativeSidecar::dispatch_wire(&mut self, ...)` through completion. An ACP +`session/prompt` could remain pending indefinitely, so one prompt held +exclusive mutable access to the complete sidecar and prevented unrelated +request dispatch. `dispatch_wire` remains as a direct in-process compatibility +API, but production stdio ingress cannot reach it. + +The retired ACP-specific interrupt workaround examined one additional ordinary +frame. If that frame was unrelated, it stored the frame in `pending_frame` and +resumed awaiting the prompt. A later cancel remained behind that frame: + +```text +prompt A starts +-> unrelated request B arrives +-> B occupies pending_frame +-> cancel A arrives behind B +-> prompt A, request B, and cancel A cannot complete +``` + +Output had an independent progress defect: producer-side publication could +wait synchronously for writer capacity. If the host did not drain a lane, a +producer could park indefinitely and stop protocol progress. The physical +writer may still block on its dedicated thread; producers now use reserved, +nonblocking or async broker publication and never wait on that thread's +condition variable. + +These are generic routing and ownership defects. ACP must remain an opaque +extension over the generic extension contract; native-sidecar code must not +decode ACP payloads. + +## P0 scope + +P0 consists of five coupled changes: + +- [x] P0.1: Decouple every ordinary extension and generic request from protocol + ingress through register/prepare/start; route shutdown directly with a + bounded drain. +- [x] P0.2: Partition connection/session/VM ownership, VM state handles, + extension services, lifecycle ordering, and internal-event work. +- [x] P0.3: Route progress-critical messages directly with reserved admission, + including bounded shutdown/disconnect drain and cleanup. +- [x] P0.4: Replace synchronous producer-side output waits with an output broker. +- [x] P0.5: Delete the ACP prompt interrupt workaround; the production-used + real-loop harness passes without prompt-specific stdio routing. + +P1/P2 fairness, blocking-I/O migration, observability, architecture cleanup, and +expanded load/fuzz work are recorded in the existing +`~/.agents/friction/agentos.md` occurrence. They are not allowed to weaken the +P0 invariants, but they are not P0 completion gates unless a P1 defect prevents +a P0 progress test from passing. + +## Non-negotiable invariants + +- [x] There is no global FIFO that serializes ordinary business requests. +- [x] There is no global `Arc>` or equivalent lock held + across request execution. +- [x] Different connections, VMs, and extension sessions can have operations in + flight concurrently. +- [x] A long operation never holds a mutable connection/session/VM critical + section while it waits for external activity. Its lightweight operation + registration remains live so teardown can cancel and drain it; that + registration is not an exclusive state lease and does not serialize + ordinary operations. +- [x] Same-entity conflicts have an explicit ordering key and bounded admission. +- [x] Every admitted request has exactly one terminal response. +- [x] Terminal responses preserve request ID and ownership and may complete out + of order. +- [x] Active and queued operation counts and retained bytes are bounded by + configuration. +- [x] A limit rejection names the observed use, limit, and configuration path. +- [x] Cancellation, permission responses, registered sidecar responses, + shutdown, and terminal transport failure cannot wait for ordinary + operation admission. +- [x] No output producer synchronously waits on a condition variable. +- [x] Ordinary output saturation cannot consume reserved control progress. +- [x] No fire-and-forget task may lose a panic, error, cancellation, or terminal + response. +- [x] Shutdown stops new ordinary admission, signals active work, and drains or + cancels it within a configured deadline. +- [x] Core native-sidecar routing remains extension-agnostic. + +## Target architecture + +```text +fd 0 reader fd 3 control reader + | | + | bounded decoded frames | direct registered responses + v v ++--------------------+ +------------------------+ +| ProtocolIngress |<------------| ProgressControlRouter | +| - validate | | - callback response | +| - reserve | | - cancel/permission | +| - register | | - shutdown/error | +| - route only | +------------------------+ ++---------+----------+ + | + | admitted operation + v ++--------------------+ short commands +----------------------+ +| RequestSupervisor |---------------------------->| Sidecar coordinators | +| - bounded permits | | - connection/session | +| - operation table | | - per-VM state | +| - cancellation | | - extension services | +| - terminal guard | +----------------------+ ++---------+----------+ + | + +---------------------------> +----------------------+ + | event subscriptions | EventBroker | + | | - durable demux | + | | - ownership waiters | + | | - no VM lock on wait | + | +----------------------+ + | + | terminal response / events + v ++----------------------+ +| ProtocolFrameWriter | +| - reserved control | +| - ordinary events | +| - async backpressure | ++-----+------------+---+ + | | + v v + stdout fd 3 + writer writer +``` + +Physical readers and writers may remain dedicated threads/tasks. The defect is +producer-side waiting in a progress-critical domain, not the use of a blocking +stdout writer on its own dedicated thread. + +## P0.1 — Concurrent request supervision + +### Admission model + +There is no general ordinary-work FIFO after routing. The existing bounded +transport ingress channel absorbs decoder/router scheduling jitter only. Once +the router receives a valid `RequestFrame`, it either: + +1. reserves an in-flight operation permit, retained request bytes, and one + terminal-response reservation; registers the operation; and starts it, or +2. emits a typed overload response through reserved rejection capacity. + +Add explicit runtime protocol configuration: + +- `runtime.protocol.maxInFlightRequests` +- `runtime.protocol.maxInFlightRequestBytes` +- `runtime.protocol.maxTerminalFrames` +- `runtime.protocol.maxTerminalBytes` +- `runtime.protocol.terminalFallbackBytes` +- `runtime.protocol.maxProgressFrames` +- `runtime.protocol.maxProgressBytes` +- `runtime.protocol.maxRejectionFrames` +- `runtime.protocol.maxRejectionBytes` +- `runtime.protocol.shutdownGraceMs` + +The values must be positive, included in runtime validation, exposed in queue +metrics, and covered by the limits inventory/architecture guards applicable to +runtime protocol configuration. `shutdownGraceMs` bounds only the draining +phase after shutdown closes ordinary admission; it does not extend an +operation's own timeout or permit new work. + +A separate general request backlog is intentionally omitted. The already +bounded ingress channel is the only pre-admission ordinary backlog. This avoids +admitting work that cannot run and prevents cancellation from being hidden +behind an application FIFO. + +### Operation record + +Each admitted request has one logical tracked record split across the registry, +its operation handle, and the task supervisor: + +```rust +struct OperationRecord { + generation: u64, + metadata: RequestOperationMetadata, // ownership + ordering + request_bytes: usize, + state: RequestOperationState, + cancellation: CancellationToken, + terminal: TerminalResponseGuard, +} + +// RequestOperation carries the registry key/generation and admission +// accounting. JoinSet supervision, detached completion, and the terminal +// output reservation remain outside the registry record. +``` + +Required state transitions: + +```text +Admitted -> Running -> Completing -> Terminal + -> Cancelling -> Completing -> Terminal +Admitted/Running -> Failed -> Terminal +Admitted/Running -> Shutdown -> Completing -> Terminal +``` + +The terminal guard atomically permits exactly one terminal response. A late +completion after cancellation is recorded and discarded without producing a +second response. Dropping an unfinished record is a hard logged invariant +failure and must synthesize a typed terminal error when transport is still +available. + +Request IDs are unique within a connection, so the registry key is +`(connection_id, request_id)`. A duplicate in-flight ID receives a typed +conflict response and does not replace the original operation. The same numeric +request ID on two authenticated connections is valid and must not collide. + +### Task execution + +Operations run on the process-owned Tokio runtime using its tracked task +facility. They do not create per-request OS threads or runtimes. The supervisor +owns every task handle and observes: + +- normal completion, +- returned error, +- panic/join failure, +- explicit cancellation, +- connection disposal, +- process shutdown. + +Long operations may await external activity in their own task. They acquire +mutable coordinator/state access only for short commands and release it before +an external await. Their nonexclusive ownership registration remains until the +terminal response so disposal cannot remove state underneath an active task. + +### P0.1 checklist and tests + +- [x] Add in-flight request count and byte configuration with validation. +- [x] Implement count/byte admission reservations. +- [x] Implement the operation registry and duplicate-ID protection for ordinary + admitted work. Progress requests use the separate P0.3 registry. +- [x] Implement tracked task completion and terminal-response guard. +- [x] Convert ingress request handling from inline await to register-and-start. +- [x] Preserve request ID and ownership on out-of-order completion. +- [x] Propagate task errors and panics to one typed terminal response. +- [x] Release every admission reservation on all terminal paths. + +Tests: + +- [x] Rust real-loop test: blocking request 10 starts; independent request 11 + completes before request 10 is released. +- [x] Rust real-loop test: two blocking operations in different sessions both + reach their start gates. +- [x] Rust real-loop test: request 11 responds before request 10 and both retain + correct IDs and ownership. +- [x] Rust real-loop test: duplicate in-flight request ID is rejected without + affecting the original. +- [x] Rust real-loop test: the same numeric request ID on two connections does + not collide. +- [x] Rust real-loop test: operation panic produces one terminal error and frees + admission. +- [x] Rust real-loop test: count saturation returns a typed error naming + `runtime.protocol.maxInFlightRequests`. +- [x] Rust real-loop test: byte saturation returns a typed error naming + `runtime.protocol.maxInFlightRequestBytes`. + +All eight scenarios above run through the production-used +`run_protocol_engine` harness in `stdio/request_concurrency_tests.rs`. The +harness supplies bounded ingress/control channels, the real operation and +output brokers, and deterministic gates; it does not call `dispatch_wire` +directly as its concurrency proof. + +## P0.2 — Ownership partitioning and ordering + +### Why ownership changed + +The old `dispatch(&mut self)` and borrowed extension host allowed an async +future to retain exclusive access to all sidecar state. Spawning that future +did not create concurrency; wrapping the sidecar in one async mutex would have +recreated the same serialization. Production `ExtensionContext` now owns an +`Arc` and has no lifetime-bound mutable host borrow. + +The target separates cloneable service handles from entity-owned mutable state. + +### Process and connection state + +A small connection/session coordinator owns: + +- connection authentication and version state, +- session membership, +- VM membership indexes, +- request-operation ownership indexes, +- disposal state. + +Its commands must not perform guest execution, filesystem/network I/O, adapter +I/O, output writes, or unbounded waits. It may mutate indexes and return +cloneable entity handles. + +Connection/session disposal changes the entity state to `Closing` before +signalling owned operations. New requests for a closing entity are rejected. + +### VM state + +Each live VM has two complementary objects. Cloneable, thread-affine +`VmHandle(Rc>)` values provide short `try_read` and +`try_command` state sections. A `VmCoordinator` owns bounded operation and +lifecycle admission. Different handles and coordinators make progress +independently, and no state borrow crosses an external await. + +P0 ordering classes: + +```rust +enum RequestOrderingKey { + Connection(String), + Session { connection_id: String, session_id: String }, + VmLifecycle { connection_id: String, session_id: String, vm_id: String }, + VmOperation { connection_id: String, session_id: String, vm_id: String }, + Extension { + namespace: String, + connection_id: String, + key: Vec, + policy: ExtensionOrderingPolicy, + }, + Unordered, +} +``` + +Ordering semantics: + +- Authentication and connection/session membership changes use the relevant + connection/session coordinator. +- VM create is ordered with session disposal and VM membership insertion. +- Configure, dispose, layer topology changes, root snapshot/import/export, and + package linking use `VmLifecycle`. +- Filesystem, kernel, execution, stdin, process inspection/control, and VM fetch + use `VmOperation`. +- A VM lifecycle-exclusive command excludes VM operations for that VM. +- Ordinary VM operations are admitted independently but enter the VM + coordinator only for their state critical sections. +- Extension operations use an extension-provided opaque ordering key. Core does + not decode the extension payload. Core-exclusive keys reject an overlapping + operation with a typed bounded conflict; extensions may explicitly retain + conflict enforcement when their protocol requires a richer typed response. +- ACP uses its durable session ID as an extension-managed ordering key. Its + bounded route guard returns the existing typed `session_busy` policy for a + second active prompt in the same ACP session; prompts in different ACP + sessions run concurrently. + +A request-level operation may issue multiple VM commands over its lifetime. It +must not retain a mutable VM-state borrow or exclusive command guard between +commands. It does retain its nonexclusive ownership registration until terminal +completion. This lets teardown find the operation without preventing +`readFile` from completing while an ACP prompt waits on adapter output in the +same VM. + +### Event broker + +The production event topology has two parts. The central protocol pump is the +sole consumer of runtime producer wakes and claims root, attached-child, and +detached-child internal work. `ProcessEventBroker` owns bounded durable public +event state and demultiplexes it by connection/session/VM/process ownership. + +An extension or request registers an ownership-scoped waiter, then awaits that +waiter without retaining the process registry, VM coordinator, or extension +resource coordinator. Event handling that mutates a VM is submitted as a short +VM command; the waiter itself never makes a VM actor sleep until its timeout. + +The broker preserves the one-consumer rule where a protocol requires it. For +ACP, at most one adapter JSON-RPC response loop consumes stdout for one adapter +process, while different routes have independent consumers. Targeted public +waiters use a separate post-pump notification, so they cannot consume the +pump's producer wake. After each bounded turn, durable-source probes re-arm the +pump when one coalesced wake represented multiple executor events. + +### Cloneable extension services + +Production extensions use a transport-agnostic cloneable trait: + +```rust +pub trait ExtensionServices: Send + Sync { + fn guest_filesystem_call(...) -> ExtensionFuture<'static, ...>; + fn poll_process_event(...) -> ExtensionFuture<'static, ...>; + fn invoke_callback_async(...) -> ExtensionFuture<'static, ...>; + // Other VM/process/resource operations use the same owned form. +} +``` + +`RoutedExtensionServices` implements this trait through a bounded command +channel and the process-event broker. Service calls return owned `'static` +futures or immediate results, communicate with coordinators, and never expose +internal maps or stdio/fd details. + +The generic `Extension` contract remains namespace-based and opaque. Its +implemented hooks are `request_class() -> ExtensionRequestClass::{Ordinary, +Progress}`, `request_ordering_key()`, and `request_ordering_policy()`. +Core supplies reserved progress admission; ACP alone decodes its opaque payload +and signals its keyed route state. + +Native-sidecar must not import or decode `agentos-protocol`. + +`impl ExtensionHost for NativeSidecar` remains only for the direct in-process +compatibility API and is unreachable from the production protocol engine. An +owned production extension request cannot regain whole-sidecar access through +a trait object. Retiring the compatibility API is P1 cleanup, not a P0 protocol +progress blocker. + +### P0.2 checklist and tests + +- [x] Introduce cloneable connection/session and VM service handles. +- [x] Ensure different VM coordinators can execute independently. +- [x] Move long waits outside coordinator critical sections. +- [x] Replace `ExtensionContext`'s mutable host borrow with cloneable + transport-agnostic services. +- [x] Split long event waits into the ownership-aware event broker. +- [x] Claim root, attached-child, and detached-child internal JavaScript/Python + runtime events exactly once and service them as bounded owned VM work. +- [x] Retain claimed internal event work durably when service admission is full; + do not drop, duplicate, or hot-requeue it. +- [x] Make extension request futures independently tracked and spawnable. +- [x] Add extension-owned opaque ordering/progress classification. +- [x] Preserve all ownership validation at the coordinator boundary. +- [x] Preserve explicit same-session ACP `session_busy` behavior. +- [x] Prevent lifecycle operations from racing conflicting same-VM operations. + +Tests: + +- [x] Rust test: a gated VM-A operation does not delay a VM-B operation. +- [x] Rust test: prompt wait releases VM coordination so same-VM filesystem + access completes. +- [x] Rust test: a long event waiter does not retain VM coordination and wakes + only for its matching ownership/process event. +- [x] Rust test: root Python VFS work and attached/detached child RPC/output/exit + work progress while an independent ordinary request is gated. +- [x] Rust test: repeated process-event notifications and service-capacity + saturation preserve exact-one-consumer delivery without loss or spin. +- [x] Rust test: internal event-service failure is observable, releases + admission, and VM disposal cancels/drains blocked VM-bound service work. +- [x] Rust test: configure/dispose are ordered against same-VM operations. +- [x] Rust test: session disposal prevents new owned operations and cancels + existing ones. +- [x] ACP test: same-session second prompt receives typed `session_busy`. +- [x] ACP test: different-session prompts both start and complete independently. +- [x] Ownership test: a connection cannot use another connection's operation or + extension-session key. + +Production stdio uses cloneable owned services and independently supervises +generic and extension futures. VM state is partitioned per handle, ordering is +enforced by `OwnershipCoordinator`, and claimed internal work uses independent +active/deferred bounds while remaining cancellation- and disposal-tracked. + +## P0.3 — Direct progress routing + +### Progress-critical classes + +The following traffic must have reserved admission independent of ordinary +request saturation: + +- shutdown control, +- transport termination/error, +- registered `SidecarResponseFrame`, +- cancellation of an active operation, +- ACP permission response for an active prompt, +- terminal response/rejection emission. + +A progress message is still bounded. It uses an existing reserved control +capacity or a new explicitly configured reserved capacity; it is never admitted +to an unbounded collection. + +### Generic extension progress routing + +ACP cancellation and permission responses remain encoded as opaque extension +requests. The extension classifies these frames through a generic hook. The +router uses the returned namespace plus opaque target key to find or signal the +active extension operation. + +The implemented generic hooks are: + +```rust +fn request_class(...) -> ExtensionRequestClass; // Ordinary | Progress +fn request_ordering_key(...) -> Option>; +fn request_ordering_policy(...) -> ExtensionOrderingPolicy; +``` + +Core gives `Progress` independent reserved admission and invokes the opaque +extension request. Only the ACP extension decodes the payload, locates its +route key, and interprets cancel or permission semantics. + +### ACP route state machine + +ACP keys live routes by the existing full-identity durable route key. Each +route uses short atomic state transitions: + +```text +Idle + -> StartingOrRestoring + -> Idle + -> PromptRunning { prompt_id, cancellation, completion } + -> Stopping + -> removed +``` + +Rules: + +- Open/restore is single-flight per route; concurrent callers never launch two + adapter processes. +- Prompt installs its cancellation/completion state before durable acceptance + or adapter write, closing the cancel-before-registration race. +- A second prompt on the same route receives typed `session_busy`; prompts on + different routes run concurrently. +- Read/list/history operations may run during a prompt. +- Configuration operations that would start a competing adapter response loop + receive typed busy while a prompt runs. +- Cancel signals the installed token directly and also cancels a permission + waiter. It never starts a competing adapter response loop. +- Permission response validates the option before consuming the pending waiter; + an invalid option leaves the waiter live for a later valid response. +- Unload/delete enters `Stopping`, signals cancellation, awaits prompt durable + terminal commit outside the route lock, and then tears down the adapter. +- KillProcess executes independently. The prompt observes ProcessExited and + commits its real terminal result; no router may drop the prompt future and + substitute a synthetic terminal response. +- Cancel, permission, process exit, disconnect, and shutdown races commit + exactly one durable prompt outcome and empty every live waiter/route entry. + +A progress request receives its own exactly-once acknowledgement. Signalling +the target does not consume the target's terminal response reservation. + +### Direct registered sidecar responses + +A matching `SidecarResponseFrame` routes directly from the control reader to +its registered waiter. It does not enter ordinary ingress, acquire ordinary +operation admission, or scan unrelated events. Production ACP uses +`invoke_callback_async`; its matching response settles the registered waiter +directly. The synchronous direct in-process compatibility callback API remains +P1 cleanup. + +### Shutdown + +Shutdown behavior: + +1. atomically enter `Draining`, +2. reject new ordinary requests, +3. continue accepting progress messages and registered responses, +4. signal cancellation to every active operation, +5. wait up to `runtime.protocol.shutdownGraceMs` for tracked operations, +6. synthesize terminal shutdown responses for unfinished operations when the + transport remains writable, +7. close output only after terminal/control drainage, +8. report every forced cancellation or failed terminal delivery. + +No active task, waiter, admission reservation, or response reservation may +remain after shutdown completion. + +### P0.3 checklist and tests + +- [x] Implement reserved direct routing for shutdown and transport failure. +- [x] Preserve direct registered sidecar-response delivery. +- [x] Implement generic extension-owned progress classification. +- [x] Route ACP cancel directly to the active prompt token. +- [x] Route ACP permission response directly to its active waiter. +- [x] Implement the keyed ACP route state machine and single-consumer adapter + response-loop guard. +- [x] Make open/restore single-flight and unload/delete cancellation-aware. +- [x] Remove synthetic KillProcess interruption in favor of cooperative + ProcessExited observation. +- [x] Give every progress request an exactly-once acknowledgement. +- [x] Implement supervisor drain/cancel shutdown sequencing. +- [x] Add and validate `runtime.protocol.shutdownGraceMs` and include it in the + runtime limits inventory. +- [x] Release all operation and waiter state on connection loss. + +Tests: + +- [x] Rust real-loop test: prompt A, unrelated B, cancel A; B completes and A + cancels without either frame trapping the other. +- [x] Rust real-loop test: cancel works when ordinary operation admission is + saturated. +- [x] ACP test: permission response resumes its target prompt while unrelated + requests are active. +- [x] Callback test: matching sidecar response reaches its waiter under ordinary + saturation. +- [x] Progress test: a duplicate live `(connection_id, request_id)` is rejected + without consuming the original request's exactly-once acknowledgement. +- [x] Shutdown test: active gated operations are cancelled/drained and no task + or reservation leaks. +- [x] Disconnect test: all connection-owned operations and waiters terminate. +- [x] Race test: cancel versus natural completion yields exactly one target + terminal response and one cancel acknowledgement. +- [x] ACP race tests: cancel before reservation, before durable acceptance, + before adapter write, during output wait, during permission wait, and + after terminal commit. +- [x] ACP test: invalid permission option leaves the waiter live; a subsequent + valid option succeeds. +- [x] ACP test: unload/delete during a prompt reaches durable terminal state + before teardown and rejects new prompt/config work while stopping. +- [x] ACP test: adapter kill progresses independently and the prompt observes + ProcessExited without dropping its future. +- [x] ACP test: concurrent open/restore launches exactly one adapter. +- [x] ACP test: one adapter has at most one response-loop consumer while + different routes can have concurrent consumers. + +Progress work uses its own reserved-lane-bounded, connection-scoped registry. +Its handle is retained until broker publication, claims exactly one +acknowledgement, and rejects a duplicate live request ID without affecting the +original. Shutdown closes ordinary admission, signals tracked work, continues +progress/control routing through the grace period, force-terminalizes any +unfinished operation exactly once, aborts only after takeover, drains control +output, and reports failed delivery. + +## P0.4 — Nonblocking output broker + +### Required lanes and logical classes + +Maintain the physical lane contract: + +- fd 0: host `RequestFrame` ingress, +- stdout: non-heartbeat ordinary `EventFrame` egress, +- fd 3: responses, sidecar requests, heartbeats, registered callback traffic, + and typed shutdown/control. + +Combined stdio compatibility may multiplex physical writes, but logical +ordinary and control admission remain independent. + +Within fd 3/control output, use independent logical queues and budgets: + +1. **Progress** — shutdown, cancel/permission acknowledgements, sidecar callback + requests, terminal transport errors, and other frames needed to unblock + active work. +2. **Rejection** — typed responses for requests rejected before ordinary + operation admission. +3. **Terminal** — exactly one response for each admitted request. +4. **Observability** — heartbeat and limit-warning delivery; best-effort or + coalesced and never permitted to consume required progress capacity. + +Ordinary non-heartbeat events keep their independent stdout queue/budget. A +shared physical fd does not imply a shared admission budget. The sum of the +configured logical control capacities must be validated against any physical +control bound retained by the implementation. + +The control writer drains progress before rejection, rejection before terminal, +and terminal before best-effort observability, preserving FIFO within each +class. Combined stdio drains all logical control classes before ordinary +events. + +### Producer API + +`ProtocolFrameWriter` exposes the implemented broker operations: + +```rust +fn try_reserve_terminal(...); +fn try_reserve_progress(...); +fn publish_reserved_terminal_for_operation(...); +fn publish_reserved_progress_for_request(...); +async fn publish(...); // ordinary producers +fn try_publish_rejection(...); +fn try_publish_observability(...); +``` + +Rules: + +- The ingress router only calls nonblocking methods backed by already-reserved + capacity. +- A request terminal response consumes the reservation acquired at request + admission. +- A progress rejection/acknowledgement consumes reserved control/rejection + capacity. +- An ordinary producer task may asynchronously await ordinary capacity. +- No producer calls `Condvar::wait`, blocking channel `send`, or blocking + stdout/fd writes. +- The encoded frame owns its count/byte reservation until the physical writer + has completed or failed the write. +- Writer failure closes the broker and wakes every waiter with a terminal error. +- Closing the broker atomically records the terminal error, drains and drops + queued encoded frames so their reservations release, and wakes all async + budget waiters. Closure is idempotent. +- `SidecarRequestFrame` uses timed progress publication; its registered waiter + is cancelled if publication fails or its deadline expires. +- Limit warnings remain in their existing bounded warning source and are + retried/coalesced or explicitly logged to stderr when observability admission + is unavailable. They never await from ingress. +- Heartbeats are coalesced best-effort observability and cannot consume + terminal/progress/rejection reservations. + +### Ordinary event backpressure + +P0 requires ingress independence, bounded memory, and no silent event loss. +When ordinary event capacity is unavailable: + +- event state remains in its bounded durable producer queue, +- at most one coalesced output-ready wake remains queued/in flight, +- the event pump stops draining that producer, +- the output broker wakes the producer after capacity is released. + +A request task returning a finite event batch may await broker capacity in that +request task. It may not hold a coordinator while waiting. + +### Response reservation and overload + +Admission reserves one terminal frame and +`runtime.protocol.terminalFallbackBytes`, not the maximum wire-frame size. The +configuration must satisfy: + +```text +maxTerminalFrames >= maxInFlightRequests +maxTerminalBytes >= maxInFlightRequests * terminalFallbackBytes +``` + +This guarantees a small typed terminal fallback for every admitted operation +without reducing default concurrency to `maxControlBytes / maxFrameBytes`. + +When the real terminal response exceeds its fallback reservation, the +completion task asynchronously acquires the additional bytes from the terminal +budget. It holds no coordinator while waiting. If the response exceeds the +wire-frame maximum, or cannot be retained before bounded shutdown, it uses the +already-reserved fallback to emit a typed frame/egress-limit terminal response. + +Terminal capacity is independent of progress capacity. Admitting many prompts +therefore cannot consume the `SidecarRequestFrame` capacity those prompts need +for host callbacks. + +Keep a fixed rejection capacity so failure to admit an ordinary request can +still return a typed overload response. Ordinary events and admitted terminal +responses cannot consume this reserve. + +If neither ordinary operation admission nor rejection reservation is +available, the router must stop dequeuing ordinary ingress while continuing +progress/control handling. If the reader itself reaches bounded ingress and no +rejection can be retained, transition to a typed terminal transport failure and +close rather than silently dropping a request and pretending it was answered. + +### P0.4 checklist and tests + +- [x] Implement logical ordinary/control output broker lanes. +- [x] Split control output into terminal, progress, rejection, and + observability classes with validated configuration math. +- [x] Replace synchronous producer-side condition-variable waits. +- [x] Reserve terminal response capacity during request admission. +- [x] Add reserved rejection/progress capacity. +- [x] Route response, event, sidecar request, warning, cancel acknowledgement, + and terminal error emission through the broker. +- [x] Make broker close drain queued frames, release reservations, and wake all + publishers with the same terminal error. +- [x] Define and implement rejection-reserve exhaustion as pause-or-typed-close, + never silent loss. +- [x] Ensure live extension events use async/nonblocking broker semantics. +- [x] Stop and re-arm ordinary event drainage on output backpressure. +- [x] Wake all producers and waiters when a writer fails. +- [x] Keep actual physical writes off the ingress router. + +Tests: + +- [x] Output test: saturating ordinary events does not prevent a response. +- [x] Output test: saturating ordinary events does not prevent cancel/shutdown. +- [x] Output test: a deliberately non-reading host does not park ingress. +- [x] Output test: reservation remains charged through physical write + completion. +- [x] Output test: writer failure releases reservations and fails waiters. +- [x] Output test: closing a full broker wakes a publisher already waiting for + budget and returns all usage to zero. +- [x] Output test: admitted request always emits one terminal response even when + ordinary output is full. +- [x] Event test: backpressured durable events resume without loss or + duplication. +- [x] Combined-stdio test: logical control priority survives physical + multiplexing. +- [x] Output classification test covers response/error, progress ack, sidecar + request, live/batch event, warning, and heartbeat classes. +- [x] Output test: terminal reservations for all admitted requests do not + consume callback/progress capacity. +- [x] Overload test: exhausted rejection capacity follows the specified + pause-or-typed-close policy and never silently drops a response. + +The aggregate suite covers real cancel/shutdown routing while ordinary output +is full, an admitted request's terminal response under the same saturation, +exact-256 executor-source continuation after a coalesced wake, separate public +and pump wake ownership, and per-child relay gating that prevents exit from +overtaking retained stdout. Durable-event stop/re-arm has no loss, duplication, +or empty hot-spin. + +## P0.5 — ACP workaround removal + +After P0.1–P0.4 are active: + +- remove `pending_frame` from the protocol loop, +- remove `dispatch_with_prompt_interrupt`, +- remove `BlockingExtensionRequest`, +- remove the old blocking-request single-frame interruption plumbing when no + longer used, +- remove tests that validate the workaround's mechanics, +- retain ACP cancellation/permission behavior through P0.3 generic extension + progress routing. + +No production native-sidecar code may recognize ACP methods or payload types. + +### P0.5 checklist and tests + +- [x] Delete the single `pending_frame` slot. +- [x] Delete `dispatch_with_prompt_interrupt`. +- [x] Delete obsolete blocking extension interruption types/hooks. +- [x] Delete or rewrite workaround-specific unit tests. +- [x] Confirm native-sidecar remains independent of `agentos-protocol`. +- [x] Confirm all retained cancellation tests use generic direct routing. + +Tests: + +- [x] Architecture guard: no `pending_frame` or + `dispatch_with_prompt_interrupt` remains. +- [x] Architecture guard: native-sidecar does not import ACP protocol types. +- [x] Full real-loop interleaving suite passes without prompt-specific stdio + routing. + +## Real protocol-loop test harness + +The production `run_async` wiring delegates to `run_protocol_engine`. +`stdio/request_concurrency_tests.rs` drives that exact engine with: + +- bounded ordinary ingress, +- bounded progress/control ingress, +- a deterministic output broker/sink, +- a configured native sidecar plus fake extensions, +- shutdown and writer-failure signals. + +The harness does not call `dispatch_wire` as its concurrency proof. + +Its fake extension uses oneshot/notify gates: + +- start gate confirms an operation is actually running, +- release gate controls completion, +- cancellation token confirms direct cancellation, +- no sleep determines ordering, +- short timeouts are failure bounds only, +- every failing test releases or aborts all gates before cleanup. + +The extracted harness lives outside the inline production test module; focused +private state-machine and source-wake tests remain inline where they need +private access. + +## Required integration coverage + +Rust is authoritative. The non-nightly TypeScript public-client coverage runs +against a freshly built shared sidecar and proves both of these flows: + +1. A delayed host-tool response crosses exactly 256 ACP updates, the terminal + response arrives, every update remains ordered/exactly once, and the same + adapter session is reused. +2. Two prompts remain simultaneously in flight while public file write/read + calls complete; both prompts then cancel and unload cleanly. + +The migration-parity suite additionally covers filesystem/process/snapshot, +registered bindings, host-loopback fetch, and ACP lifecycle behavior. + +## Implementation order + +- [x] 1. Land the extracted real-loop harness and initial red regression. +- [x] 2. Add runtime request-admission configuration and reservations. +- [x] 3. Add request supervisor, tracked terminal guard, and shutdown registry. +- [x] 4. Introduce cloneable state/VM/extension service handles. +- [x] 5. Move request dispatch to tracked independently executing operations. +- [x] 6. Add extension-owned ordering and progress classification. +- [x] 7. Route cancel, permission, callback response, shutdown, and errors + directly. +- [x] 8. Add output broker with terminal/control reservations. +- [x] 9. Move every output producer to the broker. +- [x] 10. Remove ACP stdio interruption and `pending_frame`. +- [x] 11. Add and pass public-client integration smoke coverage. +- [x] 12. Run final validation and complete an architecture review against every + invariant and checkbox. + +## Validation commands + +Required focused commands: + +```bash +cargo test -p agentos-native-sidecar --lib request_concurrency -- --test-threads=1 +cargo test -p agentos-native-sidecar --lib protocol_output -- --test-threads=1 +cargo test -p agentos-native-sidecar --lib deferred_tcp_connect -- --test-threads=1 +cargo test -p agentos-native-sidecar --test architecture_guards -- --test-threads=1 +cargo test -p agentos-sidecar --lib -- --test-threads=1 +``` + +Required crate/workspace gates: + +```bash +cargo test -p agentos-native-sidecar --lib +cargo test -p agentos-native-sidecar --test service --no-run +cargo check --workspace +``` + +The explicit `--lib` filters are intentional: an unqualified package filter +also compiles every integration target, which duplicates the expensive native +link and can exhaust the workspace disk before the focused unit tests start. +The separate `service --no-run` command keeps the public integration harness +compile-checked without weakening that coverage. + +Do not run an unfiltered `cargo test -p agentos-sidecar`; its integration +binaries can spawn real processes and hang. The `--lib` command above runs the +bounded ACP and durable-session unit suite without executing those integration +binaries. + +If TypeScript public-client coverage changes: + +```bash +pnpm --dir packages/core check-types +pnpm --dir packages/core build +pnpm --dir packages/core exec vitest run tests/migration-parity.test.ts \ + tests/acp-reactor-regression.test.ts --fileParallelism=false --reporter=verbose +``` + +## Final acceptance checklist + +- [x] All P0.1 checkboxes and tests are complete. +- [x] All P0.2 checkboxes and tests are complete. +- [x] All P0.3 checkboxes and tests are complete. +- [x] All P0.4 checkboxes and tests are complete. +- [x] All P0.5 checkboxes and tests are complete. +- [x] No global ordinary request FIFO or global sidecar mutex exists. +- [x] No ingress path awaits business execution or output capacity. +- [x] Slow prompts, slow output, cancellation, and shutdown interleavings pass. +- [x] Different VMs and extension sessions demonstrate concurrent progress. +- [x] Same-entity ordering and ownership isolation are explicit and tested. +- [x] Admission, output, cancellation, panic, disconnect, and shutdown release + all tracked reservations. +- [x] The friction log contains every deferred non-P0 audit item. +- [x] The final report lists any unchecked item as a blocker; P0 must not be + reported complete while an item remains unchecked. + +## Validation record — 2026-08-05 + +Passed in this working copy: + +- `cargo fmt --all -- --check` +- `cargo check --workspace` +- `cargo build -p agentos-sidecar --bin agentos-sidecar` +- `cargo test -p agentos-runtime --lib`: 62 passed, 1 intentionally ignored +- `cargo test -p agentos-native-sidecar-core --lib`: 84 passed +- `cargo test -p agentos-native-sidecar --lib -- --test-threads=1`: 324 + passed, 1 intentionally ignored +- focused native-sidecar suites: request concurrency 20/20, protocol output + 5/5, deferred TCP connect 3/3, plus the deferred-event ownership, + source-rearm, exact-256-update, child-relay ordering, Python service, and + binding-rollback regressions +- `cargo test -p agentos-native-sidecar --test architecture_guards + -- --test-threads=1`: 40/40 +- `cargo test -p agentos-native-sidecar --test service --no-run` +- `cargo test -p agentos-sidecar --lib`: 74/74 +- `pnpm --dir packages/core check-types` +- `pnpm --dir packages/core build` +- public TypeScript integration coverage: ACP reactor 2/2 and migration parity + 4/4, including the real host-loopback fetch and ACP lifecycle flow + +Additional non-P0 workspace baseline: + +- `pnpm --dir packages/core test:pr` reaches 98/99 unit tests before an + unchanged public-export test expects `AgentOs.prototype.pread`, which is + absent from both this revision and its parent. The two required integration + files were therefore run directly and pass 6/6. +- Repository-wide `pnpm check-types` stops at + `examples/js-filesystem`: its package-local `node_modules` and local + `@rivet-dev/agentos` link are absent. The changed public package's scoped + typecheck and build pass. Both workspace baselines are recorded in the + agentOS friction log. diff --git a/scripts/ci.sh b/scripts/ci.sh index 0cd0fc5370..d40612e992 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -34,7 +34,6 @@ run_step node scripts/check-rust-package-metadata.mjs run_step node --test scripts/check-agentos-client-protocol-compat.test.mjs run_step node scripts/check-agentos-client-protocol-compat.mjs run_step pnpm check-layout -run_step node --test scripts/generate-agentos-mirror.test.mjs if [[ -f scripts/check-registry-test-runtime-boundary.test.mjs ]]; then run_step node --test scripts/check-registry-test-runtime-boundary.test.mjs run_step node scripts/check-registry-test-runtime-boundary.mjs