See `../CLAUDE.md` for crate-wide runtime and testing rules.
## Local Patterns
- `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 secure-exec-sidecar code must not decode ACP payloads or depend on `agent-os-protocol`.
- Sidecar-to-host callback protocol must stay agent-agnostic: use `HostCallback{callback_key}` for generic host callbacks, and keep toolkit-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/agent-os-sidecar`, not `crates/sidecar/src`.
- Tool CLI `--json` and `--json-file` payloads in `src/tools.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.
- `net.poll` waits in `src/execution.rs` must stay explicitly bounded. The sync-RPC handler runs on the sidecar's main sync-RPC thread, so guest `wait_ms` values must be clamped via `clamp_javascript_net_poll_wait(...)` to the 50 ms ceiling; longer waits should return the currently observed socket state after the ceiling expires instead of blocking dispose/shutdown or unrelated VM work.
- `kill_process` signal parsing in `src/execution.rs` must stay aligned with the guest `child_process.kill(...)` bridge contract: accept the full 1..31 signal table plus common aliases (`SIGIOT` -> `SIGABRT`, `SIGPOLL` -> `SIGIO`), and terminate shared-V8 child executions directly for non-streamed signals so child polls still observe prompt exits.
- `child_process.poll` in `src/execution.rs` must reserve `Value::Null` for the real "no events pending" case. If a tracked child disappears after descendant-event drain, return a typed `ECHILD` execution error instead so guest poll loops stop instead of spinning on a ghost child.
- Child stdin plumbing in `src/execution.rs` must mirror the root-process path for nested `child_process` children too: always call `child.execution.write_stdin()` / `close_stdin()` and the kernel pipe helpers together. Kernel-only writes or closes leave shared-V8 WASM children stuck behind the local `kernel_stdin` bridge, so pipelines like `echo hello | wc -c` never observe EOF.
- Child JavaScript executions use `service_javascript_sync_rpc(...)` in `src/execution.rs`, not the top-level `src/service.rs`, for `process.*` bridge calls. When changing guest self-signal behavior (`process.kill`, `process.abort()`, signal-shape exit reporting), mirror the top-level bookkeeping there too or spawned Node children will regress to `unsupported JavaScript sync RPC method process.kill` and report plain exit codes.
- Nested JavaScript child signal registration currently arrives through the sync-RPC method `process.signal_state`, not just `ActiveExecutionEvent::SignalState`. When fixing descendant `SIGCHLD` or job-control behavior in `src/execution.rs`, keep the nested `process.signal_state` bookkeeping aligned with the top-level `src/service.rs` handler or grandchildren will silently lose their registered signal handlers.
- The sidecar protocol `Authenticate` handshake must carry `secure_exec_bridge::bridge_contract().version`; `src/service.rs` should reject mismatches with `bridge_version_mismatch` before opening a connection so bridge-contract drift fails fast instead of crashing later on the first divergent RPC.
- `plugins/host_dir.rs` metadata writes must keep the old symlink-leaf safety contract for plain ops (`chmod`/`chown`/`utimes` still reject symlink leaves), while the richer timestamp path should only mutate symlink metadata when the caller explicitly requests nofollow semantics (`lutimes` / `utimes_spec(..., false)`).
- Mounted filesystem shutdown now happens explicitly during `src/vm.rs` disposal/reconfigure, not just in `MountTable::drop`. Bridge-backed mounts can therefore emit `SidecarRequestPayload::JsBridgeCall` during teardown, and host-visible flush failures should surface as the structured event `filesystem.mount.shutdown_failed` with the mount metadata plus the original error code/message.
- Guest runtime env setup in `src/execution.rs` must add writable `host_dir` / `module_access` mount roots to `AGENT_OS_EXTRA_FS_WRITE_PATHS`, not just the VM shadow cwd. Without those extra write roots, guest `fs.*` bridge calls misclassify writable mounts as read-only (`EROFS`) and cross-mount rename tests never reach the intended `EXDEV` path.
- Python mapped host-path access in `src/filesystem.rs` must stay on the anchored-fd path: open the mapped root once, resolve descendants with `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`, and perform the actual syscall through `/proc/self/fd/<fd>` or an anchored parent dir. Do not reintroduce resolve-then-use `PathBuf` opens, and when filtering `read_dir` results from a proc-fd directory, rebuild child host paths from the resolved directory host path plus `file_name()` instead of reusing `DirEntry::path()`, which points back into procfs.
- In `tests/builtin_conformance.rs`, isolated extra tests that open a host listener should finish sidecar/session/VM setup before starting any accept deadline. Those tests run in subprocesses that can queue behind the shared sidecar-runtime lock, so a server timeout that starts before VM creation will flake under the normal parallel `cargo test` harness.